Jetti
A fast, cross-platform tool for organizing git repositories into a consistent directory structure by host, owner, and repo name.
Jetti is a Rust alternative to ghq, installable via Cargo with no dependencies beyond git.
Why Jetti?
- Cross-platform — installs anywhere Cargo runs, no Go toolchain needed
- Multi-host — GitHub, GitLab, Codeberg, and any git host out of the box
- SSH by default — uses SSH clone URLs, matching GitHub’s recommendation
- Fast batch operations — parallel
fetch,update, andstatusacross all repos - Scriptable — clean stdout/stderr separation for shell integration
- Pretty — colored tree view for humans, flat output for pipes
Directory structure
Jetti organizes repositories into ~/dev/<host>/<owner>/<repo>:
~/dev/
├── github.com/
│ ├── rust-lang/
│ │ ├── cargo/
│ │ └── log/
│ └── rapidity-rs/
│ └── jetti/
├── gitlab.com/
│ └── user/
│ └── project/
└── codeberg.org/
└── forgejo/
└── forgejo/
Quick example
# Clone a repo
jetti clone rust-lang/log
# See all your repos
jetti list
# Check status across everything
jetti status
# Fetch all repos in parallel
jetti fetch
# Jump to a repo with fzf
cd $(jetti list --full-path | fzf)
Getting Started
Install
cargo install jetti
Jetti requires git to be installed and available in your PATH.
Clone your first repo
jetti clone owner/repo
This clones git@github.com:owner/repo.git into ~/dev/github.com/owner/repo/.
Jetti supports several specifier formats:
# Short form — uses default host (github.com) and SSH
jetti clone owner/repo
# Explicit host
jetti clone gitlab.com/owner/repo
jetti clone codeberg.org/owner/repo
# Full URLs — protocol is inferred from the URL
jetti clone https://github.com/owner/repo.git
jetti clone git@github.com:owner/repo.git
Browse your repos
# Tree view (in a terminal)
jetti list
# Flat output (for piping)
jetti list | fzf
# Absolute paths
jetti list --full-path
Keep repos up to date
# Fetch all repos in parallel
jetti fetch
# Fetch and fast-forward all repos
jetti update
# Check for uncommitted work or unpushed commits
jetti status
Next steps
- See Commands for the full reference
- Set up Shell Integration for the
jshortcut - Customize your setup in Configuration
Commands
Jetti provides the following commands:
| Command | Alias | Description |
|---|---|---|
| clone | get, g | Clone a repository |
| list | ls | List cloned repositories |
| fetch | Fetch updates for all repos | |
| update | Fetch and fast-forward all repos | |
| status | st | Show repo status dashboard |
| rm | remove | Remove a cloned repository |
| config | cfg | View or manage configuration |
| root | Print the root directory | |
| completions | Generate shell completions |
Global options
These options are available on all commands:
--root <PATH> Override the root directory
-h, --help Print help
-V, --version Print version
clone
Clone a repository into the organized directory structure.
Aliases: get, g
Usage
jetti clone [OPTIONS] <REPO>
Arguments
<REPO>— Repository specifier in any of these formats:owner/repo— uses the default host (github.com) and configured protocol (SSH)host/owner/repo— explicit hosthttps://host/owner/repo.git— full HTTPS URL (protocol inferred)git@host:owner/repo.git— full SSH URL (protocol inferred)
Options
-d, --depth <N> Create a shallow clone with the given depth
-s, --shallow Shorthand for --depth 1
--https Clone using HTTPS instead of SSH
--ssh Clone using SSH instead of HTTPS
Protocol resolution
The clone protocol is determined in this order:
- Inferred from URL —
git@...always uses SSH,https://...always uses HTTPS --https/--sshflag — overrides the default for this clone (mutually exclusive)- Config
protocol— your default (SSH unless changed)
Examples
# Clone from GitHub (SSH by default)
jetti clone rust-lang/log
# Clone from GitLab
jetti clone gitlab.com/user/project
# Shallow clone
jetti clone owner/repo --shallow
# Force HTTPS
jetti clone owner/repo --https
# Full URL — protocol inferred
jetti clone https://github.com/owner/repo.git
Behavior
- Creates the directory structure
<root>/<host>/<owner>/<repo> - If the repo already exists, prints the path and exits
- If a non-git directory already exists at the destination, clone stops with an error instead of deleting it
- Status messages go to stderr; only the resulting path goes to stdout
Shell integration
Because only the path goes to stdout, you can use:
cd $(jetti clone owner/repo)
list
List cloned repositories.
Alias: ls
Usage
jetti list [OPTIONS]
Options
-f, --full-path Show absolute paths instead of relative
-p, --prefix <PREFIX> Filter repositories by prefix
Output modes
Tree view — shown by default in a terminal:
github.com
├── rust-lang
│ ├── cfg-if
│ └── log
└── rapidity-rs
└── jetti
codeberg.org
└── forgejo
└── forgejo
Flat view — used automatically when piping, or with --full-path:
github.com/rapidity-rs/jetti
github.com/rust-lang/cfg-if
github.com/rust-lang/log
codeberg.org/forgejo/forgejo
The flat output is one path per line, sorted, designed for piping to fzf, grep, wc, etc.
Examples
# Tree view
jetti list
# Filter to a specific host or owner
jetti list -p github.com/rust-lang
# Absolute paths
jetti list --full-path
# Fuzzy find
jetti list | fzf
# Count repos
jetti list | wc -l
fetch
Fetch updates for all repositories in parallel.
Runs git fetch on each repo. This updates remote refs without modifying your working tree or branches — it’s always safe to run.
Usage
jetti fetch [OPTIONS]
Options
-p, --prefix <PREFIX> Filter repositories by prefix
-j, --jobs <N> Number of parallel jobs (default: 8)
Examples
# Fetch all repos
jetti fetch
# Fetch only GitHub repos
jetti fetch -p github.com
# Fetch with 16 parallel jobs
jetti fetch -j 16
Display
Shows a tree-structured progress view with spinners, then a summary:
github.com
└── rust-lang
├── ✓ log fetched
└── · cfg-if up to date
done: 2 repos: 1 fetched, 1 up to date
update
Fetch and fast-forward all repositories in parallel.
Runs git fetch followed by git merge --ff-only on each repo. This is safe:
- Dirty repos are skipped — repos with uncommitted changes are not touched
- Only fast-forwards — if your branch has diverged from upstream, the update is skipped with a warning
- No merge commits — only clean fast-forwards are applied
Usage
jetti update [OPTIONS]
Options
-p, --prefix <PREFIX> Filter repositories by prefix
-j, --jobs <N> Number of parallel jobs (default: 8)
Examples
# Update all repos
jetti update
# Update only GitLab repos
jetti update -p gitlab.com
# Single-threaded (sequential)
jetti update -j 1
Display
github.com
└── rust-lang
├── ✓ log fast-forwarded
├── · cfg-if up to date
└── ! my-fork diverged, merge manually
done: 3 repos: 1 updated, 1 up to date, 1 need attention
status
Show the status of all repositories — branch, uncommitted changes, and ahead/behind counts.
Alias: st
Usage
jetti status [OPTIONS]
Options
-p, --prefix <PREFIX> Filter repositories by prefix
-j, --jobs <N> Number of parallel jobs (default: 8)
What it shows
For each repo, status reports:
- Branch name — the current checked-out branch
- Modified file count — number of uncommitted changes
- ↑N — commits ahead of upstream (unpushed)
- ↓N — commits behind upstream (can pull)
Display
github.com
└── rust-lang
├── · log main
└── ! my-fork main · 3 modified · ↑2
codeberg.org
└── forgejo
└── · forgejo main
done: 3 repos: 2 clean, 1 need attention
Repos are categorized as:
- clean — on a branch, nothing modified, not ahead/behind
- need attention — has dirty files or unpushed commits
- behind upstream — can be fast-forwarded with
jetti update
Examples
# Check everything
jetti status
# Check only your own repos
jetti status -p github.com/your-username
rm
Remove a cloned repository.
Alias: remove
Usage
jetti rm [OPTIONS] <REPO>
Arguments
<REPO>— Repository specifier (same formats asclone)
Options
--force Skip the confirmation prompt
Behavior
- Prompts for confirmation before deleting (unless
--force) - Removes the repository directory only when the target is an actual git repo
- Cleans up empty parent directories (owner, host) back to the root
- Accepts the same specifier formats as
clone
Examples
# Remove with confirmation
jetti rm owner/repo
# Remove without confirmation
jetti rm owner/repo --force
# Remove a GitLab repo
jetti rm gitlab.com/owner/repo --force
config
View or manage configuration.
Alias: cfg
Usage
jetti config [SUBCOMMAND]
Subcommands
jetti config (no subcommand)
Print the current configuration, including all settings and known hosts.
config: ~/.config/jetti/config.toml
root: /Users/taylor/dev
default_host: github.com
protocol: ssh
hosts:
· github.com
· gitlab.com
· codeberg.org
If no config file exists, shows defaults and a hint to create one.
jetti config init
Create the default config file at ~/.config/jetti/config.toml. Does nothing if it already exists.
jetti config edit
Open the config file in $EDITOR (or $VISUAL). Creates the default config first if it doesn’t exist.
jetti config path
Print the path to the config file.
Examples
# See current settings
jetti config
# Create a config file
jetti config init
# Edit in your preferred editor
jetti config edit
# Find the config file
cat $(jetti config path)
root
Print the root directory path where repositories are stored.
Usage
jetti root
Examples
# Print the root
jetti root
# /Users/taylor/dev
# Use in scripts
cd $(jetti root)
# Override for a single command
jetti root --root /tmp/repos
# /tmp/repos
Default
The default root is ~/dev. This can be changed in the configuration.
completions
Generate shell completions for bash, zsh, or fish.
Usage
jetti completions <SHELL>
Supported shells
bashzshfish
Setup
Bash
jetti completions bash > ~/.local/share/bash-completion/completions/jetti
Zsh
# Ensure the directory is in your fpath
mkdir -p ~/.zfunc
jetti completions zsh > ~/.zfunc/_jetti
# Add to .zshrc if not already present:
# fpath=(~/.zfunc $fpath)
# autoload -Uz compinit && compinit
Fish
jetti completions fish > ~/.config/fish/completions/jetti.fish
Completions take effect in new shell sessions, or after sourcing the file.
Configuration
Jetti looks for a config file at ~/.config/jetti/config.toml. If the file doesn’t exist, sensible defaults are used.
Managing the config
# View current settings (shows defaults if no file exists)
jetti config
# Create the default config file
jetti config init
# Open in your editor
jetti config edit
# Print the config file path
jetti config path
Config file reference
# Root directory for repositories (default: ~/dev)
root = "/home/user/dev"
# Default host when only owner/repo is given (default: github.com)
default_host = "github.com"
# Clone protocol: "ssh" (default) or "https"
protocol = "ssh"
# Known hosts — each entry defines SSH and HTTPS URL prefixes
[[hosts]]
name = "github.com"
ssh_prefix = "git@github.com:"
https_prefix = "https://github.com/"
[[hosts]]
name = "gitlab.com"
ssh_prefix = "git@gitlab.com:"
https_prefix = "https://gitlab.com/"
[[hosts]]
name = "codeberg.org"
ssh_prefix = "git@codeberg.org:"
https_prefix = "https://codeberg.org/"
All fields are optional. Missing fields use the defaults shown above.
Settings
root
The base directory where all repositories are cloned. Repos are stored at <root>/<host>/<owner>/<repo>.
Default: ~/dev
Can also be overridden per-command with the --root flag.
default_host
The host assumed when you provide only owner/repo without a host prefix.
Default: github.com
protocol
The default protocol for cloning. When a full URL is provided (e.g. https://... or git@...), the protocol is inferred from the URL instead.
Default: ssh
Values: ssh, https
Can also be overridden per-clone with jetti clone --https or jetti clone --ssh.
hosts
A list of known git hosts. Each host has:
name— the hostname (e.g.github.com)ssh_prefix— the prefix for SSH clone URLs (e.g.git@github.com:)https_prefix— the prefix for HTTPS clone URLs (e.g.https://github.com/)
Defaults: github.com, gitlab.com, codeberg.org
Note: If you define a
[[hosts]]section in your config file, it replaces the built-in defaults entirely — it does not extend them. Include all hosts you need.
Unknown hosts work too — jetti constructs reasonable SSH and HTTPS URLs from the hostname automatically. You only need to add a host entry if its URL format is non-standard.
Adding a custom host
To add a self-hosted GitLab, Gitea, or other forge:
[[hosts]]
name = "git.example.com"
ssh_prefix = "git@git.example.com:"
https_prefix = "https://git.example.com/"
Then:
jetti clone git.example.com/team/project
Environment variables
XDG_CONFIG_HOME— if set, the config file is at$XDG_CONFIG_HOME/jetti/config.tomlinstead of~/.config/jetti/config.toml
Shell Integration
Jump to a repo with fzf
The most common integration is a function that lets you fuzzy-find and cd into a repo:
Bash / Zsh
Add to your ~/.bashrc or ~/.zshrc:
j() {
local dir
dir=$(jetti list --full-path | fzf) && cd "$dir"
}
Fish
Add to your ~/.config/fish/functions/j.fish:
function j
set dir (jetti list --full-path | fzf)
and cd $dir
end
Clone and cd in one step
jc() {
cd "$(jetti clone "$@")"
}
This works because jetti clone prints only the path to stdout. Usage:
jc rust-lang/log
# You're now in ~/dev/github.com/rust-lang/log
Shell completions
Generate completions so your shell can tab-complete jetti commands and options:
# Bash
jetti completions bash > ~/.local/share/bash-completion/completions/jetti
# Zsh
mkdir -p ~/.zfunc
jetti completions zsh > ~/.zfunc/_jetti
# Fish
jetti completions fish > ~/.config/fish/completions/jetti.fish
For Zsh, make sure ~/.zfunc is in your fpath:
# In .zshrc, before compinit:
fpath=(~/.zfunc $fpath)
autoload -Uz compinit && compinit
Using with tmux or scripts
Since jetti separates status output (stderr) from data (stdout), it works cleanly in scripts:
# Get the path to a repo without any status noise
path=$(jetti clone owner/repo 2>/dev/null)
# List all repos as an array
repos=($(jetti list))
# Count repos per host
jetti list | cut -d/ -f1 | sort | uniq -c