Visitar URL original
GitHub - sadpandajoe/repo-cli: A lightweight CLI tool for managing git worktrees with PR tracking · GitHub
Skip to content
sadpandajoePublic

About

A lightweight CLI tool for managing git worktrees with PR tracking

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

repo-cli

A lightweight CLI for managing git worktrees, with automatic PR tracking and first-class support for AI-agent worktrees.

Simplifies creating isolated branches as git worktrees, seeing their live PR state, and housekeeping the ones that have landed — across every repository you work in.

The lens model (2.0)

Earlier versions kept a ledger: repo-cli only knew about worktrees it created itself. 2.0 replaces that with a lens — it discovers every worktree across your registered clone roots, whoever made it, and shows them in one view:

  • Discovery over registration — repo list sees worktrees created by repo-cli, by Codex, by Claude, or by hand (git worktree add). Each row is tagged with its provenance (repo-cli · codex · claude · manual).
  • Automatic PR linking — each branch's live GitHub PR is resolved automatically (batched, cached, offline-tolerant). No more repo pr link for the common case.
  • Multiple roots per repo — a repo can have several clone roots (a bare clone plus a normal checkout, say); repo-cli tracks worktrees and branches across all of them.
  • Agent-aware, agent-safe — agent worktrees are surfaced but never mutated: repo-cli only touches git worktrees and local branches, never an agent's own application state, and agent rows are opt-in for any destructive action.

What's new in 2.0

  • repo list 2.0 — Source · PR · Age · Path columns, provenance tags, --source/--prs filters, --json.
  • repo root — register additional clone roots for a repo so discovery spans all of them.
  • repo env — run Codex-compatible environment files (setup/run/cleanup) from inside a worktree.
  • repo clean — retire merged-PR worktrees, prune stale records, and delete orphaned claude/* branches, all reviewed.
  • .worktreeinclude seeding — new worktrees inherit a clone's gitignored .env-type files (repo create, or backfill with repo env seed).
  • repo doctor v2 — git-version floor, root health, drift, PR-cache age, and gh auth status.

Features

  • Worktree Management — create and delete worktrees without manual git commands
  • Discovery & Provenance — one view of every worktree across roots, tagged by who made it
  • Automatic PR Tracking — live GitHub PR state per branch, cached and offline-tolerant
  • Agent Environments — run Codex-style env files; seed .worktreeinclude files into new worktrees
  • Housekeeping — repo clean safely retires landed worktrees and orphaned agent branches
  • Repo Aliasing — use short aliases instead of full URLs
  • Rich Table Display — worktrees, branches, source, PR status, and age at a glance
  • Shell Auto-complete — tab completion for repos, branches, and commands

Installation

Prerequisites

  • Python 3.11+
  • Git 2.31+ (for git rev-parse --path-format=absolute and per-worktree extensions.worktreeConfig)
  • gh CLI (optional, for PR features) — authenticated via gh auth login

Install from source

# Clone the repository
gh repo clone sadpandajoe/repo-cli
cd repo-cli

# Create virtual environment and install
uv venv
source .venv/bin/activate  # On Windows: .venv\Scripts\activate
uv pip install -e .

Verify installation

repo --help
repo --version  # Check installed version

You should see the CLI help with available commands.

Upgrading

Automatic Upgrade (Recommended)

Check for updates and upgrade automatically:

# Check if newer version is available
repo upgrade-check

# Upgrade to the latest version
repo upgrade

# Skip safety checks (use with caution)
repo upgrade --force

The upgrade command will:

  • Auto-detect your installation directory
  • Check for uncommitted changes and warn you
  • Pull latest changes from your current branch
  • Reinstall dependencies (using uv or pip)
  • Provide clear progress indicators

Manual Upgrade

If you prefer manual control:

# If installed with uv
cd /path/to/repo-cli
git pull origin main
uv sync --dev

# If installed with pip
cd /path/to/repo-cli
git pull origin main
source .venv/bin/activate  # On Windows: .venv\Scripts\activate
pip install -e ".[dev]"

After Upgrading

Verify the new version:

repo --version

If you encounter any issues, run diagnostics:

repo doctor

Usage

Getting started

# Initialize configuration
repo init

# Register a repository (clones it as a bare repo under base_dir)
repo register myrepo git@github.com:user/repo.git

# Create a worktree for a branch
repo create myrepo feature-123

# List every worktree across all roots, with source, PR state, and age
repo list

# Navigate to a worktree (prints path with cd hint)
repo activate myrepo feature-123

# Delete a worktree (refuses agent-owned and root checkouts)
repo delete myrepo feature-123

# Also retire the branch, locally and on the remote
repo delete myrepo feature-123 --delete-remote

--delete-branch and --delete-remote each read the tip they are about to delete before prompting and then delete that exact commit — locally via git update-ref -d <ref> <oid>, on the remote via --force-with-lease at the same OID. The two are checked independently, because another clone can push to the remote without your local branch moving at all. Either check failing leaves that branch alone and exits non-zero, so a partial cleanup is never reported as success. A branch that is already gone — someone used GitHub's "Delete branch" button first — is the end state you asked for, so it is reported and exits zero; that is distinct from a tip that could not be read, which leaves the branch possibly still there and does fail.

Listing (the lens)

# All worktrees across every registered root, tagged by provenance
repo list

# Filter by who created them, or by PR state
repo list --source claude        # repo-cli | codex | claude | manual | root
repo list --prs open             # open | merged | closed | draft | override | none
repo list --refresh              # bypass the 5-minute PR cache
repo list --json                 # stable machine-readable rows

A PR is only shown against a worktree whose clone root can be proven to be the alias's github.com repository. GitHub is asked "which PR has this head ref on owner/repo" — nothing in that answer is about the clone the worktree lives in, and a fork or an enterprise clone at the same owner/repo carries the same branch names. So a root whose origin names a different repo, sits on another host, resolves through an ssh-config alias, or could not be read at all shows — and a line saying why, rather than the upstream's PR. A manual repo pr link override still applies: that is your intent, not an inference from a branch name.

Roots

A repo can have more than one clone on disk (e.g. a bare clone plus a normal checkout an agent created). Register the extra ones so discovery spans them all:

repo root add myrepo ~/opt/code/myrepo    # register another clone root
repo root list myrepo                      # show a repo's roots + health
repo root remove myrepo ~/opt/code/myrepo  # stop scanning a root

root add accepts any path inside the clone — a subdirectory, or one of its linked worktrees — and registers the clone root it belongs to, because discovery enumerates per clone root. If the clone's origin names a different repo than the alias (a fork), it asks before registering, since PRs are matched by branch name and a fork clone can mis-attribute them; with no terminal to ask on, it declines. Registering it anyway is supported — its worktrees are listed and addressable like any other — but they carry no auto-detected PR, and repo clean never proposes deleting their worktrees or branches (pruning stale registrations is unaffected — that reads no PR state).

root remove refuses to remove a repo's last root — a rootless repo is invisible to the lens, and nothing reseeds it. Register a replacement first, or drop the alias with repo unregister. Removing repo-cli's own bare clone warns that repo create still writes worktrees there and will re-register it.

Environments (Codex-compatible)

Run the setup/run/cleanup env files an agent (or you) placed in the repo, from inside a worktree:

repo env list           # environments defined for this repo
repo env up             # run the setup steps
repo env run <action>   # run a named action
repo env down           # run cleanup
repo env status         # show the active environment selection
repo env seed           # backfill .worktreeinclude files into this worktree

Selection persists per-worktree via git config --worktree; repo-cli never writes codex.* state and never prints env-file contents.

Scripts receive the worktree they run in plus a source tree: the checkout that owns the worktree when that root is a normal clone, otherwise the repo's first non-bare root (repo-cli's bare roots hold no files), otherwise the worktree itself. Roots can sit on different commits, so a worktree always reads its own clone's env files and helpers. repo env seed refuses to copy anything when it cannot list the source clone's worktrees — without that list it cannot tell a file inside another worktree from one in the source.

Cleaning up

repo clean gathers, into one reviewed list, worktrees whose PR has merged, every non-root detached worktree, stale worktree records git would prune, and orphaned claude/* branches (no worktree anywhere, no open PR):

repo clean                 # review candidates interactively (agent rows opt-in)
repo clean --yes           # accept the pre-selected rows without prompting
repo clean --include-closed  # also offer worktrees whose PR was closed (not merged)
repo clean --dry-run       # show the table and remove nothing
repo clean --force         # allow removing a dirty worktree / a branch with unlanded commits

Safety: root checkouts are never candidates, agent rows default unselected, dirty worktrees are skipped without --force, as are branches whose commits sit on no remote ref and whose PR did not merge, and --dry-run mutates nothing. Detached worktrees have no branch or PR evidence that their work is finished, so they are review-only by default: they are always unselected, --yes never removes them, and interactive cleanup requires an explicit row selection. Dirty detached worktrees additionally require --force. Git-locked detached worktrees remain visible but blocked; unlock them outside repo-cli before trying again. Use repo clean --dry-run to inspect the full candidate table safely. Stale worktree records are offered per root, because git worktree prune clears the whole root at once — the row lists every record it would remove.

Nothing is deleted on a guess. A worktree or branch is only offered when this run established its PR state — literally this run: clean always refetches, never reading the 5-minute cache other commands share, because a PR can be reopened or a merge reverted inside that window and "true four minutes ago" is not evidence for deleting now. (--dry-run refetches too; a preview that reports what a cached answer would have done is worse than a slow one.) A result served from a stale cache, a PR page that was cut short, a repo whose lookup failed, a branch with a manual pr_overrides pin, and a repo whose worktree scan came back incomplete are all reported and left alone. So is every root that cannot be proven to be the alias's github.com repository — a fork, an enterprise clone, an ssh-config alias, or an origin that could not be read — because PRs are matched by branch name and an unproven root's branches cannot be attributed to those PRs. Everything skipped is printed, so "nothing to clean" never hides "couldn't tell". A merged PR is itself proof the work landed — squash and rebase merges rewrite commit SHAs, so a normally-merged branch looks unpushed to git, and only branches with no merge to cite need --force. That proof covers the commit the PR merged and nothing pushed after it: a branch that moved on since it landed still needs --force, and the deletion compares against the tip shown in the table, so commits that arrive while the prompt is open are never destroyed by a decision taken before they existed.

PRs

PR state is linked automatically, so you rarely touch these:

repo pr open myrepo feature-123   # open the branch's PR in a browser
repo pr link 4567 myrepo feature-123   # deprecated: pin a PR override for a misdetection

repo pr open applies the same identity proof as repo list: from a worktree whose clone root cannot be shown to be the alias's github.com repository it refuses to guess, and points you at repo pr link — opening a URL is an assertion about this clone, and a shared branch name is not evidence for it.

Diagnostics & upgrades

repo --version        # 2.0.0
repo doctor           # git version, root health, drift, PR-cache age, gh auth
repo upgrade-check    # check for a newer version
repo upgrade          # pull + reinstall (see Upgrading below)

# Unregister a repository alias (errors if worktrees exist; --force overrides)
repo unregister myrepo
repo unregister myrepo --force --remove-data --yes

Shell Integration

For quick navigation, use the --print flag with shell command substitution:

# Jump directly to a worktree
cd $(repo activate myrepo feature-123 --print)

# Or create an alias in your .bashrc/.zshrc
alias repoa='cd $(repo activate "$@" --print)'
repoa myrepo feature-123

Naming Rules

Branch Names

Branch names follow Git's official rules and support hierarchical grouping with slashes:

Allowed:

  • Alphanumeric characters, dots, hyphens, underscores
  • Forward slashes for hierarchical grouping (e.g., feature/JIRA-123, bugfix/auth)
  • @ symbol (except the sequence @{)

Examples of valid branch names:

repo create myrepo main
repo create myrepo feature/JIRA-123
repo create myrepo bugfix/foo@bar
repo create myrepo user/joe/feature
repo create myrepo release/v1.2.3

Prohibited:

  • Cannot start or end with /
  • Cannot contain consecutive slashes //
  • Cannot contain .. or @{
  • Cannot end with .
  • Slash-separated components cannot start with . or end with .lock
  • Cannot contain spaces, ~, ^, :, ?, *, [, \

Repository Aliases

Repository aliases have stricter rules to prevent path traversal attacks:

Allowed:

  • Alphanumeric characters, dots, hyphens, underscores only
  • No slashes (path traversal protection)
  • Cannot contain :: (internal delimiter)

Examples:

repo register myrepo git@github.com:user/repo.git          # Valid
repo register api-core git@github.com:company/api-core.git # Valid
repo register my.repo git@github.com:user/my.repo.git      # Valid

Invalid examples:

repo register my/repo ...     # Error: slashes not allowed
repo register ../prod ...     # Error: path traversal blocked
repo register repo::name ...  # Error: :: is internal delimiter

Unregistering a Repository

Use repo unregister to remove a repo alias from your config. It is the inverse of repo register.

Unregistering also drops any PR pinned under that alias. Overrides are keyed alias::branch, so leaving them would let an old pin apply to whatever repository the alias is reused for next — and a pin overrides auto-detection everywhere. Force-registering an alias to a different URL drops them for the same reason; re-registering the same URL keeps them.

# Unregister an alias with no active worktrees (interactive confirmation)
repo unregister myrepo

# Skip the confirmation prompt
repo unregister myrepo --yes

By default, unregister refuses to remove an alias that still has worktrees — including worktrees the lens discovers under the repo's roots but has no config entry for (an agent's, say), which are listed with their provenance. It also refuses when one of the repo's roots could not be scanned, because an unreadable root cannot be shown to be empty. Delete the worktrees first with repo delete, or pass --force to also remove their config entries:

# Drop config entries for any remaining worktrees too
repo unregister myrepo --force --yes

--force only updates the config. The bare repo and worktree directories on disk are left intact unless you also pass --remove-data:

# Also rmtree the on-disk bare repo and worktree directories
repo unregister myrepo --force --remove-data --yes

--remove-data only deletes paths it positively recognizes as repo-cli-managed:

  • nested layout (v0.2.0+): <base_dir>/<alias>/.bare
  • legacy flat layout: <base_dir>/<alias>.git and <base_dir>/<alias>-<branch>

A directory at <base_dir>/<alias> without a .bare child is treated as unrelated user data and left untouched, with a warning.

Immediately before deleting anything, --remove-data re-enumerates every clone it is about to remove — straight from git, not from the config, since the alias and its roots are unregistered by that point. A worktree that appeared while the confirmation was open, or a clone that cannot be re-enumerated, aborts the deletion: the alias stays unregistered but the data is left on disk for you to inspect. --force skips this check, as it does the others.

In an interactive shell, repo unregister will also offer to delete on-disk data if you didn't pass --remove-data — answer y at the follow-up prompt. With --yes (or in a non-interactive context), data is left on disk if --remove-data wasn't explicitly passed; run repo doctor to surface orphaned directories or rm -rf them manually.

Finding Orphaned Data

repo doctor scans <base_dir> for repo directories whose alias isn't registered (e.g. left behind after repo unregister without --remove-data, or moved manually) and lists them so you can adopt them with repo register or remove them with rm -rf.

Upgrading from 1.x

2.0 migrates your ~/.repo-cli/config.yaml automatically and transparently on first run — migrations are presence-based, idempotent, and write a timestamped backup before any change. You don't run anything by hand. Two 2.0 migrations apply:

  • M1 — seeds repos.*.roots from your existing bare layout so discovery has roots to scan.
  • M2 — re-keys worktree annotations by their resolved path and moves any legacy pr: field into a branch-scoped pr_overrides section. The config version stamps to 2.0.0.

Everything keeps working during the transition; a dead legacy pr: key is harmless and is cleaned up by M2. If anything looks off after upgrading, repo doctor reports config health, root health, and drift.

Development

Setup

# Clone the repository
git clone https://github.com/sadpandajoe/repo-cli.git
cd repo-cli

# Option 1: Using uv (recommended)
uv sync --dev

# Option 2: Using pip
python -m venv .venv
source .venv/bin/activate  # On Windows: .venv\Scripts\activate
pip install -e ".[dev]"

Running Tests

Note: Complete the setup steps above before running tests. The package must be installed in development mode for tests to import the repo_cli module.

# With uv (recommended - handles installation automatically)
uv run pytest tests/ -v

# With pip (requires editable install: pip install -e ".[dev]")
pytest tests/ -v

# With coverage
uv run pytest tests/ -v --cov=repo_cli --cov-report=term-missing

Code Quality

# Install pre-commit hooks (recommended)
pre-commit install

# Run linting
uv run ruff check src/ tests/

# Run formatting
uv run ruff format src/ tests/

# Run pre-commit on all files
pre-commit run --all-files

Pre-commit Hooks

The project uses pre-commit to automatically check code quality before commits:

  • ruff - Linting with auto-fix
  • ruff-format - Code formatting
  • trailing-whitespace - Remove trailing whitespace
  • end-of-file-fixer - Ensure files end with newline
  • check-yaml - Validate YAML files
  • check-added-large-files - Prevent large files

After installing (pre-commit install), these checks run automatically on every commit.

Architecture

Nested bare-repo layout (v0.2.0+):

  • Base directory: ~/code/
  • Bare repos: ~/code/{repo}/.bare/
  • Worktrees: ~/code/{repo}/{branch}/ (branch names percent-encoded)
  • Additional roots: any other clone you register with repo root add
  • Config: ~/.repo-cli/config.yaml
  • PR cache: ~/.repo-cli/pr-cache.json (disposable; regenerated if corrupt)

Discovery lists worktrees across all registered roots. PR state is fetched via the gh CLI's GitHub GraphQL API (one call per repo, chunked, concurrent, 5-minute cache with offline stale-serve); no tokens are stored by repo-cli.

License

MIT

About

A lightweight CLI tool for managing git worktrees with PR tracking

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages