Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
tmp/
164 changes: 99 additions & 65 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
# git-wt

`git-wt` manages Git worktrees using a consistent path layout.
`git-wt` manages Git worktrees from registered bare repositories.

Every managed worktree lives under a shared root, with the worktree/branch name as an intermediate directory and the repository name as the final checkout directory:
There is no required “main” worktree.
Repositories are stored as bare Git directories, and worktrees are created on demand under a shared root:

`<worktree-root>/<worktree-name>/<repo-name>`

Defaults:

- bare repos: `$XDG_DATA_HOME/git-wt/repos/<repo-name>.git` (fallback: `~/.local/share/git-wt/repos/<repo-name>.git`)
- worktrees: `$GIT_WT_WORKTREE_ROOT/<worktree-name>/<repo-name>` (fallback: `~/worktrees/<worktree-name>/<repo-name>`)

The worktree name and branch name are identical (including `/`).
The main worktree uses the name `main`.

Example:

- worktree root: `~/src/github.com/nnutter/git-wt`
- repo name: `git-wt`
- main worktree: `~/src/github.com/nnutter/git-wt/main/git-wt`
- bare repo: `~/.local/share/git-wt/repos/git-wt.git`
- branch: `nn/my-feature`
- worktree path: `~/src/github.com/nnutter/git-wt/nn/my-feature/git-wt`
- worktree path: `~/worktrees/nn/my-feature/git-wt`

Use `git-wt migrate` to move existing worktrees (including main) into this layout.
Use `git-wt migrate` inside an existing clone to register it as a bare repo and rehome its worktrees (including the former main checkout) into this layout.
When invoked through the shell wrapper (`wt migrate`), the shell also `cd`s to `$HOME` after success.

## Installation

Expand Down Expand Up @@ -45,16 +50,17 @@ The generated function:
- routes most commands to `git-wt` (`wt create`, `wt list`, `wt prune`, …)
- after a successful `wt create`, `cd`s into the new worktree unless `--no-cd`, `-r` | `--herdr`, or automatic Herdr workspace creation applies
- provides a shell-only `switch` that `cd`s into a worktree
- after a successful `wt remove`, `cd`s to the main worktree
- after a successful `wt off`, `cd`s to the collapsed worktree root
- after a successful `wt remove` or `wt migrate`, `cd`s to `$HOME`

```bash
wt switch main
wt switch feature/login
wt create feature/login # then cd into it
wt create --no-cd feature/login # create only
wt remove feature/login # then cd main
wt repo add nnutter/git-wt
wt create --repo git-wt feature/login # then cd into it
wt switch --repo git-wt feature/login
wt create --no-cd --repo git-wt other # create only
wt remove feature/login # then cd $HOME
wt list
wt list --all
wt list --repo git-wt
```

If you use [carapace](https://carapace.sh), exclude its built-in `wt` completer (worktrunk) so zsh uses the generated completion instead:
Expand All @@ -63,97 +69,125 @@ If you use [carapace](https://carapace.sh), exclude its built-in `wt` completer
export CARAPACE_EXCLUDES=wt
```

Set this **before** `source <(carapace _carapace)`. You may need `carapace --clear-cache` after changing excludes.
Set this **before** `source <(carapace _carapace)`.
You may need `carapace --clear-cache` after changing excludes.

## Commands

### `git-wt create <name>`
### Repository selection

Create a managed worktree for a branch.
Worktree commands accept `--repo <name>` to select a registered repository.

For `remove` and `prune`, if `--repo` is omitted and the current directory is inside a managed worktree of a registered repository, that repository is used automatically.
`create` keeps an explicit `--current` flag for the same purpose.

`list` auto-detects the current repository when inside a managed worktree; outside a managed worktree (or with `--all`) it lists every registered repository. Use `--repo` to force a single repository.

Otherwise an interactive filter picker is shown for commands that need a single repository.
In non-interactive environments those commands fail unless `--repo` is set (or, for `create`, `--current`), or the cwd auto-detects a managed repo.

### `git-wt repo add <url-or-path>`

Register a bare repository.

- If the branch already exists, the worktree is created from that branch.
- If the branch does not exist, it is created from the branch pointed at by `origin/HEAD`, or if that is unset from `origin/master` then `origin/main`; set it explicitly with `--upstream` | `-u`.
- When run inside [Herdr](https://herdr.dev) (`HERDR_ENV=1`), automatically create a Herdr workspace whose `--cwd` is the new worktree and whose `--label` is the repository name.
- Use `-r` | `--herdr` to create a Herdr workspace explicitly, or `-R` | `--no-herdr` to suppress automatic creation.
- Herdr workspace creation through `wt create` implies `--no-cd`.
- Herdr workspace creation requires `herdr` on `PATH` and a running Herdr server.
- Schema-less relative paths map to GitHub: `nnutter/git-wt` → `https://github.com/nnutter/git-wt`
- Full URLs, `git@host:path`, and local paths pass through unchanged
- `--name` overrides the derived repository name (default: basename of the URL)

Example:

```bash
git-wt create feature/login
git-wt create -u origin/v1.2 hotfix/1.2.1
git-wt create -r feature/login
git-wt create -R feature/login
git-wt repo add nnutter/git-wt
git-wt repo add --name my-fork git@github.com:me/git-wt.git
git-wt repo add /path/to/existing.git
```

### `git-wt list`
### `git-wt repo list`

List managed worktrees in a table.
List registered repositories.

Columns:
### `git-wt repo remove <name>`

- `Name`: `main (<branch>)` for the main worktree, otherwise the branch name
- `Status`: first line of `git status -sb`
- `Dirty`: whether the worktree has uncommitted changes

### `git-wt migrate`
Remove a registered bare repository.
Refuses if any worktrees remain.

Bring existing Git worktrees under `git-wt` management.
### `git-wt create [name]`

- Moves the main worktree into `<root>/main/<repo-name>` when it is still a plain clone at `<root>` or on the old layout at `<root>/main`.
- Renames existing non-managed branch worktrees into the managed path format.
- Does not create worktrees for local branches that do not already have one.
Create a managed worktree for a branch.

Use `--prompt` | `-p` to review the proposed migrations before applying them.
- If the name is omitted, prompts for it (interactive terminals only)
- If the branch already exists, the worktree is created from that branch
- If the branch does not exist, it is created from the branch pointed at by `origin/HEAD`, or if that is unset from `origin/master` then `origin/main`; set it explicitly with `--upstream` | `-u`
- When run inside [Herdr](https://herdr.dev) (`HERDR_ENV=1`), automatically create a Herdr workspace whose `--cwd` is the new worktree and whose `--label` is the repository name
- Use `-r` | `--herdr` to create a Herdr workspace explicitly, or `-R` | `--no-herdr` to suppress automatic creation
- Herdr workspace creation through `wt create` implies `--no-cd`
- Herdr workspace creation requires `herdr` on `PATH` and a running Herdr server

Example:

```bash
git-wt migrate
git-wt migrate --prompt
git-wt create --repo git-wt feature/login
git-wt create --current -u origin/v1.2 hotfix/1.2.1
git-wt create --repo git-wt -r feature/login
```

### `git-wt off`
### `git-wt list`

List managed worktrees in a table.

Tear down the managed worktree layout into a single checkout at the worktree root.
- Outside a managed worktree: list worktrees from every registered repository
- Inside a managed worktree: list only that repository’s worktrees
- `--all`: list every registered repository even when inside a worktree
- `--repo <name>`: list only the named repository

- Refuses if any managed worktree (including main) is dirty unless `--force` | `-f`.
- Removes every non-main managed worktree.
- Deletes a feature branch with `git branch -d` when it is fully merged; otherwise keeps the branch.
- Moves `<root>/main/<repo-name>` to `<root>` so the repository is a normal single checkout.
Columns:

When invoked through the shell wrapper (`wt off`), the shell also `cd`s to the collapsed root after success.
- `Repo`: registered repository name
- `Name`: branch / worktree name
- `Status`: first line of `git status -sb`
- `Commit`: short commit hash
- `Dirty`: whether the worktree has uncommitted changes

### `git-wt migrate`

Register the current repository as a bare repo and rehome existing worktrees.

- Creates `$XDG_DATA_HOME/git-wt/repos/<name>.git` (override name with `--name`)
- Moves every branched worktree (including the former main checkout) to `$GIT_WT_WORKTREE_ROOT/<branch>/<repo-name>` (fallback: `~/worktrees/...`)
- If the clone has no linked worktrees and HEAD is the default branch (`origin/HEAD`, else `origin/master` / `origin/main`), only the bare repo is registered (no managed worktree is created)
- Does not create worktrees for local branches that do not already have one
- Use `--prompt` | `-p` to choose which worktrees to migrate

Example:

```bash
git-wt off
git-wt off --force
cd ~/src/github.com/nnutter/git-wt
git-wt migrate
git-wt migrate --name git-wt --prompt
```

### `git-wt prune`

Remove managed worktrees that are both clean, no uncommitted changes, and merged into their upstream branch.
Remove managed worktrees that are both clean and merged into their upstream branch.

Use `--prompt` | `-p` to choose which worktrees to prune interactively.

### `git-wt remove [name]`

Remove a managed worktree and delete its branch.

When `name` is omitted, removes the managed worktree that contains the current directory.
It refuses to remove the main worktree, and refuses dirty or unmerged worktrees by default.
When `name` is omitted, removes the managed worktree that contains the current directory (auto-detects the registered repo from cwd, or use `--repo` / the repo picker).
Refuses dirty or unmerged worktrees by default.
Use `--force` | `-f` to force (destructive) removal.

When invoked through the shell wrapper (`wt remove`), the shell also switches to the main worktree after a successful removal.
When invoked through the shell wrapper (`wt remove`), the shell also `cd`s to `$HOME` after a successful removal.

Example:

```bash
git-wt remove
git-wt remove feature/login
git-wt remove --force feature/login
git-wt remove --repo git-wt feature/login
git-wt remove --repo git-wt --force feature/login
```

### `git-wt generate zsh`
Expand All @@ -166,15 +200,15 @@ Generate a zsh wrapper function and completion (see [Shell integration](#shell-i
# once: install wrapper
git-wt generate zsh

# in a repo
wt create feature/login
wt switch feature/login
# register a repo
wt repo add nnutter/git-wt

# day to day
wt create --repo git-wt feature/login
wt switch --repo git-wt feature/login
# ... work ...
wt switch main
wt prune
wt switch --repo git-wt main # if you created a main worktree
wt prune --repo git-wt
# or:
wt remove feature/login
```

For jumping between repositories under a path, you can still use something like
[git-cd](https://github.com/nnutter/dotfiles/blob/master/bin/git-cd).
8 changes: 4 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ go 1.26.5
require (
charm.land/fang/v2 v2.0.1
charm.land/lipgloss/v2 v2.0.1
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7
github.com/charmbracelet/bubbletea v1.3.6
github.com/charmbracelet/huh v1.0.0
github.com/google/uuid v1.6.0
github.com/charmbracelet/lipgloss v1.1.0
github.com/samber/lo v1.53.0
github.com/spf13/cobra v1.10.1
github.com/stretchr/testify v1.11.1
Expand All @@ -16,10 +18,7 @@ require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/catppuccin/go v0.3.0 // indirect
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect
github.com/charmbracelet/bubbletea v1.3.6 // indirect
github.com/charmbracelet/colorprofile v0.4.2 // indirect
github.com/charmbracelet/lipgloss v1.1.0 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect
github.com/charmbracelet/x/ansi v0.11.7 // indirect
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
Expand Down Expand Up @@ -48,6 +47,7 @@ require (
github.com/muesli/termenv v0.16.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sahilm/fuzzy v0.1.1 // indirect
github.com/spf13/pflag v1.0.9 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
Expand Down
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,10 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
Expand Down Expand Up @@ -92,6 +92,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM=
github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
Expand Down
Loading