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.
Files changed (101) hide show
  1. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/AGENTS.md +102 -27
  2. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/CHANGELOG.md +43 -0
  3. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/PKG-INFO +106 -43
  4. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/README.md +105 -42
  5. git_worktrees-0.5.0/completions/gw.fish +39 -0
  6. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/completions/gwa.fish +1 -0
  7. git_worktrees-0.5.0/completions/gwbd.fish +14 -0
  8. git_worktrees-0.5.0/completions/gwbs.fish +11 -0
  9. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/completions/gwnb.fish +1 -0
  10. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/completions/gwp.fish +1 -0
  11. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/completions/gwr.fish +1 -0
  12. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/completions/gwrot.fish +1 -0
  13. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/completions/gws.fish +1 -0
  14. git_worktrees-0.5.0/completions/worktrees.fish +1 -0
  15. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/docs/BENCHMARK.md +52 -35
  16. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/docs/DEVELOPMENT.md +1 -1
  17. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/docs/REFERENCE.md +33 -21
  18. git_worktrees-0.5.0/docs/commands/branch-delete.md +77 -0
  19. git_worktrees-0.5.0/docs/commands/branch-status.md +110 -0
  20. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/functions/gwa.fish +5 -4
  21. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/functions/gwl.fish +5 -4
  22. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/functions/gwm.fish +5 -4
  23. git_worktrees-0.5.0/functions/gwr!.fish +6 -0
  24. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/functions/gwr.fish +5 -4
  25. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/pyproject.toml +3 -1
  26. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/scripts/benchmark.py +30 -5
  27. git_worktrees-0.5.0/src/worktrees/branch_cli.py +212 -0
  28. git_worktrees-0.5.0/src/worktrees/branch_forge.py +156 -0
  29. git_worktrees-0.5.0/src/worktrees/branches.py +150 -0
  30. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/src/worktrees/cli.py +278 -79
  31. git_worktrees-0.5.0/src/worktrees/forge.py +281 -0
  32. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/src/worktrees/git.py +12 -2
  33. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/src/worktrees/layout.py +14 -0
  34. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/src/worktrees/merged.py +30 -2
  35. git_worktrees-0.5.0/src/worktrees/pick.py +242 -0
  36. git_worktrees-0.5.0/src/worktrees/progress.py +73 -0
  37. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/src/worktrees/prune.py +1 -1
  38. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/src/worktrees/render.py +2 -0
  39. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/src/worktrees/repo.py +16 -0
  40. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/src/worktrees/verdicts.py +50 -50
  41. git_worktrees-0.5.0/tests/test_branch_forge.py +171 -0
  42. git_worktrees-0.5.0/tests/test_branches.py +328 -0
  43. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/tests/test_cli.py +132 -13
  44. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/tests/test_completions.py +61 -2
  45. git_worktrees-0.5.0/tests/test_forge.py +643 -0
  46. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/tests/test_shims.py +72 -16
  47. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/tests/test_worktree.py +88 -7
  48. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/uv.lock +1 -1
  49. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/zsh/plugins/worktrees/completions/_gw +20 -0
  50. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/zsh/plugins/worktrees/completions/_gwa +1 -0
  51. git_worktrees-0.5.0/zsh/plugins/worktrees/completions/_gwbd +22 -0
  52. git_worktrees-0.5.0/zsh/plugins/worktrees/completions/_gwbs +19 -0
  53. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/zsh/plugins/worktrees/completions/_gwnb +1 -0
  54. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/zsh/plugins/worktrees/completions/_gwp +1 -0
  55. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/zsh/plugins/worktrees/completions/_gwr +2 -1
  56. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/zsh/plugins/worktrees/completions/_gwrot +1 -0
  57. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/zsh/plugins/worktrees/completions/_gws +1 -0
  58. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/zsh/plugins/worktrees/functions/gwa +5 -5
  59. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/zsh/plugins/worktrees/functions/gwl +5 -5
  60. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/zsh/plugins/worktrees/functions/gwm +5 -5
  61. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/zsh/plugins/worktrees/functions/gwr +5 -5
  62. git_worktrees-0.5.0/zsh/plugins/worktrees/functions/gwr! +4 -0
  63. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/zsh/plugins/worktrees/worktrees.plugin.zsh +3 -2
  64. git_worktrees-0.4.2/completions/gw.fish +0 -22
  65. git_worktrees-0.4.2/src/worktrees/forge.py +0 -136
  66. git_worktrees-0.4.2/src/worktrees/pick.py +0 -134
  67. git_worktrees-0.4.2/tests/test_forge.py +0 -302
  68. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/.claude/settings.json +0 -0
  69. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/.github/workflows/publish.yml +0 -0
  70. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/.github/workflows/testpypi.yml +0 -0
  71. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/.github/workflows/tests.yml +0 -0
  72. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/.gitignore +0 -0
  73. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/.pre-commit-config.yaml +0 -0
  74. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/.python-version +0 -0
  75. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/.releasetools.yaml +0 -0
  76. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/LICENSE +0 -0
  77. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/completions/gwh.fish +0 -0
  78. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/completions/gwl.fish +0 -0
  79. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/completions/gwm.fish +0 -0
  80. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/conf.d/worktrees.fish +0 -0
  81. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/hatch_build.py +0 -0
  82. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/renovate.json +0 -0
  83. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/scripts/README.md +0 -0
  84. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/scripts/verify-publish.bash +0 -0
  85. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/src/worktrees/__init__.py +0 -0
  86. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/src/worktrees/errors.py +0 -0
  87. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/src/worktrees/new_branch.py +0 -0
  88. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/src/worktrees/rotate.py +0 -0
  89. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/src/worktrees/worktree.py +0 -0
  90. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/tests/conftest.py +0 -0
  91. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/tests/test_benchmark.py +0 -0
  92. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/tests/test_fixture.py +0 -0
  93. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/tests/test_gates.py +0 -0
  94. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/tests/test_new_branch.py +0 -0
  95. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/tests/test_render.py +0 -0
  96. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/tests/test_rotate.py +0 -0
  97. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/tests/test_spec.py +0 -0
  98. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/tests/test_version.py +0 -0
  99. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/zsh/plugins/worktrees/completions/_gwh +0 -0
  100. {git_worktrees-0.4.2 → git_worktrees-0.5.0}/zsh/plugins/worktrees/completions/_gwl +0 -0
  101. {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, and a shim that cannot tell them
36
- apart tries to `cd` into a JSON object. `fish -c 'gwl --json'` reaches the
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 numbered prompt reads faster:
132
+ instrument at that size, and a list moved through with the arrow keys reads
133
+ faster:
124
134
 
125
135
  ```
126
- 1 fix-parser /home/you/git/.worktrees/fix-parser/repo
127
- 2 add-tests /home/you/git/.worktrees/add-tests/repo
128
- which? [1-2, or blank to cancel]
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
- Dropping `fzf` is less code, not more. It removes the spawn, the tty rules
132
- around it, and the fallback branch a machine without `fzf` would otherwise
133
- need.
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
- - The worktree you are standing in is shown and never offered. It carries a
160
- `*` rather than a number, because picking it is the one answer that takes
161
- you nowhere. Leaving it out of the list entirely is worse: the list then
162
- shows fewer rows than `git worktree list` does, and reads as though
163
- something went missing rather than as where you already are. The main
164
- checkout is in `gws` and `gwp` for the same reason, kept and never
165
- proposed, and out of `gwr`'s candidates because `git worktree remove`
166
- refuses it whatever flag it is given.
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` is the table. A row carries the function, the flags,
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` is never painted at all:
207
- the colour is applied at the call site that formats a table, and the JSON
208
- paths do not pass through one.
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 is cross-checked against
304
- `unpushed_count`: it speaks for what reached it, never for commits nobody
305
- pushed, and no upstream at all stays `unknown` rather than becoming zero.
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.4.2
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` and `gwh` answer a question and print it. `gwa`,
35
- `gwl`, `gwm` and `gwr` land you somewhere, and each of those needs a shell
36
- function, because a binary cannot change its caller's directory.
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 is the whole install for `gws`, `gwp`, `gwnb`, `gwrot` and `gwh`. The
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
- four functions and ten completions land together. Nothing has to be sourced.
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 four. Completion needs `compinit`, which most zsh setups already
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 BRANCH WHY PATH
181
- keep main it is the main checkout, and never removable /home/you/git/repo
182
- keep dirty-work it has uncommitted changes /home/you/git/.worktrees/dirty-work/repo
183
- keep holds-secrets merged, but holds 2 ignored path(s); pass --delete-ignored /home/you/git/.worktrees/holds-secrets/repo
184
- unknown never-pushed not merged into origin/main, and no upstream says whether its commits were pushed /home/you/git/.worktrees/never-pushed/repo
185
- remove squash-merged squash-merged /home/you/git/.worktrees/squash-merged/repo
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 settles it, cross-checked against the
227
- commits an upstream has not got, because a request speaks for what reached
228
- it and nothing about what never did. `--no-forge` decides from git alone, and
229
- so does a run whose fetch just failed.
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 BRANCH WHY PATH
290
- keep dirty-work it has uncommitted changes /home/you/git/.worktrees/dirty-work/repo
291
- keep holds-secrets squash-merged, but holds 2 ignored path(s); pass --delete-ignored /home/you/git/.worktrees/holds-secrets/repo
292
- unknown never-pushed not merged into main, and no upstream says whether its commits were pushed /home/you/git/.worktrees/never-pushed/repo
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 # removes the one you are standing in
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
- main /home/you/git/repo
337
- dirty-work /home/you/git/.worktrees/dirty-work/repo
338
- holds-secrets /home/you/git/.worktrees/holds-secrets/repo
339
- * never-pushed /home/you/git/.worktrees/never-pushed/repo
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, numbered, however many there are:
366
+ asks, however many there are:
354
367
 
355
368
  ```console
356
369
  $ gwl # standing in never-pushed
357
- * never-pushed /home/you/git/.worktrees/never-pushed/repo
358
- 1 main /home/you/git/repo
359
- 2 dirty-work /home/you/git/.worktrees/dirty-work/repo
360
- 3 holds-secrets /home/you/git/.worktrees/holds-secrets/repo
361
- which? [1-3, or blank to cancel]
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
- The one you are standing in is marked `*` and carries no number. It is where
365
- you are rather than somewhere to go, and a list without it shows three rows
366
- where `git worktree list` shows four.
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` refuses a branch that is not finished and says why, the same verdict
373
- `gws` prints. `--force` removes the checkout and keeps the branch: the
374
- worktree was in the way, the work was not.
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 four, and `-y` |
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, `--json` included. `NO_COLOR=` is not a request to
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