git-worktrees 0.2.3__tar.gz → 0.3.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.2.3 → git_worktrees-0.3.1}/.agents/skills/release/SKILL.md +16 -6
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/.github/workflows/publish.yml +20 -41
- git_worktrees-0.3.1/.github/workflows/testpypi.yml +69 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/.github/workflows/tests.yml +5 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/AGENTS.md +27 -6
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/CHANGELOG.md +81 -3
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/PKG-INFO +29 -16
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/README.md +28 -15
- git_worktrees-0.3.1/docs/BENCHMARK.md +88 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/docs/DEVELOPMENT.md +80 -25
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/functions/gwa.fish +6 -1
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/functions/gwl.fish +6 -1
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/functions/gwm.fish +6 -1
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/functions/gwr.fish +6 -1
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/pyproject.toml +1 -1
- git_worktrees-0.3.1/scripts/README.md +76 -0
- git_worktrees-0.3.1/scripts/benchmark.py +363 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/src/worktrees/cli.py +147 -35
- git_worktrees-0.3.1/src/worktrees/errors.py +16 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/src/worktrees/forge.py +9 -3
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/src/worktrees/git.py +51 -14
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/src/worktrees/new_branch.py +7 -2
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/src/worktrees/pick.py +28 -4
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/src/worktrees/render.py +7 -3
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/src/worktrees/repo.py +23 -4
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/src/worktrees/rotate.py +14 -3
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/src/worktrees/verdicts.py +43 -5
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/src/worktrees/worktree.py +7 -2
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/tests/conftest.py +44 -0
- git_worktrees-0.3.1/tests/test_benchmark.py +136 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/tests/test_cli.py +97 -26
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/tests/test_completions.py +6 -19
- git_worktrees-0.3.1/tests/test_fixture.py +69 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/tests/test_forge.py +2 -9
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/tests/test_gates.py +0 -2
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/tests/test_new_branch.py +3 -15
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/tests/test_rotate.py +2 -14
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/tests/test_shims.py +30 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/tests/test_worktree.py +18 -16
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/uv.lock +1 -1
- git_worktrees-0.3.1/zsh/plugins/worktrees/functions/gwa +14 -0
- git_worktrees-0.3.1/zsh/plugins/worktrees/functions/gwl +14 -0
- git_worktrees-0.3.1/zsh/plugins/worktrees/functions/gwm +14 -0
- git_worktrees-0.3.1/zsh/plugins/worktrees/functions/gwr +14 -0
- git_worktrees-0.2.3/zsh/plugins/worktrees/functions/gwa +0 -9
- git_worktrees-0.2.3/zsh/plugins/worktrees/functions/gwl +0 -9
- git_worktrees-0.2.3/zsh/plugins/worktrees/functions/gwm +0 -9
- git_worktrees-0.2.3/zsh/plugins/worktrees/functions/gwr +0 -9
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/.claude/settings.json +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/.gitignore +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/.pre-commit-config.yaml +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/.python-version +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/LICENSE +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/completions/gw.fish +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/completions/gwa.fish +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/completions/gwh.fish +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/completions/gwl.fish +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/completions/gwm.fish +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/completions/gwnb.fish +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/completions/gwp.fish +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/completions/gwr.fish +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/completions/gwrot.fish +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/completions/gws.fish +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/conf.d/worktrees.fish +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/hatch_build.py +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/renovate.json +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/scripts/verify-publish.bash +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/src/worktrees/__init__.py +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/src/worktrees/layout.py +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/src/worktrees/merged.py +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/src/worktrees/prune.py +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/tests/test_render.py +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/tests/test_spec.py +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/tests/test_version.py +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/zsh/plugins/worktrees/completions/_gw +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/zsh/plugins/worktrees/completions/_gwa +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/zsh/plugins/worktrees/completions/_gwh +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/zsh/plugins/worktrees/completions/_gwl +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/zsh/plugins/worktrees/completions/_gwm +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/zsh/plugins/worktrees/completions/_gwnb +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/zsh/plugins/worktrees/completions/_gwp +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/zsh/plugins/worktrees/completions/_gwr +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/zsh/plugins/worktrees/completions/_gwrot +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/zsh/plugins/worktrees/completions/_gws +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.1}/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,16 +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
|
|
8
|
-
#
|
|
7
|
+
# build -> publish -> release
|
|
8
|
+
# \-> verify
|
|
9
9
|
#
|
|
10
10
|
# Nothing here creates a tag. A release is a version bump merged into main and
|
|
11
11
|
# then tagged by hand, so the irreversible step is the last one and everything
|
|
12
12
|
# before it is a normal reviewed change.
|
|
13
13
|
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
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.
|
|
17
19
|
#
|
|
18
20
|
# No API token is stored anywhere. See docs/DEVELOPMENT.md, "Publishing a
|
|
19
21
|
# version".
|
|
@@ -31,7 +33,7 @@ permissions:
|
|
|
31
33
|
|
|
32
34
|
jobs:
|
|
33
35
|
# Built once and handed to every job below, so what reaches PyPI is
|
|
34
|
-
# byte-for-byte what
|
|
36
|
+
# byte-for-byte what was tested and installed here.
|
|
35
37
|
build:
|
|
36
38
|
name: Build
|
|
37
39
|
runs-on: ubuntu-latest
|
|
@@ -104,6 +106,17 @@ jobs:
|
|
|
104
106
|
- name: Build
|
|
105
107
|
run: uv build
|
|
106
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
|
+
|
|
107
120
|
- name: Upload distributions
|
|
108
121
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
109
122
|
with:
|
|
@@ -111,44 +124,10 @@ jobs:
|
|
|
111
124
|
path: dist/
|
|
112
125
|
if-no-files-found: error
|
|
113
126
|
|
|
114
|
-
publish-test:
|
|
115
|
-
name: Publish to TestPyPI
|
|
116
|
-
runs-on: ubuntu-latest
|
|
117
|
-
needs: build
|
|
118
|
-
# Binds this job to the trusted publisher registered for `testpypi`.
|
|
119
|
-
environment: testpypi
|
|
120
|
-
permissions:
|
|
121
|
-
# Lets the job mint the OIDC token PyPI exchanges for a short-lived,
|
|
122
|
-
# scoped credential. Without it uv has nothing to present.
|
|
123
|
-
id-token: write
|
|
124
|
-
contents: read
|
|
125
|
-
steps:
|
|
126
|
-
# `--index testpypi` resolves that name from [[tool.uv.index]] in
|
|
127
|
-
# pyproject.toml, which is not part of the built artifact.
|
|
128
|
-
- name: Checkout code
|
|
129
|
-
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
130
|
-
|
|
131
|
-
- name: Set up uv
|
|
132
|
-
uses: astral-sh/setup-uv@ae62891fec2bb8e7d6c99fc78c9fec3a63790f8d # v10.0.0
|
|
133
|
-
with:
|
|
134
|
-
python-version: "3.14"
|
|
135
|
-
enable-cache: true
|
|
136
|
-
|
|
137
|
-
- name: Download distributions
|
|
138
|
-
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
|
139
|
-
with:
|
|
140
|
-
name: dist
|
|
141
|
-
path: dist/
|
|
142
|
-
|
|
143
|
-
- name: Publish
|
|
144
|
-
# `always` rather than the default `automatic`: fail loudly if OIDC is
|
|
145
|
-
# unavailable instead of quietly looking for a token.
|
|
146
|
-
run: uv publish --index testpypi --trusted-publishing always
|
|
147
|
-
|
|
148
127
|
publish:
|
|
149
128
|
name: Publish to PyPI
|
|
150
129
|
runs-on: ubuntu-latest
|
|
151
|
-
needs:
|
|
130
|
+
needs: build
|
|
152
131
|
# Binds this job to the trusted publisher registered for `pypi`. Add a
|
|
153
132
|
# required reviewer on this environment for a manual gate before anything
|
|
154
133
|
# reaches the real index.
|
|
@@ -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:
|
|
@@ -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
|
|
|
@@ -139,10 +156,14 @@ Two more about what is in the list at all:
|
|
|
139
156
|
the one a finished branch leaves you in, so leaving it out leaves out the
|
|
140
157
|
only answer that is always right. `main` finds it whatever branch it stands
|
|
141
158
|
on, because its label carries the word, once.
|
|
142
|
-
- The worktree you are standing in is
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
159
|
+
- The worktree you are standing in is shown and never offered. It carries a
|
|
160
|
+
`*` rather than a number, because picking it is the one answer that takes
|
|
161
|
+
you nowhere. Leaving it out of the list entirely is worse: the list then
|
|
162
|
+
shows fewer rows than `git worktree list` does, and reads as though
|
|
163
|
+
something went missing rather than as where you already are. The main
|
|
164
|
+
checkout is in `gws` and `gwp` for the same reason, kept and never
|
|
165
|
+
proposed, and out of `gwr`'s candidates because `git worktree remove`
|
|
166
|
+
refuses it whatever flag it is given.
|
|
146
167
|
|
|
147
168
|
`--json` and `--list` answer the same question without any of this, and an
|
|
148
169
|
agent uses those.
|
|
@@ -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,79 @@ 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.1 - 2026-09-20
|
|
21
|
+
|
|
22
|
+
`gws` and `gwp` list the main checkout, and `gwl`'s picker shows the
|
|
23
|
+
worktree you are standing in. Both listings used to leave those rows out, so
|
|
24
|
+
a repository with the main checkout and one worktree showed one row where
|
|
25
|
+
`git worktree list` shows two, which reads as though something went missing
|
|
26
|
+
rather than as where you already are.
|
|
27
|
+
|
|
28
|
+
The main checkout is `keep`, with `it is the main checkout, and never
|
|
29
|
+
removable`. Nothing proposes it, and `git worktree remove` answers
|
|
30
|
+
`fatal: '<path>' is a main working tree` whatever flag it is given, so `gwr`
|
|
31
|
+
still does not offer it.
|
|
32
|
+
|
|
33
|
+
The one you are standing in carries a `*` rather than a number:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
* cleanup /home/you/git/.worktrees/cleanup/repo
|
|
37
|
+
1 docs-ship-with-the-change (main) /home/you/git/repo
|
|
38
|
+
which? [1-1, or blank to cancel]
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Two things move with those rows. The counts include the main checkout, so a
|
|
42
|
+
repository with one linked worktree reads `1 removable, 1 kept, 0 unclear`
|
|
43
|
+
where it read `1 removable, 0 kept, 0 unclear`, and `gws --json` and
|
|
44
|
+
`gwp --json` carry the extra verdict. A caller filtering on
|
|
45
|
+
`verdict == "remove"` is unaffected; one reading `verdicts[0]` gets the main
|
|
46
|
+
checkout now.
|
|
47
|
+
|
|
48
|
+
`no worktrees besides the main checkout` is on stderr rather than stdout, and
|
|
49
|
+
says `; gwa NAME makes one`. The table is always on stdout now, so stdout is
|
|
50
|
+
never that sentence instead.
|
|
51
|
+
|
|
52
|
+
## 0.3.0 - 2026-09-20
|
|
53
|
+
|
|
54
|
+
`gwl` with no query asks which worktree rather than moving you to one.
|
|
55
|
+
Whenever exactly one was left it took it outright, so a repository holding
|
|
56
|
+
the main checkout and one worktree moved you without a word, and running it
|
|
57
|
+
twice walked you back and forth. A query that narrows to one has already said
|
|
58
|
+
which and still takes it outright. Without a terminal it now refuses at exit
|
|
59
|
+
3 and names `--list` and `--json`, where it used to print a path, so
|
|
60
|
+
`cd $(gwl)` in a script stops working. `gwl <query>` is unchanged.
|
|
61
|
+
|
|
62
|
+
`gwl --list` no longer tries to `cd` into its own listing, and `--json` no
|
|
63
|
+
longer tries to `cd` into the JSON. The shell function reads what the binary
|
|
64
|
+
printed and moves you there, and those two modes print on the same stream a
|
|
65
|
+
destination arrives on, so every one of them ended in a failed `cd`. `gwa
|
|
66
|
+
--json` was the worst: the worktree was created and then the move failed, so
|
|
67
|
+
the run looked broken and was not. It reached `gwa`, `gwl`, `gwm` and `gwr`
|
|
68
|
+
in both fish and zsh, and `fish -c 'gwl --json'` too, because a fish function
|
|
69
|
+
wins there. **That fix lives in the shell functions, so `uv tool upgrade`
|
|
70
|
+
alone does not deliver it; update the fish or zsh plugin as well.**
|
|
71
|
+
|
|
72
|
+
An invocation costs about 18 ms less on Python 3.11 and about 12 ms less on
|
|
73
|
+
3.14. `gw version` was 48.4 ms and is 30.5 ms on 3.11.13, and was 55.6 ms and
|
|
74
|
+
is 43.1 ms on 3.14.7, measured from installed wheels on one machine. Four
|
|
75
|
+
changes account for it: the command you typed imports only what it needs
|
|
76
|
+
rather than every command, records are plain classes rather than dataclasses,
|
|
77
|
+
the parser is built for the subcommand you asked for rather than all eleven,
|
|
78
|
+
and `json` is imported only where it is printed. `difflib`, which named the
|
|
79
|
+
closest command when you mistyped one, is no longer imported to start.
|
|
80
|
+
|
|
81
|
+
### Choices
|
|
82
|
+
|
|
83
|
+
`gwl` asks whenever no query was given, even when the repository holds one
|
|
84
|
+
other worktree. Taking it outright reads as a toggle: the command moves you,
|
|
85
|
+
and moves you back, and never says where. Prompting for a single option looks
|
|
86
|
+
redundant until it is the difference between being shown your options and
|
|
87
|
+
being sent somewhere. A query is the way to skip it, and it always was.
|
|
88
|
+
|
|
89
|
+
`gwl` without a terminal exits 3, joining `gwp`, which 0.2.3 moved there for
|
|
90
|
+
the same reason. A script can then tell a refusal from a typo: 2 is
|
|
91
|
+
argparse's code for a usage error and 3 is nobody else's.
|
|
92
|
+
|
|
15
93
|
## 0.2.3 - 2026-09-11
|
|
16
94
|
|
|
17
95
|
An unreachable remote is an answer rather than a failure. Every command that
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: git-worktrees
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Git worktree commands that refuse to lose work.
|
|
5
5
|
Project-URL: Bug reports, https://github.com/MihaiBojin/worktrees/issues/new
|
|
6
6
|
Project-URL: Source, https://github.com/MihaiBojin/worktrees
|
|
@@ -148,8 +148,10 @@ autoload -Uz compinit && compinit
|
|
|
148
148
|
|
|
149
149
|
### What the shell code does
|
|
150
150
|
|
|
151
|
-
Each function runs `command <name
|
|
152
|
-
|
|
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`
|
|
153
155
|
change no directory, so they ship as console scripts alone and answer the same
|
|
154
156
|
from a prompt and from a script.
|
|
155
157
|
|
|
@@ -166,17 +168,21 @@ separate.
|
|
|
166
168
|
```console
|
|
167
169
|
$ gws
|
|
168
170
|
VERDICT BRANCH WHY PATH
|
|
171
|
+
keep main it is the main checkout, and never removable /home/you/git/repo
|
|
169
172
|
keep dirty-work it has uncommitted changes /home/you/git/.worktrees/dirty-work/repo
|
|
170
173
|
keep holds-secrets merged, but holds 2 ignored path(s); pass --delete-ignored /home/you/git/.worktrees/holds-secrets/repo
|
|
171
174
|
unknown never-pushed not merged into origin/main, and no upstream says whether its commits were pushed /home/you/git/.worktrees/never-pushed/repo
|
|
172
175
|
remove squash-merged squash-merged /home/you/git/.worktrees/squash-merged/repo
|
|
173
176
|
|
|
174
|
-
1 removable,
|
|
177
|
+
1 removable, 3 kept, 1 unclear
|
|
175
178
|
unclear, and gwp does not touch these: never-pushed
|
|
176
179
|
gwp removes the 1 marked removable
|
|
177
180
|
```
|
|
178
181
|
|
|
179
|
-
Rows come in `git worktree list` order
|
|
182
|
+
Rows come in `git worktree list` order, the main checkout first. It is there
|
|
183
|
+
so the table shows what `git worktree list` shows; nothing proposes it, and
|
|
184
|
+
`git worktree remove` answers `fatal: '<path>' is a main working tree`
|
|
185
|
+
whatever flag it is given. Paths are absolute wherever this
|
|
180
186
|
prints one, so a row can be pasted into the next command. The table and the
|
|
181
187
|
count are on stdout; the two lines after them are on stderr.
|
|
182
188
|
|
|
@@ -283,9 +289,10 @@ $ gwm parser-v2 # renames the branch and moves the checkout
|
|
|
283
289
|
$ gwr # removes the one you are standing in
|
|
284
290
|
```
|
|
285
291
|
|
|
286
|
-
Each prints one destination on stdout
|
|
287
|
-
|
|
288
|
-
what it removed; empty output
|
|
292
|
+
Each prints one destination on stdout, which is what the shell function
|
|
293
|
+
reads, and prints data there instead under `--json` and `--list`. `gwr` prints
|
|
294
|
+
a path **only** when you were standing in what it removed; empty output, and
|
|
295
|
+
output that is not a directory, both mean stay put.
|
|
289
296
|
|
|
290
297
|
`gwm` is the one that is not a convenience. Renaming the directory you are
|
|
291
298
|
standing in leaves the shell with a stale `$PWD` and every later command
|
|
@@ -310,25 +317,31 @@ $ gwl --list
|
|
|
310
317
|
```
|
|
311
318
|
|
|
312
319
|
It never offers that one. Picking it is the one answer that cannot take you
|
|
313
|
-
anywhere, and
|
|
314
|
-
nothing at all, which reads as a broken command. So two worktrees and no query
|
|
315
|
-
is not a question, and asking for the one you are in says so and stays put:
|
|
320
|
+
anywhere, and asking for the one you are in says so and stays put:
|
|
316
321
|
|
|
317
322
|
```console
|
|
318
323
|
$ gwl dirty # standing in dirty-work
|
|
319
324
|
already in dirty-work
|
|
320
325
|
```
|
|
321
326
|
|
|
322
|
-
|
|
327
|
+
A query that narrows to one takes it outright. No query does not, even when
|
|
328
|
+
the repository holds exactly one other worktree: `gwl` with no argument means
|
|
329
|
+
show me the options, and being moved without being asked is not that. So it
|
|
330
|
+
asks, numbered, however many there are:
|
|
323
331
|
|
|
324
332
|
```console
|
|
325
|
-
$ gwl # standing in
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
333
|
+
$ gwl # standing in never-pushed
|
|
334
|
+
* never-pushed /home/you/git/.worktrees/never-pushed/repo
|
|
335
|
+
1 main /home/you/git/repo
|
|
336
|
+
2 dirty-work /home/you/git/.worktrees/dirty-work/repo
|
|
337
|
+
3 holds-secrets /home/you/git/.worktrees/holds-secrets/repo
|
|
329
338
|
which? [1-3, or blank to cancel]
|
|
330
339
|
```
|
|
331
340
|
|
|
341
|
+
The one you are standing in is marked `*` and carries no number. It is where
|
|
342
|
+
you are rather than somewhere to go, and a list without it shows three rows
|
|
343
|
+
where `git worktree list` shows four.
|
|
344
|
+
|
|
332
345
|
Matching is substring first and then subsequence, so `tst` finds `add-tests`,
|
|
333
346
|
and a substring hit always outranks a loose one. Without a terminal `gwl`
|
|
334
347
|
refuses at exit 3 and names `--list` and `--json`.
|
|
@@ -122,8 +122,10 @@ autoload -Uz compinit && compinit
|
|
|
122
122
|
|
|
123
123
|
### What the shell code does
|
|
124
124
|
|
|
125
|
-
Each function runs `command <name
|
|
126
|
-
|
|
125
|
+
Each function runs `command <name>` and `cd`s to what it printed, provided
|
|
126
|
+
that is a directory. `--json` and `--list` print what was asked for on the
|
|
127
|
+
same stream a destination arrives on, so the shim asks whether what it has is
|
|
128
|
+
somewhere to go and stays put when it is not. `gws`, `gwp`, `gwnb`, `gwrot` and `gwh`
|
|
127
129
|
change no directory, so they ship as console scripts alone and answer the same
|
|
128
130
|
from a prompt and from a script.
|
|
129
131
|
|
|
@@ -140,17 +142,21 @@ separate.
|
|
|
140
142
|
```console
|
|
141
143
|
$ gws
|
|
142
144
|
VERDICT BRANCH WHY PATH
|
|
145
|
+
keep main it is the main checkout, and never removable /home/you/git/repo
|
|
143
146
|
keep dirty-work it has uncommitted changes /home/you/git/.worktrees/dirty-work/repo
|
|
144
147
|
keep holds-secrets merged, but holds 2 ignored path(s); pass --delete-ignored /home/you/git/.worktrees/holds-secrets/repo
|
|
145
148
|
unknown never-pushed not merged into origin/main, and no upstream says whether its commits were pushed /home/you/git/.worktrees/never-pushed/repo
|
|
146
149
|
remove squash-merged squash-merged /home/you/git/.worktrees/squash-merged/repo
|
|
147
150
|
|
|
148
|
-
1 removable,
|
|
151
|
+
1 removable, 3 kept, 1 unclear
|
|
149
152
|
unclear, and gwp does not touch these: never-pushed
|
|
150
153
|
gwp removes the 1 marked removable
|
|
151
154
|
```
|
|
152
155
|
|
|
153
|
-
Rows come in `git worktree list` order
|
|
156
|
+
Rows come in `git worktree list` order, the main checkout first. It is there
|
|
157
|
+
so the table shows what `git worktree list` shows; nothing proposes it, and
|
|
158
|
+
`git worktree remove` answers `fatal: '<path>' is a main working tree`
|
|
159
|
+
whatever flag it is given. Paths are absolute wherever this
|
|
154
160
|
prints one, so a row can be pasted into the next command. The table and the
|
|
155
161
|
count are on stdout; the two lines after them are on stderr.
|
|
156
162
|
|
|
@@ -257,9 +263,10 @@ $ gwm parser-v2 # renames the branch and moves the checkout
|
|
|
257
263
|
$ gwr # removes the one you are standing in
|
|
258
264
|
```
|
|
259
265
|
|
|
260
|
-
Each prints one destination on stdout
|
|
261
|
-
|
|
262
|
-
what it removed; empty output
|
|
266
|
+
Each prints one destination on stdout, which is what the shell function
|
|
267
|
+
reads, and prints data there instead under `--json` and `--list`. `gwr` prints
|
|
268
|
+
a path **only** when you were standing in what it removed; empty output, and
|
|
269
|
+
output that is not a directory, both mean stay put.
|
|
263
270
|
|
|
264
271
|
`gwm` is the one that is not a convenience. Renaming the directory you are
|
|
265
272
|
standing in leaves the shell with a stale `$PWD` and every later command
|
|
@@ -284,25 +291,31 @@ $ gwl --list
|
|
|
284
291
|
```
|
|
285
292
|
|
|
286
293
|
It never offers that one. Picking it is the one answer that cannot take you
|
|
287
|
-
anywhere, and
|
|
288
|
-
nothing at all, which reads as a broken command. So two worktrees and no query
|
|
289
|
-
is not a question, and asking for the one you are in says so and stays put:
|
|
294
|
+
anywhere, and asking for the one you are in says so and stays put:
|
|
290
295
|
|
|
291
296
|
```console
|
|
292
297
|
$ gwl dirty # standing in dirty-work
|
|
293
298
|
already in dirty-work
|
|
294
299
|
```
|
|
295
300
|
|
|
296
|
-
|
|
301
|
+
A query that narrows to one takes it outright. No query does not, even when
|
|
302
|
+
the repository holds exactly one other worktree: `gwl` with no argument means
|
|
303
|
+
show me the options, and being moved without being asked is not that. So it
|
|
304
|
+
asks, numbered, however many there are:
|
|
297
305
|
|
|
298
306
|
```console
|
|
299
|
-
$ gwl # standing in
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
307
|
+
$ gwl # standing in never-pushed
|
|
308
|
+
* never-pushed /home/you/git/.worktrees/never-pushed/repo
|
|
309
|
+
1 main /home/you/git/repo
|
|
310
|
+
2 dirty-work /home/you/git/.worktrees/dirty-work/repo
|
|
311
|
+
3 holds-secrets /home/you/git/.worktrees/holds-secrets/repo
|
|
303
312
|
which? [1-3, or blank to cancel]
|
|
304
313
|
```
|
|
305
314
|
|
|
315
|
+
The one you are standing in is marked `*` and carries no number. It is where
|
|
316
|
+
you are rather than somewhere to go, and a list without it shows three rows
|
|
317
|
+
where `git worktree list` shows four.
|
|
318
|
+
|
|
306
319
|
Matching is substring first and then subsequence, so `tst` finds `add-tests`,
|
|
307
320
|
and a substring hit always outranks a loose one. Without a terminal `gwl`
|
|
308
321
|
refuses at exit 3 and names `--list` and `--json`.
|