git-worktrees 0.4.3__tar.gz → 0.5.1__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.1}/.github/workflows/publish.yml +23 -10
  2. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/.github/workflows/testpypi.yml +5 -0
  3. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/AGENTS.md +98 -23
  4. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/CHANGELOG.md +45 -0
  5. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/PKG-INFO +100 -39
  6. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/README.md +99 -38
  7. git_worktrees-0.5.1/completions/gw.fish +39 -0
  8. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/completions/gwa.fish +1 -0
  9. git_worktrees-0.5.1/completions/gwbd.fish +14 -0
  10. git_worktrees-0.5.1/completions/gwbs.fish +11 -0
  11. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/completions/gwnb.fish +1 -0
  12. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/completions/gwp.fish +1 -0
  13. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/completions/gwr.fish +1 -0
  14. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/completions/gwrot.fish +1 -0
  15. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/completions/gws.fish +1 -0
  16. git_worktrees-0.5.1/completions/worktrees.fish +1 -0
  17. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/docs/BENCHMARK.md +52 -35
  18. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/docs/DEVELOPMENT.md +14 -8
  19. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/docs/REFERENCE.md +33 -21
  20. git_worktrees-0.5.1/docs/commands/branch-delete.md +77 -0
  21. git_worktrees-0.5.1/docs/commands/branch-status.md +110 -0
  22. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/functions/gwa.fish +5 -4
  23. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/functions/gwl.fish +5 -4
  24. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/functions/gwm.fish +5 -4
  25. git_worktrees-0.5.1/functions/gwr!.fish +6 -0
  26. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/functions/gwr.fish +5 -4
  27. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/pyproject.toml +3 -1
  28. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/scripts/benchmark.py +30 -5
  29. git_worktrees-0.5.1/src/worktrees/branch_cli.py +212 -0
  30. git_worktrees-0.5.1/src/worktrees/branch_forge.py +156 -0
  31. git_worktrees-0.5.1/src/worktrees/branches.py +150 -0
  32. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/src/worktrees/cli.py +278 -79
  33. git_worktrees-0.5.1/src/worktrees/forge.py +281 -0
  34. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/src/worktrees/git.py +12 -2
  35. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/src/worktrees/layout.py +14 -0
  36. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/src/worktrees/merged.py +30 -2
  37. git_worktrees-0.5.1/src/worktrees/pick.py +242 -0
  38. git_worktrees-0.5.1/src/worktrees/progress.py +73 -0
  39. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/src/worktrees/prune.py +1 -1
  40. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/src/worktrees/render.py +2 -0
  41. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/src/worktrees/repo.py +11 -0
  42. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/src/worktrees/verdicts.py +34 -44
  43. git_worktrees-0.5.1/tests/test_branch_forge.py +171 -0
  44. git_worktrees-0.5.1/tests/test_branches.py +328 -0
  45. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/tests/test_cli.py +132 -13
  46. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/tests/test_completions.py +61 -2
  47. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/tests/test_forge.py +243 -0
  48. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/tests/test_shims.py +72 -16
  49. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/tests/test_worktree.py +88 -7
  50. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/uv.lock +1 -1
  51. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/zsh/plugins/worktrees/completions/_gw +20 -0
  52. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/zsh/plugins/worktrees/completions/_gwa +1 -0
  53. git_worktrees-0.5.1/zsh/plugins/worktrees/completions/_gwbd +22 -0
  54. git_worktrees-0.5.1/zsh/plugins/worktrees/completions/_gwbs +19 -0
  55. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/zsh/plugins/worktrees/completions/_gwnb +1 -0
  56. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/zsh/plugins/worktrees/completions/_gwp +1 -0
  57. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/zsh/plugins/worktrees/completions/_gwr +2 -1
  58. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/zsh/plugins/worktrees/completions/_gwrot +1 -0
  59. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/zsh/plugins/worktrees/completions/_gws +1 -0
  60. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/zsh/plugins/worktrees/functions/gwa +5 -5
  61. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/zsh/plugins/worktrees/functions/gwl +5 -5
  62. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/zsh/plugins/worktrees/functions/gwm +5 -5
  63. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/zsh/plugins/worktrees/functions/gwr +5 -5
  64. git_worktrees-0.5.1/zsh/plugins/worktrees/functions/gwr! +4 -0
  65. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/zsh/plugins/worktrees/worktrees.plugin.zsh +3 -2
  66. git_worktrees-0.4.3/completions/gw.fish +0 -22
  67. git_worktrees-0.4.3/src/worktrees/forge.py +0 -136
  68. git_worktrees-0.4.3/src/worktrees/pick.py +0 -134
  69. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/.claude/settings.json +0 -0
  70. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/.github/workflows/tests.yml +0 -0
  71. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/.gitignore +0 -0
  72. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/.pre-commit-config.yaml +0 -0
  73. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/.python-version +0 -0
  74. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/.releasetools.yaml +0 -0
  75. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/LICENSE +0 -0
  76. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/completions/gwh.fish +0 -0
  77. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/completions/gwl.fish +0 -0
  78. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/completions/gwm.fish +0 -0
  79. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/conf.d/worktrees.fish +0 -0
  80. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/hatch_build.py +0 -0
  81. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/renovate.json +0 -0
  82. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/scripts/README.md +0 -0
  83. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/scripts/verify-publish.bash +0 -0
  84. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/src/worktrees/__init__.py +0 -0
  85. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/src/worktrees/errors.py +0 -0
  86. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/src/worktrees/new_branch.py +0 -0
  87. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/src/worktrees/rotate.py +0 -0
  88. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/src/worktrees/worktree.py +0 -0
  89. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/tests/conftest.py +0 -0
  90. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/tests/test_benchmark.py +0 -0
  91. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/tests/test_fixture.py +0 -0
  92. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/tests/test_gates.py +0 -0
  93. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/tests/test_new_branch.py +0 -0
  94. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/tests/test_render.py +0 -0
  95. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/tests/test_rotate.py +0 -0
  96. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/tests/test_spec.py +0 -0
  97. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/tests/test_version.py +0 -0
  98. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/zsh/plugins/worktrees/completions/_gwh +0 -0
  99. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/zsh/plugins/worktrees/completions/_gwl +0 -0
  100. {git_worktrees-0.4.3 → git_worktrees-0.5.1}/zsh/plugins/worktrees/completions/_gwm +0 -0
@@ -152,25 +152,23 @@ jobs:
152
152
  name: dist
153
153
  path: dist/
154
154
 
155
+ # Sign with this job's OIDC identity. uv uploads the attestations with
156
+ # their distributions.
157
+ - name: Attest
158
+ run: uvx pypi-attestations@0.0.30 sign dist/*
159
+
155
160
  - name: Publish
156
161
  run: uv publish --trusted-publishing always
157
162
 
158
- # Uploaded and installable are two claims with two timings, so they are two
159
- # jobs. An index serves what it has accepted only after a delay of its own
160
- # choosing: 0.2.1 took longer than the script waited, and a release that had
161
- # published correctly went red and had to be re-run by hand. This job holds
162
- # the second claim, `release` needs only the first, and a red `verify` says
163
- # PyPI is slow rather than that the version is not out.
163
+ # PyPI can take time to serve an accepted upload. Verification runs in a
164
+ # separate job so that delay does not block the GitHub release.
164
165
  verify:
165
- name: Verify it installs from PyPI
166
+ name: Verify PyPI install and attestations
166
167
  runs-on: ubuntu-26.04
167
168
  needs: publish
168
169
  permissions:
169
170
  contents: read
170
171
  steps:
171
- # The script reads the name and version from pyproject.toml through
172
- # `uv version`, and installs from the index rather than from the
173
- # artifact, so there is nothing to download here.
174
172
  - name: Checkout code
175
173
  uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
176
174
 
@@ -183,6 +181,21 @@ jobs:
183
181
  - name: Verify it installs from PyPI
184
182
  run: scripts/verify-publish.bash --prod
185
183
 
184
+ # The filenames identify the distributions to fetch from PyPI.
185
+ - name: Download distributions
186
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
187
+ with:
188
+ name: dist
189
+ path: dist/
190
+
191
+ # The install check waits for PyPI to serve the files.
192
+ - name: Verify attestations on PyPI
193
+ run: |
194
+ for f in dist/*.whl dist/*.tar.gz; do
195
+ uvx pypi-attestations@0.0.30 verify pypi \
196
+ --repository "https://github.com/$GITHUB_REPOSITORY" "pypi:${f#dist/}"
197
+ done
198
+
186
199
  release:
187
200
  name: Release
188
201
  runs-on: ubuntu-26.04
@@ -59,6 +59,11 @@ jobs:
59
59
  - name: Build
60
60
  run: uv build
61
61
 
62
+ # Sign with this job's OIDC identity. uv uploads the attestations with
63
+ # their distributions.
64
+ - name: Attest
65
+ run: uvx pypi-attestations@0.0.30 sign dist/*
66
+
62
67
  - name: Publish
63
68
  # `always` rather than the default `automatic`: fail loudly if OIDC is
64
69
  # unavailable instead of quietly looking for a token.
@@ -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,51 @@ 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.1 - 2026-09-29
21
+
22
+ ### Added
23
+
24
+ The wheel and source distribution on PyPI carry PEP 740 attestations signed
25
+ by the GitHub workflow that built them. PyPI shows the provenance on the
26
+ release's page, and `uvx pypi-attestations verify pypi --repository
27
+ https://github.com/MihaiBojin/worktrees pypi:<file>` checks a file against
28
+ this repository before you install it.
29
+
30
+ ## 0.5.0 - 2026-09-29
31
+
32
+ ### Added
33
+
34
+ Local branches can be assessed with `worktrees branch status` (`gwbs`)
35
+ and deleted with `worktrees branch delete` (`gwbd`). Deletion accepts
36
+ exact names, offers a selector when no names are supplied, and supports
37
+ `--all` and `--dry-run`. A branch qualifies through Git history, matching
38
+ changed paths, or a merged GitHub PR or GitLab MR tied to its exact tip.
39
+ The command protects checked-out branches, rechecks refs before deletion,
40
+ and prints the command to restore each branch.
41
+
42
+ `gwr!` runs `gwr --delete-ignored` in fish and zsh, permitting removal of
43
+ ignored files while retaining the command's checks and directory change.
44
+ Update the fish or zsh plugin to install this shortcut.
45
+
46
+ ### Changed
47
+
48
+ `gwl` and `gwr` pick with the arrow keys from a table of worktree, branch and
49
+ path: enter takes the highlighted row, a digit takes that row, and esc
50
+ cancels. `gwl` offers the main checkout even from inside it. `gwr` shows
51
+ the main checkout dimmed, starts on the worktree you stand in, and at a
52
+ terminal asks for confirmation when a branch is unfinished (keeping the
53
+ branch) or holds ignored files. An exact branch or path selects that
54
+ worktree alone.
55
+
56
+ `gwl --list` prints the same columns and is never coloured. `gws` and `gwp`
57
+ print BRANCH, PATH, VERDICT, WHY. The `gwa`, `gwl`, `gwm` and `gwr` shell
58
+ functions print `--list` and `--json` output.
59
+
60
+ `gws`, `gwp` and `gwr` show a spinner naming the check in progress and judge
61
+ worktrees in parallel. Every command that fetches reuses a fetch under 10
62
+ minutes old, and forge answers that cannot change yet are kept; the README
63
+ lists each case. `--force-refresh` fetches and asks again.
64
+
20
65
  ## 0.4.3 - 2026-09-22
21
66
 
22
67
  ### 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.1
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