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.
Files changed (85) hide show
  1. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/.agents/skills/release/SKILL.md +16 -6
  2. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/.github/workflows/publish.yml +20 -41
  3. git_worktrees-0.3.1/.github/workflows/testpypi.yml +69 -0
  4. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/.github/workflows/tests.yml +5 -0
  5. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/AGENTS.md +27 -6
  6. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/CHANGELOG.md +81 -3
  7. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/PKG-INFO +29 -16
  8. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/README.md +28 -15
  9. git_worktrees-0.3.1/docs/BENCHMARK.md +88 -0
  10. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/docs/DEVELOPMENT.md +80 -25
  11. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/functions/gwa.fish +6 -1
  12. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/functions/gwl.fish +6 -1
  13. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/functions/gwm.fish +6 -1
  14. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/functions/gwr.fish +6 -1
  15. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/pyproject.toml +1 -1
  16. git_worktrees-0.3.1/scripts/README.md +76 -0
  17. git_worktrees-0.3.1/scripts/benchmark.py +363 -0
  18. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/src/worktrees/cli.py +147 -35
  19. git_worktrees-0.3.1/src/worktrees/errors.py +16 -0
  20. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/src/worktrees/forge.py +9 -3
  21. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/src/worktrees/git.py +51 -14
  22. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/src/worktrees/new_branch.py +7 -2
  23. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/src/worktrees/pick.py +28 -4
  24. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/src/worktrees/render.py +7 -3
  25. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/src/worktrees/repo.py +23 -4
  26. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/src/worktrees/rotate.py +14 -3
  27. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/src/worktrees/verdicts.py +43 -5
  28. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/src/worktrees/worktree.py +7 -2
  29. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/tests/conftest.py +44 -0
  30. git_worktrees-0.3.1/tests/test_benchmark.py +136 -0
  31. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/tests/test_cli.py +97 -26
  32. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/tests/test_completions.py +6 -19
  33. git_worktrees-0.3.1/tests/test_fixture.py +69 -0
  34. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/tests/test_forge.py +2 -9
  35. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/tests/test_gates.py +0 -2
  36. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/tests/test_new_branch.py +3 -15
  37. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/tests/test_rotate.py +2 -14
  38. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/tests/test_shims.py +30 -0
  39. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/tests/test_worktree.py +18 -16
  40. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/uv.lock +1 -1
  41. git_worktrees-0.3.1/zsh/plugins/worktrees/functions/gwa +14 -0
  42. git_worktrees-0.3.1/zsh/plugins/worktrees/functions/gwl +14 -0
  43. git_worktrees-0.3.1/zsh/plugins/worktrees/functions/gwm +14 -0
  44. git_worktrees-0.3.1/zsh/plugins/worktrees/functions/gwr +14 -0
  45. git_worktrees-0.2.3/zsh/plugins/worktrees/functions/gwa +0 -9
  46. git_worktrees-0.2.3/zsh/plugins/worktrees/functions/gwl +0 -9
  47. git_worktrees-0.2.3/zsh/plugins/worktrees/functions/gwm +0 -9
  48. git_worktrees-0.2.3/zsh/plugins/worktrees/functions/gwr +0 -9
  49. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/.claude/settings.json +0 -0
  50. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/.gitignore +0 -0
  51. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/.pre-commit-config.yaml +0 -0
  52. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/.python-version +0 -0
  53. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/LICENSE +0 -0
  54. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/completions/gw.fish +0 -0
  55. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/completions/gwa.fish +0 -0
  56. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/completions/gwh.fish +0 -0
  57. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/completions/gwl.fish +0 -0
  58. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/completions/gwm.fish +0 -0
  59. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/completions/gwnb.fish +0 -0
  60. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/completions/gwp.fish +0 -0
  61. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/completions/gwr.fish +0 -0
  62. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/completions/gwrot.fish +0 -0
  63. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/completions/gws.fish +0 -0
  64. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/conf.d/worktrees.fish +0 -0
  65. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/hatch_build.py +0 -0
  66. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/renovate.json +0 -0
  67. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/scripts/verify-publish.bash +0 -0
  68. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/src/worktrees/__init__.py +0 -0
  69. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/src/worktrees/layout.py +0 -0
  70. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/src/worktrees/merged.py +0 -0
  71. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/src/worktrees/prune.py +0 -0
  72. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/tests/test_render.py +0 -0
  73. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/tests/test_spec.py +0 -0
  74. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/tests/test_version.py +0 -0
  75. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/zsh/plugins/worktrees/completions/_gw +0 -0
  76. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/zsh/plugins/worktrees/completions/_gwa +0 -0
  77. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/zsh/plugins/worktrees/completions/_gwh +0 -0
  78. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/zsh/plugins/worktrees/completions/_gwl +0 -0
  79. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/zsh/plugins/worktrees/completions/_gwm +0 -0
  80. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/zsh/plugins/worktrees/completions/_gwnb +0 -0
  81. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/zsh/plugins/worktrees/completions/_gwp +0 -0
  82. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/zsh/plugins/worktrees/completions/_gwr +0 -0
  83. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/zsh/plugins/worktrees/completions/_gwrot +0 -0
  84. {git_worktrees-0.2.3 → git_worktrees-0.3.1}/zsh/plugins/worktrees/completions/_gws +0 -0
  85. {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:draft <version>`, from the ReleaseTools plugin. It
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 --rebase --delete-branch
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
- `--rebase` keeps the commit message rather than replacing it with the pull
95
- request title.
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 rebase onto a main that moved is a tree neither the
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 TestPyPI before PyPI.
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-test -> publish -> release
8
- # \-> verify
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
- # TestPyPI gates the real upload deliberately: a PyPI upload cannot be undone
15
- # or replaced, so if the rehearsal fails there is nothing to pin against and
16
- # the run stops there.
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 TestPyPI accepted.
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: publish-test
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
- - One match takes it outright, with no prompt at all.
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 listed and never offered. Picking it is
143
- the one answer that takes you nowhere, and a picker whose single candidate
144
- is where you already are prints nothing at all, which reads as a broken
145
- command.
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 follow
5
- [Semantic Versioning](https://semver.org/spec/v2.0.0.html), and the shape
6
- follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
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.2.3
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>`, reads the one path the binary printed and
152
- `cd`s there, and carries nothing else. `gws`, `gwp`, `gwnb`, `gwrot` and `gwh`
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, 2 kept, 1 unclear
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. Paths are absolute wherever this
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 and nothing else, which is what the
287
- shell function reads. `gwr` prints a path **only** when you were standing in
288
- what it removed; empty output means stay put.
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 a picker whose single candidate is where you already are prints
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
- With more than one worth offering it asks, numbered:
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 the main checkout
326
- 1 dirty-work /home/you/git/.worktrees/dirty-work/repo
327
- 2 holds-secrets /home/you/git/.worktrees/holds-secrets/repo
328
- 3 never-pushed /home/you/git/.worktrees/never-pushed/repo
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>`, reads the one path the binary printed and
126
- `cd`s there, and carries nothing else. `gws`, `gwp`, `gwnb`, `gwrot` and `gwh`
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, 2 kept, 1 unclear
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. Paths are absolute wherever this
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 and nothing else, which is what the
261
- shell function reads. `gwr` prints a path **only** when you were standing in
262
- what it removed; empty output means stay put.
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 a picker whose single candidate is where you already are prints
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
- With more than one worth offering it asks, numbered:
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 the main checkout
300
- 1 dirty-work /home/you/git/.worktrees/dirty-work/repo
301
- 2 holds-secrets /home/you/git/.worktrees/holds-secrets/repo
302
- 3 never-pushed /home/you/git/.worktrees/never-pushed/repo
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`.