git-worktrees 0.2.3__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.3 → git_worktrees-0.3.0}/.agents/skills/release/SKILL.md +16 -6
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/.github/workflows/publish.yml +20 -41
- git_worktrees-0.3.0/.github/workflows/testpypi.yml +69 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/.github/workflows/tests.yml +5 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/AGENTS.md +19 -2
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/CHANGELOG.md +49 -3
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/PKG-INFO +14 -10
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/README.md +13 -9
- git_worktrees-0.3.0/docs/BENCHMARK.md +88 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/docs/DEVELOPMENT.md +80 -25
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/functions/gwa.fish +6 -1
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/functions/gwl.fish +6 -1
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/functions/gwm.fish +6 -1
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/functions/gwr.fish +6 -1
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/pyproject.toml +1 -1
- git_worktrees-0.3.0/scripts/README.md +76 -0
- git_worktrees-0.3.0/scripts/benchmark.py +363 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/src/worktrees/cli.py +127 -28
- git_worktrees-0.3.0/src/worktrees/errors.py +16 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/src/worktrees/forge.py +9 -3
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/src/worktrees/git.py +51 -14
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/src/worktrees/new_branch.py +7 -2
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/src/worktrees/pick.py +11 -3
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/src/worktrees/render.py +7 -3
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/src/worktrees/repo.py +23 -4
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/src/worktrees/rotate.py +14 -3
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/src/worktrees/verdicts.py +20 -4
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/src/worktrees/worktree.py +7 -2
- {git_worktrees-0.2.3 → 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.3 → git_worktrees-0.3.0}/tests/test_cli.py +24 -16
- {git_worktrees-0.2.3 → 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.3 → git_worktrees-0.3.0}/tests/test_forge.py +2 -9
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/tests/test_gates.py +0 -2
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/tests/test_new_branch.py +3 -15
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/tests/test_rotate.py +2 -14
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/tests/test_shims.py +30 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/tests/test_worktree.py +18 -16
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/uv.lock +1 -1
- 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.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.0}/.claude/settings.json +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/.gitignore +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/.pre-commit-config.yaml +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/.python-version +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/LICENSE +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/completions/gw.fish +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/completions/gwa.fish +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/completions/gwh.fish +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/completions/gwl.fish +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/completions/gwm.fish +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/completions/gwnb.fish +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/completions/gwp.fish +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/completions/gwr.fish +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/completions/gwrot.fish +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/completions/gws.fish +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/conf.d/worktrees.fish +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/hatch_build.py +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/renovate.json +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/scripts/verify-publish.bash +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/src/worktrees/__init__.py +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/src/worktrees/layout.py +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/src/worktrees/merged.py +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/src/worktrees/prune.py +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/tests/test_render.py +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/tests/test_spec.py +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/tests/test_version.py +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/zsh/plugins/worktrees/completions/_gw +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/zsh/plugins/worktrees/completions/_gwa +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/zsh/plugins/worktrees/completions/_gwh +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/zsh/plugins/worktrees/completions/_gwl +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/zsh/plugins/worktrees/completions/_gwm +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/zsh/plugins/worktrees/completions/_gwnb +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/zsh/plugins/worktrees/completions/_gwp +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/zsh/plugins/worktrees/completions/_gwr +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/zsh/plugins/worktrees/completions/_gwrot +0 -0
- {git_worktrees-0.2.3 → git_worktrees-0.3.0}/zsh/plugins/worktrees/completions/_gws +0 -0
- {git_worktrees-0.2.3 → 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,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
|
|
|
@@ -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,47 @@ 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
|
+
|
|
15
61
|
## 0.2.3 - 2026-09-11
|
|
16
62
|
|
|
17
63
|
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.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
|
|
@@ -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
|
|
|
@@ -283,9 +285,10 @@ $ gwm parser-v2 # renames the branch and moves the checkout
|
|
|
283
285
|
$ gwr # removes the one you are standing in
|
|
284
286
|
```
|
|
285
287
|
|
|
286
|
-
Each prints one destination on stdout
|
|
287
|
-
|
|
288
|
-
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.
|
|
289
292
|
|
|
290
293
|
`gwm` is the one that is not a convenience. Renaming the directory you are
|
|
291
294
|
standing in leaves the shell with a stale `$PWD` and every later command
|
|
@@ -310,16 +313,17 @@ $ gwl --list
|
|
|
310
313
|
```
|
|
311
314
|
|
|
312
315
|
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:
|
|
316
|
+
anywhere, and asking for the one you are in says so and stays put:
|
|
316
317
|
|
|
317
318
|
```console
|
|
318
319
|
$ gwl dirty # standing in dirty-work
|
|
319
320
|
already in dirty-work
|
|
320
321
|
```
|
|
321
322
|
|
|
322
|
-
|
|
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:
|
|
323
327
|
|
|
324
328
|
```console
|
|
325
329
|
$ gwl # standing in the main checkout
|
|
@@ -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
|
|
|
@@ -257,9 +259,10 @@ $ gwm parser-v2 # renames the branch and moves the checkout
|
|
|
257
259
|
$ gwr # removes the one you are standing in
|
|
258
260
|
```
|
|
259
261
|
|
|
260
|
-
Each prints one destination on stdout
|
|
261
|
-
|
|
262
|
-
what it removed; empty output
|
|
262
|
+
Each prints one destination on stdout, which is what the shell function
|
|
263
|
+
reads, and prints data there instead under `--json` and `--list`. `gwr` prints
|
|
264
|
+
a path **only** when you were standing in what it removed; empty output, and
|
|
265
|
+
output that is not a directory, both mean stay put.
|
|
263
266
|
|
|
264
267
|
`gwm` is the one that is not a convenience. Renaming the directory you are
|
|
265
268
|
standing in leaves the shell with a stale `$PWD` and every later command
|
|
@@ -284,16 +287,17 @@ $ gwl --list
|
|
|
284
287
|
```
|
|
285
288
|
|
|
286
289
|
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:
|
|
290
|
+
anywhere, and asking for the one you are in says so and stays put:
|
|
290
291
|
|
|
291
292
|
```console
|
|
292
293
|
$ gwl dirty # standing in dirty-work
|
|
293
294
|
already in dirty-work
|
|
294
295
|
```
|
|
295
296
|
|
|
296
|
-
|
|
297
|
+
A query that narrows to one takes it outright. No query does not, even when
|
|
298
|
+
the repository holds exactly one other worktree: `gwl` with no argument means
|
|
299
|
+
show me the options, and being moved without being asked is not that. So it
|
|
300
|
+
asks, numbered, however many there are:
|
|
297
301
|
|
|
298
302
|
```console
|
|
299
303
|
$ gwl # standing in the main checkout
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# What an invocation costs
|
|
2
|
+
|
|
3
|
+
Generated. Everything else this program does is a `git` subprocess or
|
|
4
|
+
a forge round trip, and both are facts about a repository and a
|
|
5
|
+
network rather than about the code, so starting is the only thing
|
|
6
|
+
timed here.
|
|
7
|
+
|
|
8
|
+
```console
|
|
9
|
+
$ uv run scripts/benchmark.py > b.json
|
|
10
|
+
$ uv run scripts/benchmark.py --markdown --from b.json > docs/BENCHMARK.md
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The JSON keeps every module `-X importtime` named. This is one view of
|
|
14
|
+
it, and a question about a module missing below is a filter rather
|
|
15
|
+
than another run.
|
|
16
|
+
|
|
17
|
+
Measured 2026-09-13T00:01:21Z against `7f9f332`, version 0.2.3, on macOS-26.6.2-arm64-arm-64bit-Mach-O. 50 runs each, wall clock divided, after 3 discarded.
|
|
18
|
+
|
|
19
|
+
| | 3.11.13 | 3.12.14 | 3.13.15 | 3.14.7 |
|
|
20
|
+
| --- | --- | --- | --- | --- |
|
|
21
|
+
| the interpreter alone | 16.9 ms | 18.8 ms | 19.4 ms | 19.7 ms |
|
|
22
|
+
| gw version | 27.4 ms | 30.1 ms | 29.7 ms | 40.7 ms |
|
|
23
|
+
| gwh | 27.6 ms | 29.8 ms | 29.6 ms | 40.3 ms |
|
|
24
|
+
| gws --help | 28.0 ms | 31.0 ms | 30.6 ms | 41.5 ms |
|
|
25
|
+
| /bin/echo, for scale | 2.5 ms | 2.5 ms | 2.6 ms | 2.6 ms |
|
|
26
|
+
|
|
27
|
+
Every column is a uv-managed interpreter. That is the point of `--managed-python`:
|
|
28
|
+
without it uv takes whatever it finds first, and a run compares packagers rather
|
|
29
|
+
than versions. Homebrew's 3.14 ships 76 extension modules as shared objects
|
|
30
|
+
where the build below ships 2, and on macOS every `dlopen` pays a code-signature
|
|
31
|
+
check, which was worth 8 ms of interpreter startup on its own.
|
|
32
|
+
|
|
33
|
+
| how each was built | 3.11.13 | 3.12.14 | 3.13.15 | 3.14.7 |
|
|
34
|
+
| --- | --- | --- | --- | --- |
|
|
35
|
+
| modules linked into the executable | 99 | 95 | 97 | 101 |
|
|
36
|
+
| modules `dlopen`ed from `lib-dynload` | 4 | 3 | 2 | 2 |
|
|
37
|
+
|
|
38
|
+
### 3.11.13, the 8 costliest imports
|
|
39
|
+
|
|
40
|
+
| module | self | cumulative |
|
|
41
|
+
| --- | --- | --- |
|
|
42
|
+
| `typing` | 1.4 ms | 1.4 ms |
|
|
43
|
+
| `enum` | 1.0 ms | 1.0 ms |
|
|
44
|
+
| `time` | 0.6 ms | 0.6 ms |
|
|
45
|
+
| `ipaddress` | 0.6 ms | 0.6 ms |
|
|
46
|
+
| `urllib.parse` | 0.5 ms | 1.1 ms |
|
|
47
|
+
| `gettext` | 0.5 ms | 0.5 ms |
|
|
48
|
+
| `argparse` | 0.5 ms | 3.2 ms |
|
|
49
|
+
| `pathlib` | 0.4 ms | 1.9 ms |
|
|
50
|
+
|
|
51
|
+
### 3.12.14, the 8 costliest imports
|
|
52
|
+
|
|
53
|
+
| module | self | cumulative |
|
|
54
|
+
| --- | --- | --- |
|
|
55
|
+
| `typing` | 1.5 ms | 1.6 ms |
|
|
56
|
+
| `urllib.parse` | 0.9 ms | 1.8 ms |
|
|
57
|
+
| `ipaddress` | 0.8 ms | 0.8 ms |
|
|
58
|
+
| `enum` | 0.8 ms | 0.8 ms |
|
|
59
|
+
| `time` | 0.6 ms | 0.6 ms |
|
|
60
|
+
| `argparse` | 0.5 ms | 3.0 ms |
|
|
61
|
+
| `site` | 0.5 ms | 3.5 ms |
|
|
62
|
+
| `nt` | 0.5 ms | 0.5 ms |
|
|
63
|
+
|
|
64
|
+
### 3.13.15, the 8 costliest imports
|
|
65
|
+
|
|
66
|
+
| module | self | cumulative |
|
|
67
|
+
| --- | --- | --- |
|
|
68
|
+
| `typing` | 1.2 ms | 1.2 ms |
|
|
69
|
+
| `enum` | 0.7 ms | 0.7 ms |
|
|
70
|
+
| `time` | 0.6 ms | 0.6 ms |
|
|
71
|
+
| `argparse` | 0.5 ms | 3.0 ms |
|
|
72
|
+
| `gettext` | 0.5 ms | 0.5 ms |
|
|
73
|
+
| `pathlib._local` | 0.5 ms | 1.3 ms |
|
|
74
|
+
| `site` | 0.5 ms | 3.6 ms |
|
|
75
|
+
| `encodings` | 0.5 ms | 1.1 ms |
|
|
76
|
+
|
|
77
|
+
### 3.14.7, the 8 costliest imports
|
|
78
|
+
|
|
79
|
+
| module | self | cumulative |
|
|
80
|
+
| --- | --- | --- |
|
|
81
|
+
| `typing` | 1.3 ms | 1.3 ms |
|
|
82
|
+
| `enum` | 0.7 ms | 0.7 ms |
|
|
83
|
+
| `time` | 0.7 ms | 0.7 ms |
|
|
84
|
+
| `contextlib` | 0.6 ms | 1.9 ms |
|
|
85
|
+
| `site` | 0.5 ms | 4.0 ms |
|
|
86
|
+
| `argparse` | 0.5 ms | 2.8 ms |
|
|
87
|
+
| `_collections_abc` | 0.5 ms | 0.5 ms |
|
|
88
|
+
| `encodings` | 0.5 ms | 1.2 ms |
|