git-worktrees 0.1.0__tar.gz → 0.2.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 (77) hide show
  1. {git_worktrees-0.1.0 → git_worktrees-0.2.1}/.github/workflows/publish.yml +15 -13
  2. {git_worktrees-0.1.0 → git_worktrees-0.2.1}/AGENTS.md +67 -11
  3. git_worktrees-0.2.1/CHANGELOG.md +195 -0
  4. git_worktrees-0.2.1/PKG-INFO +487 -0
  5. git_worktrees-0.2.1/README.md +462 -0
  6. git_worktrees-0.2.1/completions/gw.fish +22 -0
  7. git_worktrees-0.2.1/completions/gwa.fish +12 -0
  8. git_worktrees-0.2.1/completions/gwh.fish +5 -0
  9. git_worktrees-0.2.1/completions/gwl.fish +14 -0
  10. git_worktrees-0.2.1/completions/gwm.fish +10 -0
  11. git_worktrees-0.2.1/completions/gwnb.fish +12 -0
  12. git_worktrees-0.2.1/completions/gwp.fish +16 -0
  13. git_worktrees-0.2.1/completions/gwr.fish +17 -0
  14. git_worktrees-0.2.1/completions/gwrot.fish +12 -0
  15. git_worktrees-0.2.1/completions/gws.fish +15 -0
  16. git_worktrees-0.2.1/conf.d/worktrees.fish +3 -0
  17. {git_worktrees-0.1.0 → git_worktrees-0.2.1}/docs/DEVELOPMENT.md +9 -7
  18. {git_worktrees-0.1.0 → git_worktrees-0.2.1}/pyproject.toml +3 -2
  19. {git_worktrees-0.1.0 → git_worktrees-0.2.1}/scripts/verify-publish.bash +36 -8
  20. {git_worktrees-0.1.0 → git_worktrees-0.2.1}/src/worktrees/cli.py +423 -91
  21. {git_worktrees-0.1.0 → git_worktrees-0.2.1}/src/worktrees/git.py +22 -0
  22. {git_worktrees-0.1.0 → git_worktrees-0.2.1}/src/worktrees/pick.py +6 -1
  23. {git_worktrees-0.1.0 → git_worktrees-0.2.1}/src/worktrees/prune.py +20 -15
  24. git_worktrees-0.2.1/src/worktrees/render.py +88 -0
  25. {git_worktrees-0.1.0 → git_worktrees-0.2.1}/src/worktrees/repo.py +13 -2
  26. {git_worktrees-0.1.0 → git_worktrees-0.2.1}/src/worktrees/verdicts.py +12 -2
  27. {git_worktrees-0.1.0 → git_worktrees-0.2.1}/src/worktrees/worktree.py +36 -7
  28. {git_worktrees-0.1.0 → git_worktrees-0.2.1}/tests/test_cli.py +260 -9
  29. git_worktrees-0.2.1/tests/test_completions.py +189 -0
  30. {git_worktrees-0.1.0 → git_worktrees-0.2.1}/tests/test_gates.py +62 -1
  31. git_worktrees-0.2.1/tests/test_render.py +75 -0
  32. {git_worktrees-0.1.0 → git_worktrees-0.2.1}/tests/test_version.py +5 -5
  33. {git_worktrees-0.1.0 → git_worktrees-0.2.1}/tests/test_worktree.py +139 -2
  34. {git_worktrees-0.1.0 → git_worktrees-0.2.1}/uv.lock +1 -1
  35. git_worktrees-0.2.1/zsh/plugins/worktrees/completions/_gw +53 -0
  36. git_worktrees-0.2.1/zsh/plugins/worktrees/completions/_gwa +11 -0
  37. git_worktrees-0.2.1/zsh/plugins/worktrees/completions/_gwh +6 -0
  38. git_worktrees-0.2.1/zsh/plugins/worktrees/completions/_gwl +25 -0
  39. git_worktrees-0.2.1/zsh/plugins/worktrees/completions/_gwm +10 -0
  40. git_worktrees-0.2.1/zsh/plugins/worktrees/completions/_gwnb +11 -0
  41. git_worktrees-0.2.1/zsh/plugins/worktrees/completions/_gwp +15 -0
  42. git_worktrees-0.2.1/zsh/plugins/worktrees/completions/_gwr +28 -0
  43. git_worktrees-0.2.1/zsh/plugins/worktrees/completions/_gwrot +11 -0
  44. git_worktrees-0.2.1/zsh/plugins/worktrees/completions/_gws +14 -0
  45. {git_worktrees-0.1.0 → git_worktrees-0.2.1}/zsh/plugins/worktrees/worktrees.plugin.zsh +1 -1
  46. git_worktrees-0.1.0/CHANGELOG.md +0 -70
  47. git_worktrees-0.1.0/PKG-INFO +0 -335
  48. git_worktrees-0.1.0/README.md +0 -310
  49. git_worktrees-0.1.0/conf.d/worktrees.fish +0 -2
  50. {git_worktrees-0.1.0 → git_worktrees-0.2.1}/.agents/skills/release/SKILL.md +0 -0
  51. {git_worktrees-0.1.0 → git_worktrees-0.2.1}/.claude/settings.json +0 -0
  52. {git_worktrees-0.1.0 → git_worktrees-0.2.1}/.github/workflows/tests.yml +0 -0
  53. {git_worktrees-0.1.0 → git_worktrees-0.2.1}/.gitignore +0 -0
  54. {git_worktrees-0.1.0 → git_worktrees-0.2.1}/.pre-commit-config.yaml +0 -0
  55. {git_worktrees-0.1.0 → git_worktrees-0.2.1}/.python-version +0 -0
  56. {git_worktrees-0.1.0 → git_worktrees-0.2.1}/LICENSE +0 -0
  57. {git_worktrees-0.1.0 → git_worktrees-0.2.1}/functions/gwa.fish +0 -0
  58. {git_worktrees-0.1.0 → git_worktrees-0.2.1}/functions/gwl.fish +0 -0
  59. {git_worktrees-0.1.0 → git_worktrees-0.2.1}/functions/gwm.fish +0 -0
  60. {git_worktrees-0.1.0 → git_worktrees-0.2.1}/functions/gwr.fish +0 -0
  61. {git_worktrees-0.1.0 → git_worktrees-0.2.1}/renovate.json +0 -0
  62. {git_worktrees-0.1.0 → git_worktrees-0.2.1}/src/worktrees/__init__.py +0 -0
  63. {git_worktrees-0.1.0 → git_worktrees-0.2.1}/src/worktrees/forge.py +0 -0
  64. {git_worktrees-0.1.0 → git_worktrees-0.2.1}/src/worktrees/layout.py +0 -0
  65. {git_worktrees-0.1.0 → git_worktrees-0.2.1}/src/worktrees/merged.py +0 -0
  66. {git_worktrees-0.1.0 → git_worktrees-0.2.1}/src/worktrees/new_branch.py +0 -0
  67. {git_worktrees-0.1.0 → git_worktrees-0.2.1}/src/worktrees/rotate.py +0 -0
  68. {git_worktrees-0.1.0 → git_worktrees-0.2.1}/tests/conftest.py +0 -0
  69. {git_worktrees-0.1.0 → git_worktrees-0.2.1}/tests/test_forge.py +0 -0
  70. {git_worktrees-0.1.0 → git_worktrees-0.2.1}/tests/test_new_branch.py +0 -0
  71. {git_worktrees-0.1.0 → git_worktrees-0.2.1}/tests/test_rotate.py +0 -0
  72. {git_worktrees-0.1.0 → git_worktrees-0.2.1}/tests/test_shims.py +0 -0
  73. {git_worktrees-0.1.0 → git_worktrees-0.2.1}/tests/test_spec.py +0 -0
  74. {git_worktrees-0.1.0 → git_worktrees-0.2.1}/zsh/plugins/worktrees/functions/gwa +0 -0
  75. {git_worktrees-0.1.0 → git_worktrees-0.2.1}/zsh/plugins/worktrees/functions/gwl +0 -0
  76. {git_worktrees-0.1.0 → git_worktrees-0.2.1}/zsh/plugins/worktrees/functions/gwm +0 -0
  77. {git_worktrees-0.1.0 → git_worktrees-0.2.1}/zsh/plugins/worktrees/functions/gwr +0 -0
@@ -14,7 +14,8 @@ name: Publish
14
14
  # or replaced, so if the rehearsal fails there is nothing to pin against and
15
15
  # the run stops there.
16
16
  #
17
- # No API token is stored anywhere. See README, "Publishing a version".
17
+ # No API token is stored anywhere. See docs/DEVELOPMENT.md, "Publishing a
18
+ # version".
18
19
 
19
20
  on:
20
21
  push:
@@ -73,15 +74,19 @@ jobs:
73
74
  # release that should go ahead. The API needs no local history.
74
75
  - name: Check the commit is on main
75
76
  env:
76
- GH_TOKEN: ${{ github.token }}
77
- GH_REPO: ${{ github.repository }}
77
+ # GH_REPO carries the value the runner publishes as GITHUB_REPOSITORY: gh looks for
78
+ # GH_REPO and never at GITHUB_REPOSITORY. GITHUB_TOKEN is not a default variable at
79
+ # all, so the token has to be handed over rather than renamed.
80
+ GH_TOKEN: ${{ github.token }} # no runner default
81
+ GH_REPO: ${{ github.repository }} # = $GITHUB_REPOSITORY; gh reads GH_REPO
78
82
  run: rt github::assert_on_branch main
79
83
 
80
84
  - name: Wait for the tests on this commit
81
85
  env:
82
- GH_TOKEN: ${{ github.token }}
83
- GH_REPO: ${{ github.repository }}
84
- run: rt github::await_workflow "$GITHUB_SHA" tests.yml
86
+ GH_TOKEN: ${{ github.token }} # no runner default
87
+ GH_REPO: ${{ github.repository }} # = $GITHUB_REPOSITORY; gh reads GH_REPO
88
+ SHA: ${{ github.sha }} # = $GITHUB_SHA
89
+ run: rt github::await_workflow "$SHA" tests.yml
85
90
 
86
91
  - name: Install dependencies
87
92
  run: uv sync --all-extras --locked
@@ -167,13 +172,10 @@ jobs:
167
172
  - name: Publish
168
173
  run: uv publish --trusted-publishing always
169
174
 
170
- # An index serves what it has just accepted only after a delay, and a fixed
171
- # sleep either wastes time or fails a publish that worked. This backs off
172
- # across about 7.75 minutes and stops as soon as the version is there.
173
- - name: Wait for the PyPI index to catch up
174
- run: |
175
- rt net::await_url "https://pypi.org/pypi/$(uv version | tr ' ' '/')/json"
176
-
175
+ # An index serves what it has just accepted only after a delay, so the
176
+ # script retries the resolution rather than polling a different endpoint
177
+ # first: the JSON API answered for 0.2.0 while the simple index uv reads
178
+ # had not caught up, and the run went red on a publish that had worked.
177
179
  - name: Verify it installs from PyPI
178
180
  run: scripts/verify-publish.bash --prod
179
181
 
@@ -23,7 +23,7 @@ call `command <name>` and `cd` to what that prints.**
23
23
 
24
24
  | | changes directory | ships as |
25
25
  | --- | --- | --- |
26
- | `gws`, `gwp`, `gwnb`, `gwrot` | no | a console script, and nothing else |
26
+ | `gws`, `gwp`, `gwnb`, `gwrot`, `gwh` | no | a console script, and nothing else |
27
27
  | a command that lands you somewhere | yes | a console script, plus a function of the same name |
28
28
 
29
29
  A binary cannot `cd` its caller. That is the only thing shell code is here for,
@@ -85,9 +85,16 @@ Nothing installs this from a checkout on the user's disk. It is consumed as a
85
85
  third-party plugin and a third-party tool, so anything that assumes a local
86
86
  clone at a known path is wrong here.
87
87
 
88
- A completion asks the CLI for its candidates rather than deriving them. The
89
- ranking is tested in Python; a shell file that reimplements it is a second
90
- answer to the same question.
88
+ A completion asks the CLI for its candidates rather than deriving them, and
89
+ `--complete` is how: `gw --complete` prints every subcommand and shorthand,
90
+ `gwl --complete` prints every worktree, both as name and description separated
91
+ by a tab, which fish reads directly and zsh splits for `_describe`. Hidden from
92
+ `--help`, because a person has `gwh` and `--list` for the same answers.
93
+
94
+ No shell file holds a candidate or a rank. What each file does carry is its own
95
+ command's flags, and `tests/test_completions.py` diffs those against `--help`
96
+ in both directions for both shells: a renamed flag fails there rather than
97
+ going quiet in a completion nobody is watching.
91
98
 
92
99
  ## The picker is Python's, and there is no fzf
93
100
 
@@ -101,9 +108,9 @@ number of linked worktrees in one is four. A fuzzy finder is the wrong
101
108
  instrument at that size, and a numbered prompt reads faster:
102
109
 
103
110
  ```
104
- 1 fix-parser ~/git/.worktrees/fix-parser/repo
105
- 2 add-tests ~/git/.worktrees/add-tests/repo
106
- >
111
+ 1 fix-parser /home/you/git/.worktrees/fix-parser/repo
112
+ 2 add-tests /home/you/git/.worktrees/add-tests/repo
113
+ which? [1-2, or blank to cancel]
107
114
  ```
108
115
 
109
116
  Dropping `fzf` is less code, not more. It removes the spawn, the tty rules
@@ -123,12 +130,62 @@ tightly they cluster. About twenty lines, `difflib` not required.
123
130
  Two rules, both the shape `gwp`'s prompt already has:
124
131
 
125
132
  - One match takes it outright, with no prompt at all.
126
- - No terminal, no prompt. Refuse at exit 2 and name the flag that answers
133
+ - No terminal, no prompt. Refuse at exit 3 and name the flag that answers
127
134
  without one, rather than blocking on something nothing can drive.
128
135
 
136
+ Two more about what is in the list at all:
137
+
138
+ - The main checkout is a destination. It is the one that is always there and
139
+ the one a finished branch leaves you in, so leaving it out leaves out the
140
+ only answer that is always right. `main` finds it whatever branch it stands
141
+ on, because its label carries the word, once.
142
+ - The worktree you are standing in is listed and never offered. Picking it is
143
+ the one answer that takes you nowhere, and a picker whose single candidate
144
+ is where you already are prints nothing at all, which reads as a broken
145
+ command.
146
+
129
147
  `--json` and `--list` answer the same question without any of this, and an
130
148
  agent uses those.
131
149
 
150
+ ## One row per command, under every name it answers to
151
+
152
+ `_COMMANDS` in `cli.py` is the table. A row carries the function, the flags,
153
+ the one-line description, the console script, the argument shape, the
154
+ shorthands and whether the command changes the caller's directory. `gws`,
155
+ `gw status`, `gw st` and `gw s` reach the same function because that row says
156
+ so, and `gwh` prints the table rather than a second list that can disagree
157
+ with it.
158
+
159
+ `version` is the one row that installs no console script, and `gwh` prints a
160
+ `—` in its first column to say so. A tool's version is asked through the
161
+ tool's name, so `gw version` and `gw v` are the two spellings and no parser
162
+ carries a `--version` flag.
163
+
164
+ `_canonical()` raises at import when two rows answer to one name. A dict
165
+ comprehension would keep the last one and leave a command reachable under a
166
+ name that runs a different one.
167
+
168
+ A shorthand is for typing, so it is short and it is not clever: the first
169
+ letters of the word, or the two-letter name the same job has in git and in a
170
+ shell. `rm` and `mv` mean what they mean everywhere else.
171
+
172
+ ## Colour is a property of the stream, not of the program
173
+
174
+ `render.py` holds both halves of it: the codes, and a table whose columns line
175
+ up. `render.setup()` runs once per invocation and decides per stream.
176
+
177
+ Padding runs on the text and painting after it. The other order counts an
178
+ escape sequence as width, and every column under a coloured cell sits crooked
179
+ by exactly the length of the code.
180
+
181
+ stdout carries the path `cd $(gwa x)` reads and the JSON `jq` parses, so a
182
+ redirect, a pipe, a non-empty `NO_COLOR`, or `TERM=dumb` mean the bytes go out
183
+ as they would have without the module. `NO_COLOR=` is not a request to turn it
184
+ off: that is the no-color.org rule, and it is why `supported` tests the value
185
+ and not the key. `--json` is never painted at all:
186
+ the colour is applied at the call site that formats a table, and the JSON
187
+ paths do not pass through one.
188
+
132
189
  ## Every git command is one spec
133
190
 
134
191
  The spec is the command as you would type it, with `$name` where a value goes.
@@ -186,9 +243,8 @@ no call site can assemble its way past it. `reset --hard`, a forced `checkout`
186
243
  or `switch`, `clean -f`, a bare `push --force`, `worktree remove --force` and
187
244
  `branch -D` are refused absolutely. There is no flag, and `--yes` least of all.
188
245
 
189
- `verdicts.py` declares no mutating command. `prune.py` declares the six that
190
- mutate. A read-only command may not call one: a test reads the verbose log and
191
- asserts it.
246
+ `verdicts.py` declares no mutating command. A read-only command may not call
247
+ one: a test reads the verbose log and asserts it.
192
248
 
193
249
  ## Content cannot settle a stacked branch
194
250
 
@@ -0,0 +1,195 @@
1
+ # Changelog
2
+
3
+ Newest first. Each entry says what changed for somebody running these
4
+ commands, and the choices behind it. Dates are ISO 8601, versions follow
5
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html), and the shape
6
+ follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
7
+
8
+ An entry earns its place by being observable. If running a command produces
9
+ no different result, no different output and no different exit code, it is
10
+ not in here, whatever it cost to build.
11
+
12
+ A release closes with a `### Choices` section when a decision in it is worth
13
+ the reader's time: what was chosen, and what the alternative failed to do.
14
+
15
+ ## 0.2.1 - 2026-09-11
16
+
17
+ `--version` is gone from every command. `gw version`, or `gw v`, prints the
18
+ version this package installs; `gws --version` and the rest now fail as an
19
+ unrecognised argument, so a script reading the version back has one line to
20
+ change.
21
+
22
+ A name `gw` does not carry names the closest one that exists, instead of
23
+ printing a usage line and all twenty-two commands under an argparse error:
24
+
25
+ $ gw verison
26
+ gw: there is no command 'verison'
27
+ the closest is: gw version
28
+ gw help lists every command
29
+
30
+ ### Choices
31
+
32
+ `version` installs no binary of its own, and `gwh` prints a `—` in its
33
+ column to say so. A tenth console script, for a question asked once, would
34
+ be a name nobody types. Keeping `--version` beside it was the alternative:
35
+ two spellings of one answer, on every command, each to be completed,
36
+ documented and tested.
37
+
38
+ ## 0.2.0 - 2026-09-11
39
+
40
+ A squash-merged branch goes with its checkout, every removal names the
41
+ ignored files it deletes, `gwl` finds every worktree and `gwp` says why it
42
+ removed nothing. Every command is also reachable by a short name, output takes
43
+ colour, and both shells complete every command.
44
+
45
+ A branch the probe proved squash-merged is deleted with its worktree. It used
46
+ to survive: the verdict came from comparing content, and the delete then went
47
+ through `git branch -d`, which reads history and refuses exactly that case, so
48
+ `gwp` removed the checkout and left the branch behind with git's refusal
49
+ printed under it. The delete is now `git update-ref -d <ref> <sha>` against the
50
+ sha the verdict was formed on, so a commit landing between the verdict and the
51
+ removal fails it rather than going with it. `git branch -D` stays refused, and
52
+ so does `update-ref -d` with no sha, which is the same thing spelled longer.
53
+
54
+ Every removal names the ignored files it is about to delete, not just the ones
55
+ under `--delete-ignored`. `gwr --force` used to print a worktree, remove it,
56
+ and say nothing about the `.env` inside it; `git worktree remove` takes the
57
+ whole directory whatever flag got it there, so `--delete-ignored` decides
58
+ consent and never decides what is deleted.
59
+
60
+ `gwr` no longer calls a finished branch unfinished. A worktree holding ignored
61
+ files was refused with `is not finished: squash-merged, but holds 6 ignored
62
+ path(s)`, contradicting itself in one line, and the flag it offered was
63
+ `--force`, which keeps a branch whose work already landed and deletes those
64
+ files anyway. It now says `is finished` and names `--delete-ignored` alone.
65
+
66
+ `gwl` offers the main checkout, and never the worktree you are standing in.
67
+ In a repository with one linked worktree, standing in it, `gwl` printed
68
+ nothing and exited 0: the main checkout was filtered out of the candidates,
69
+ leaving one, which the picker took outright and handed back the path you were
70
+ already at. `gwl main` now finds the main checkout whatever branch it stands
71
+ on, two worktrees and no query go to the other one, and asking for the one
72
+ you are in says `already in <branch>` and stays put. `--list` shows every
73
+ worktree and marks that one. A repository with no linked worktrees says so
74
+ and names `gwa`, where it used to answer as though nothing were there.
75
+
76
+ `gws --json` carries `ignored` and `sha` on every verdict. The count of
77
+ ignored paths reads as a number rather than as a phrase inside `why`, and the
78
+ sha is the commit the verdict was formed against, which is the value a caller
79
+ would otherwise have to go and read for itself.
80
+
81
+ `gwp` with nothing removable prints the verdict table. It used to print
82
+ `nothing to remove; gws says why`, so the reason each worktree stayed cost a
83
+ second command to read.
84
+
85
+ `gwh` is the help, so nothing needs `gw --help` typed out. It joins `gwa`,
86
+ `gwl`, `gwm`, `gwr`, `gws`, `gwp`, `gwnb`, `gwrot` and `gw` on `$PATH`, and
87
+ `gw` still takes the same set as subcommands. `gw` with no subcommand prints
88
+ that table too, where it used to print argparse's usage. `gw` has its own
89
+ entry point now, so a usage error from it names `gw` rather than `worktrees`.
90
+
91
+ Every subcommand answers to a shorthand as well as its name: `s` and `st` for
92
+ `status`, `p` for `prune`, `a` for `add`, `l` and `ls` for `list`, `m` and
93
+ `mv` for `move`, `rm` for `remove`, `nb` and `new` for `new-branch`, `rot` for
94
+ `rotate`, `h` for `help`. One table in the CLI carries every name, so what
95
+ `gwh` prints, what the completions offer and what `gw` accepts cannot
96
+ disagree, and it refuses to import if two commands ever claim one name.
97
+
98
+ Output is coloured, and `NO_COLOR` turns it off. The check reads the value
99
+ rather than the key, which is the no-color.org rule: `NO_COLOR=` unsets the
100
+ request instead of making it. A redirect or a pipe turns colour off as well,
101
+ because `cd $(gwa x)` and `jq` read that output and neither wants escapes.
102
+
103
+ Completion covers every command in fish and in zsh, ten files per shell. The
104
+ candidates come from the CLI and never from a shell file: `gw --complete`
105
+ prints every subcommand and shorthand, `gwl --complete` and `gwr --complete`
106
+ print worktrees, each as a name and a description separated by a tab, which
107
+ fish reads directly and zsh splits for `_describe`. A command added to the
108
+ table needs no edit in either shell.
109
+
110
+ `gwl` and `gwr` do not offer the same set. `gwr` cannot remove the main
111
+ checkout, so offering it would complete to "no worktree matches"; for `gwl`
112
+ it is the one destination always there.
113
+
114
+ `gwa` says when it leaves something behind. It creates the directories before
115
+ git is asked, so a refusal used to strand an empty one silently.
116
+
117
+ The worktree picker prints destinations in full rather than abbreviating them
118
+ to `~/git/...`. The abbreviation was not what got printed and not what you
119
+ could paste.
120
+
121
+ ### Choices
122
+
123
+ A branch is deleted by `update-ref -d <ref> <sha>` rather than by `branch -D`.
124
+ Both force the delete; only one names what it expects to find, which turns a
125
+ concurrent commit into a failure instead of a loss. The guard reads that last
126
+ argument rather than counting the arguments: git takes `""` as "no old value"
127
+ and deletes the branch at exit 0, so a rule checking the shape would have
128
+ passed the one spelling that matters. `branch -d` was the
129
+ original choice and it inverts on the case this tool exists for: it reads
130
+ history, a squash merge leaves none, and deferring to it meant the content
131
+ probe bought the checkout and never the branch.
132
+
133
+ Completion flags live in the shell files, because that is the half that rots
134
+ without failing: rename a flag and nothing breaks, the candidate just stops
135
+ being offered. `test_completions.py` diffs both shells against `--help` in
136
+ both directions rather than trusting either copy, and drives every candidate
137
+ `gwr` offers back through `gwr` rather than comparing against a second copy
138
+ of the filter.
139
+
140
+ ## 0.1.0 - 2026-09-11
141
+
142
+ The first release. Eight commands for git worktrees, four of which answer a
143
+ question and print it, and four of which land you somewhere.
144
+
145
+ `gws` says which of a repository's worktrees are finished and why, in three
146
+ verdicts. `remove` was proved finished, `keep` has a reason not to be, and
147
+ `unknown` could not tell. It removes nothing and has no flag that could.
148
+
149
+ `gwp` removes the ones `gws` marks `remove`, under the same flags, printing
150
+ what goes and the command that puts it back before asking. `-y` skips the
151
+ question, and a run whose stdin is not a terminal refuses rather than
152
+ blocking.
153
+
154
+ Two failures in git are why those two exist. A squash merge makes
155
+ `git branch -d` report "not fully merged" and point at `-D`, which deletes
156
+ anything; the probe replays the branch's tree as one commit on the merge
157
+ base and asks `git cherry` whether that patch is already upstream. And
158
+ `git worktree remove` deletes `.env` and `node_modules/` at exit 0 with no
159
+ `--force` and no word about it, so ignored paths are counted and the
160
+ worktree is kept until you pass `--delete-ignored`.
161
+
162
+ Where content cannot tell, the forge is asked. A branch merged as part of a
163
+ stack is that case: its changes reach the head branch across several
164
+ squashes, so a diff cannot separate it from a branch with work left. A
165
+ merged pull request settles it, cross-checked against the commits an
166
+ upstream has not got. `--no-forge` decides from git alone.
167
+
168
+ `gwa NAME` creates a worktree at `<PARENT>/.worktrees/<NAME>/<REPO>` and
169
+ prints where it is. `gwl [QUERY]` picks one of the existing ones, taking a
170
+ single match outright and asking when there are several. `gwm NEW` renames
171
+ this worktree's branch and moves the checkout to match. `gwr` removes one
172
+ whose branch is finished, and the branch with it.
173
+
174
+ Those four change your shell's directory, which a binary cannot do, so each
175
+ ships a shell function of the same name for fish and for zsh. The function
176
+ reads the path the binary printed and does the `cd`, and carries nothing
177
+ else.
178
+
179
+ `gwnb NAME` starts a branch off the head branch as the remote has it after a
180
+ fetch. `gwrot` starts the next branch in a series, named
181
+ `<stem>-YYYY-MM-DD_NNN` at the first number free today. **Both are alpha and
182
+ may go**: they start branches rather than worktrees, `origin new-branch` and
183
+ `origin rotate` already do that, and only one of the two sets survives.
184
+
185
+ Six git commands are refused wherever they appear and whatever flags are
186
+ passed: `reset --hard`, a forced `checkout` or `switch`, `clean -f`, a bare
187
+ `push --force`, `worktree remove --force`, and `branch -D`. A branch is
188
+ deleted on git's own proof of merge or not at all.
189
+
190
+ Every command takes `--json`, so an agent reads verdicts as data rather than
191
+ parsing a table. Data goes to stdout and every diagnostic to stderr, the
192
+ prompts included, so `--json` parses in every mode. `--version` reports the
193
+ version of the installed distribution.
194
+
195
+ Python 3.11 or newer, and no dependencies.