Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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, and status across 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

Commands

Jetti provides the following commands:

CommandAliasDescription
cloneget, gClone a repository
listlsList cloned repositories
fetchFetch updates for all repos
updateFetch and fast-forward all repos
statusstShow repo status dashboard
rmremoveRemove a cloned repository
configcfgView or manage configuration
rootPrint the root directory
completionsGenerate 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 host
    • https://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:

  1. Inferred from URLgit@... always uses SSH, https://... always uses HTTPS
  2. --https / --ssh flag — overrides the default for this clone (mutually exclusive)
  3. 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 as clone)

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

  • bash
  • zsh
  • fish

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.toml instead 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