git-worktrees 0.4.3__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 (100) hide show
  1. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/AGENTS.md +98 -23
  2. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/CHANGELOG.md +35 -0
  3. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/PKG-INFO +100 -39
  4. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/README.md +99 -38
  5. git_worktrees-0.5.0/completions/gw.fish +39 -0
  6. {git_worktrees-0.4.3 → 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.3 → git_worktrees-0.5.0}/completions/gwnb.fish +1 -0
  10. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/completions/gwp.fish +1 -0
  11. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/completions/gwr.fish +1 -0
  12. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/completions/gwrot.fish +1 -0
  13. {git_worktrees-0.4.3 → 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.3 → git_worktrees-0.5.0}/docs/BENCHMARK.md +52 -35
  16. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/docs/DEVELOPMENT.md +1 -1
  17. {git_worktrees-0.4.3 → 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.3 → git_worktrees-0.5.0}/functions/gwa.fish +5 -4
  21. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/functions/gwl.fish +5 -4
  22. {git_worktrees-0.4.3 → 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.3 → git_worktrees-0.5.0}/functions/gwr.fish +5 -4
  25. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/pyproject.toml +3 -1
  26. {git_worktrees-0.4.3 → 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.3 → 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.3 → git_worktrees-0.5.0}/src/worktrees/git.py +12 -2
  33. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/src/worktrees/layout.py +14 -0
  34. {git_worktrees-0.4.3 → 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.3 → git_worktrees-0.5.0}/src/worktrees/prune.py +1 -1
  38. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/src/worktrees/render.py +2 -0
  39. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/src/worktrees/repo.py +11 -0
  40. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/src/worktrees/verdicts.py +34 -44
  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.3 → git_worktrees-0.5.0}/tests/test_cli.py +132 -13
  44. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/tests/test_completions.py +61 -2
  45. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/tests/test_forge.py +243 -0
  46. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/tests/test_shims.py +72 -16
  47. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/tests/test_worktree.py +88 -7
  48. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/uv.lock +1 -1
  49. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/zsh/plugins/worktrees/completions/_gw +20 -0
  50. {git_worktrees-0.4.3 → 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.3 → git_worktrees-0.5.0}/zsh/plugins/worktrees/completions/_gwnb +1 -0
  54. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/zsh/plugins/worktrees/completions/_gwp +1 -0
  55. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/zsh/plugins/worktrees/completions/_gwr +2 -1
  56. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/zsh/plugins/worktrees/completions/_gwrot +1 -0
  57. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/zsh/plugins/worktrees/completions/_gws +1 -0
  58. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/zsh/plugins/worktrees/functions/gwa +5 -5
  59. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/zsh/plugins/worktrees/functions/gwl +5 -5
  60. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/zsh/plugins/worktrees/functions/gwm +5 -5
  61. {git_worktrees-0.4.3 → 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.3 → git_worktrees-0.5.0}/zsh/plugins/worktrees/worktrees.plugin.zsh +3 -2
  64. git_worktrees-0.4.3/completions/gw.fish +0 -22
  65. git_worktrees-0.4.3/src/worktrees/forge.py +0 -136
  66. git_worktrees-0.4.3/src/worktrees/pick.py +0 -134
  67. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/.claude/settings.json +0 -0
  68. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/.github/workflows/publish.yml +0 -0
  69. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/.github/workflows/testpypi.yml +0 -0
  70. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/.github/workflows/tests.yml +0 -0
  71. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/.gitignore +0 -0
  72. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/.pre-commit-config.yaml +0 -0
  73. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/.python-version +0 -0
  74. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/.releasetools.yaml +0 -0
  75. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/LICENSE +0 -0
  76. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/completions/gwh.fish +0 -0
  77. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/completions/gwl.fish +0 -0
  78. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/completions/gwm.fish +0 -0
  79. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/conf.d/worktrees.fish +0 -0
  80. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/hatch_build.py +0 -0
  81. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/renovate.json +0 -0
  82. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/scripts/README.md +0 -0
  83. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/scripts/verify-publish.bash +0 -0
  84. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/src/worktrees/__init__.py +0 -0
  85. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/src/worktrees/errors.py +0 -0
  86. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/src/worktrees/new_branch.py +0 -0
  87. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/src/worktrees/rotate.py +0 -0
  88. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/src/worktrees/worktree.py +0 -0
  89. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/tests/conftest.py +0 -0
  90. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/tests/test_benchmark.py +0 -0
  91. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/tests/test_fixture.py +0 -0
  92. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/tests/test_gates.py +0 -0
  93. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/tests/test_new_branch.py +0 -0
  94. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/tests/test_render.py +0 -0
  95. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/tests/test_rotate.py +0 -0
  96. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/tests/test_spec.py +0 -0
  97. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/tests/test_version.py +0 -0
  98. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/zsh/plugins/worktrees/completions/_gwh +0 -0
  99. {git_worktrees-0.4.3 → git_worktrees-0.5.0}/zsh/plugins/worktrees/completions/_gwl +0 -0
  100. {git_worktrees-0.4.3 → 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
@@ -17,6 +17,41 @@ 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
+
20
55
  ## 0.4.3 - 2026-09-22
21
56
 
22
57
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: git-worktrees
3
- Version: 0.4.3
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
@@ -288,10 +298,10 @@ away:
288
298
 
289
299
  ```console
290
300
  $ gwp
291
- VERDICT BRANCH WHY PATH
292
- keep dirty-work it has uncommitted changes /home/you/git/.worktrees/dirty-work/repo
293
- keep holds-secrets squash-merged, but holds 2 ignored path(s); pass --delete-ignored /home/you/git/.worktrees/holds-secrets/repo
294
- 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
295
305
 
296
306
  nothing to remove; 0 removable, 2 kept, 1 unclear
297
307
  ```
@@ -311,7 +321,7 @@ name goes last, so `auth/oauth` cannot collide with `auth`.
311
321
  $ gwa fix-parser # creates it and lands you in it
312
322
  $ gwl parse # one match takes it outright
313
323
  $ gwm parser-v2 # renames the branch and moves the checkout
314
- $ gwr # removes the one you are standing in
324
+ $ gwr # asks which, starting on the one you are in
315
325
  ```
316
326
 
317
327
  Each prints one destination on stdout, which is what the shell function
@@ -335,10 +345,11 @@ whatever branch it stands on, and `--list` marks the one you are in:
335
345
 
336
346
  ```console
337
347
  $ gwl --list
338
- main /home/you/git/repo
339
- dirty-work /home/you/git/.worktrees/dirty-work/repo
340
- holds-secrets /home/you/git/.worktrees/holds-secrets/repo
341
- * 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
342
353
  ```
343
354
 
344
355
  It never offers that one. Picking it is the one answer that cannot take you
@@ -352,28 +363,42 @@ already in dirty-work
352
363
  A query that narrows to one takes it outright. No query does not, even when
353
364
  the repository holds exactly one other worktree: `gwl` with no argument means
354
365
  show me the options, and being moved without being asked is not that. So it
355
- asks, numbered, however many there are:
366
+ asks, however many there are:
356
367
 
357
368
  ```console
358
369
  $ gwl # standing in never-pushed
359
- * never-pushed /home/you/git/.worktrees/never-pushed/repo
360
- 1 main /home/you/git/repo
361
- 2 dirty-work /home/you/git/.worktrees/dirty-work/repo
362
- 3 holds-secrets /home/you/git/.worktrees/holds-secrets/repo
363
- 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]
364
376
  ```
365
377
 
366
- The one you are standing in is marked `*` and carries no number. It is where
367
- you are rather than somewhere to go, and a list without it shows three rows
368
- 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.
369
388
 
370
389
  Matching is substring first and then subsequence, so `tst` finds `add-tests`,
371
390
  and a substring hit always outranks a loose one. Without a terminal `gwl`
372
391
  refuses at exit 3 and names `--list` and `--json`.
373
392
 
374
- `gwr` refuses a branch that is not finished and says why, the same verdict
375
- `gws` prints. `--force` removes the checkout and keeps the branch: the
376
- 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.
377
402
 
378
403
  A finished branch whose worktree holds ignored files is refused too, and named
379
404
  as finished, because it is:
@@ -383,11 +408,19 @@ $ gwr holds-secrets
383
408
  holds-secrets is finished: squash-merged, but holds 2 ignored path(s); pass --delete-ignored
384
409
  ```
385
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
+
386
415
  `--force` is not the answer to that one. It deletes those files just the same,
387
416
  since `git worktree remove` takes the whole directory, and it keeps a branch
388
417
  whose work already landed. Every ignored path is named before anything is
389
418
  deleted, under `--delete-ignored` and under `--force` alike.
390
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
+
391
424
  ## gwnb and gwrot (alpha)
392
425
 
393
426
  Both are alpha and both start branches rather than worktrees. See the note
@@ -466,15 +499,42 @@ print one thing each and have nothing to be quiet about, takes `--json`, `-q`,
466
499
 
467
500
  | | |
468
501
  | --- | --- |
469
- | `gws` | `--branch NAME`, `--no-fetch`, `--delete-ignored`, `--no-forge` |
470
- | `gwp` | those four, and `-y` |
471
- | `gwr` | `-f`, `--delete-ignored`, `--no-fetch`, `--no-forge`, `-y` |
472
- | `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` |
473
508
  | `gwl` | `-l` |
474
509
  | `gwm`, `gwh` | none |
475
510
 
476
511
  `gw <command> --help` prints one command's own list.
477
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
+
478
538
  A remote that cannot be reached is not fatal anywhere. The fetch fails, the
479
539
  command says so on stderr, and it answers from the refs already here, which is
480
540
  where `--no-fetch` sends it on request. The forge is not asked either: it is
@@ -485,7 +545,8 @@ forge asked.
485
545
  Data goes to stdout and diagnostics to stderr, the prompt included, so `--json`
486
546
  is parseable in every mode. Colour is decided per stream and only for a
487
547
  terminal, so a redirect, a pipe, a non-empty `NO_COLOR` or `TERM=dumb` give the
488
- 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
489
550
  turn it off, which is the no-color.org rule.
490
551
 
491
552
  Every command is a console script, so a script reaches it with no shell loaded