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.
- {git_worktrees-0.1.0 → git_worktrees-0.2.1}/.github/workflows/publish.yml +15 -13
- {git_worktrees-0.1.0 → git_worktrees-0.2.1}/AGENTS.md +67 -11
- git_worktrees-0.2.1/CHANGELOG.md +195 -0
- git_worktrees-0.2.1/PKG-INFO +487 -0
- git_worktrees-0.2.1/README.md +462 -0
- git_worktrees-0.2.1/completions/gw.fish +22 -0
- git_worktrees-0.2.1/completions/gwa.fish +12 -0
- git_worktrees-0.2.1/completions/gwh.fish +5 -0
- git_worktrees-0.2.1/completions/gwl.fish +14 -0
- git_worktrees-0.2.1/completions/gwm.fish +10 -0
- git_worktrees-0.2.1/completions/gwnb.fish +12 -0
- git_worktrees-0.2.1/completions/gwp.fish +16 -0
- git_worktrees-0.2.1/completions/gwr.fish +17 -0
- git_worktrees-0.2.1/completions/gwrot.fish +12 -0
- git_worktrees-0.2.1/completions/gws.fish +15 -0
- git_worktrees-0.2.1/conf.d/worktrees.fish +3 -0
- {git_worktrees-0.1.0 → git_worktrees-0.2.1}/docs/DEVELOPMENT.md +9 -7
- {git_worktrees-0.1.0 → git_worktrees-0.2.1}/pyproject.toml +3 -2
- {git_worktrees-0.1.0 → git_worktrees-0.2.1}/scripts/verify-publish.bash +36 -8
- {git_worktrees-0.1.0 → git_worktrees-0.2.1}/src/worktrees/cli.py +423 -91
- {git_worktrees-0.1.0 → git_worktrees-0.2.1}/src/worktrees/git.py +22 -0
- {git_worktrees-0.1.0 → git_worktrees-0.2.1}/src/worktrees/pick.py +6 -1
- {git_worktrees-0.1.0 → git_worktrees-0.2.1}/src/worktrees/prune.py +20 -15
- git_worktrees-0.2.1/src/worktrees/render.py +88 -0
- {git_worktrees-0.1.0 → git_worktrees-0.2.1}/src/worktrees/repo.py +13 -2
- {git_worktrees-0.1.0 → git_worktrees-0.2.1}/src/worktrees/verdicts.py +12 -2
- {git_worktrees-0.1.0 → git_worktrees-0.2.1}/src/worktrees/worktree.py +36 -7
- {git_worktrees-0.1.0 → git_worktrees-0.2.1}/tests/test_cli.py +260 -9
- git_worktrees-0.2.1/tests/test_completions.py +189 -0
- {git_worktrees-0.1.0 → git_worktrees-0.2.1}/tests/test_gates.py +62 -1
- git_worktrees-0.2.1/tests/test_render.py +75 -0
- {git_worktrees-0.1.0 → git_worktrees-0.2.1}/tests/test_version.py +5 -5
- {git_worktrees-0.1.0 → git_worktrees-0.2.1}/tests/test_worktree.py +139 -2
- {git_worktrees-0.1.0 → git_worktrees-0.2.1}/uv.lock +1 -1
- git_worktrees-0.2.1/zsh/plugins/worktrees/completions/_gw +53 -0
- git_worktrees-0.2.1/zsh/plugins/worktrees/completions/_gwa +11 -0
- git_worktrees-0.2.1/zsh/plugins/worktrees/completions/_gwh +6 -0
- git_worktrees-0.2.1/zsh/plugins/worktrees/completions/_gwl +25 -0
- git_worktrees-0.2.1/zsh/plugins/worktrees/completions/_gwm +10 -0
- git_worktrees-0.2.1/zsh/plugins/worktrees/completions/_gwnb +11 -0
- git_worktrees-0.2.1/zsh/plugins/worktrees/completions/_gwp +15 -0
- git_worktrees-0.2.1/zsh/plugins/worktrees/completions/_gwr +28 -0
- git_worktrees-0.2.1/zsh/plugins/worktrees/completions/_gwrot +11 -0
- git_worktrees-0.2.1/zsh/plugins/worktrees/completions/_gws +14 -0
- {git_worktrees-0.1.0 → git_worktrees-0.2.1}/zsh/plugins/worktrees/worktrees.plugin.zsh +1 -1
- git_worktrees-0.1.0/CHANGELOG.md +0 -70
- git_worktrees-0.1.0/PKG-INFO +0 -335
- git_worktrees-0.1.0/README.md +0 -310
- git_worktrees-0.1.0/conf.d/worktrees.fish +0 -2
- {git_worktrees-0.1.0 → git_worktrees-0.2.1}/.agents/skills/release/SKILL.md +0 -0
- {git_worktrees-0.1.0 → git_worktrees-0.2.1}/.claude/settings.json +0 -0
- {git_worktrees-0.1.0 → git_worktrees-0.2.1}/.github/workflows/tests.yml +0 -0
- {git_worktrees-0.1.0 → git_worktrees-0.2.1}/.gitignore +0 -0
- {git_worktrees-0.1.0 → git_worktrees-0.2.1}/.pre-commit-config.yaml +0 -0
- {git_worktrees-0.1.0 → git_worktrees-0.2.1}/.python-version +0 -0
- {git_worktrees-0.1.0 → git_worktrees-0.2.1}/LICENSE +0 -0
- {git_worktrees-0.1.0 → git_worktrees-0.2.1}/functions/gwa.fish +0 -0
- {git_worktrees-0.1.0 → git_worktrees-0.2.1}/functions/gwl.fish +0 -0
- {git_worktrees-0.1.0 → git_worktrees-0.2.1}/functions/gwm.fish +0 -0
- {git_worktrees-0.1.0 → git_worktrees-0.2.1}/functions/gwr.fish +0 -0
- {git_worktrees-0.1.0 → git_worktrees-0.2.1}/renovate.json +0 -0
- {git_worktrees-0.1.0 → git_worktrees-0.2.1}/src/worktrees/__init__.py +0 -0
- {git_worktrees-0.1.0 → git_worktrees-0.2.1}/src/worktrees/forge.py +0 -0
- {git_worktrees-0.1.0 → git_worktrees-0.2.1}/src/worktrees/layout.py +0 -0
- {git_worktrees-0.1.0 → git_worktrees-0.2.1}/src/worktrees/merged.py +0 -0
- {git_worktrees-0.1.0 → git_worktrees-0.2.1}/src/worktrees/new_branch.py +0 -0
- {git_worktrees-0.1.0 → git_worktrees-0.2.1}/src/worktrees/rotate.py +0 -0
- {git_worktrees-0.1.0 → git_worktrees-0.2.1}/tests/conftest.py +0 -0
- {git_worktrees-0.1.0 → git_worktrees-0.2.1}/tests/test_forge.py +0 -0
- {git_worktrees-0.1.0 → git_worktrees-0.2.1}/tests/test_new_branch.py +0 -0
- {git_worktrees-0.1.0 → git_worktrees-0.2.1}/tests/test_rotate.py +0 -0
- {git_worktrees-0.1.0 → git_worktrees-0.2.1}/tests/test_shims.py +0 -0
- {git_worktrees-0.1.0 → git_worktrees-0.2.1}/tests/test_spec.py +0 -0
- {git_worktrees-0.1.0 → git_worktrees-0.2.1}/zsh/plugins/worktrees/functions/gwa +0 -0
- {git_worktrees-0.1.0 → git_worktrees-0.2.1}/zsh/plugins/worktrees/functions/gwl +0 -0
- {git_worktrees-0.1.0 → git_worktrees-0.2.1}/zsh/plugins/worktrees/functions/gwm +0 -0
- {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
|
|
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
|
-
|
|
77
|
-
GH_REPO
|
|
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
|
-
|
|
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,
|
|
171
|
-
#
|
|
172
|
-
#
|
|
173
|
-
|
|
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
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
105
|
-
2 add-tests
|
|
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
|
|
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.
|
|
190
|
-
|
|
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.
|