git-worktrees 0.4.2__tar.gz → 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/AGENTS.md +102 -27
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/CHANGELOG.md +43 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/PKG-INFO +106 -43
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/README.md +105 -42
- git_worktrees-0.5.0/completions/gw.fish +39 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/completions/gwa.fish +1 -0
- git_worktrees-0.5.0/completions/gwbd.fish +14 -0
- git_worktrees-0.5.0/completions/gwbs.fish +11 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/completions/gwnb.fish +1 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/completions/gwp.fish +1 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/completions/gwr.fish +1 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/completions/gwrot.fish +1 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/completions/gws.fish +1 -0
- git_worktrees-0.5.0/completions/worktrees.fish +1 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/docs/BENCHMARK.md +52 -35
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/docs/DEVELOPMENT.md +1 -1
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/docs/REFERENCE.md +33 -21
- git_worktrees-0.5.0/docs/commands/branch-delete.md +77 -0
- git_worktrees-0.5.0/docs/commands/branch-status.md +110 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/functions/gwa.fish +5 -4
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/functions/gwl.fish +5 -4
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/functions/gwm.fish +5 -4
- git_worktrees-0.5.0/functions/gwr!.fish +6 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/functions/gwr.fish +5 -4
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/pyproject.toml +3 -1
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/scripts/benchmark.py +30 -5
- git_worktrees-0.5.0/src/worktrees/branch_cli.py +212 -0
- git_worktrees-0.5.0/src/worktrees/branch_forge.py +156 -0
- git_worktrees-0.5.0/src/worktrees/branches.py +150 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/src/worktrees/cli.py +278 -79
- git_worktrees-0.5.0/src/worktrees/forge.py +281 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/src/worktrees/git.py +12 -2
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/src/worktrees/layout.py +14 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/src/worktrees/merged.py +30 -2
- git_worktrees-0.5.0/src/worktrees/pick.py +242 -0
- git_worktrees-0.5.0/src/worktrees/progress.py +73 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/src/worktrees/prune.py +1 -1
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/src/worktrees/render.py +2 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/src/worktrees/repo.py +16 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/src/worktrees/verdicts.py +50 -50
- git_worktrees-0.5.0/tests/test_branch_forge.py +171 -0
- git_worktrees-0.5.0/tests/test_branches.py +328 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/tests/test_cli.py +132 -13
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/tests/test_completions.py +61 -2
- git_worktrees-0.5.0/tests/test_forge.py +643 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/tests/test_shims.py +72 -16
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/tests/test_worktree.py +88 -7
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/uv.lock +1 -1
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/zsh/plugins/worktrees/completions/_gw +20 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/zsh/plugins/worktrees/completions/_gwa +1 -0
- git_worktrees-0.5.0/zsh/plugins/worktrees/completions/_gwbd +22 -0
- git_worktrees-0.5.0/zsh/plugins/worktrees/completions/_gwbs +19 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/zsh/plugins/worktrees/completions/_gwnb +1 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/zsh/plugins/worktrees/completions/_gwp +1 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/zsh/plugins/worktrees/completions/_gwr +2 -1
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/zsh/plugins/worktrees/completions/_gwrot +1 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/zsh/plugins/worktrees/completions/_gws +1 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/zsh/plugins/worktrees/functions/gwa +5 -5
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/zsh/plugins/worktrees/functions/gwl +5 -5
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/zsh/plugins/worktrees/functions/gwm +5 -5
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/zsh/plugins/worktrees/functions/gwr +5 -5
- git_worktrees-0.5.0/zsh/plugins/worktrees/functions/gwr! +4 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/zsh/plugins/worktrees/worktrees.plugin.zsh +3 -2
- git_worktrees-0.4.2/completions/gw.fish +0 -22
- git_worktrees-0.4.2/src/worktrees/forge.py +0 -136
- git_worktrees-0.4.2/src/worktrees/pick.py +0 -134
- git_worktrees-0.4.2/tests/test_forge.py +0 -302
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/.claude/settings.json +0 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/.github/workflows/publish.yml +0 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/.github/workflows/testpypi.yml +0 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/.github/workflows/tests.yml +0 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/.gitignore +0 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/.pre-commit-config.yaml +0 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/.python-version +0 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/.releasetools.yaml +0 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/LICENSE +0 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/completions/gwh.fish +0 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/completions/gwl.fish +0 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/completions/gwm.fish +0 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/conf.d/worktrees.fish +0 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/hatch_build.py +0 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/renovate.json +0 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/scripts/README.md +0 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/scripts/verify-publish.bash +0 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/src/worktrees/__init__.py +0 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/src/worktrees/errors.py +0 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/src/worktrees/new_branch.py +0 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/src/worktrees/rotate.py +0 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/src/worktrees/worktree.py +0 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/tests/conftest.py +0 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/tests/test_benchmark.py +0 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/tests/test_fixture.py +0 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/tests/test_gates.py +0 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/tests/test_new_branch.py +0 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/tests/test_render.py +0 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/tests/test_rotate.py +0 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/tests/test_spec.py +0 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/tests/test_version.py +0 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/zsh/plugins/worktrees/completions/_gwh +0 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/zsh/plugins/worktrees/completions/_gwl +0 -0
- {git_worktrees-0.4.2 → git_worktrees-0.5.0}/zsh/plugins/worktrees/completions/_gwm +0 -0
|
@@ -29,16 +29,18 @@ prompt and from a script unless the function is a pure adapter.
|
|
|
29
29
|
|
|
30
30
|
The rule: **a function exists only for a command that must change the caller's
|
|
31
31
|
directory, it carries the same name as the binary, and its body does nothing but
|
|
32
|
-
call `command <name>` and `cd` to what that prints, when that is a directory
|
|
32
|
+
call `command <name>` and `cd` to what that prints, when that is a directory,
|
|
33
|
+
and print it otherwise.**
|
|
33
34
|
|
|
34
35
|
The last clause is not a hedge. `--json` and `--list` print what was asked for
|
|
35
|
-
on the same stream a destination arrives on
|
|
36
|
-
apart tries to `cd` into a JSON object
|
|
36
|
+
on the same stream a destination arrives on. A shim that cannot tell them
|
|
37
|
+
apart tries to `cd` into a JSON object, and one that only refuses to prints
|
|
38
|
+
nothing at all. `fish -c 'gwl --json'` reaches the
|
|
37
39
|
function too, so this is the agent path as much as the human one.
|
|
38
40
|
|
|
39
41
|
| | changes directory | ships as |
|
|
40
42
|
| --- | --- | --- |
|
|
41
|
-
| `gws`, `gwp`, `gwnb`, `gwrot`, `gwh` | no | a console script, and nothing else |
|
|
43
|
+
| `gws`, `gwp`, `gwbs`, `gwbd`, `gwnb`, `gwrot`, `gwh` | no | a console script, and nothing else |
|
|
42
44
|
| a command that lands you somewhere | yes | a console script, plus a function of the same name |
|
|
43
45
|
|
|
44
46
|
A binary cannot `cd` its caller. That is the only thing shell code is here for,
|
|
@@ -60,12 +62,19 @@ function gwa --wraps gwa
|
|
|
60
62
|
set -l code $pipestatus[1]
|
|
61
63
|
test $code -eq 0; or return $code
|
|
62
64
|
test -n "$dest"; or return 0
|
|
65
|
+
test -d "$dest"; or begin; printf '%s\n' $dest; return 0; end
|
|
63
66
|
cd -- $dest
|
|
64
67
|
end
|
|
65
68
|
```
|
|
66
69
|
|
|
67
70
|
`command` is what stops the function calling itself, in all three shells.
|
|
68
71
|
|
|
72
|
+
One other kind of function exists: a shortcut, a banged name that calls a shim
|
|
73
|
+
with its flags fixed and does nothing else. `gwr!` is `gwr --delete-ignored`.
|
|
74
|
+
It calls the `gwr` function rather than `command gwr`, so the caller still
|
|
75
|
+
lands where the binary says, and it has no console script of its own.
|
|
76
|
+
`SHORTCUTS` in `tests/test_shims.py` lists them and holds each to one line.
|
|
77
|
+
|
|
69
78
|
## Two commands are alpha
|
|
70
79
|
|
|
71
80
|
`gwnb` and `gwrot` start branches, not worktrees. `origin new-branch` and
|
|
@@ -120,17 +129,34 @@ external picker.
|
|
|
120
129
|
|
|
121
130
|
The set is small. Across the repositories this was written for, the largest
|
|
122
131
|
number of linked worktrees in one is four. A fuzzy finder is the wrong
|
|
123
|
-
instrument at that size, and a
|
|
132
|
+
instrument at that size, and a list moved through with the arrow keys reads
|
|
133
|
+
faster:
|
|
124
134
|
|
|
125
135
|
```
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
136
|
+
# WORKTREE BRANCH PATH
|
|
137
|
+
> 1 fix-parser fix-parser /home/you/git/.worktrees/fix-parser/repo
|
|
138
|
+
2 add-tests add-tests /home/you/git/.worktrees/add-tests/repo
|
|
139
|
+
which? [up/down and enter, 1-2, or esc to cancel]
|
|
129
140
|
```
|
|
130
141
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
142
|
+
Up and down move the highlight and wrap, enter takes it, a digit takes that
|
|
143
|
+
row outright, and esc or ctrl-d cancels. The terminal goes into cbreak mode
|
|
144
|
+
through the stdlib's `termios`, so ctrl-c still raises `KeyboardInterrupt`.
|
|
145
|
+
Both switches use `TCSANOW`: `TCSAFLUSH` discards keys typed before the list
|
|
146
|
+
appears, and `TCSADRAIN` blocks on an echo queue nothing reads, which is how
|
|
147
|
+
the pty tests hang.
|
|
148
|
+
|
|
149
|
+
The list is redrawn in place when stderr is a terminal that can move its
|
|
150
|
+
cursor. On `TERM=dumb`, or a stderr that is not a terminal, it is printed
|
|
151
|
+
once and each move prints the row it lands on, so nothing writes a cursor
|
|
152
|
+
code where it would show up as text.
|
|
153
|
+
|
|
154
|
+
A query that is exactly one worktree's branch or path is that worktree
|
|
155
|
+
alone. `gwr one` beside `one-more` is not a question, and neither is
|
|
156
|
+
`gwr "$(gwl two)"`.
|
|
157
|
+
|
|
158
|
+
Dropping `fzf` is less code, not more. It removes the spawn and the fallback
|
|
159
|
+
branch a machine without `fzf` would otherwise need.
|
|
134
160
|
|
|
135
161
|
Matching is substring first, then subsequence, which is the one idea worth
|
|
136
162
|
taking from `fzf`: the query's letters appearing in order, ranked by how
|
|
@@ -156,21 +182,27 @@ Two more about what is in the list at all:
|
|
|
156
182
|
the one a finished branch leaves you in, so leaving it out leaves out the
|
|
157
183
|
only answer that is always right. `main` finds it whatever branch it stands
|
|
158
184
|
on, because its label carries the word, once.
|
|
159
|
-
-
|
|
160
|
-
`*` rather than a number
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
185
|
+
- In `gwl`, a linked worktree you are standing in is shown and never
|
|
186
|
+
offered. It carries a `*` rather than a number and the highlight skips it,
|
|
187
|
+
because picking it is the one answer that takes you nowhere. Leaving it
|
|
188
|
+
out of the list entirely is worse: the list then shows fewer rows than
|
|
189
|
+
`git worktree list` does, and reads as though something went missing
|
|
190
|
+
rather than as where you already are. The main checkout is offered even
|
|
191
|
+
from inside it, with a `*` after its name, since from a subdirectory it is
|
|
192
|
+
the way back to the top.
|
|
193
|
+
- In `gwr`, the main checkout is shown dimmed and unnumbered, because `git
|
|
194
|
+
worktree remove` refuses it whatever flag it is given, and the highlight
|
|
195
|
+
starts on the worktree you are standing in. `gws` and `gwp` keep the main
|
|
196
|
+
checkout in their lists for the same reason, and never propose it.
|
|
167
197
|
|
|
168
198
|
`--json` and `--list` answer the same question without any of this, and an
|
|
169
199
|
agent uses those.
|
|
170
200
|
|
|
171
201
|
## One row per command, under every name it answers to
|
|
172
202
|
|
|
173
|
-
`_COMMANDS` in `cli.py`
|
|
203
|
+
`_COMMANDS` in `cli.py` holds the top-level commands; `_BRANCH_COMMANDS`
|
|
204
|
+
holds `branch status` and `branch delete`. `command_rows()` lists the
|
|
205
|
+
executable commands for help and completion checks. A row carries the function, the flags,
|
|
174
206
|
the one-line description, the console script, the argument shape, the
|
|
175
207
|
shorthands and whether the command changes the caller's directory. `gws`,
|
|
176
208
|
`gw status`, `gw st` and `gw s` reach the same function because that row says
|
|
@@ -203,9 +235,40 @@ stdout carries the path `cd $(gwa x)` reads and the JSON `jq` parses, so a
|
|
|
203
235
|
redirect, a pipe, a non-empty `NO_COLOR`, or `TERM=dumb` mean the bytes go out
|
|
204
236
|
as they would have without the module. `NO_COLOR=` is not a request to turn it
|
|
205
237
|
off: that is the no-color.org rule, and it is why `supported` tests the value
|
|
206
|
-
and not the key. `--json`
|
|
207
|
-
|
|
208
|
-
|
|
238
|
+
and not the key. `--json` and `gwl --list` are never painted at all, even
|
|
239
|
+
on a terminal: both are what a script reads. The colour is applied at the
|
|
240
|
+
call site that formats a table, and neither path passes a code to one.
|
|
241
|
+
|
|
242
|
+
`progress.py` draws a spinner on stderr and one line saying what is being
|
|
243
|
+
checked, while `gws`, `gwp` and `gwr` fetch and judge. It runs only when
|
|
244
|
+
stderr is a terminal that can clear a line, and never under `-q` or `-v`,
|
|
245
|
+
since the verbose log writes to the line it redraws. Anything else printed on
|
|
246
|
+
stderr while it spins goes through `progress.line`, which clears the spinner
|
|
247
|
+
first.
|
|
248
|
+
|
|
249
|
+
## A recent fetch, and a forge answer that cannot change, are reused
|
|
250
|
+
|
|
251
|
+
The README's "What is reused, and for how long" table lists every case and
|
|
252
|
+
its lifetime. It is the one copy: the constants are `cli._FRESH`,
|
|
253
|
+
`forge._IDLE_FOR` and `forge._PUSHED_FOR`, and a change to one changes that
|
|
254
|
+
table in the same commit.
|
|
255
|
+
|
|
256
|
+
The fetch is most of what an assessment costs: about 1.1 s of a 1.9 s `gws`
|
|
257
|
+
here, 750 ms of it the SSH handshake. `_should_fetch` decides for all eight
|
|
258
|
+
commands that fetch. FETCH_HEAD is written per worktree, so
|
|
259
|
+
`repo.fetched_at` reads every copy.
|
|
260
|
+
|
|
261
|
+
An open request is reused only when it cannot merge until its author pushes;
|
|
262
|
+
`forge._idle` holds that rule. Anything that could merge it on its own, or
|
|
263
|
+
soon, means asking every time. No request on a pushed branch lasts a minute
|
|
264
|
+
because a push is how a request gets opened, and from here it is usually
|
|
265
|
+
the next step. A forge that fails to answer is never kept: `_ask` returns a
|
|
266
|
+
`NONE` request for an empty answer and None for no answer, and only the
|
|
267
|
+
first is cached.
|
|
268
|
+
|
|
269
|
+
The per-worktree checks run on eight threads. Each is independent, and the
|
|
270
|
+
time goes waiting on git and on `gh`, one round trip per branch git cannot
|
|
271
|
+
settle.
|
|
209
272
|
|
|
210
273
|
## Every git command is one spec
|
|
211
274
|
|
|
@@ -289,6 +352,18 @@ one: a test reads the verbose log and asserts it.
|
|
|
289
352
|
|
|
290
353
|
## Content cannot settle a stacked branch
|
|
291
354
|
|
|
355
|
+
Branch assessment is documented in `docs/commands/branch-status.md`, and
|
|
356
|
+
deletion in `docs/commands/branch-delete.md`. `gwbs` assesses local refs,
|
|
357
|
+
including branches without worktrees. `gwbd` rechecks its verdicts after
|
|
358
|
+
confirmation and deletes refs with the full expected SHA and `--no-deref`.
|
|
359
|
+
|
|
360
|
+
The branch content check compares every path changed from the merge base to
|
|
361
|
+
the tip with the current head tree, including modes and deletions. It can
|
|
362
|
+
prove a re-imported change even when the head branch has unrelated edits.
|
|
363
|
+
Forge evidence must match the source tip, source repository, and target
|
|
364
|
+
branch, with a merge commit contained in the head branch. A closed unmerged
|
|
365
|
+
request is never proof. These branch checks do not change worktree verdicts.
|
|
366
|
+
|
|
292
367
|
`merged_reason` answers by content: an ancestor, or a patch already upstream.
|
|
293
368
|
That works for a branch squashed whole into one commit and fails for one
|
|
294
369
|
merged as part of a stack, where the changes arrive across several squashes
|
|
@@ -300,10 +375,10 @@ regions. `git merge-tree` reports a conflict, and a diff cannot tell that
|
|
|
300
375
|
from real work left over. There is no content probe that answers this.
|
|
301
376
|
|
|
302
377
|
So the forge answers it, asked last because it costs a round trip and only
|
|
303
|
-
where git already failed. A merged request
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
`--no-forge` keeps a run offline.
|
|
378
|
+
where git already failed. A merged request with a configured upstream that
|
|
379
|
+
Git reports as `[gone]` permits pruning, subject to the worktree protections.
|
|
380
|
+
When the upstream exists, `unpushed_count` checks for local commits it lacks.
|
|
381
|
+
No upstream configured stays `unknown`. `--no-forge` keeps a run offline.
|
|
307
382
|
|
|
308
383
|
## Three verdicts, and the third is not a softer second
|
|
309
384
|
|
|
@@ -17,6 +17,49 @@ not in here, whatever it cost to build.
|
|
|
17
17
|
A release closes with a `### Choices` section when a decision in it is worth
|
|
18
18
|
the reader's time: what was chosen, and what the alternative failed to do.
|
|
19
19
|
|
|
20
|
+
## 0.5.0 - 2026-09-29
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
Local branches can be assessed with `worktrees branch status` (`gwbs`)
|
|
25
|
+
and deleted with `worktrees branch delete` (`gwbd`). Deletion accepts
|
|
26
|
+
exact names, offers a selector when no names are supplied, and supports
|
|
27
|
+
`--all` and `--dry-run`. A branch qualifies through Git history, matching
|
|
28
|
+
changed paths, or a merged GitHub PR or GitLab MR tied to its exact tip.
|
|
29
|
+
The command protects checked-out branches, rechecks refs before deletion,
|
|
30
|
+
and prints the command to restore each branch.
|
|
31
|
+
|
|
32
|
+
`gwr!` runs `gwr --delete-ignored` in fish and zsh, permitting removal of
|
|
33
|
+
ignored files while retaining the command's checks and directory change.
|
|
34
|
+
Update the fish or zsh plugin to install this shortcut.
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
`gwl` and `gwr` pick with the arrow keys from a table of worktree, branch and
|
|
39
|
+
path: enter takes the highlighted row, a digit takes that row, and esc
|
|
40
|
+
cancels. `gwl` offers the main checkout even from inside it. `gwr` shows
|
|
41
|
+
the main checkout dimmed, starts on the worktree you stand in, and at a
|
|
42
|
+
terminal asks for confirmation when a branch is unfinished (keeping the
|
|
43
|
+
branch) or holds ignored files. An exact branch or path selects that
|
|
44
|
+
worktree alone.
|
|
45
|
+
|
|
46
|
+
`gwl --list` prints the same columns and is never coloured. `gws` and `gwp`
|
|
47
|
+
print BRANCH, PATH, VERDICT, WHY. The `gwa`, `gwl`, `gwm` and `gwr` shell
|
|
48
|
+
functions print `--list` and `--json` output.
|
|
49
|
+
|
|
50
|
+
`gws`, `gwp` and `gwr` show a spinner naming the check in progress and judge
|
|
51
|
+
worktrees in parallel. Every command that fetches reuses a fetch under 10
|
|
52
|
+
minutes old, and forge answers that cannot change yet are kept; the README
|
|
53
|
+
lists each case. `--force-refresh` fetches and asks again.
|
|
54
|
+
|
|
55
|
+
## 0.4.3 - 2026-09-22
|
|
56
|
+
|
|
57
|
+
### Fixed
|
|
58
|
+
|
|
59
|
+
`gwp` can prune a branch whose pull request is merged and whose configured
|
|
60
|
+
upstream is gone. Branches with no upstream configured remain unknown.
|
|
61
|
+
Forge-confirmed branches with ignored files require `--delete-ignored`.
|
|
62
|
+
|
|
20
63
|
## 0.4.2 - 2026-09-22
|
|
21
64
|
|
|
22
65
|
CLI behavior is unchanged from 0.4.1.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: git-worktrees
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Git worktree commands that refuse to lose work.
|
|
5
5
|
Project-URL: Bug reports, https://github.com/MihaiBojin/worktrees/issues/new
|
|
6
6
|
Project-URL: Source, https://github.com/MihaiBojin/worktrees
|
|
@@ -31,18 +31,21 @@ Description-Content-Type: text/markdown
|
|
|
31
31
|
|
|
32
32
|
Git worktree commands that refuse to lose work.
|
|
33
33
|
|
|
34
|
-
`gws`, `gwp`, `gwnb`, `gwrot
|
|
35
|
-
`gwl`, `gwm
|
|
36
|
-
|
|
34
|
+
`gws`, `gwp`, `gwbs`, `gwbd`, `gwnb`, `gwrot`, and `gwh` run as console
|
|
35
|
+
scripts. `gwa`, `gwl`, `gwm`, and `gwr` change the caller's directory and need
|
|
36
|
+
a shell function of the same name.
|
|
37
37
|
|
|
38
38
|
| | |
|
|
39
39
|
| --- | --- |
|
|
40
40
|
| `gws` | say which worktrees are finished, and why. Removes nothing, and has no flag that could |
|
|
41
41
|
| `gwp` | remove the ones `gws` marks removable, having asked first |
|
|
42
|
+
| `gwbs [NAME ...]` | assess local branches, including those without worktrees |
|
|
43
|
+
| `gwbd [NAME ...]` | delete branches proved finished, with confirmation and undo commands |
|
|
42
44
|
| `gwa NAME [BASE]` | create a worktree for `NAME` and land you in it |
|
|
43
45
|
| `gwl [QUERY]` | pick one of this repository's worktrees and land you in it |
|
|
44
46
|
| `gwm NEW` | rename this worktree's branch and move the checkout to match |
|
|
45
47
|
| `gwr [QUERY]` | remove a worktree whose branch is finished, and the branch |
|
|
48
|
+
| `gwr! [QUERY]` | `gwr --delete-ignored`, a shell function in fish and zsh |
|
|
46
49
|
| `gwnb NAME` | alpha. Fetch, then branch `NAME` off the head branch and check it out |
|
|
47
50
|
| `gwrot` | alpha. Start the next branch after this one, or catch the head branch up |
|
|
48
51
|
| `gwh` | print that table, with every name each command answers to |
|
|
@@ -54,6 +57,8 @@ function, because a binary cannot change its caller's directory.
|
|
|
54
57
|
| --- | --- |
|
|
55
58
|
| `gws` | `gw status`, `st`, `s` |
|
|
56
59
|
| `gwp` | `gw prune`, `p` |
|
|
60
|
+
| `gwbs` | `gw branch status` |
|
|
61
|
+
| `gwbd` | `gw branch delete` |
|
|
57
62
|
| `gwa` | `gw add`, `a` |
|
|
58
63
|
| `gwl` | `gw list`, `ls`, `l` |
|
|
59
64
|
| `gwm` | `gw move`, `mv`, `m` |
|
|
@@ -64,6 +69,11 @@ function, because a binary cannot change its caller's directory.
|
|
|
64
69
|
|
|
65
70
|
`worktrees` is the same program under its long name.
|
|
66
71
|
|
|
72
|
+
The command reference covers [branch status](docs/commands/branch-status.md)
|
|
73
|
+
and [branch delete](docs/commands/branch-delete.md), including content proofs,
|
|
74
|
+
GitHub and GitLab merge evidence, selection, and recovery. `gwbd` with no
|
|
75
|
+
arguments opens a selector; `gwbd --all --dry-run` previews every branch.
|
|
76
|
+
|
|
67
77
|
Ctrl-C interrupts a running command with exit code 130 and no traceback.
|
|
68
78
|
At a selection or removal prompt, it cancels without choosing or removing
|
|
69
79
|
a worktree.
|
|
@@ -121,7 +131,7 @@ Python 3.11 or newer, and no dependencies. The standard library answers every
|
|
|
121
131
|
question this asks, so an invocation pays for the interpreter and nothing
|
|
122
132
|
else.
|
|
123
133
|
|
|
124
|
-
That
|
|
134
|
+
That installs `gws`, `gwp`, `gwbs`, `gwbd`, `gwnb`, `gwrot`, and `gwh`. The
|
|
125
135
|
four that land you somewhere need a shell function too, and every command has
|
|
126
136
|
a completion, both of which arrive through a plugin manager.
|
|
127
137
|
|
|
@@ -132,7 +142,7 @@ fisher install MihaiBojin/worktrees
|
|
|
132
142
|
```
|
|
133
143
|
|
|
134
144
|
Fisher copies `functions/` and `completions/` from the repository root, so the
|
|
135
|
-
|
|
145
|
+
five functions and ten completions land together. Nothing has to be sourced.
|
|
136
146
|
|
|
137
147
|
### zsh
|
|
138
148
|
|
|
@@ -143,7 +153,7 @@ MihaiBojin/worktrees path:zsh/plugins/worktrees # in zsh_plugins.txt
|
|
|
143
153
|
```
|
|
144
154
|
|
|
145
155
|
The plugin puts its own `functions/` and `completions/` on `fpath` and
|
|
146
|
-
autoloads the
|
|
156
|
+
autoloads the five. Completion needs `compinit`, which most zsh setups already
|
|
147
157
|
run; without one, add it after the plugin loads:
|
|
148
158
|
|
|
149
159
|
```zsh
|
|
@@ -177,12 +187,12 @@ separate.
|
|
|
177
187
|
|
|
178
188
|
```console
|
|
179
189
|
$ gws
|
|
180
|
-
VERDICT
|
|
181
|
-
keep
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
190
|
+
BRANCH PATH VERDICT WHY
|
|
191
|
+
main /home/you/git/repo keep it is the main checkout, and never removable
|
|
192
|
+
dirty-work /home/you/git/.worktrees/dirty-work/repo keep it has uncommitted changes
|
|
193
|
+
holds-secrets /home/you/git/.worktrees/holds-secrets/repo keep merged, but holds 2 ignored path(s); pass --delete-ignored
|
|
194
|
+
never-pushed /home/you/git/.worktrees/never-pushed/repo unknown not merged into origin/main, and no upstream says whether its commits were pushed
|
|
195
|
+
squash-merged /home/you/git/.worktrees/squash-merged/repo remove squash-merged
|
|
186
196
|
|
|
187
197
|
1 removable, 3 kept, 1 unclear
|
|
188
198
|
2 worktree(s) under /home/you/git/.worktrees belong to another repository
|
|
@@ -223,10 +233,12 @@ Where git cannot tell, the forge is asked. A branch merged as part of a
|
|
|
223
233
|
stack is that case: its changes reach the head branch across several
|
|
224
234
|
squashes, so the intermediate state it holds differs from the final one in
|
|
225
235
|
the same regions, and a diff cannot separate a stale branch from one with
|
|
226
|
-
work left. A merged pull request
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
236
|
+
work left. A merged pull request with a configured upstream that is gone
|
|
237
|
+
makes the branch eligible for pruning. When the upstream still exists,
|
|
238
|
+
unpushed commits keep the branch. A branch with no upstream configured
|
|
239
|
+
stays `unknown`. Worktree protections still apply, including the check for
|
|
240
|
+
ignored files. `--no-forge` decides from git alone, and so does a run whose
|
|
241
|
+
fetch just failed.
|
|
230
242
|
|
|
231
243
|
The question goes to the repository the command is standing in and nowhere
|
|
232
244
|
else. `GH_REPO` and `GITLAB_REPO` beat that directory, so both are dropped
|
|
@@ -286,10 +298,10 @@ away:
|
|
|
286
298
|
|
|
287
299
|
```console
|
|
288
300
|
$ gwp
|
|
289
|
-
VERDICT
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
301
|
+
BRANCH PATH VERDICT WHY
|
|
302
|
+
dirty-work /home/you/git/.worktrees/dirty-work/repo keep it has uncommitted changes
|
|
303
|
+
holds-secrets /home/you/git/.worktrees/holds-secrets/repo keep squash-merged, but holds 2 ignored path(s); pass --delete-ignored
|
|
304
|
+
never-pushed /home/you/git/.worktrees/never-pushed/repo unknown not merged into main, and no upstream says whether its commits were pushed
|
|
293
305
|
|
|
294
306
|
nothing to remove; 0 removable, 2 kept, 1 unclear
|
|
295
307
|
```
|
|
@@ -309,7 +321,7 @@ name goes last, so `auth/oauth` cannot collide with `auth`.
|
|
|
309
321
|
$ gwa fix-parser # creates it and lands you in it
|
|
310
322
|
$ gwl parse # one match takes it outright
|
|
311
323
|
$ gwm parser-v2 # renames the branch and moves the checkout
|
|
312
|
-
$ gwr #
|
|
324
|
+
$ gwr # asks which, starting on the one you are in
|
|
313
325
|
```
|
|
314
326
|
|
|
315
327
|
Each prints one destination on stdout, which is what the shell function
|
|
@@ -333,10 +345,11 @@ whatever branch it stands on, and `--list` marks the one you are in:
|
|
|
333
345
|
|
|
334
346
|
```console
|
|
335
347
|
$ gwl --list
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
348
|
+
WORKTREE BRANCH PATH
|
|
349
|
+
repo main /home/you/git/repo
|
|
350
|
+
dirty-work dirty-work /home/you/git/.worktrees/dirty-work/repo
|
|
351
|
+
holds-secrets holds-secrets /home/you/git/.worktrees/holds-secrets/repo
|
|
352
|
+
* never-pushed never-pushed /home/you/git/.worktrees/never-pushed/repo
|
|
340
353
|
```
|
|
341
354
|
|
|
342
355
|
It never offers that one. Picking it is the one answer that cannot take you
|
|
@@ -350,28 +363,42 @@ already in dirty-work
|
|
|
350
363
|
A query that narrows to one takes it outright. No query does not, even when
|
|
351
364
|
the repository holds exactly one other worktree: `gwl` with no argument means
|
|
352
365
|
show me the options, and being moved without being asked is not that. So it
|
|
353
|
-
asks,
|
|
366
|
+
asks, however many there are:
|
|
354
367
|
|
|
355
368
|
```console
|
|
356
369
|
$ gwl # standing in never-pushed
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
370
|
+
# WORKTREE BRANCH PATH
|
|
371
|
+
* never-pushed never-pushed /home/you/git/.worktrees/never-pushed/repo
|
|
372
|
+
> 1 repo main /home/you/git/repo
|
|
373
|
+
2 dirty-work dirty-work /home/you/git/.worktrees/dirty-work/repo
|
|
374
|
+
3 holds-secrets holds-secrets /home/you/git/.worktrees/holds-secrets/repo
|
|
375
|
+
which? [up/down and enter, 1-3, or esc to cancel]
|
|
362
376
|
```
|
|
363
377
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
378
|
+
WORKTREE is the directory under `.worktrees`, or the main checkout's own
|
|
379
|
+
directory name. It parts from BRANCH when somebody switches branch inside a
|
|
380
|
+
worktree. Up and down move the highlight, enter takes it, a digit takes that
|
|
381
|
+
row outright, and esc cancels.
|
|
382
|
+
|
|
383
|
+
A linked worktree you are standing in is marked `*` and carries no number.
|
|
384
|
+
It is where you are rather than somewhere to go, and a list without it shows
|
|
385
|
+
three rows where `git worktree list` shows four. The main checkout is always
|
|
386
|
+
numbered, even when you are inside it, and carries a `*` after its name
|
|
387
|
+
then: from a subdirectory, picking it takes you back to the top.
|
|
367
388
|
|
|
368
389
|
Matching is substring first and then subsequence, so `tst` finds `add-tests`,
|
|
369
390
|
and a substring hit always outranks a loose one. Without a terminal `gwl`
|
|
370
391
|
refuses at exit 3 and names `--list` and `--json`.
|
|
371
392
|
|
|
372
|
-
`gwr`
|
|
373
|
-
|
|
374
|
-
|
|
393
|
+
`gwr` lists the main checkout dimmed and unnumbered, since git will not
|
|
394
|
+
remove it, and starts the highlight on the worktree you are standing in. A
|
|
395
|
+
name or path that is exactly one worktree's, as `gwl --list` and `gwl` print
|
|
396
|
+
them, takes that one without asking which.
|
|
397
|
+
|
|
398
|
+
`gwr` says why a branch is not finished, the same verdict `gws` prints. At a
|
|
399
|
+
terminal it then asks whether to remove the checkout anyway, and keeps the
|
|
400
|
+
branch: the worktree was in the way, the work was not. `--force` is the same
|
|
401
|
+
answer without the question, and `--yes` on its own refuses.
|
|
375
402
|
|
|
376
403
|
A finished branch whose worktree holds ignored files is refused too, and named
|
|
377
404
|
as finished, because it is:
|
|
@@ -381,11 +408,19 @@ $ gwr holds-secrets
|
|
|
381
408
|
holds-secrets is finished: squash-merged, but holds 2 ignored path(s); pass --delete-ignored
|
|
382
409
|
```
|
|
383
410
|
|
|
411
|
+
At a terminal, and without `--yes`, `gwr` asks instead: it names every
|
|
412
|
+
ignored path and removes the worktree on a `y`. `--yes` never answers that
|
|
413
|
+
question, so a script still needs `--delete-ignored`.
|
|
414
|
+
|
|
384
415
|
`--force` is not the answer to that one. It deletes those files just the same,
|
|
385
416
|
since `git worktree remove` takes the whole directory, and it keeps a branch
|
|
386
417
|
whose work already landed. Every ignored path is named before anything is
|
|
387
418
|
deleted, under `--delete-ignored` and under `--force` alike.
|
|
388
419
|
|
|
420
|
+
`gwr!` is `gwr --delete-ignored`, for when the ignored files are ones you meant
|
|
421
|
+
to lose. It is a shell function in fish and zsh, not a console script, and it
|
|
422
|
+
calls the `gwr` function so you still land where `gwr` says.
|
|
423
|
+
|
|
389
424
|
## gwnb and gwrot (alpha)
|
|
390
425
|
|
|
391
426
|
Both are alpha and both start branches rather than worktrees. See the note
|
|
@@ -464,15 +499,42 @@ print one thing each and have nothing to be quiet about, takes `--json`, `-q`,
|
|
|
464
499
|
|
|
465
500
|
| | |
|
|
466
501
|
| --- | --- |
|
|
467
|
-
| `gws` | `--branch NAME`, `--no-fetch`, `--delete-ignored`, `--no-forge` |
|
|
468
|
-
| `gwp` | those
|
|
469
|
-
| `gwr` | `-f`, `--delete-ignored`, `--no-fetch`, `--no-forge`, `-y` |
|
|
470
|
-
| `gwa`, `gwnb`, `gwrot` | `--no-fetch` |
|
|
502
|
+
| `gws` | `--branch NAME`, `--no-fetch`, `--force-refresh`, `--delete-ignored`, `--no-forge` |
|
|
503
|
+
| `gwp` | those five, and `-y` |
|
|
504
|
+
| `gwr` | `-f`, `--delete-ignored`, `--no-fetch`, `--force-refresh`, `--no-forge`, `-y` |
|
|
505
|
+
| `gwa`, `gwnb`, `gwrot` | `--no-fetch`, `--force-refresh` |
|
|
506
|
+
| `gwbs` | `--no-fetch`, `--force-refresh`, `--no-forge` |
|
|
507
|
+
| `gwbd` | those three, `--all`, `--dry-run` and `-y` |
|
|
471
508
|
| `gwl` | `-l` |
|
|
472
509
|
| `gwm`, `gwh` | none |
|
|
473
510
|
|
|
474
511
|
`gw <command> --help` prints one command's own list.
|
|
475
512
|
|
|
513
|
+
While `gws`, `gwp` and `gwr` fetch and judge, a spinner on stderr names the
|
|
514
|
+
branch and the check in progress.
|
|
515
|
+
|
|
516
|
+
### What is reused, and for how long
|
|
517
|
+
|
|
518
|
+
A fetch and a question to the forge are most of what a run costs, so an answer
|
|
519
|
+
that is still true is reused. `--force-refresh` fetches and asks again whatever
|
|
520
|
+
is kept, and `--no-fetch` never fetches.
|
|
521
|
+
|
|
522
|
+
| what | when | reused for |
|
|
523
|
+
| --- | --- | --- |
|
|
524
|
+
| a fetch, by `gws`, `gwp`, `gwr`, `gwbs`, `gwbd`, `gwa`, `gwnb`, `gwrot` | from any worktree of the repository | 10 minutes; the command says how old it is |
|
|
525
|
+
| a request, merged or closed | on the commit it was asked about | until `--force-refresh` |
|
|
526
|
+
| a request, open | a draft, a conflict, or on GitLab a needed rebase, with no auto-merge and no check running | 10 minutes |
|
|
527
|
+
| a request, open | auto-merge or merge-when-pipeline-succeeds set | never |
|
|
528
|
+
| a request, open | a check still running or pending | never |
|
|
529
|
+
| a request, open | the forge calls it mergeable, or cannot say | never |
|
|
530
|
+
| no request | the branch has no upstream | 10 minutes |
|
|
531
|
+
| no request | the branch has an upstream | 1 minute |
|
|
532
|
+
| no answer | `gh`/`glab` missing, not signed in, failing or timing out | never |
|
|
533
|
+
|
|
534
|
+
Forge answers live in `git-worktrees-forge.json` in the repository's common git
|
|
535
|
+
directory, filed under the branch and its commit. A new commit on the branch is
|
|
536
|
+
a new question, so nothing kept outlives the commit it was given for.
|
|
537
|
+
|
|
476
538
|
A remote that cannot be reached is not fatal anywhere. The fetch fails, the
|
|
477
539
|
command says so on stderr, and it answers from the refs already here, which is
|
|
478
540
|
where `--no-fetch` sends it on request. The forge is not asked either: it is
|
|
@@ -483,7 +545,8 @@ forge asked.
|
|
|
483
545
|
Data goes to stdout and diagnostics to stderr, the prompt included, so `--json`
|
|
484
546
|
is parseable in every mode. Colour is decided per stream and only for a
|
|
485
547
|
terminal, so a redirect, a pipe, a non-empty `NO_COLOR` or `TERM=dumb` give the
|
|
486
|
-
bytes a pipe would have got
|
|
548
|
+
bytes a pipe would have got. `--json` and `gwl --list` are never coloured, even
|
|
549
|
+
on a terminal. `NO_COLOR=` is not a request to
|
|
487
550
|
turn it off, which is the no-color.org rule.
|
|
488
551
|
|
|
489
552
|
Every command is a console script, so a script reaches it with no shell loaded
|