git-worktrees 0.2.0__tar.gz → 0.2.3__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.2.0 → git_worktrees-0.2.3}/.github/workflows/publish.yml +39 -14
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/.github/workflows/tests.yml +7 -1
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/.gitignore +3 -0
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/AGENTS.md +27 -2
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/CHANGELOG.md +56 -0
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/PKG-INFO +23 -7
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/README.md +21 -6
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/completions/gw.fish +0 -1
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/completions/gwa.fish +0 -1
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/completions/gwh.fish +0 -1
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/completions/gwl.fish +0 -1
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/completions/gwm.fish +0 -1
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/completions/gwnb.fish +0 -1
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/completions/gwp.fish +0 -1
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/completions/gwr.fish +1 -1
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/completions/gwrot.fish +0 -1
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/completions/gws.fish +0 -1
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/docs/DEVELOPMENT.md +7 -0
- git_worktrees-0.2.3/hatch_build.py +49 -0
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/pyproject.toml +16 -3
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/scripts/verify-publish.bash +37 -8
- git_worktrees-0.2.3/src/worktrees/__init__.py +15 -0
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/src/worktrees/cli.py +111 -40
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/src/worktrees/forge.py +8 -2
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/src/worktrees/git.py +89 -46
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/src/worktrees/new_branch.py +10 -5
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/src/worktrees/prune.py +36 -0
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/src/worktrees/repo.py +37 -25
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/src/worktrees/rotate.py +6 -3
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/src/worktrees/verdicts.py +5 -2
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/src/worktrees/worktree.py +8 -5
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/tests/test_cli.py +152 -2
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/tests/test_completions.py +1 -1
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/tests/test_gates.py +53 -2
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/tests/test_shims.py +16 -3
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/tests/test_version.py +35 -5
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/uv.lock +36 -1
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/zsh/plugins/worktrees/completions/_gw +0 -1
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/zsh/plugins/worktrees/completions/_gwa +0 -1
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/zsh/plugins/worktrees/completions/_gwh +0 -1
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/zsh/plugins/worktrees/completions/_gwl +0 -1
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/zsh/plugins/worktrees/completions/_gwm +0 -1
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/zsh/plugins/worktrees/completions/_gwnb +0 -1
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/zsh/plugins/worktrees/completions/_gwp +0 -1
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/zsh/plugins/worktrees/completions/_gwr +1 -1
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/zsh/plugins/worktrees/completions/_gwrot +0 -1
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/zsh/plugins/worktrees/completions/_gws +0 -1
- git_worktrees-0.2.0/src/worktrees/__init__.py +0 -8
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/.agents/skills/release/SKILL.md +0 -0
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/.claude/settings.json +0 -0
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/.pre-commit-config.yaml +0 -0
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/.python-version +0 -0
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/LICENSE +0 -0
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/conf.d/worktrees.fish +0 -0
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/functions/gwa.fish +0 -0
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/functions/gwl.fish +0 -0
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/functions/gwm.fish +0 -0
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/functions/gwr.fish +0 -0
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/renovate.json +0 -0
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/src/worktrees/layout.py +0 -0
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/src/worktrees/merged.py +0 -0
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/src/worktrees/pick.py +0 -0
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/src/worktrees/render.py +0 -0
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/tests/conftest.py +0 -0
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/tests/test_forge.py +0 -0
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/tests/test_new_branch.py +0 -0
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/tests/test_render.py +0 -0
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/tests/test_rotate.py +0 -0
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/tests/test_spec.py +0 -0
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/tests/test_worktree.py +0 -0
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/zsh/plugins/worktrees/functions/gwa +0 -0
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/zsh/plugins/worktrees/functions/gwl +0 -0
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/zsh/plugins/worktrees/functions/gwm +0 -0
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/zsh/plugins/worktrees/functions/gwr +0 -0
- {git_worktrees-0.2.0 → git_worktrees-0.2.3}/zsh/plugins/worktrees/worktrees.plugin.zsh +0 -0
|
@@ -5,6 +5,7 @@ name: Publish
|
|
|
5
5
|
# so a rename means re-registering the publisher on PyPI first.
|
|
6
6
|
#
|
|
7
7
|
# build -> publish-test -> publish -> release
|
|
8
|
+
# \-> verify
|
|
8
9
|
#
|
|
9
10
|
# Nothing here creates a tag. A release is a version bump merged into main and
|
|
10
11
|
# then tagged by hand, so the irreversible step is the last one and everything
|
|
@@ -74,19 +75,28 @@ jobs:
|
|
|
74
75
|
# release that should go ahead. The API needs no local history.
|
|
75
76
|
- name: Check the commit is on main
|
|
76
77
|
env:
|
|
77
|
-
|
|
78
|
-
GH_REPO
|
|
78
|
+
# GH_REPO carries the value the runner publishes as GITHUB_REPOSITORY: gh looks for
|
|
79
|
+
# GH_REPO and never at GITHUB_REPOSITORY. GITHUB_TOKEN is not a default variable at
|
|
80
|
+
# all, so the token has to be handed over rather than renamed.
|
|
81
|
+
GH_TOKEN: ${{ github.token }} # no runner default
|
|
82
|
+
GH_REPO: ${{ github.repository }} # = $GITHUB_REPOSITORY; gh reads GH_REPO
|
|
79
83
|
run: rt github::assert_on_branch main
|
|
80
84
|
|
|
81
85
|
- name: Wait for the tests on this commit
|
|
82
86
|
env:
|
|
83
|
-
GH_TOKEN: ${{ github.token }}
|
|
84
|
-
GH_REPO: ${{ github.repository }}
|
|
85
|
-
|
|
87
|
+
GH_TOKEN: ${{ github.token }} # no runner default
|
|
88
|
+
GH_REPO: ${{ github.repository }} # = $GITHUB_REPOSITORY; gh reads GH_REPO
|
|
89
|
+
SHA: ${{ github.sha }} # = $GITHUB_SHA
|
|
90
|
+
run: rt github::await_workflow "$SHA" tests.yml
|
|
86
91
|
|
|
87
92
|
- name: Install dependencies
|
|
88
93
|
run: uv sync --all-extras --locked
|
|
89
94
|
|
|
95
|
+
# Neither shell is on the image, and the shim tests fail rather than
|
|
96
|
+
# skip under CI, so this job needs them exactly as tests.yml does.
|
|
97
|
+
- name: Install fish and zsh
|
|
98
|
+
run: sudo apt-get update && sudo apt-get install -y fish zsh
|
|
99
|
+
|
|
90
100
|
# The tag is the last chance to catch a regression, and it costs 20s.
|
|
91
101
|
- name: Run tests
|
|
92
102
|
run: uv run --no-sync pytest tests -q
|
|
@@ -162,18 +172,33 @@ jobs:
|
|
|
162
172
|
name: dist
|
|
163
173
|
path: dist/
|
|
164
174
|
|
|
165
|
-
- name: Install the release tools
|
|
166
|
-
uses: releasetools/cli@v0
|
|
167
|
-
|
|
168
175
|
- name: Publish
|
|
169
176
|
run: uv publish --trusted-publishing always
|
|
170
177
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
178
|
+
# Uploaded and installable are two claims with two timings, so they are two
|
|
179
|
+
# jobs. An index serves what it has accepted only after a delay of its own
|
|
180
|
+
# choosing: 0.2.1 took longer than the script waited, and a release that had
|
|
181
|
+
# published correctly went red and had to be re-run by hand. This job holds
|
|
182
|
+
# the second claim, `release` needs only the first, and a red `verify` says
|
|
183
|
+
# PyPI is slow rather than that the version is not out.
|
|
184
|
+
verify:
|
|
185
|
+
name: Verify it installs from PyPI
|
|
186
|
+
runs-on: ubuntu-latest
|
|
187
|
+
needs: publish
|
|
188
|
+
permissions:
|
|
189
|
+
contents: read
|
|
190
|
+
steps:
|
|
191
|
+
# The script reads the name and version from pyproject.toml through
|
|
192
|
+
# `uv version`, and installs from the index rather than from the
|
|
193
|
+
# artifact, so there is nothing to download here.
|
|
194
|
+
- name: Checkout code
|
|
195
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
196
|
+
|
|
197
|
+
- name: Set up uv
|
|
198
|
+
uses: astral-sh/setup-uv@ae62891fec2bb8e7d6c99fc78c9fec3a63790f8d # v10.0.0
|
|
199
|
+
with:
|
|
200
|
+
python-version: "3.14"
|
|
201
|
+
enable-cache: true
|
|
177
202
|
|
|
178
203
|
- name: Verify it installs from PyPI
|
|
179
204
|
run: scripts/verify-publish.bash --prod
|
|
@@ -67,11 +67,17 @@ jobs:
|
|
|
67
67
|
# resolving something the lock file never described.
|
|
68
68
|
run: uv sync --all-extras --locked
|
|
69
69
|
|
|
70
|
+
# Neither shell is on the image. Without them the nine assertions that
|
|
71
|
+
# drive a real fish or zsh call pytest.skip, which is invisible in
|
|
72
|
+
# `pytest -q`, so the run goes green without either shim ever running.
|
|
73
|
+
- name: Install fish and zsh
|
|
74
|
+
run: sudo apt-get update && sudo apt-get install -y fish zsh
|
|
75
|
+
|
|
70
76
|
# The suite drives real git: it builds repositories, adds worktrees and
|
|
71
77
|
# removes them. Recording the version makes a failure that is git's
|
|
72
78
|
# rather than ours readable from the log alone.
|
|
73
79
|
- name: Record the git version
|
|
74
|
-
run: git --version
|
|
80
|
+
run: git --version && fish --version && zsh --version
|
|
75
81
|
|
|
76
82
|
- name: Run tests
|
|
77
83
|
run: uv run --no-sync pytest tests -q
|
|
@@ -156,6 +156,11 @@ shorthands and whether the command changes the caller's directory. `gws`,
|
|
|
156
156
|
so, and `gwh` prints the table rather than a second list that can disagree
|
|
157
157
|
with it.
|
|
158
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
|
+
|
|
159
164
|
`_canonical()` raises at import when two rows answer to one name. A dict
|
|
160
165
|
comprehension would keep the last one and leave a command reachable under a
|
|
161
166
|
name that runs a different one.
|
|
@@ -233,10 +238,30 @@ tested on what it returns rather than on its exit code. `upstream_of` takes
|
|
|
233
238
|
`ok=(0, 1, 128)`, so a typo in one of its flags exits 128, reads as "no
|
|
234
239
|
upstream", and turns every branch into `unknown` with nothing failing.
|
|
235
240
|
|
|
241
|
+
The two calls that reach the network, `fetch` and `remote set-head --auto`,
|
|
242
|
+
take `ok=(0, 128)` for the same reason in reverse: a remote that cannot be
|
|
243
|
+
reached is an answer rather than a failure. The caller reads the result, says
|
|
244
|
+
so on stderr, and carries on with the refs already here, which is where
|
|
245
|
+
`--no-fetch` sends it on request. It also stops asking: a failed fetch means
|
|
246
|
+
the head-branch ladder is told it is offline, rather than spending a second
|
|
247
|
+
round trip proving it.
|
|
248
|
+
|
|
249
|
+
One `git status --porcelain --ignored=traditional` answers both questions a
|
|
250
|
+
worktree is asked. It is a strict superset of plain `--porcelain`, so `dirty`
|
|
251
|
+
is "a line that does not start with `!! `" and `ignored` is the rest. Asking
|
|
252
|
+
both ways was 18 of an assessment's 35 calls over nine worktrees.
|
|
253
|
+
|
|
236
254
|
`guard()` runs on the resolved argv inside the wrapper, not at declaration, so
|
|
237
255
|
no call site can assemble its way past it. `reset --hard`, a forced `checkout`
|
|
238
|
-
or `switch`, `clean -f`, a bare `push --force`, `worktree remove --force
|
|
239
|
-
`branch -D`
|
|
256
|
+
or `switch`, `clean -f`, a bare `push --force`, `worktree remove --force`,
|
|
257
|
+
`branch -D` and an `update-ref` delete that names no full sha are refused
|
|
258
|
+
absolutely. There is no flag, and `--yes` least of all.
|
|
259
|
+
|
|
260
|
+
Those seven are `RULES`, a tuple, each row carrying the name `--explain`
|
|
261
|
+
prints, the clause the refusal ends with, and the test. The footer `--explain`
|
|
262
|
+
prints is built from it. A rule written in one place and described in another
|
|
263
|
+
is a rule that goes quiet: the hand-written footer fell a rule behind the
|
|
264
|
+
moment `update-ref` was added, and nothing failed.
|
|
240
265
|
|
|
241
266
|
`verdicts.py` declares no mutating command. A read-only command may not call
|
|
242
267
|
one: a test reads the verbose log and asserts it.
|
|
@@ -12,6 +12,62 @@ not in here, whatever it cost to build.
|
|
|
12
12
|
A release closes with a `### Choices` section when a decision in it is worth
|
|
13
13
|
the reader's time: what was chosen, and what the alternative failed to do.
|
|
14
14
|
|
|
15
|
+
## 0.2.3 - 2026-09-11
|
|
16
|
+
|
|
17
|
+
An unreachable remote is an answer rather than a failure. Every command that
|
|
18
|
+
touches the network used to exit 1 with git's own message, so the warnings
|
|
19
|
+
written for the offline case never reached anybody. A failed fetch now says
|
|
20
|
+
the refs are the ones already here, and the assessment carries on.
|
|
21
|
+
|
|
22
|
+
Refusing to prompt exits 3 everywhere. `gwp` used to exit 2 when stdin was
|
|
23
|
+
not a terminal, which is argparse's code for a usage error, so a script could
|
|
24
|
+
not tell a refusal from a typo.
|
|
25
|
+
|
|
26
|
+
Removing a worktree names the stash entries made on its branch. A stash
|
|
27
|
+
outlives the branch while its subject goes on naming it, which is how it
|
|
28
|
+
becomes unfindable.
|
|
29
|
+
|
|
30
|
+
`gwr` asks the forge once, about the branch it is removing, instead of about
|
|
31
|
+
every worktree: three round trips became one in a three-worktree repository.
|
|
32
|
+
`--no-forge` turns it off there, as it already did for `gws` and `gwp`.
|
|
33
|
+
|
|
34
|
+
`--explain` lists all seven refusals the guard enforces. The footer was
|
|
35
|
+
written by hand and had been a rule behind since `update-ref` was added.
|
|
36
|
+
|
|
37
|
+
An invocation costs about 15 ms less, and an assessment makes half as many
|
|
38
|
+
`git status` calls.
|
|
39
|
+
|
|
40
|
+
### Choices
|
|
41
|
+
|
|
42
|
+
The version is written into the wheel at build time rather than read back
|
|
43
|
+
through `importlib.metadata`, which cost 10 ms of a 67 ms invocation for a
|
|
44
|
+
string two commands print. Deriving it from the tag with `hatch-vcs` was the
|
|
45
|
+
alternative, and it inverts which of the tag and `pyproject.toml` has to
|
|
46
|
+
agree with the other.
|
|
47
|
+
|
|
48
|
+
## 0.2.1 - 2026-09-11
|
|
49
|
+
|
|
50
|
+
`--version` is gone from every command. `gw version`, or `gw v`, prints the
|
|
51
|
+
version this package installs; `gws --version` and the rest now fail as an
|
|
52
|
+
unrecognised argument, so a script reading the version back has one line to
|
|
53
|
+
change.
|
|
54
|
+
|
|
55
|
+
A name `gw` does not carry names the closest one that exists, instead of
|
|
56
|
+
printing a usage line and all twenty-two commands under an argparse error:
|
|
57
|
+
|
|
58
|
+
$ gw verison
|
|
59
|
+
gw: there is no command 'verison'
|
|
60
|
+
the closest is: gw version
|
|
61
|
+
gw help lists every command
|
|
62
|
+
|
|
63
|
+
### Choices
|
|
64
|
+
|
|
65
|
+
`version` installs no binary of its own, and `gwh` prints a `—` in its
|
|
66
|
+
column to say so. A tenth console script, for a question asked once, would
|
|
67
|
+
be a name nobody types. Keeping `--version` beside it was the alternative:
|
|
68
|
+
two spellings of one answer, on every command, each to be completed,
|
|
69
|
+
documented and tested.
|
|
70
|
+
|
|
15
71
|
## 0.2.0 - 2026-09-11
|
|
16
72
|
|
|
17
73
|
A squash-merged branch goes with its checkout, every removal names the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: git-worktrees
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
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
|
|
@@ -19,6 +19,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.14
|
|
20
20
|
Requires-Python: >=3.11
|
|
21
21
|
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: hatchling==1.28.0; extra == 'dev'
|
|
22
23
|
Requires-Dist: pre-commit==4.6.1; extra == 'dev'
|
|
23
24
|
Requires-Dist: pytest==9.1.1; extra == 'dev'
|
|
24
25
|
Description-Content-Type: text/markdown
|
|
@@ -222,6 +223,12 @@ function returns that set for both. What goes and what puts it back is printed
|
|
|
222
223
|
before anything does, and neither `--quiet` nor `--yes` silences it. The sha in
|
|
223
224
|
the restore line is full length, because that line is meant to be pasted.
|
|
224
225
|
|
|
226
|
+
A stash made on a branch that is about to go is named too. `refs/stash` is
|
|
227
|
+
per-repository and pins its own commits, so the entry survives the branch and
|
|
228
|
+
`git stash branch <new> stash@{0}` still recovers it. What does not survive is
|
|
229
|
+
the branch name in its subject, so the line says which entry is about to start
|
|
230
|
+
pointing at nothing.
|
|
231
|
+
|
|
225
232
|
The checkout goes through plain `git worktree remove`, so every refusal git
|
|
226
233
|
makes still applies. The branch goes through `git update-ref -d`, naming the
|
|
227
234
|
sha the verdict was formed against: a branch somebody committed to in between
|
|
@@ -240,7 +247,8 @@ squash-merged /home/you/git/.worktrees/squash-merged/repo
|
|
|
240
247
|
not a terminal, so nothing can answer for the 1 above; pass --yes to remove them
|
|
241
248
|
```
|
|
242
249
|
|
|
243
|
-
That exits
|
|
250
|
+
That exits 3 and touches nothing, the code every refusal uses. `gwp --yes --json`
|
|
251
|
+
is what an agent runs.
|
|
244
252
|
|
|
245
253
|
With nothing to remove it prints the table `gws` prints, rather than the name
|
|
246
254
|
of the command that would have printed it. The reason each worktree stayed is
|
|
@@ -404,6 +412,7 @@ gwr [QUERY] gw remove (rm) remove one whose branch is finished
|
|
|
404
412
|
gwnb NAME gw new-branch (nb, new) branch off the head branch (alpha)
|
|
405
413
|
gwrot gw rotate (rot) the next branch in a series (alpha)
|
|
406
414
|
gwh gw help (h) every command and alias, this list
|
|
415
|
+
— gw version (v) the version this package installs
|
|
407
416
|
```
|
|
408
417
|
|
|
409
418
|
`gw help`, `gw h` and `gw` with no subcommand print the same thing. One table
|
|
@@ -412,21 +421,26 @@ cannot disagree, and it raises at import if two commands ever claim one name.
|
|
|
412
421
|
|
|
413
422
|
## Flags
|
|
414
423
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
424
|
+
`gw version`, or `gw v`, prints the version. No command takes a `--version`
|
|
425
|
+
flag. Every command but that one and `gwh`, which
|
|
426
|
+
print one thing each and have nothing to be quiet about, takes `--json`, `-q`,
|
|
427
|
+
`-v` and `--explain`. Beyond those:
|
|
418
428
|
|
|
419
429
|
| | |
|
|
420
430
|
| --- | --- |
|
|
421
431
|
| `gws` | `--branch NAME`, `--no-fetch`, `--delete-ignored`, `--no-forge` |
|
|
422
432
|
| `gwp` | those four, and `-y` |
|
|
423
|
-
| `gwr` | `-f`, `--delete-ignored`, `--no-fetch`, `-y` |
|
|
433
|
+
| `gwr` | `-f`, `--delete-ignored`, `--no-fetch`, `--no-forge`, `-y` |
|
|
424
434
|
| `gwa`, `gwnb`, `gwrot` | `--no-fetch` |
|
|
425
435
|
| `gwl` | `-l` |
|
|
426
436
|
| `gwm`, `gwh` | none |
|
|
427
437
|
|
|
428
438
|
`gw <command> --help` prints one command's own list.
|
|
429
439
|
|
|
440
|
+
A remote that cannot be reached is not fatal anywhere. The fetch fails, the
|
|
441
|
+
command says so on stderr, and it answers from the refs already here, which is
|
|
442
|
+
where `--no-fetch` sends it on request.
|
|
443
|
+
|
|
430
444
|
Data goes to stdout and diagnostics to stderr, the prompt included, so `--json`
|
|
431
445
|
is parseable in every mode. Colour is decided per stream and only for a
|
|
432
446
|
terminal, so a redirect, a pipe, a non-empty `NO_COLOR` or `TERM=dumb` give the
|
|
@@ -458,12 +472,14 @@ and whatever a caller asks for:
|
|
|
458
472
|
```
|
|
459
473
|
reset --hard a forced checkout or switch clean -f
|
|
460
474
|
push --force worktree remove --force branch -D
|
|
475
|
+
an update-ref delete that names no full sha
|
|
461
476
|
```
|
|
462
477
|
|
|
463
478
|
The check runs on the argument list as it is about to be handed to git, so no
|
|
464
479
|
code path can assemble its way past one. `gws --explain` prints every git
|
|
465
480
|
command the program can issue, marking the ones that take the repository's
|
|
466
|
-
shared refs and therefore run serially
|
|
481
|
+
shared refs and therefore run serially, and ends with that list of seven,
|
|
482
|
+
generated from the rules rather than written out beside them.
|
|
467
483
|
|
|
468
484
|
`--force-with-lease` is not `--force` and is allowed, and so is
|
|
469
485
|
`update-ref -d <ref> <sha>`, which is how a branch is deleted here: against the
|
|
@@ -197,6 +197,12 @@ function returns that set for both. What goes and what puts it back is printed
|
|
|
197
197
|
before anything does, and neither `--quiet` nor `--yes` silences it. The sha in
|
|
198
198
|
the restore line is full length, because that line is meant to be pasted.
|
|
199
199
|
|
|
200
|
+
A stash made on a branch that is about to go is named too. `refs/stash` is
|
|
201
|
+
per-repository and pins its own commits, so the entry survives the branch and
|
|
202
|
+
`git stash branch <new> stash@{0}` still recovers it. What does not survive is
|
|
203
|
+
the branch name in its subject, so the line says which entry is about to start
|
|
204
|
+
pointing at nothing.
|
|
205
|
+
|
|
200
206
|
The checkout goes through plain `git worktree remove`, so every refusal git
|
|
201
207
|
makes still applies. The branch goes through `git update-ref -d`, naming the
|
|
202
208
|
sha the verdict was formed against: a branch somebody committed to in between
|
|
@@ -215,7 +221,8 @@ squash-merged /home/you/git/.worktrees/squash-merged/repo
|
|
|
215
221
|
not a terminal, so nothing can answer for the 1 above; pass --yes to remove them
|
|
216
222
|
```
|
|
217
223
|
|
|
218
|
-
That exits
|
|
224
|
+
That exits 3 and touches nothing, the code every refusal uses. `gwp --yes --json`
|
|
225
|
+
is what an agent runs.
|
|
219
226
|
|
|
220
227
|
With nothing to remove it prints the table `gws` prints, rather than the name
|
|
221
228
|
of the command that would have printed it. The reason each worktree stayed is
|
|
@@ -379,6 +386,7 @@ gwr [QUERY] gw remove (rm) remove one whose branch is finished
|
|
|
379
386
|
gwnb NAME gw new-branch (nb, new) branch off the head branch (alpha)
|
|
380
387
|
gwrot gw rotate (rot) the next branch in a series (alpha)
|
|
381
388
|
gwh gw help (h) every command and alias, this list
|
|
389
|
+
— gw version (v) the version this package installs
|
|
382
390
|
```
|
|
383
391
|
|
|
384
392
|
`gw help`, `gw h` and `gw` with no subcommand print the same thing. One table
|
|
@@ -387,21 +395,26 @@ cannot disagree, and it raises at import if two commands ever claim one name.
|
|
|
387
395
|
|
|
388
396
|
## Flags
|
|
389
397
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
398
|
+
`gw version`, or `gw v`, prints the version. No command takes a `--version`
|
|
399
|
+
flag. Every command but that one and `gwh`, which
|
|
400
|
+
print one thing each and have nothing to be quiet about, takes `--json`, `-q`,
|
|
401
|
+
`-v` and `--explain`. Beyond those:
|
|
393
402
|
|
|
394
403
|
| | |
|
|
395
404
|
| --- | --- |
|
|
396
405
|
| `gws` | `--branch NAME`, `--no-fetch`, `--delete-ignored`, `--no-forge` |
|
|
397
406
|
| `gwp` | those four, and `-y` |
|
|
398
|
-
| `gwr` | `-f`, `--delete-ignored`, `--no-fetch`, `-y` |
|
|
407
|
+
| `gwr` | `-f`, `--delete-ignored`, `--no-fetch`, `--no-forge`, `-y` |
|
|
399
408
|
| `gwa`, `gwnb`, `gwrot` | `--no-fetch` |
|
|
400
409
|
| `gwl` | `-l` |
|
|
401
410
|
| `gwm`, `gwh` | none |
|
|
402
411
|
|
|
403
412
|
`gw <command> --help` prints one command's own list.
|
|
404
413
|
|
|
414
|
+
A remote that cannot be reached is not fatal anywhere. The fetch fails, the
|
|
415
|
+
command says so on stderr, and it answers from the refs already here, which is
|
|
416
|
+
where `--no-fetch` sends it on request.
|
|
417
|
+
|
|
405
418
|
Data goes to stdout and diagnostics to stderr, the prompt included, so `--json`
|
|
406
419
|
is parseable in every mode. Colour is decided per stream and only for a
|
|
407
420
|
terminal, so a redirect, a pipe, a non-empty `NO_COLOR` or `TERM=dumb` give the
|
|
@@ -433,12 +446,14 @@ and whatever a caller asks for:
|
|
|
433
446
|
```
|
|
434
447
|
reset --hard a forced checkout or switch clean -f
|
|
435
448
|
push --force worktree remove --force branch -D
|
|
449
|
+
an update-ref delete that names no full sha
|
|
436
450
|
```
|
|
437
451
|
|
|
438
452
|
The check runs on the argument list as it is about to be handed to git, so no
|
|
439
453
|
code path can assemble its way past one. `gws --explain` prints every git
|
|
440
454
|
command the program can issue, marking the ones that take the repository's
|
|
441
|
-
shared refs and therefore run serially
|
|
455
|
+
shared refs and therefore run serially, and ends with that list of seven,
|
|
456
|
+
generated from the rules rather than written out beside them.
|
|
442
457
|
|
|
443
458
|
`--force-with-lease` is not `--force` and is allowed, and so is
|
|
444
459
|
`update-ref -d <ref> <sha>`, which is how a branch is deleted here: against the
|
|
@@ -19,5 +19,4 @@ complete -c gw -n 'not __fish_use_subcommand' -s q -l quiet -d 'less output'
|
|
|
19
19
|
complete -c gw -n 'not __fish_use_subcommand' -s v -l verbose -d 'print every git command'
|
|
20
20
|
complete -c gw -n 'not __fish_use_subcommand' -l explain -d 'print every git command and exit'
|
|
21
21
|
|
|
22
|
-
complete -c gw -l version -d "show program's version number and exit"
|
|
23
22
|
complete -c gw -s h -l help -d 'show this help message and exit'
|
|
@@ -9,5 +9,4 @@ complete -c gwa -s q -l quiet -d 'the path alone'
|
|
|
9
9
|
complete -c gwa -s v -l verbose -d 'print every git command'
|
|
10
10
|
complete -c gwa -l explain -d 'print every git command and exit'
|
|
11
11
|
|
|
12
|
-
complete -c gwa -l version -d 'show the version and exit'
|
|
13
12
|
complete -c gwa -s h -l help -d 'show the help and exit'
|
|
@@ -11,5 +11,4 @@ complete -c gwl -s q -l quiet -d 'the path alone'
|
|
|
11
11
|
complete -c gwl -s v -l verbose -d 'print every git command'
|
|
12
12
|
complete -c gwl -l explain -d 'print every git command and exit'
|
|
13
13
|
|
|
14
|
-
complete -c gwl -l version -d 'show the version and exit'
|
|
15
14
|
complete -c gwl -s h -l help -d 'show the help and exit'
|
|
@@ -7,5 +7,4 @@ complete -c gwm -s q -l quiet -d 'the path alone'
|
|
|
7
7
|
complete -c gwm -s v -l verbose -d 'print every git command'
|
|
8
8
|
complete -c gwm -l explain -d 'print every git command and exit'
|
|
9
9
|
|
|
10
|
-
complete -c gwm -l version -d 'show the version and exit'
|
|
11
10
|
complete -c gwm -s h -l help -d 'show the help and exit'
|
|
@@ -9,5 +9,4 @@ complete -c gwnb -s q -l quiet -d 'say nothing on success'
|
|
|
9
9
|
complete -c gwnb -s v -l verbose -d 'print every git command'
|
|
10
10
|
complete -c gwnb -l explain -d 'print every git command and exit'
|
|
11
11
|
|
|
12
|
-
complete -c gwnb -l version -d 'show the version and exit'
|
|
13
12
|
complete -c gwnb -s h -l help -d 'show the help and exit'
|
|
@@ -13,5 +13,4 @@ complete -c gwp -s q -l quiet -d 'verdicts only'
|
|
|
13
13
|
complete -c gwp -s v -l verbose -d 'print every git command'
|
|
14
14
|
complete -c gwp -l explain -d 'print every git command and exit'
|
|
15
15
|
|
|
16
|
-
complete -c gwp -l version -d 'show the version and exit'
|
|
17
16
|
complete -c gwp -s h -l help -d 'show the help and exit'
|
|
@@ -7,6 +7,7 @@ complete -c gwr -a '(command gwr --complete)'
|
|
|
7
7
|
complete -c gwr -s f -l force -d 'remove it even when unfinished'
|
|
8
8
|
complete -c gwr -l delete-ignored -d 'also delete its gitignored files'
|
|
9
9
|
complete -c gwr -l no-fetch -d 'use the refs already here'
|
|
10
|
+
complete -c gwr -l no-forge -d 'decide from git alone; never ask the forge'
|
|
10
11
|
complete -c gwr -s y -l yes -d 'do not ask'
|
|
11
12
|
|
|
12
13
|
complete -c gwr -l json -d 'the result as data'
|
|
@@ -14,5 +15,4 @@ complete -c gwr -s q -l quiet -d 'the path alone'
|
|
|
14
15
|
complete -c gwr -s v -l verbose -d 'print every git command'
|
|
15
16
|
complete -c gwr -l explain -d 'print every git command and exit'
|
|
16
17
|
|
|
17
|
-
complete -c gwr -l version -d 'show the version and exit'
|
|
18
18
|
complete -c gwr -s h -l help -d 'show the help and exit'
|
|
@@ -9,5 +9,4 @@ complete -c gwrot -s q -l quiet -d 'say nothing on success'
|
|
|
9
9
|
complete -c gwrot -s v -l verbose -d 'print every git command'
|
|
10
10
|
complete -c gwrot -l explain -d 'print every git command and exit'
|
|
11
11
|
|
|
12
|
-
complete -c gwrot -l version -d 'show the version and exit'
|
|
13
12
|
complete -c gwrot -s h -l help -d 'show the help and exit'
|
|
@@ -12,5 +12,4 @@ complete -c gws -s q -l quiet -d 'verdicts only'
|
|
|
12
12
|
complete -c gws -s v -l verbose -d 'print every git command'
|
|
13
13
|
complete -c gws -l explain -d 'print every git command and exit'
|
|
14
14
|
|
|
15
|
-
complete -c gws -l version -d 'show the version and exit'
|
|
16
15
|
complete -c gws -s h -l help -d 'show the help and exit'
|
|
@@ -182,6 +182,7 @@ Pushing the tag starts `publish.yml`:
|
|
|
182
182
|
|
|
183
183
|
```
|
|
184
184
|
build ──> publish-test ──> publish ──> release
|
|
185
|
+
└─> verify
|
|
185
186
|
```
|
|
186
187
|
|
|
187
188
|
`build` runs three guards before it does anything, cheapest first. The tag has
|
|
@@ -196,6 +197,12 @@ replaced, so a failed rehearsal stops the run while there is still nothing to
|
|
|
196
197
|
pin against. The merge happens first and the irreversible step is last, so
|
|
197
198
|
everything recoverable is already done by the time anything is published.
|
|
198
199
|
|
|
200
|
+
`release` needs the upload and nothing else. `verify` installs the version
|
|
201
|
+
from PyPI and runs what it installed, on its own, because an index serves what
|
|
202
|
+
it has accepted after a delay it does not bound: 0.2.1 took longer than the
|
|
203
|
+
script waited and a correct release went red. A red `verify` says PyPI is
|
|
204
|
+
slow, not that the version is missing. It gives up after about 32 minutes.
|
|
205
|
+
|
|
199
206
|
`workflow_dispatch` against a tag ref re-runs a release whose publish failed.
|
|
200
207
|
|
|
201
208
|
No API token is stored anywhere. Both uploads use PyPI Trusted Publishing over
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Write the version `pyproject.toml` declares into the package it builds.
|
|
2
|
+
|
|
3
|
+
Reading it back through `importlib.metadata` at import instead costs 10 ms of
|
|
4
|
+
a 67 ms invocation, on every command, for a string two of them print. A
|
|
5
|
+
constant in the wheel costs nothing, and `pyproject.toml` stays the one place
|
|
6
|
+
the version is written.
|
|
7
|
+
|
|
8
|
+
Not `hatch-vcs`: deriving the version from a tag inverts which of the two has
|
|
9
|
+
to agree with the other, and `publish.yml` refuses a tag whose commit carries
|
|
10
|
+
a different version.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
|
|
19
|
+
|
|
20
|
+
GENERATED = Path("src") / "worktrees" / "_version.py"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def rendered(version: str) -> str:
|
|
24
|
+
"""The file's whole contents."""
|
|
25
|
+
return (
|
|
26
|
+
'"""Written at build time. See hatch_build.py."""\n'
|
|
27
|
+
f'\n__version__ = "{version}"\n'
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def generate(root: Path, version: str, build_data: dict[str, Any]) -> Path:
|
|
32
|
+
"""Write the file, and tell the builder to ship it.
|
|
33
|
+
|
|
34
|
+
Both halves, because either alone is silent. `.gitignore` keeps the
|
|
35
|
+
generated file out of the repository and hatchling reads `.gitignore`, so
|
|
36
|
+
without the artifacts entry the wheel ships without it and every install
|
|
37
|
+
falls back to the metadata lookup with nothing to say so.
|
|
38
|
+
"""
|
|
39
|
+
target = root / GENERATED
|
|
40
|
+
target.write_text(rendered(version))
|
|
41
|
+
build_data.setdefault("artifacts", []).append(f"/{GENERATED.as_posix()}")
|
|
42
|
+
return target
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class VersionHook(BuildHookInterface):
|
|
46
|
+
PLUGIN_NAME = "custom"
|
|
47
|
+
|
|
48
|
+
def initialize(self, version: str, build_data: dict[str, Any]) -> None:
|
|
49
|
+
generate(Path(self.root), self.metadata.version, build_data)
|
|
@@ -9,10 +9,10 @@ authors = [
|
|
|
9
9
|
]
|
|
10
10
|
# Static, and the single source of truth for the version. Bump it in a pull
|
|
11
11
|
# request and run `uv lock`; the release tag only has to agree with it.
|
|
12
|
-
version = "0.2.
|
|
12
|
+
version = "0.2.3"
|
|
13
13
|
# The standard library answers every question this asks. A dependency here
|
|
14
|
-
# would be paid on every invocation, and
|
|
15
|
-
#
|
|
14
|
+
# would be paid on every invocation, and an invocation is 67 ms of which 25 ms
|
|
15
|
+
# is the interpreter starting.
|
|
16
16
|
dependencies = []
|
|
17
17
|
requires-python = ">=3.11"
|
|
18
18
|
classifiers = [
|
|
@@ -30,6 +30,9 @@ classifiers = [
|
|
|
30
30
|
[project.optional-dependencies]
|
|
31
31
|
dev = [
|
|
32
32
|
# keep-sorted start
|
|
33
|
+
# The build backend, because tests/test_version.py drives the build hook
|
|
34
|
+
# that writes the version into the wheel.
|
|
35
|
+
"hatchling==1.28.0",
|
|
33
36
|
"pre-commit==4.6.1",
|
|
34
37
|
"pytest==9.1.1",
|
|
35
38
|
# keep-sorted end
|
|
@@ -70,6 +73,11 @@ build-backend = "hatchling.build"
|
|
|
70
73
|
[tool.hatch.build.targets.wheel]
|
|
71
74
|
packages = ["src/worktrees"]
|
|
72
75
|
|
|
76
|
+
# Writes src/worktrees/_version.py from the version above, so an invocation
|
|
77
|
+
# reads a constant rather than asking importlib.metadata. See hatch_build.py.
|
|
78
|
+
[tool.hatch.build.targets.wheel.hooks.custom]
|
|
79
|
+
path = "hatch_build.py"
|
|
80
|
+
|
|
73
81
|
[tool.ruff]
|
|
74
82
|
line-length = 88
|
|
75
83
|
target-version = "py311"
|
|
@@ -102,5 +110,10 @@ warn_unused_ignores = true
|
|
|
102
110
|
# keep-sorted end
|
|
103
111
|
files = ["src", "tests"]
|
|
104
112
|
|
|
113
|
+
[[tool.mypy.overrides]]
|
|
114
|
+
# Generated at build time and absent from a checkout.
|
|
115
|
+
module = "worktrees._version"
|
|
116
|
+
ignore_missing_imports = true
|
|
117
|
+
|
|
105
118
|
[tool.pytest.ini_options]
|
|
106
119
|
testpaths = ["tests"]
|
|
@@ -34,21 +34,50 @@ echo "Verifying $NAME==$VERSION from $WHICH..." >&2
|
|
|
34
34
|
TMP="$(mktemp -d)"
|
|
35
35
|
trap 'rm -rf "$TMP"' EXIT
|
|
36
36
|
|
|
37
|
-
#
|
|
37
|
+
# An index accepts an upload before it serves it, and the delay differs between the JSON
|
|
38
|
+
# API and the simple index resolved against here: 0.2.0 answered on the former while this
|
|
39
|
+
# still could not find it. So the wait is this resolution retried rather than a different
|
|
40
|
+
# endpoint polled, because this is the condition a green publish is claiming.
|
|
41
|
+
#
|
|
42
|
+
# Eight attempts backing off from 15s is about 32 minutes, and it stops the moment the
|
|
43
|
+
# version resolves. Nothing waits on this job, so the budget is how long PyPI is given
|
|
44
|
+
# before somebody is told it is slow.
|
|
45
|
+
readonly ATTEMPTS=8
|
|
46
|
+
readonly BASE=15
|
|
47
|
+
|
|
48
|
+
# The version the installed command reports, compared against the one asked
|
|
38
49
|
# of the index. Running it and discarding the output would pass while every
|
|
39
50
|
# command on the machine printed a number from a previous release.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
51
|
+
attempt=0
|
|
52
|
+
delay="$BASE"
|
|
53
|
+
while :; do
|
|
54
|
+
attempt=$((attempt + 1))
|
|
55
|
+
|
|
56
|
+
if REPORTED="$(UV_CACHE_DIR="$TMP/cache" uv tool run \
|
|
57
|
+
--isolated \
|
|
58
|
+
--index "$INDEX" \
|
|
59
|
+
--index-strategy unsafe-best-match \
|
|
60
|
+
--from "$NAME==$VERSION" \
|
|
61
|
+
gw version 2>"$TMP/resolve.err")"; then
|
|
62
|
+
break
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
if [ "$attempt" -ge "$ATTEMPTS" ]; then
|
|
66
|
+
cat "$TMP/resolve.err" >&2
|
|
67
|
+
echo "$WHICH did not serve $NAME==$VERSION after $ATTEMPTS attempts." >&2
|
|
68
|
+
exit 1
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
echo "waiting ${delay}s for $WHICH to serve $NAME==$VERSION (attempt $attempt/$ATTEMPTS)..." >&2
|
|
72
|
+
sleep "$delay"
|
|
73
|
+
delay=$((delay * 2))
|
|
74
|
+
done
|
|
46
75
|
readonly REPORTED
|
|
47
76
|
|
|
48
77
|
[ "$REPORTED" = "$VERSION" ] || {
|
|
49
78
|
echo "Version mismatch after publishing to $WHICH." >&2
|
|
50
79
|
echo " asked the index for: $VERSION" >&2
|
|
51
|
-
echo "
|
|
80
|
+
echo " gw version says: $REPORTED" >&2
|
|
52
81
|
exit 1
|
|
53
82
|
}
|
|
54
83
|
|