git-worktrees 0.2.1__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/.agents/skills/release/SKILL.md +16 -6
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/.github/workflows/publish.yml +43 -40
- git_worktrees-0.3.0/.github/workflows/testpypi.yml +69 -0
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/.github/workflows/tests.yml +12 -1
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/.gitignore +3 -0
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/AGENTS.md +41 -4
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/CHANGELOG.md +82 -3
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/PKG-INFO +31 -13
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/README.md +29 -12
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/completions/gwr.fish +1 -0
- git_worktrees-0.3.0/docs/BENCHMARK.md +88 -0
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/docs/DEVELOPMENT.md +86 -24
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/functions/gwa.fish +6 -1
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/functions/gwl.fish +6 -1
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/functions/gwm.fish +6 -1
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/functions/gwr.fish +6 -1
- git_worktrees-0.3.0/hatch_build.py +49 -0
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/pyproject.toml +16 -3
- git_worktrees-0.3.0/scripts/README.md +76 -0
- git_worktrees-0.3.0/scripts/benchmark.py +363 -0
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/scripts/verify-publish.bash +4 -3
- git_worktrees-0.3.0/src/worktrees/__init__.py +15 -0
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/src/worktrees/cli.py +185 -58
- git_worktrees-0.3.0/src/worktrees/errors.py +16 -0
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/src/worktrees/forge.py +17 -5
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/src/worktrees/git.py +138 -58
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/src/worktrees/new_branch.py +17 -7
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/src/worktrees/pick.py +11 -3
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/src/worktrees/prune.py +36 -0
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/src/worktrees/render.py +7 -3
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/src/worktrees/repo.py +59 -28
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/src/worktrees/rotate.py +20 -6
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/src/worktrees/verdicts.py +25 -6
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/src/worktrees/worktree.py +15 -7
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/tests/conftest.py +44 -0
- git_worktrees-0.3.0/tests/test_benchmark.py +136 -0
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/tests/test_cli.py +145 -17
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/tests/test_completions.py +6 -19
- git_worktrees-0.3.0/tests/test_fixture.py +69 -0
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/tests/test_forge.py +2 -9
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/tests/test_gates.py +51 -2
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/tests/test_new_branch.py +3 -15
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/tests/test_rotate.py +2 -14
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/tests/test_shims.py +46 -3
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/tests/test_version.py +30 -0
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/tests/test_worktree.py +18 -16
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/uv.lock +36 -1
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/zsh/plugins/worktrees/completions/_gwr +1 -0
- git_worktrees-0.3.0/zsh/plugins/worktrees/functions/gwa +14 -0
- git_worktrees-0.3.0/zsh/plugins/worktrees/functions/gwl +14 -0
- git_worktrees-0.3.0/zsh/plugins/worktrees/functions/gwm +14 -0
- git_worktrees-0.3.0/zsh/plugins/worktrees/functions/gwr +14 -0
- git_worktrees-0.2.1/src/worktrees/__init__.py +0 -8
- git_worktrees-0.2.1/zsh/plugins/worktrees/functions/gwa +0 -9
- git_worktrees-0.2.1/zsh/plugins/worktrees/functions/gwl +0 -9
- git_worktrees-0.2.1/zsh/plugins/worktrees/functions/gwm +0 -9
- git_worktrees-0.2.1/zsh/plugins/worktrees/functions/gwr +0 -9
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/.claude/settings.json +0 -0
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/.pre-commit-config.yaml +0 -0
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/.python-version +0 -0
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/LICENSE +0 -0
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/completions/gw.fish +0 -0
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/completions/gwa.fish +0 -0
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/completions/gwh.fish +0 -0
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/completions/gwl.fish +0 -0
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/completions/gwm.fish +0 -0
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/completions/gwnb.fish +0 -0
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/completions/gwp.fish +0 -0
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/completions/gwrot.fish +0 -0
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/completions/gws.fish +0 -0
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/conf.d/worktrees.fish +0 -0
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/renovate.json +0 -0
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/src/worktrees/layout.py +0 -0
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/src/worktrees/merged.py +0 -0
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/tests/test_render.py +0 -0
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/tests/test_spec.py +0 -0
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/zsh/plugins/worktrees/completions/_gw +0 -0
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/zsh/plugins/worktrees/completions/_gwa +0 -0
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/zsh/plugins/worktrees/completions/_gwh +0 -0
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/zsh/plugins/worktrees/completions/_gwl +0 -0
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/zsh/plugins/worktrees/completions/_gwm +0 -0
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/zsh/plugins/worktrees/completions/_gwnb +0 -0
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/zsh/plugins/worktrees/completions/_gwp +0 -0
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/zsh/plugins/worktrees/completions/_gwrot +0 -0
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/zsh/plugins/worktrees/completions/_gws +0 -0
- {git_worktrees-0.2.1 → git_worktrees-0.3.0}/zsh/plugins/worktrees/worktrees.plugin.zsh +0 -0
|
@@ -43,7 +43,7 @@ rather than passing it.
|
|
|
43
43
|
git switch --create release/v<version> --no-track origin/main
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
Then run `/release-notes:
|
|
46
|
+
Then run `/release-notes:prepare <version>`, from the ReleaseTools plugin. It
|
|
47
47
|
rules on every commit since the previous tag, writes the body to
|
|
48
48
|
`.git/RELEASE_EDITMSG`, and puts the entry in `CHANGELOG.md` under
|
|
49
49
|
`## <version> - <date>`. It shows the draft and waits before its last step,
|
|
@@ -86,15 +86,18 @@ tagged and nothing published.
|
|
|
86
86
|
## 4. Merge, then wait for main
|
|
87
87
|
|
|
88
88
|
```bash
|
|
89
|
-
gh pr merge --
|
|
89
|
+
gh pr merge --squash --delete-branch
|
|
90
90
|
git switch main && git pull --ff-only
|
|
91
91
|
rt github::await_workflow "$(git rev-parse HEAD)" tests.yml
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
-
`--
|
|
95
|
-
|
|
94
|
+
`--squash`, because the `main` ruleset requires signed commits and linear
|
|
95
|
+
history: GitHub cannot sign the commits a rebase merge writes and refuses it
|
|
96
|
+
with `Base branch requires signed commits`, and a merge commit is off. A
|
|
97
|
+
squash is signed by GitHub's web-flow key, which is expected here and not
|
|
98
|
+
something to route around. The subject becomes `Release <version> (#NN)`.
|
|
96
99
|
|
|
97
|
-
The wait matters. A
|
|
100
|
+
The wait matters. A squash onto a main that moved is a tree neither the
|
|
98
101
|
branch nor main has tested, and the tag must only ever land on a commit
|
|
99
102
|
already proved green. `github::await_workflow` blocks until `tests.yml` finishes
|
|
100
103
|
on that commit and exits non-zero if it failed.
|
|
@@ -113,7 +116,14 @@ gh run watch "$(gh run list --workflow publish.yml --limit 1 --json databaseId -
|
|
|
113
116
|
|
|
114
117
|
Pushing the tag is the release. `publish.yml` re-checks the tag against
|
|
115
118
|
`pyproject.toml`, that the commit is on `main`, and the tests, then builds
|
|
116
|
-
once and uploads to
|
|
119
|
+
once, installs the wheel it built and uploads it to PyPI. There is no
|
|
120
|
+
TestPyPI upload here; `testpypi.yml` published this commit when the merge put
|
|
121
|
+
it on `main`.
|
|
122
|
+
|
|
123
|
+
`release` needs that upload alone, so the GitHub release appears while
|
|
124
|
+
`verify` is still installing from PyPI. A red `verify` under a green
|
|
125
|
+
`publish` means the index has not caught up, not that the version is
|
|
126
|
+
missing.
|
|
117
127
|
|
|
118
128
|
Report the PyPI URL and the GitHub release URL when it finishes.
|
|
119
129
|
|
|
@@ -4,15 +4,18 @@ name: Publish
|
|
|
4
4
|
# request against the repository, this workflow's FILENAME and the environment,
|
|
5
5
|
# so a rename means re-registering the publisher on PyPI first.
|
|
6
6
|
#
|
|
7
|
-
# build -> publish
|
|
7
|
+
# build -> 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
|
|
11
12
|
# before it is a normal reviewed change.
|
|
12
13
|
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
14
|
+
# There is no TestPyPI rehearsal. testpypi.yml publishes every commit on main
|
|
15
|
+
# there as <version>.post<epoch> and installs it back, so the tree this tag
|
|
16
|
+
# names has already proved it uploads to an index and installs from one. What
|
|
17
|
+
# is left to prove before the upload PyPI cannot take back is that the wheel
|
|
18
|
+
# runs, and build installs it and does that.
|
|
16
19
|
#
|
|
17
20
|
# No API token is stored anywhere. See docs/DEVELOPMENT.md, "Publishing a
|
|
18
21
|
# version".
|
|
@@ -30,7 +33,7 @@ permissions:
|
|
|
30
33
|
|
|
31
34
|
jobs:
|
|
32
35
|
# Built once and handed to every job below, so what reaches PyPI is
|
|
33
|
-
# byte-for-byte what
|
|
36
|
+
# byte-for-byte what was tested and installed here.
|
|
34
37
|
build:
|
|
35
38
|
name: Build
|
|
36
39
|
runs-on: ubuntu-latest
|
|
@@ -91,6 +94,11 @@ jobs:
|
|
|
91
94
|
- name: Install dependencies
|
|
92
95
|
run: uv sync --all-extras --locked
|
|
93
96
|
|
|
97
|
+
# Neither shell is on the image, and the shim tests fail rather than
|
|
98
|
+
# skip under CI, so this job needs them exactly as tests.yml does.
|
|
99
|
+
- name: Install fish and zsh
|
|
100
|
+
run: sudo apt-get update && sudo apt-get install -y fish zsh
|
|
101
|
+
|
|
94
102
|
# The tag is the last chance to catch a regression, and it costs 20s.
|
|
95
103
|
- name: Run tests
|
|
96
104
|
run: uv run --no-sync pytest tests -q
|
|
@@ -98,6 +106,17 @@ jobs:
|
|
|
98
106
|
- name: Build
|
|
99
107
|
run: uv build
|
|
100
108
|
|
|
109
|
+
# The wheel is not the tree: hatch_build.py writes _version.py into it,
|
|
110
|
+
# and one that lacks it falls back to the metadata lookup and says
|
|
111
|
+
# nothing. Installing it costs four seconds and happens before anything
|
|
112
|
+
# is uploaded anywhere.
|
|
113
|
+
- name: Install the wheel and run it
|
|
114
|
+
run: |
|
|
115
|
+
REPORTED="$(uv tool run --isolated --from dist/*.whl gw version)"
|
|
116
|
+
DECLARED="$(uv version --short)"
|
|
117
|
+
echo "the wheel reports $REPORTED, pyproject declares $DECLARED" >&2
|
|
118
|
+
[ "$REPORTED" = "$DECLARED" ]
|
|
119
|
+
|
|
101
120
|
- name: Upload distributions
|
|
102
121
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
103
122
|
with:
|
|
@@ -105,20 +124,18 @@ jobs:
|
|
|
105
124
|
path: dist/
|
|
106
125
|
if-no-files-found: error
|
|
107
126
|
|
|
108
|
-
publish
|
|
109
|
-
name: Publish to
|
|
127
|
+
publish:
|
|
128
|
+
name: Publish to PyPI
|
|
110
129
|
runs-on: ubuntu-latest
|
|
111
130
|
needs: build
|
|
112
|
-
# Binds this job to the trusted publisher registered for `
|
|
113
|
-
environment
|
|
131
|
+
# Binds this job to the trusted publisher registered for `pypi`. Add a
|
|
132
|
+
# required reviewer on this environment for a manual gate before anything
|
|
133
|
+
# reaches the real index.
|
|
134
|
+
environment: pypi
|
|
114
135
|
permissions:
|
|
115
|
-
# Lets the job mint the OIDC token PyPI exchanges for a short-lived,
|
|
116
|
-
# scoped credential. Without it uv has nothing to present.
|
|
117
136
|
id-token: write
|
|
118
137
|
contents: read
|
|
119
138
|
steps:
|
|
120
|
-
# `--index testpypi` resolves that name from [[tool.uv.index]] in
|
|
121
|
-
# pyproject.toml, which is not part of the built artifact.
|
|
122
139
|
- name: Checkout code
|
|
123
140
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
124
141
|
|
|
@@ -135,22 +152,24 @@ jobs:
|
|
|
135
152
|
path: dist/
|
|
136
153
|
|
|
137
154
|
- name: Publish
|
|
138
|
-
|
|
139
|
-
# unavailable instead of quietly looking for a token.
|
|
140
|
-
run: uv publish --index testpypi --trusted-publishing always
|
|
155
|
+
run: uv publish --trusted-publishing always
|
|
141
156
|
|
|
142
|
-
|
|
143
|
-
|
|
157
|
+
# Uploaded and installable are two claims with two timings, so they are two
|
|
158
|
+
# jobs. An index serves what it has accepted only after a delay of its own
|
|
159
|
+
# choosing: 0.2.1 took longer than the script waited, and a release that had
|
|
160
|
+
# published correctly went red and had to be re-run by hand. This job holds
|
|
161
|
+
# the second claim, `release` needs only the first, and a red `verify` says
|
|
162
|
+
# PyPI is slow rather than that the version is not out.
|
|
163
|
+
verify:
|
|
164
|
+
name: Verify it installs from PyPI
|
|
144
165
|
runs-on: ubuntu-latest
|
|
145
|
-
needs: publish
|
|
146
|
-
# Binds this job to the trusted publisher registered for `pypi`. Add a
|
|
147
|
-
# required reviewer on this environment for a manual gate before anything
|
|
148
|
-
# reaches the real index.
|
|
149
|
-
environment: pypi
|
|
166
|
+
needs: publish
|
|
150
167
|
permissions:
|
|
151
|
-
id-token: write
|
|
152
168
|
contents: read
|
|
153
169
|
steps:
|
|
170
|
+
# The script reads the name and version from pyproject.toml through
|
|
171
|
+
# `uv version`, and installs from the index rather than from the
|
|
172
|
+
# artifact, so there is nothing to download here.
|
|
154
173
|
- name: Checkout code
|
|
155
174
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
156
175
|
|
|
@@ -160,22 +179,6 @@ jobs:
|
|
|
160
179
|
python-version: "3.14"
|
|
161
180
|
enable-cache: true
|
|
162
181
|
|
|
163
|
-
- name: Download distributions
|
|
164
|
-
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
|
165
|
-
with:
|
|
166
|
-
name: dist
|
|
167
|
-
path: dist/
|
|
168
|
-
|
|
169
|
-
- name: Install the release tools
|
|
170
|
-
uses: releasetools/cli@v0
|
|
171
|
-
|
|
172
|
-
- name: Publish
|
|
173
|
-
run: uv publish --trusted-publishing always
|
|
174
|
-
|
|
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.
|
|
179
182
|
- name: Verify it installs from PyPI
|
|
180
183
|
run: scripts/verify-publish.bash --prod
|
|
181
184
|
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
name: TestPyPI
|
|
2
|
+
|
|
3
|
+
# Renaming this file breaks the upload. TestPyPI's Trusted Publishing matches a
|
|
4
|
+
# request against the repository, this workflow's FILENAME and the environment,
|
|
5
|
+
# so a rename means re-registering the publisher on TestPyPI first.
|
|
6
|
+
#
|
|
7
|
+
# Every commit on main is published here as <version>.post<epoch>. A build made
|
|
8
|
+
# after <version> shipped is a post release of it, which is what PEP 440's post
|
|
9
|
+
# segment says, and it sorts above <version> and below the next patch, so the
|
|
10
|
+
# newest snapshot is what TestPyPI serves.
|
|
11
|
+
#
|
|
12
|
+
# This is what proves the publishing path. The tree a release tag names has
|
|
13
|
+
# already been uploaded and installed back from an index by the time the tag
|
|
14
|
+
# exists, which is why publish.yml rehearses nothing of its own.
|
|
15
|
+
#
|
|
16
|
+
# The suite is not run again here. tests.yml runs it on this same commit, and a
|
|
17
|
+
# snapshot a test would have caught costs one number on a test index.
|
|
18
|
+
|
|
19
|
+
on:
|
|
20
|
+
push:
|
|
21
|
+
branches:
|
|
22
|
+
- main
|
|
23
|
+
workflow_dispatch:
|
|
24
|
+
|
|
25
|
+
permissions:
|
|
26
|
+
contents: read
|
|
27
|
+
|
|
28
|
+
jobs:
|
|
29
|
+
snapshot:
|
|
30
|
+
name: Publish a snapshot
|
|
31
|
+
runs-on: ubuntu-latest
|
|
32
|
+
# Binds this job to the trusted publisher registered for `testpypi`.
|
|
33
|
+
environment: testpypi
|
|
34
|
+
permissions:
|
|
35
|
+
# Lets the job mint the OIDC token TestPyPI exchanges for a short-lived,
|
|
36
|
+
# scoped credential. Without it uv has nothing to present.
|
|
37
|
+
id-token: write
|
|
38
|
+
contents: read
|
|
39
|
+
steps:
|
|
40
|
+
# `--index testpypi` resolves that name from [[tool.uv.index]] in
|
|
41
|
+
# pyproject.toml, and the script reads the version from the same file.
|
|
42
|
+
- name: Checkout code
|
|
43
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
44
|
+
|
|
45
|
+
- name: Set up uv
|
|
46
|
+
uses: astral-sh/setup-uv@ae62891fec2bb8e7d6c99fc78c9fec3a63790f8d # v10.0.0
|
|
47
|
+
with:
|
|
48
|
+
python-version: "3.14"
|
|
49
|
+
enable-cache: true
|
|
50
|
+
|
|
51
|
+
# Epoch seconds rather than the run number: a re-run repeats the number
|
|
52
|
+
# and TestPyPI refuses the duplicate, while the clock has moved on.
|
|
53
|
+
# `--frozen` writes pyproject.toml and leaves uv.lock alone, which is
|
|
54
|
+
# correct here because nothing in this job resolves dependencies.
|
|
55
|
+
- name: Number the snapshot
|
|
56
|
+
run: uv version --frozen "$(uv version --short).post$(date +%s)"
|
|
57
|
+
|
|
58
|
+
- name: Build
|
|
59
|
+
run: uv build
|
|
60
|
+
|
|
61
|
+
- name: Publish
|
|
62
|
+
# `always` rather than the default `automatic`: fail loudly if OIDC is
|
|
63
|
+
# unavailable instead of quietly looking for a token.
|
|
64
|
+
run: uv publish --index testpypi --trusted-publishing always
|
|
65
|
+
|
|
66
|
+
# Uploaded is not installable, and this is the claim the release path
|
|
67
|
+
# inherits from these runs.
|
|
68
|
+
- name: Verify it installs from TestPyPI
|
|
69
|
+
run: scripts/verify-publish.bash
|
|
@@ -56,6 +56,11 @@ jobs:
|
|
|
56
56
|
- name: Checkout code
|
|
57
57
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
58
58
|
|
|
59
|
+
# python-version makes setup-uv run `uv python install`, so each entry
|
|
60
|
+
# gets a uv-managed cpython-X.Y.Z rather than whatever the runner image
|
|
61
|
+
# ships. Packagers differ enough to matter: one that links its C
|
|
62
|
+
# extensions into the executable and one that dlopens 76 of them do not
|
|
63
|
+
# start in the same time. scripts/README.md has the measurement.
|
|
59
64
|
- name: Set up uv and Python ${{ matrix.python-version }}
|
|
60
65
|
uses: astral-sh/setup-uv@ae62891fec2bb8e7d6c99fc78c9fec3a63790f8d # v10.0.0
|
|
61
66
|
with:
|
|
@@ -67,11 +72,17 @@ jobs:
|
|
|
67
72
|
# resolving something the lock file never described.
|
|
68
73
|
run: uv sync --all-extras --locked
|
|
69
74
|
|
|
75
|
+
# Neither shell is on the image. Without them the nine assertions that
|
|
76
|
+
# drive a real fish or zsh call pytest.skip, which is invisible in
|
|
77
|
+
# `pytest -q`, so the run goes green without either shim ever running.
|
|
78
|
+
- name: Install fish and zsh
|
|
79
|
+
run: sudo apt-get update && sudo apt-get install -y fish zsh
|
|
80
|
+
|
|
70
81
|
# The suite drives real git: it builds repositories, adds worktrees and
|
|
71
82
|
# removes them. Recording the version makes a failure that is git's
|
|
72
83
|
# rather than ours readable from the log alone.
|
|
73
84
|
- name: Record the git version
|
|
74
|
-
run: git --version
|
|
85
|
+
run: git --version && fish --version && zsh --version
|
|
75
86
|
|
|
76
87
|
- name: Run tests
|
|
77
88
|
run: uv run --no-sync pytest tests -q
|
|
@@ -6,10 +6,20 @@ through `subprocess` so env, cwd, both streams and the exit code stay per-test.
|
|
|
6
6
|
|
|
7
7
|
```console
|
|
8
8
|
uv sync --all-extras
|
|
9
|
+
uv run --no-sync pre-commit install
|
|
9
10
|
uv run --no-sync pytest tests
|
|
10
11
|
uv run --no-sync pre-commit run --all-files
|
|
11
12
|
```
|
|
12
13
|
|
|
14
|
+
`pre-commit run --all-files` reads `git ls-files`, so a file that is new and
|
|
15
|
+
unstaged is invisible to it. Stage before running the hooks, or install them
|
|
16
|
+
and let the commit do it.
|
|
17
|
+
|
|
18
|
+
Read [scripts/README.md](scripts/README.md) before measuring anything. It says
|
|
19
|
+
what is worth timing and what is not, and why every interpreter the benchmark
|
|
20
|
+
touches comes from uv: a run that compares packagers rather than versions
|
|
21
|
+
produces a number that looks like a CPython regression and is not one.
|
|
22
|
+
|
|
13
23
|
## Binaries and shell functions are not interchangeable
|
|
14
24
|
|
|
15
25
|
A shell function of a given name beats a binary of that name in bash, zsh and
|
|
@@ -19,7 +29,12 @@ prompt and from a script unless the function is a pure adapter.
|
|
|
19
29
|
|
|
20
30
|
The rule: **a function exists only for a command that must change the caller's
|
|
21
31
|
directory, it carries the same name as the binary, and its body does nothing but
|
|
22
|
-
call `command <name>` and `cd` to what that prints.**
|
|
32
|
+
call `command <name>` and `cd` to what that prints, when that is a directory.**
|
|
33
|
+
|
|
34
|
+
The last clause is not a hedge. `--json` and `--list` print what was asked for
|
|
35
|
+
on the same stream a destination arrives on, and a shim that cannot tell them
|
|
36
|
+
apart tries to `cd` into a JSON object. `fish -c 'gwl --json'` reaches the
|
|
37
|
+
function too, so this is the agent path as much as the human one.
|
|
23
38
|
|
|
24
39
|
| | changes directory | ships as |
|
|
25
40
|
| --- | --- | --- |
|
|
@@ -129,7 +144,9 @@ tightly they cluster. About twenty lines, `difflib` not required.
|
|
|
129
144
|
|
|
130
145
|
Two rules, both the shape `gwp`'s prompt already has:
|
|
131
146
|
|
|
132
|
-
-
|
|
147
|
+
- A query that narrows to one takes it outright, with no prompt at all. No
|
|
148
|
+
query asks however many there are, because being moved without being asked
|
|
149
|
+
is not what `gwl` with no argument means.
|
|
133
150
|
- No terminal, no prompt. Refuse at exit 3 and name the flag that answers
|
|
134
151
|
without one, rather than blocking on something nothing can drive.
|
|
135
152
|
|
|
@@ -238,10 +255,30 @@ tested on what it returns rather than on its exit code. `upstream_of` takes
|
|
|
238
255
|
`ok=(0, 1, 128)`, so a typo in one of its flags exits 128, reads as "no
|
|
239
256
|
upstream", and turns every branch into `unknown` with nothing failing.
|
|
240
257
|
|
|
258
|
+
The two calls that reach the network, `fetch` and `remote set-head --auto`,
|
|
259
|
+
take `ok=(0, 128)` for the same reason in reverse: a remote that cannot be
|
|
260
|
+
reached is an answer rather than a failure. The caller reads the result, says
|
|
261
|
+
so on stderr, and carries on with the refs already here, which is where
|
|
262
|
+
`--no-fetch` sends it on request. It also stops asking: a failed fetch means
|
|
263
|
+
the head-branch ladder is told it is offline, rather than spending a second
|
|
264
|
+
round trip proving it.
|
|
265
|
+
|
|
266
|
+
One `git status --porcelain --ignored=traditional` answers both questions a
|
|
267
|
+
worktree is asked. It is a strict superset of plain `--porcelain`, so `dirty`
|
|
268
|
+
is "a line that does not start with `!! `" and `ignored` is the rest. Asking
|
|
269
|
+
both ways was 18 of an assessment's 35 calls over nine worktrees.
|
|
270
|
+
|
|
241
271
|
`guard()` runs on the resolved argv inside the wrapper, not at declaration, so
|
|
242
272
|
no call site can assemble its way past it. `reset --hard`, a forced `checkout`
|
|
243
|
-
or `switch`, `clean -f`, a bare `push --force`, `worktree remove --force
|
|
244
|
-
`branch -D`
|
|
273
|
+
or `switch`, `clean -f`, a bare `push --force`, `worktree remove --force`,
|
|
274
|
+
`branch -D` and an `update-ref` delete that names no full sha are refused
|
|
275
|
+
absolutely. There is no flag, and `--yes` least of all.
|
|
276
|
+
|
|
277
|
+
Those seven are `RULES`, a tuple, each row carrying the name `--explain`
|
|
278
|
+
prints, the clause the refusal ends with, and the test. The footer `--explain`
|
|
279
|
+
prints is built from it. A rule written in one place and described in another
|
|
280
|
+
is a rule that goes quiet: the hand-written footer fell a rule behind the
|
|
281
|
+
moment `update-ref` was added, and nothing failed.
|
|
245
282
|
|
|
246
283
|
`verdicts.py` declares no mutating command. A read-only command may not call
|
|
247
284
|
one: a test reads the verbose log and asserts it.
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
3
|
Newest first. Each entry says what changed for somebody running these
|
|
4
|
-
commands, and the choices behind it. Dates are ISO 8601, versions
|
|
5
|
-
[Semantic Versioning](https://semver.org/spec/v2.0.0.html), and the
|
|
6
|
-
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
4
|
+
commands, and the choices behind it. Dates are ISO 8601, released versions
|
|
5
|
+
follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html), and the
|
|
6
|
+
shape follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
7
|
+
|
|
8
|
+
Every version below was tagged and published. The snapshots `main` sends to
|
|
9
|
+
TestPyPI are neither, and they are numbered `<version>.post<epoch>`, which
|
|
10
|
+
PEP 440 orders after the release it follows and SemVer does not describe at
|
|
11
|
+
all. `docs/DEVELOPMENT.md` says how and why.
|
|
7
12
|
|
|
8
13
|
An entry earns its place by being observable. If running a command produces
|
|
9
14
|
no different result, no different output and no different exit code, it is
|
|
@@ -12,6 +17,80 @@ not in here, whatever it cost to build.
|
|
|
12
17
|
A release closes with a `### Choices` section when a decision in it is worth
|
|
13
18
|
the reader's time: what was chosen, and what the alternative failed to do.
|
|
14
19
|
|
|
20
|
+
## 0.3.0 - 2026-09-20
|
|
21
|
+
|
|
22
|
+
`gwl` with no query asks which worktree rather than moving you to one.
|
|
23
|
+
Whenever exactly one was left it took it outright, so a repository holding
|
|
24
|
+
the main checkout and one worktree moved you without a word, and running it
|
|
25
|
+
twice walked you back and forth. A query that narrows to one has already said
|
|
26
|
+
which and still takes it outright. Without a terminal it now refuses at exit
|
|
27
|
+
3 and names `--list` and `--json`, where it used to print a path, so
|
|
28
|
+
`cd $(gwl)` in a script stops working. `gwl <query>` is unchanged.
|
|
29
|
+
|
|
30
|
+
`gwl --list` no longer tries to `cd` into its own listing, and `--json` no
|
|
31
|
+
longer tries to `cd` into the JSON. The shell function reads what the binary
|
|
32
|
+
printed and moves you there, and those two modes print on the same stream a
|
|
33
|
+
destination arrives on, so every one of them ended in a failed `cd`. `gwa
|
|
34
|
+
--json` was the worst: the worktree was created and then the move failed, so
|
|
35
|
+
the run looked broken and was not. It reached `gwa`, `gwl`, `gwm` and `gwr`
|
|
36
|
+
in both fish and zsh, and `fish -c 'gwl --json'` too, because a fish function
|
|
37
|
+
wins there. **That fix lives in the shell functions, so `uv tool upgrade`
|
|
38
|
+
alone does not deliver it; update the fish or zsh plugin as well.**
|
|
39
|
+
|
|
40
|
+
An invocation costs about 18 ms less on Python 3.11 and about 12 ms less on
|
|
41
|
+
3.14. `gw version` was 48.4 ms and is 30.5 ms on 3.11.13, and was 55.6 ms and
|
|
42
|
+
is 43.1 ms on 3.14.7, measured from installed wheels on one machine. Four
|
|
43
|
+
changes account for it: the command you typed imports only what it needs
|
|
44
|
+
rather than every command, records are plain classes rather than dataclasses,
|
|
45
|
+
the parser is built for the subcommand you asked for rather than all eleven,
|
|
46
|
+
and `json` is imported only where it is printed. `difflib`, which named the
|
|
47
|
+
closest command when you mistyped one, is no longer imported to start.
|
|
48
|
+
|
|
49
|
+
### Choices
|
|
50
|
+
|
|
51
|
+
`gwl` asks whenever no query was given, even when the repository holds one
|
|
52
|
+
other worktree. Taking it outright reads as a toggle: the command moves you,
|
|
53
|
+
and moves you back, and never says where. Prompting for a single option looks
|
|
54
|
+
redundant until it is the difference between being shown your options and
|
|
55
|
+
being sent somewhere. A query is the way to skip it, and it always was.
|
|
56
|
+
|
|
57
|
+
`gwl` without a terminal exits 3, joining `gwp`, which 0.2.3 moved there for
|
|
58
|
+
the same reason. A script can then tell a refusal from a typo: 2 is
|
|
59
|
+
argparse's code for a usage error and 3 is nobody else's.
|
|
60
|
+
|
|
61
|
+
## 0.2.3 - 2026-09-11
|
|
62
|
+
|
|
63
|
+
An unreachable remote is an answer rather than a failure. Every command that
|
|
64
|
+
touches the network used to exit 1 with git's own message, so the warnings
|
|
65
|
+
written for the offline case never reached anybody. A failed fetch now says
|
|
66
|
+
the refs are the ones already here, and the assessment carries on.
|
|
67
|
+
|
|
68
|
+
Refusing to prompt exits 3 everywhere. `gwp` used to exit 2 when stdin was
|
|
69
|
+
not a terminal, which is argparse's code for a usage error, so a script could
|
|
70
|
+
not tell a refusal from a typo.
|
|
71
|
+
|
|
72
|
+
Removing a worktree names the stash entries made on its branch. A stash
|
|
73
|
+
outlives the branch while its subject goes on naming it, which is how it
|
|
74
|
+
becomes unfindable.
|
|
75
|
+
|
|
76
|
+
`gwr` asks the forge once, about the branch it is removing, instead of about
|
|
77
|
+
every worktree: three round trips became one in a three-worktree repository.
|
|
78
|
+
`--no-forge` turns it off there, as it already did for `gws` and `gwp`.
|
|
79
|
+
|
|
80
|
+
`--explain` lists all seven refusals the guard enforces. The footer was
|
|
81
|
+
written by hand and had been a rule behind since `update-ref` was added.
|
|
82
|
+
|
|
83
|
+
An invocation costs about 15 ms less, and an assessment makes half as many
|
|
84
|
+
`git status` calls.
|
|
85
|
+
|
|
86
|
+
### Choices
|
|
87
|
+
|
|
88
|
+
The version is written into the wheel at build time rather than read back
|
|
89
|
+
through `importlib.metadata`, which cost 10 ms of a 67 ms invocation for a
|
|
90
|
+
string two commands print. Deriving it from the tag with `hatch-vcs` was the
|
|
91
|
+
alternative, and it inverts which of the tag and `pyproject.toml` has to
|
|
92
|
+
agree with the other.
|
|
93
|
+
|
|
15
94
|
## 0.2.1 - 2026-09-11
|
|
16
95
|
|
|
17
96
|
`--version` is gone from every command. `gw version`, or `gw v`, prints the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: git-worktrees
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Git worktree commands that refuse to lose work.
|
|
5
5
|
Project-URL: Bug reports, https://github.com/MihaiBojin/worktrees/issues/new
|
|
6
6
|
Project-URL: Source, https://github.com/MihaiBojin/worktrees
|
|
@@ -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
|
|
@@ -147,8 +148,10 @@ autoload -Uz compinit && compinit
|
|
|
147
148
|
|
|
148
149
|
### What the shell code does
|
|
149
150
|
|
|
150
|
-
Each function runs `command <name
|
|
151
|
-
|
|
151
|
+
Each function runs `command <name>` and `cd`s to what it printed, provided
|
|
152
|
+
that is a directory. `--json` and `--list` print what was asked for on the
|
|
153
|
+
same stream a destination arrives on, so the shim asks whether what it has is
|
|
154
|
+
somewhere to go and stays put when it is not. `gws`, `gwp`, `gwnb`, `gwrot` and `gwh`
|
|
152
155
|
change no directory, so they ship as console scripts alone and answer the same
|
|
153
156
|
from a prompt and from a script.
|
|
154
157
|
|
|
@@ -222,6 +225,12 @@ function returns that set for both. What goes and what puts it back is printed
|
|
|
222
225
|
before anything does, and neither `--quiet` nor `--yes` silences it. The sha in
|
|
223
226
|
the restore line is full length, because that line is meant to be pasted.
|
|
224
227
|
|
|
228
|
+
A stash made on a branch that is about to go is named too. `refs/stash` is
|
|
229
|
+
per-repository and pins its own commits, so the entry survives the branch and
|
|
230
|
+
`git stash branch <new> stash@{0}` still recovers it. What does not survive is
|
|
231
|
+
the branch name in its subject, so the line says which entry is about to start
|
|
232
|
+
pointing at nothing.
|
|
233
|
+
|
|
225
234
|
The checkout goes through plain `git worktree remove`, so every refusal git
|
|
226
235
|
makes still applies. The branch goes through `git update-ref -d`, naming the
|
|
227
236
|
sha the verdict was formed against: a branch somebody committed to in between
|
|
@@ -240,7 +249,8 @@ squash-merged /home/you/git/.worktrees/squash-merged/repo
|
|
|
240
249
|
not a terminal, so nothing can answer for the 1 above; pass --yes to remove them
|
|
241
250
|
```
|
|
242
251
|
|
|
243
|
-
That exits
|
|
252
|
+
That exits 3 and touches nothing, the code every refusal uses. `gwp --yes --json`
|
|
253
|
+
is what an agent runs.
|
|
244
254
|
|
|
245
255
|
With nothing to remove it prints the table `gws` prints, rather than the name
|
|
246
256
|
of the command that would have printed it. The reason each worktree stayed is
|
|
@@ -275,9 +285,10 @@ $ gwm parser-v2 # renames the branch and moves the checkout
|
|
|
275
285
|
$ gwr # removes the one you are standing in
|
|
276
286
|
```
|
|
277
287
|
|
|
278
|
-
Each prints one destination on stdout
|
|
279
|
-
|
|
280
|
-
what it removed; empty output
|
|
288
|
+
Each prints one destination on stdout, which is what the shell function
|
|
289
|
+
reads, and prints data there instead under `--json` and `--list`. `gwr` prints
|
|
290
|
+
a path **only** when you were standing in what it removed; empty output, and
|
|
291
|
+
output that is not a directory, both mean stay put.
|
|
281
292
|
|
|
282
293
|
`gwm` is the one that is not a convenience. Renaming the directory you are
|
|
283
294
|
standing in leaves the shell with a stale `$PWD` and every later command
|
|
@@ -302,16 +313,17 @@ $ gwl --list
|
|
|
302
313
|
```
|
|
303
314
|
|
|
304
315
|
It never offers that one. Picking it is the one answer that cannot take you
|
|
305
|
-
anywhere, and
|
|
306
|
-
nothing at all, which reads as a broken command. So two worktrees and no query
|
|
307
|
-
is not a question, and asking for the one you are in says so and stays put:
|
|
316
|
+
anywhere, and asking for the one you are in says so and stays put:
|
|
308
317
|
|
|
309
318
|
```console
|
|
310
319
|
$ gwl dirty # standing in dirty-work
|
|
311
320
|
already in dirty-work
|
|
312
321
|
```
|
|
313
322
|
|
|
314
|
-
|
|
323
|
+
A query that narrows to one takes it outright. No query does not, even when
|
|
324
|
+
the repository holds exactly one other worktree: `gwl` with no argument means
|
|
325
|
+
show me the options, and being moved without being asked is not that. So it
|
|
326
|
+
asks, numbered, however many there are:
|
|
315
327
|
|
|
316
328
|
```console
|
|
317
329
|
$ gwl # standing in the main checkout
|
|
@@ -422,13 +434,17 @@ print one thing each and have nothing to be quiet about, takes `--json`, `-q`,
|
|
|
422
434
|
| --- | --- |
|
|
423
435
|
| `gws` | `--branch NAME`, `--no-fetch`, `--delete-ignored`, `--no-forge` |
|
|
424
436
|
| `gwp` | those four, and `-y` |
|
|
425
|
-
| `gwr` | `-f`, `--delete-ignored`, `--no-fetch`, `-y` |
|
|
437
|
+
| `gwr` | `-f`, `--delete-ignored`, `--no-fetch`, `--no-forge`, `-y` |
|
|
426
438
|
| `gwa`, `gwnb`, `gwrot` | `--no-fetch` |
|
|
427
439
|
| `gwl` | `-l` |
|
|
428
440
|
| `gwm`, `gwh` | none |
|
|
429
441
|
|
|
430
442
|
`gw <command> --help` prints one command's own list.
|
|
431
443
|
|
|
444
|
+
A remote that cannot be reached is not fatal anywhere. The fetch fails, the
|
|
445
|
+
command says so on stderr, and it answers from the refs already here, which is
|
|
446
|
+
where `--no-fetch` sends it on request.
|
|
447
|
+
|
|
432
448
|
Data goes to stdout and diagnostics to stderr, the prompt included, so `--json`
|
|
433
449
|
is parseable in every mode. Colour is decided per stream and only for a
|
|
434
450
|
terminal, so a redirect, a pipe, a non-empty `NO_COLOR` or `TERM=dumb` give the
|
|
@@ -460,12 +476,14 @@ and whatever a caller asks for:
|
|
|
460
476
|
```
|
|
461
477
|
reset --hard a forced checkout or switch clean -f
|
|
462
478
|
push --force worktree remove --force branch -D
|
|
479
|
+
an update-ref delete that names no full sha
|
|
463
480
|
```
|
|
464
481
|
|
|
465
482
|
The check runs on the argument list as it is about to be handed to git, so no
|
|
466
483
|
code path can assemble its way past one. `gws --explain` prints every git
|
|
467
484
|
command the program can issue, marking the ones that take the repository's
|
|
468
|
-
shared refs and therefore run serially
|
|
485
|
+
shared refs and therefore run serially, and ends with that list of seven,
|
|
486
|
+
generated from the rules rather than written out beside them.
|
|
469
487
|
|
|
470
488
|
`--force-with-lease` is not `--force` and is allowed, and so is
|
|
471
489
|
`update-ref -d <ref> <sha>`, which is how a branch is deleted here: against the
|