git-worktrees 0.6.0__tar.gz → 0.6.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 (102) hide show
  1. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/.github/workflows/publish.yml +18 -3
  2. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/.releasetools.yaml +3 -1
  3. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/AGENTS.md +8 -0
  4. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/CHANGELOG.md +19 -0
  5. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/PKG-INFO +49 -7
  6. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/README.md +48 -6
  7. git_worktrees-0.6.1/conf.d/worktrees.fish +7 -0
  8. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/docs/DEVELOPMENT.md +11 -2
  9. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/hatch_build.py +2 -2
  10. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/pyproject.toml +1 -1
  11. git_worktrees-0.6.1/scripts/bump_version.py +77 -0
  12. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/src/worktrees/cli.py +59 -1
  13. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/tests/test_shims.py +36 -0
  14. git_worktrees-0.6.1/tests/test_version.py +233 -0
  15. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/uv.lock +1 -1
  16. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/zsh/plugins/worktrees/worktrees.plugin.zsh +6 -1
  17. git_worktrees-0.6.0/conf.d/worktrees.fish +0 -3
  18. git_worktrees-0.6.0/tests/test_version.py +0 -87
  19. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/.claude/settings.json +0 -0
  20. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/.github/workflows/codeql.yml +0 -0
  21. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/.github/workflows/testpypi.yml +0 -0
  22. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/.github/workflows/tests.yml +0 -0
  23. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/.gitignore +0 -0
  24. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/.pre-commit-config.yaml +0 -0
  25. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/.python-version +0 -0
  26. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/LICENSE +0 -0
  27. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/Makefile +0 -0
  28. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/completions/gw.fish +0 -0
  29. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/completions/gwa.fish +0 -0
  30. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/completions/gwbc.fish +0 -0
  31. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/completions/gwbd.fish +0 -0
  32. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/completions/gwbs.fish +0 -0
  33. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/completions/gwh.fish +0 -0
  34. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/completions/gwl.fish +0 -0
  35. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/completions/gwm.fish +0 -0
  36. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/completions/gwp.fish +0 -0
  37. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/completions/gwr.fish +0 -0
  38. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/completions/gwrot.fish +0 -0
  39. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/completions/gws.fish +0 -0
  40. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/completions/worktrees.fish +0 -0
  41. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/docs/BENCHMARK.md +0 -0
  42. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/docs/REFERENCE.md +0 -0
  43. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/docs/commands/branch-delete.md +0 -0
  44. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/docs/commands/branch-status.md +0 -0
  45. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/functions/gwa.fish +0 -0
  46. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/functions/gwl.fish +0 -0
  47. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/functions/gwm.fish +0 -0
  48. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/functions/gwr!.fish +0 -0
  49. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/functions/gwr.fish +0 -0
  50. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/renovate.json +0 -0
  51. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/scripts/README.md +0 -0
  52. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/scripts/benchmark.py +0 -0
  53. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/scripts/verify-publish.bash +0 -0
  54. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/src/worktrees/__init__.py +0 -0
  55. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/src/worktrees/branch_cli.py +0 -0
  56. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/src/worktrees/branch_forge.py +0 -0
  57. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/src/worktrees/branches.py +0 -0
  58. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/src/worktrees/errors.py +0 -0
  59. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/src/worktrees/forge.py +0 -0
  60. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/src/worktrees/git.py +0 -0
  61. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/src/worktrees/layout.py +0 -0
  62. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/src/worktrees/merged.py +0 -0
  63. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/src/worktrees/new_branch.py +0 -0
  64. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/src/worktrees/pick.py +0 -0
  65. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/src/worktrees/progress.py +0 -0
  66. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/src/worktrees/prune.py +0 -0
  67. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/src/worktrees/render.py +0 -0
  68. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/src/worktrees/repo.py +0 -0
  69. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/src/worktrees/rotate.py +0 -0
  70. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/src/worktrees/verdicts.py +0 -0
  71. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/src/worktrees/worktree.py +0 -0
  72. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/tests/conftest.py +0 -0
  73. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/tests/test_benchmark.py +0 -0
  74. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/tests/test_branch_forge.py +0 -0
  75. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/tests/test_branches.py +0 -0
  76. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/tests/test_cli.py +0 -0
  77. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/tests/test_completions.py +0 -0
  78. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/tests/test_fixture.py +0 -0
  79. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/tests/test_forge.py +0 -0
  80. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/tests/test_gates.py +0 -0
  81. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/tests/test_new_branch.py +0 -0
  82. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/tests/test_render.py +0 -0
  83. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/tests/test_rotate.py +0 -0
  84. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/tests/test_spec.py +0 -0
  85. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/tests/test_worktree.py +0 -0
  86. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/zsh/plugins/worktrees/completions/_gw +0 -0
  87. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/zsh/plugins/worktrees/completions/_gwa +0 -0
  88. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/zsh/plugins/worktrees/completions/_gwbc +0 -0
  89. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/zsh/plugins/worktrees/completions/_gwbd +0 -0
  90. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/zsh/plugins/worktrees/completions/_gwbs +0 -0
  91. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/zsh/plugins/worktrees/completions/_gwh +0 -0
  92. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/zsh/plugins/worktrees/completions/_gwl +0 -0
  93. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/zsh/plugins/worktrees/completions/_gwm +0 -0
  94. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/zsh/plugins/worktrees/completions/_gwp +0 -0
  95. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/zsh/plugins/worktrees/completions/_gwr +0 -0
  96. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/zsh/plugins/worktrees/completions/_gwrot +0 -0
  97. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/zsh/plugins/worktrees/completions/_gws +0 -0
  98. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/zsh/plugins/worktrees/functions/gwa +0 -0
  99. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/zsh/plugins/worktrees/functions/gwl +0 -0
  100. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/zsh/plugins/worktrees/functions/gwm +0 -0
  101. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/zsh/plugins/worktrees/functions/gwr +0 -0
  102. {git_worktrees-0.6.0 → git_worktrees-0.6.1}/zsh/plugins/worktrees/functions/gwr! +0 -0
@@ -192,12 +192,27 @@ jobs:
192
192
  name: dist
193
193
  path: dist/
194
194
 
195
- # The install check waits for PyPI to serve the files.
195
+ # The install check resolves the wheel alone, so the sdist can still be
196
+ # missing from PyPI here. A file PyPI does not serve yet is asked again,
197
+ # backing off from 15s over eight attempts as the install check does;
198
+ # any other failure ends the job at once.
196
199
  - name: Verify attestations on PyPI
197
200
  run: |
198
201
  for f in dist/*.whl dist/*.tar.gz; do
199
- uvx pypi-attestations@0.0.30 verify pypi \
200
- --repository "https://github.com/$GITHUB_REPOSITORY" "pypi:${f#dist/}"
202
+ file="${f#dist/}"
203
+ delay=15
204
+ for attempt in 1 2 3 4 5 6 7 8; do
205
+ code=0
206
+ uvx pypi-attestations@0.0.30 verify pypi \
207
+ --repository "https://github.com/$GITHUB_REPOSITORY" "pypi:$file" 2>err.txt || code=$?
208
+ cat err.txt >&2
209
+ [ "$code" -ne 0 ] || break
210
+ grep -q "Could not find the artifact" err.txt || exit 1
211
+ [ "$attempt" -lt 8 ] || exit 1
212
+ echo "waiting ${delay}s for PyPI to serve $file (attempt $attempt/8)..." >&2
213
+ sleep "$delay"
214
+ delay=$((delay * 2))
215
+ done
201
216
  done
202
217
 
203
218
  release:
@@ -10,7 +10,9 @@ projects:
10
10
  - path: ./
11
11
  manifest: pyproject.toml
12
12
  changelog: CHANGELOG.md
13
- bump: uv version {version}
13
+ # `uv version` for pyproject.toml and uv.lock, then the copies the shell
14
+ # plugins and the README's install lines carry.
15
+ bump: python3 scripts/bump_version.py {version}
14
16
 
15
17
  release:
16
18
  branch: main
@@ -105,6 +105,14 @@ zsh/plugins/worktrees/completions/_gwa
105
105
  Neither manager puts a binary on `$PATH`. The console scripts arrive through
106
106
  `uv tool install` from the published package, which is a separate step.
107
107
 
108
+ Both plugins export `WORKTREES_PLUGIN_VERSION`, the release they came from:
109
+ `conf.d/worktrees.fish` and `worktrees.plugin.zsh`, one assignment each.
110
+ `gwh` and `gw version` compare it with `__version__`, in Python; the shell
111
+ files hold the value and no logic. Nothing built can stamp a file a plugin
112
+ manager copies from git, so `scripts/bump_version.py` writes it at release,
113
+ along with the tag in the README's install lines, and `tests/test_version.py`
114
+ fails when a copy disagrees with `pyproject.toml`.
115
+
108
116
  Nothing installs this from a checkout on the user's disk. It is consumed as a
109
117
  third-party plugin and a third-party tool, so anything that assumes a local
110
118
  clone at a known path is wrong here.
@@ -17,6 +17,25 @@ not in here, whatever it cost to build.
17
17
  A release closes with a `### Choices` section when a decision in it is worth
18
18
  the reader's time: what was chosen, and what the alternative failed to do.
19
19
 
20
+ ## 0.6.1 - 2026-11-02
21
+
22
+ ### Added
23
+
24
+ `gwh` says which release the shell plugin in this shell came from, and whether
25
+ it matches the commands. When the plugin is older, it names the tag to pin it
26
+ to, and when newer, it gives the `uv tool upgrade git-worktrees` to run.
27
+ `gw version` says the same on stderr, and its stdout is still the version
28
+ alone. Both plugins export `WORKTREES_PLUGIN_VERSION` for this. A shell whose
29
+ plugin predates this release reports none.
30
+
31
+ ### Changed
32
+
33
+ The README installs the plugins at the release's tag, `fisher install
34
+ MihaiBojin/worktrees@v0.6.1` for fish and `branch:v0.6.1` for Antidote, and
35
+ says how to move each to another release. Moving an existing install onto a
36
+ tag takes an edit to `fish_plugins` and `fisher update`, or `antidote purge
37
+ MihaiBojin/worktrees` before editing the tag.
38
+
20
39
  ## 0.6.0 - 2026-11-02
21
40
 
22
41
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: git-worktrees
3
- Version: 0.6.0
3
+ Version: 0.6.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
@@ -132,21 +132,35 @@ That installs `gws`, `gwp`, `gwbs`, `gwbd`, `gwbc`, `gwrot`, and `gwh`. The
132
132
  four that land you somewhere need a shell function too, and every command has
133
133
  a completion, both of which arrive through a plugin manager.
134
134
 
135
+ Install the plugin at the tag of the release `uv` installed. Without a tag
136
+ both managers take `main`, which can be ahead of the last release, and the
137
+ functions then call commands they were not tested against. `gwh` says which
138
+ release the plugin here came from, and whether it is the same one.
139
+
140
+ To upgrade, move both: `uv tool upgrade git-worktrees` for the commands, then
141
+ the plugin's tag to the same release, as each manager below describes.
142
+
135
143
  ### fish
136
144
 
137
145
  ```console
138
- fisher install MihaiBojin/worktrees
146
+ fisher install MihaiBojin/worktrees@v0.6.1
139
147
  ```
140
148
 
141
- Fisher copies `functions/` and `completions/` from the repository root, so the
142
- five functions and ten completions land together. Nothing has to be sourced.
149
+ Fisher copies `functions/`, `completions/` and `conf.d/` from the repository
150
+ root, so the functions for `gwa`, `gwl`, `gwm`, `gwr` and `gwr!` land together
151
+ with a completion for every command. Nothing has to be sourced.
152
+
153
+ To change release, edit the tag on that line of
154
+ `$__fish_config_dir/fish_plugins` and run `fisher update`. The same edit moves
155
+ an install without a tag onto one; `fisher install` with a tag refuses while
156
+ the untagged one holds the files.
143
157
 
144
158
  ### zsh
145
159
 
146
- Antidote takes a `path:` into the repository:
160
+ Antidote takes a `path:` into the repository, and `branch:` takes a tag:
147
161
 
148
162
  ```
149
- MihaiBojin/worktrees path:zsh/plugins/worktrees # in zsh_plugins.txt
163
+ MihaiBojin/worktrees branch:v0.6.1 path:zsh/plugins/worktrees # in zsh_plugins.txt
150
164
  ```
151
165
 
152
166
  The plugin puts its own `functions/` and `completions/` on `fpath` and
@@ -157,6 +171,19 @@ run; without one, add it after the plugin loads:
157
171
  autoload -Uz compinit && compinit
158
172
  ```
159
173
 
174
+ Antidote checks a tag out only when it clones, and `antidote update` leaves a
175
+ tag where it is. To change release, remove the clone, then edit the tag, then
176
+ open a new shell, which clones it again at the new tag:
177
+
178
+ ```zsh
179
+ antidote purge MihaiBojin/worktrees
180
+ ```
181
+
182
+ The order matters. Antidote rebuilds its bundle only when `zsh_plugins.txt` is
183
+ newer than the bundle, so a tag edited before the purge leaves a new shell
184
+ with no plugin until the file changes again. The same three steps move a clone
185
+ without a tag onto one.
186
+
160
187
  ### What the shell code does
161
188
 
162
189
  Each function runs `command <name>` and `cd`s to what it printed, provided
@@ -166,6 +193,16 @@ somewhere to go and stays put when it is not. `gws`, `gwp`, `gwbc`, `gwrot` and
166
193
  change no directory, so they ship as console scripts alone and answer the same
167
194
  from a prompt and from a script.
168
195
 
196
+ Both plugins export `WORKTREES_PLUGIN_VERSION`, the release they came from,
197
+ since the environment is all a binary sees of the shell that started it. `gwh`
198
+ and `gw version` compare it with their own and say whether the two match,
199
+ which is older, and what to run: `gwh` in its text, `gw version` on stderr, so
200
+ its stdout stays the version alone for a script to read.
201
+
202
+ A shell without the variable has no plugin loaded, or one older than the
203
+ release that started exporting it, and nothing can tell those two apart. A
204
+ bash started from fish or zsh inherits the variable without the functions.
205
+
169
206
  The completions hold no candidate of their own. `gw --complete` prints every
170
207
  subcommand and shorthand, `gwl --complete` prints every worktree, and both
171
208
  shells narrow what those return, so the matching stays in one place and a
@@ -495,7 +532,7 @@ their own and `gwbc` puts them on a branch.
495
532
 
496
533
  ```console
497
534
  $ gwh
498
- worktrees 0.5.1: git worktree commands that refuse to lose work
535
+ worktrees 0.6.1: git worktree commands that refuse to lose work
499
536
 
500
537
  COMMAND GW DOES
501
538
  gws gw status (s, st) which worktrees are finished, and why
@@ -510,6 +547,11 @@ gwh gw help (h) every command and alias, this list
510
547
  gwbc NAME gw branch create branch off the head branch
511
548
  gwbs [NAME ...] gw branch status which branches are finished, and why
512
549
  gwbd [NAME ...] gw branch delete delete local branches proved finished
550
+
551
+ gwa, gwl, gwm and gwr change the directory you are standing in.
552
+ A binary cannot do that, so each needs the shell function of the same name
553
+ from the plugin; the rest need nothing but $PATH.
554
+ The shell plugin here is 0.6.1, the same release as these commands.
513
555
  ```
514
556
 
515
557
  `gw help`, `gw h` and `gw` with no subcommand print the same thing. One table
@@ -106,21 +106,35 @@ That installs `gws`, `gwp`, `gwbs`, `gwbd`, `gwbc`, `gwrot`, and `gwh`. The
106
106
  four that land you somewhere need a shell function too, and every command has
107
107
  a completion, both of which arrive through a plugin manager.
108
108
 
109
+ Install the plugin at the tag of the release `uv` installed. Without a tag
110
+ both managers take `main`, which can be ahead of the last release, and the
111
+ functions then call commands they were not tested against. `gwh` says which
112
+ release the plugin here came from, and whether it is the same one.
113
+
114
+ To upgrade, move both: `uv tool upgrade git-worktrees` for the commands, then
115
+ the plugin's tag to the same release, as each manager below describes.
116
+
109
117
  ### fish
110
118
 
111
119
  ```console
112
- fisher install MihaiBojin/worktrees
120
+ fisher install MihaiBojin/worktrees@v0.6.1
113
121
  ```
114
122
 
115
- Fisher copies `functions/` and `completions/` from the repository root, so the
116
- five functions and ten completions land together. Nothing has to be sourced.
123
+ Fisher copies `functions/`, `completions/` and `conf.d/` from the repository
124
+ root, so the functions for `gwa`, `gwl`, `gwm`, `gwr` and `gwr!` land together
125
+ with a completion for every command. Nothing has to be sourced.
126
+
127
+ To change release, edit the tag on that line of
128
+ `$__fish_config_dir/fish_plugins` and run `fisher update`. The same edit moves
129
+ an install without a tag onto one; `fisher install` with a tag refuses while
130
+ the untagged one holds the files.
117
131
 
118
132
  ### zsh
119
133
 
120
- Antidote takes a `path:` into the repository:
134
+ Antidote takes a `path:` into the repository, and `branch:` takes a tag:
121
135
 
122
136
  ```
123
- MihaiBojin/worktrees path:zsh/plugins/worktrees # in zsh_plugins.txt
137
+ MihaiBojin/worktrees branch:v0.6.1 path:zsh/plugins/worktrees # in zsh_plugins.txt
124
138
  ```
125
139
 
126
140
  The plugin puts its own `functions/` and `completions/` on `fpath` and
@@ -131,6 +145,19 @@ run; without one, add it after the plugin loads:
131
145
  autoload -Uz compinit && compinit
132
146
  ```
133
147
 
148
+ Antidote checks a tag out only when it clones, and `antidote update` leaves a
149
+ tag where it is. To change release, remove the clone, then edit the tag, then
150
+ open a new shell, which clones it again at the new tag:
151
+
152
+ ```zsh
153
+ antidote purge MihaiBojin/worktrees
154
+ ```
155
+
156
+ The order matters. Antidote rebuilds its bundle only when `zsh_plugins.txt` is
157
+ newer than the bundle, so a tag edited before the purge leaves a new shell
158
+ with no plugin until the file changes again. The same three steps move a clone
159
+ without a tag onto one.
160
+
134
161
  ### What the shell code does
135
162
 
136
163
  Each function runs `command <name>` and `cd`s to what it printed, provided
@@ -140,6 +167,16 @@ somewhere to go and stays put when it is not. `gws`, `gwp`, `gwbc`, `gwrot` and
140
167
  change no directory, so they ship as console scripts alone and answer the same
141
168
  from a prompt and from a script.
142
169
 
170
+ Both plugins export `WORKTREES_PLUGIN_VERSION`, the release they came from,
171
+ since the environment is all a binary sees of the shell that started it. `gwh`
172
+ and `gw version` compare it with their own and say whether the two match,
173
+ which is older, and what to run: `gwh` in its text, `gw version` on stderr, so
174
+ its stdout stays the version alone for a script to read.
175
+
176
+ A shell without the variable has no plugin loaded, or one older than the
177
+ release that started exporting it, and nothing can tell those two apart. A
178
+ bash started from fish or zsh inherits the variable without the functions.
179
+
143
180
  The completions hold no candidate of their own. `gw --complete` prints every
144
181
  subcommand and shorthand, `gwl --complete` prints every worktree, and both
145
182
  shells narrow what those return, so the matching stays in one place and a
@@ -469,7 +506,7 @@ their own and `gwbc` puts them on a branch.
469
506
 
470
507
  ```console
471
508
  $ gwh
472
- worktrees 0.5.1: git worktree commands that refuse to lose work
509
+ worktrees 0.6.1: git worktree commands that refuse to lose work
473
510
 
474
511
  COMMAND GW DOES
475
512
  gws gw status (s, st) which worktrees are finished, and why
@@ -484,6 +521,11 @@ gwh gw help (h) every command and alias, this list
484
521
  gwbc NAME gw branch create branch off the head branch
485
522
  gwbs [NAME ...] gw branch status which branches are finished, and why
486
523
  gwbd [NAME ...] gw branch delete delete local branches proved finished
524
+
525
+ gwa, gwl, gwm and gwr change the directory you are standing in.
526
+ A binary cannot do that, so each needs the shell function of the same name
527
+ from the plugin; the rest need nothing but $PATH.
528
+ The shell plugin here is 0.6.1, the same release as these commands.
487
529
  ```
488
530
 
489
531
  `gw help`, `gw h` and `gw` with no subcommand print the same thing. One table
@@ -0,0 +1,7 @@
1
+ # Fisher autoloads functions/ and completions/ on demand, and the binaries
2
+ # come from `uv tool install` rather than from this package.
3
+ #
4
+ # The release this plugin came from, which `gwh` and `gw version` compare
5
+ # with their own. Exported, because the environment is all a binary can see
6
+ # of the shell that started it.
7
+ set -gx WORKTREES_PLUGIN_VERSION 0.6.1
@@ -157,7 +157,7 @@ network. `GIT_CONFIG_GLOBAL` carries that: without it git still reads
157
157
  ```
158
158
 
159
159
  That does the whole of it: cut `release/v0.2.0` from `origin/main`,
160
- `uv version 0.2.0`, open the pull request, wait for its checks, squash-merge,
160
+ `scripts/bump_version.py 0.2.0`, open the pull request, wait for its checks, squash-merge,
161
161
  wait for main's own run on the merged commit, and only then tag. The tag lands
162
162
  on a commit already proved green, so it never has to move.
163
163
 
@@ -201,13 +201,22 @@ published. Nothing is generated from pull request titles.
201
201
  By hand it is the same four commands:
202
202
 
203
203
  ```console
204
- uv version 0.2.0 # rewrites pyproject.toml and re-locks
204
+ python3 scripts/bump_version.py 0.2.0 # uv version, then the plugins and README
205
205
  # open a pull request, review it, merge it
206
206
  git switch main && git pull
207
207
  git tag -a v0.2.0 -m v0.2.0
208
208
  git push origin v0.2.0
209
209
  ```
210
210
 
211
+ `uv version` alone is not the bump. It rewrites `pyproject.toml` and
212
+ `uv.lock`, and the shell plugins carry a copy it never sees: Fisher and
213
+ Antidote install them from the tag rather than from anything built, so each
214
+ exports `WORKTREES_PLUGIN_VERSION` written into it, and the README's install
215
+ lines name the tag. `scripts/bump_version.py` writes every one of them, and
216
+ checks each line matches before it writes any. `tests/test_version.py` fails
217
+ when one disagrees with `pyproject.toml`, so a release pull request missing
218
+ one fails its checks before anything is tagged.
219
+
211
220
  The version lives in `pyproject.toml`, in the repository, on every commit,
212
221
  and in git history. The tag agrees with it or nothing is published. A tag
213
222
  moved onto a commit carrying a different version is refused, which is the
@@ -2,8 +2,8 @@
2
2
 
3
3
  Reading it back through `importlib.metadata` at import instead costs 10 ms of
4
4
  a 67 ms invocation, on every command, for a string two of them print. A
5
- constant in the wheel costs nothing, and `pyproject.toml` stays the one place
6
- the version is written.
5
+ constant in the wheel costs nothing, and the package reads its version from
6
+ `pyproject.toml` alone. The shell plugins' copy is `scripts/bump_version.py`'s.
7
7
 
8
8
  Not `hatch-vcs`: deriving the version from a tag inverts which of the two has
9
9
  to agree with the other, and `publish.yml` refuses a tag whose commit carries
@@ -9,7 +9,7 @@ authors = [
9
9
  ]
10
10
  # Static, and the single source of truth for the version. Bump it in a pull
11
11
  # request and run `uv lock`; the release tag only has to agree with it.
12
- version = "0.6.0"
12
+ version = "0.6.1"
13
13
  # The standard library answers every question this asks. A dependency here
14
14
  # would be paid on every invocation, and an invocation is 67 ms of which 25 ms
15
15
  # is the interpreter starting.
@@ -0,0 +1,77 @@
1
+ """Set the release version in every file that names it.
2
+
3
+ python3 scripts/bump_version.py 0.7.0
4
+
5
+ `uv version` writes `pyproject.toml` and `uv.lock`, and the wheel reads it
6
+ from there. The shell plugins carry a copy of their own, because Fisher and
7
+ Antidote install them from a git tag rather than from anything built, and the
8
+ README's install lines name that tag. `.releasetools.yaml` runs this as the
9
+ release's bump, and `tests/test_version.py` fails when a copy disagrees with
10
+ `pyproject.toml`.
11
+
12
+ Every line is checked before anything is written: a line that no longer
13
+ matches stops the bump with nothing changed, rather than releasing a version
14
+ some copy never heard of.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import re
20
+ import subprocess
21
+ import sys
22
+ from pathlib import Path
23
+
24
+ ROOT = Path(__file__).resolve().parents[1]
25
+
26
+ # Each file, and the lines in it that name the release, `{}` where it goes.
27
+ STAMPS: dict[str, tuple[str, ...]] = {
28
+ "conf.d/worktrees.fish": ("set -gx WORKTREES_PLUGIN_VERSION {}",),
29
+ "zsh/plugins/worktrees/worktrees.plugin.zsh": (
30
+ "export WORKTREES_PLUGIN_VERSION={}",
31
+ ),
32
+ "README.md": (
33
+ "MihaiBojin/worktrees@v{}",
34
+ "MihaiBojin/worktrees branch:v{}",
35
+ "worktrees {}: git worktree commands",
36
+ "The shell plugin here is {}, the same release",
37
+ ),
38
+ }
39
+
40
+
41
+ def _pattern(template: str) -> re.Pattern[str]:
42
+ """The template with any version where `{}` is."""
43
+ before, after = (re.escape(part) for part in template.split("{}"))
44
+ return re.compile(before + r"[0-9][0-9A-Za-z.+]*" + after)
45
+
46
+
47
+ def stamped(root: Path, version: str) -> dict[Path, str]:
48
+ """Every file's text with `version` written in, or SystemExit naming the
49
+ line that matched nothing."""
50
+ texts = {}
51
+ for name, templates in STAMPS.items():
52
+ path = root / name
53
+ text = path.read_text()
54
+ for template in templates:
55
+ # subn reads a backslash in the replacement as a group reference.
56
+ line = template.format(version).replace("\\", r"\\")
57
+ text, count = _pattern(template).subn(line, text)
58
+ if not count:
59
+ raise SystemExit(f"{name}: no line matches {template!r}")
60
+ texts[path] = text
61
+ return texts
62
+
63
+
64
+ def main(argv: list[str]) -> int:
65
+ if len(argv) != 1:
66
+ print("usage: bump_version.py VERSION", file=sys.stderr)
67
+ return 2
68
+ (version,) = argv
69
+ texts = stamped(ROOT, version)
70
+ subprocess.run(["uv", "version", version], cwd=ROOT, check=True)
71
+ for path, text in texts.items():
72
+ path.write_text(text)
73
+ return 0
74
+
75
+
76
+ if __name__ == "__main__":
77
+ raise SystemExit(main(sys.argv[1:]))
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import argparse
6
+ import os
6
7
  import sys
7
8
  import time
8
9
  from collections.abc import Callable
@@ -1059,9 +1060,64 @@ def run_remove(args: argparse.Namespace) -> int:
1059
1060
  # --------------------------------------------------------------------------
1060
1061
 
1061
1062
 
1063
+ # Exported by conf.d/worktrees.fish and worktrees.plugin.zsh, set to the
1064
+ # release each came from. scripts/bump_version.py writes both.
1065
+ PLUGIN_VERSION = "WORKTREES_PLUGIN_VERSION"
1066
+
1067
+
1068
+ def _release(version: str) -> tuple[int, ...] | None:
1069
+ """`x.y.z` as numbers to compare, or None for anything else, such as the
1070
+ `0+unknown` of a source tree nothing installed."""
1071
+ parts = version.split(".")
1072
+ if len(parts) != 3 or not all(p.isdigit() for p in parts):
1073
+ return None
1074
+ return tuple(int(p) for p in parts)
1075
+
1076
+
1077
+ def plugin_state() -> tuple[str, bool]:
1078
+ """What the shell plugin here says about its release, against this
1079
+ package's, and whether the two are the same release.
1080
+
1081
+ A shell without the variable has no plugin loaded, or one from before the
1082
+ plugin carried it. Nothing here can tell those two apart.
1083
+ """
1084
+ plugin = os.environ.get(PLUGIN_VERSION, "")
1085
+ if not plugin:
1086
+ return (
1087
+ "No shell plugin here says which release it is: none is loaded,\n"
1088
+ "or it is older than the release that started saying so.",
1089
+ False,
1090
+ )
1091
+ if plugin == __version__:
1092
+ return (
1093
+ f"The shell plugin here is {plugin}, the same release as these commands.",
1094
+ True,
1095
+ )
1096
+ ours, theirs = _release(__version__), _release(plugin)
1097
+ if ours and theirs and theirs < ours:
1098
+ return (
1099
+ f"The shell plugin here is {plugin}, older than these commands.\n"
1100
+ f"Pin it to v{__version__}: https://github.com/MihaiBojin/worktrees#install",
1101
+ False,
1102
+ )
1103
+ if ours and theirs:
1104
+ return (
1105
+ f"The shell plugin here is {plugin}, newer than these commands.\n"
1106
+ "Upgrade them with `uv tool upgrade git-worktrees`.",
1107
+ False,
1108
+ )
1109
+ return (
1110
+ f"The shell plugin here is {plugin}, and these commands are {__version__}.",
1111
+ False,
1112
+ )
1113
+
1114
+
1062
1115
  def run_version(args: argparse.Namespace) -> int:
1063
- """The version of the installed distribution."""
1116
+ """The version of the installed distribution on stdout, which is what a
1117
+ script reads, and the shell plugin's on stderr."""
1064
1118
  print(__version__)
1119
+ text, same = plugin_state()
1120
+ _err(render.note(text) if same else render.warn(text))
1065
1121
  return 0
1066
1122
 
1067
1123
 
@@ -1109,6 +1165,8 @@ def run_help(args: argparse.Namespace) -> int:
1109
1165
  "standing in.\nA binary cannot do that, so each needs the shell function "
1110
1166
  "of the same name\nfrom the plugin; the rest need nothing but $PATH."
1111
1167
  )
1168
+ text, same = plugin_state()
1169
+ print(o(text, "" if same else YELLOW))
1112
1170
  print()
1113
1171
  # Which commands take the common flags is the table's answer, not a second
1114
1172
  # list here: a row with no flags takes none, and saying so in prose is how
@@ -9,8 +9,10 @@ from __future__ import annotations
9
9
 
10
10
  import os
11
11
  import pty
12
+ import shlex
12
13
  import shutil
13
14
  import subprocess
15
+ import sys
14
16
  import tomllib
15
17
  from pathlib import Path
16
18
  from typing import NoReturn
@@ -323,6 +325,40 @@ def test_the_zsh_shim_prints_output_that_is_not_a_directory(tmp_path) -> None:
323
325
  assert out.splitlines() == [payload, "/"], out
324
326
 
325
327
 
328
+ @pytest.mark.parametrize(
329
+ ("shell", "plugin"),
330
+ [
331
+ ("fish", ROOT / "conf.d" / "worktrees.fish"),
332
+ ("zsh", ROOT / "zsh/plugins/worktrees/worktrees.plugin.zsh"),
333
+ ],
334
+ )
335
+ def test_each_plugin_tells_the_binary_its_release(shell: str, plugin: Path) -> None:
336
+ """Through the environment, so only an exported variable arrives: a fish
337
+ `set -g` or a bare zsh assignment stays in the shell, and the binary then
338
+ reports no plugin at all."""
339
+ binary = shutil.which(shell)
340
+ if not binary: # pragma: no cover
341
+ _missing(shell)
342
+ gw = shlex.join(
343
+ [
344
+ sys.executable,
345
+ "-c",
346
+ "from worktrees.cli import gw; raise SystemExit(gw())",
347
+ "version",
348
+ ]
349
+ )
350
+ no_config = "--no-config" if shell == "fish" else "-f"
351
+ env = {k: v for k, v in os.environ.items() if k != "WORKTREES_PLUGIN_VERSION"}
352
+ proc = subprocess.run(
353
+ [binary, no_config, "-c", f"source {shlex.quote(str(plugin))}; {gw}"],
354
+ capture_output=True,
355
+ text=True,
356
+ env=env,
357
+ )
358
+ assert proc.returncode == 0, proc.stderr
359
+ assert "the same release as these commands" in proc.stderr, proc.stderr
360
+
361
+
326
362
  # --------------------------------------------------------------------------
327
363
  # a line printed to be pasted lands in whichever shell the reader has
328
364
  # --------------------------------------------------------------------------
@@ -0,0 +1,233 @@
1
+ """`pyproject.toml` declares the version and `gw version` reads that one. The
2
+ shell plugins and the README's install lines carry copies, which
3
+ `scripts/bump_version.py` writes and these hold to it."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import os
8
+ import shutil
9
+ import subprocess
10
+ import sys
11
+ import tomllib
12
+ from pathlib import Path
13
+
14
+ import pytest
15
+
16
+ ROOT = Path(__file__).resolve().parents[1]
17
+ sys.path.insert(0, str(ROOT))
18
+ sys.path.insert(0, str(ROOT / "scripts"))
19
+
20
+ import bump_version # noqa: E402
21
+
22
+ from worktrees import cli # noqa: E402
23
+
24
+
25
+ def _declared() -> str:
26
+ return str(
27
+ tomllib.loads((ROOT / "pyproject.toml").read_text())["project"]["version"]
28
+ )
29
+
30
+
31
+ def test_the_package_declares_its_version_nowhere() -> None:
32
+ """A second copy is a second thing to bump, and `uv version` bumps one.
33
+
34
+ `uv version 0.2.0` rewrites pyproject.toml and uv.lock. It does not touch
35
+ Python source, so a literal here would survive a release and every
36
+ console script would report the version before it.
37
+ """
38
+ source = (ROOT / "src" / "worktrees" / "__init__.py").read_text()
39
+ declared = tomllib.loads((ROOT / "pyproject.toml").read_text())["project"][
40
+ "version"
41
+ ]
42
+ assert declared not in source, (
43
+ f"{declared} is written into __init__.py as well as pyproject.toml"
44
+ )
45
+ assert 'version("git-worktrees")' in source
46
+
47
+
48
+ def test_version_matches_the_distribution() -> None:
49
+ from worktrees import __version__
50
+
51
+ declared = tomllib.loads((ROOT / "pyproject.toml").read_text())["project"][
52
+ "version"
53
+ ]
54
+ assert __version__ == declared
55
+
56
+
57
+ def test_both_names_that_take_a_subcommand_report_it() -> None:
58
+ """Driven, because the console script is what a person runs."""
59
+ declared = tomllib.loads((ROOT / "pyproject.toml").read_text())["project"][
60
+ "version"
61
+ ]
62
+ for entry in ("gw", "main"):
63
+ proc = subprocess.run(
64
+ [
65
+ sys.executable,
66
+ "-c",
67
+ f"from worktrees.cli import {entry}; raise SystemExit({entry}())",
68
+ "version",
69
+ ],
70
+ capture_output=True,
71
+ text=True,
72
+ check=False,
73
+ )
74
+ assert proc.returncode == 0, proc.stderr
75
+ assert proc.stdout.strip() == declared, entry
76
+
77
+
78
+ def test_the_build_hook_writes_and_ships_the_version(tmp_path) -> None:
79
+ """Both halves, because either one alone fails in silence.
80
+
81
+ `.gitignore` keeps the generated file out of the repository, and hatchling
82
+ reads `.gitignore`, so a hook that only writes it builds a wheel without
83
+ it. Every install then falls back to the metadata lookup and nothing says
84
+ so.
85
+ """
86
+ import hatch_build
87
+
88
+ assert hatch_build.GENERATED.as_posix() in (ROOT / ".gitignore").read_text()
89
+
90
+ (tmp_path / hatch_build.GENERATED.parent).mkdir(parents=True)
91
+ build_data: dict[str, list[str]] = {}
92
+ written = hatch_build.generate(tmp_path, "9.9.9", build_data)
93
+
94
+ namespace: dict[str, str] = {}
95
+ exec(written.read_text(), namespace)
96
+ assert namespace["__version__"] == "9.9.9"
97
+ assert f"/{hatch_build.GENERATED.as_posix()}" in build_data["artifacts"]
98
+
99
+
100
+ def test_a_checkout_falls_back_to_the_metadata_lookup() -> None:
101
+ """No generated file is the normal state of a clone, and it still answers."""
102
+ source = (ROOT / "src" / "worktrees" / "__init__.py").read_text()
103
+ assert "from ._version import __version__" in source
104
+ assert "except ImportError" in source
105
+
106
+
107
+ # --------------------------------------------------------------------------
108
+ # the copies the shell plugins carry
109
+ # --------------------------------------------------------------------------
110
+
111
+
112
+ @pytest.mark.parametrize(
113
+ ("name", "template"),
114
+ [(n, t) for n, ts in bump_version.STAMPS.items() for t in ts],
115
+ )
116
+ def test_every_copy_names_the_declared_release(name: str, template: str) -> None:
117
+ """A release pull request whose bump missed a copy fails here, before
118
+ anything is tagged."""
119
+ assert template.format(_declared()) in (ROOT / name).read_text()
120
+
121
+
122
+ def _copy(tmp_path: Path) -> Path:
123
+ """The script and the files it stamps, laid out as the repository has them."""
124
+ for name in [*bump_version.STAMPS, "scripts/bump_version.py"]:
125
+ (tmp_path / name).parent.mkdir(parents=True, exist_ok=True)
126
+ shutil.copy(ROOT / name, tmp_path / name)
127
+ return tmp_path
128
+
129
+
130
+ def _bump(root: Path, version: str) -> tuple[subprocess.CompletedProcess[str], Path]:
131
+ """Run the copied script with a `uv` that records what it was asked."""
132
+ seen = root / "uv-argv"
133
+ fake = root / "bin" / "uv"
134
+ fake.parent.mkdir()
135
+ fake.write_text(f'#!/bin/sh\necho "$PWD $*" > "{seen}"\n')
136
+ fake.chmod(0o755)
137
+ proc = subprocess.run(
138
+ [sys.executable, str(root / "scripts" / "bump_version.py"), version],
139
+ capture_output=True,
140
+ text=True,
141
+ env={**os.environ, "PATH": f"{fake.parent}{os.pathsep}{os.environ['PATH']}"},
142
+ )
143
+ return proc, seen
144
+
145
+
146
+ def test_the_bump_writes_every_copy_and_runs_uv_version(tmp_path) -> None:
147
+ root = _copy(tmp_path)
148
+ proc, seen = _bump(root, "9.9.9")
149
+ assert proc.returncode == 0, proc.stderr
150
+ assert seen.read_text().split() == [str(root.resolve()), "version", "9.9.9"]
151
+ for name, templates in bump_version.STAMPS.items():
152
+ text = (root / name).read_text()
153
+ for template in templates:
154
+ assert template.format("9.9.9") in text, (name, template)
155
+ assert template.format(_declared()) not in text, (name, template)
156
+
157
+
158
+ def test_a_line_the_bump_cannot_find_changes_nothing(tmp_path) -> None:
159
+ """Checked before anything is written, so a renamed variable stops the
160
+ release with every file as it was rather than half of them bumped."""
161
+ root = _copy(tmp_path)
162
+ zsh = root / "zsh/plugins/worktrees/worktrees.plugin.zsh"
163
+ zsh.write_text(zsh.read_text().replace("WORKTREES_PLUGIN_VERSION", "RENAMED"))
164
+ before = {name: (root / name).read_text() for name in bump_version.STAMPS}
165
+
166
+ proc, seen = _bump(root, "9.9.9")
167
+
168
+ assert proc.returncode != 0
169
+ assert "export WORKTREES_PLUGIN_VERSION" in proc.stderr
170
+ assert not seen.exists()
171
+ assert {name: (root / name).read_text() for name in bump_version.STAMPS} == before
172
+
173
+
174
+ # --------------------------------------------------------------------------
175
+ # what gwh and gw version say about the plugin
176
+ # --------------------------------------------------------------------------
177
+
178
+
179
+ @pytest.mark.parametrize(
180
+ ("plugin", "same", "says"),
181
+ [
182
+ ("0.6.0", True, "the same release as these commands"),
183
+ ("0.5.1", False, "older than these commands.\nPin it to v0.6.0"),
184
+ ("0.10.0", False, "newer than these commands.\nUpgrade them"),
185
+ ("0.6.0.post1", False, "and these commands are 0.6.0"),
186
+ ("", False, "No shell plugin here says which release it is"),
187
+ ],
188
+ )
189
+ def test_the_plugin_is_compared_with_these_commands(
190
+ monkeypatch, plugin: str, same: bool, says: str
191
+ ) -> None:
192
+ """0.10.0 is newer than 0.6.0, which a string comparison gets backwards.
193
+ Anything that is not `x.y.z` is printed beside ours and not ranked."""
194
+ monkeypatch.setattr(cli, "__version__", "0.6.0")
195
+ monkeypatch.setenv(cli.PLUGIN_VERSION, plugin)
196
+ text, matched = cli.plugin_state()
197
+ assert matched is same
198
+ assert says in text
199
+
200
+
201
+ def _gw(*args: str, plugin: str | None) -> subprocess.CompletedProcess[str]:
202
+ env = {k: v for k, v in os.environ.items() if k != cli.PLUGIN_VERSION}
203
+ if plugin is not None:
204
+ env[cli.PLUGIN_VERSION] = plugin
205
+ return subprocess.run(
206
+ [
207
+ sys.executable,
208
+ "-c",
209
+ "from worktrees.cli import gw; raise SystemExit(gw())",
210
+ *args,
211
+ ],
212
+ capture_output=True,
213
+ text=True,
214
+ env=env,
215
+ )
216
+
217
+
218
+ @pytest.mark.parametrize("plugin", [None, "0.0.1", "99.0.0", _declared()])
219
+ def test_gw_version_keeps_stdout_to_the_version(plugin: str | None) -> None:
220
+ """`scripts/verify-publish.bash` compares stdout with the version it asked
221
+ the index for, so what the plugin says goes to stderr."""
222
+ proc = _gw("version", plugin=plugin)
223
+ assert proc.returncode == 0, proc.stderr
224
+ assert proc.stdout == f"{_declared()}\n"
225
+ assert "shell plugin" in proc.stderr
226
+
227
+
228
+ @pytest.mark.parametrize("plugin", [None, "0.0.1", "99.0.0", "0.6.0.post1789247568"])
229
+ def test_gwh_says_what_the_plugin_is_inside_eighty_columns(plugin: str | None) -> None:
230
+ proc = _gw("help", plugin=plugin)
231
+ assert proc.returncode == 0, proc.stderr
232
+ assert "shell plugin" in proc.stdout
233
+ assert [ln for ln in proc.stdout.splitlines() if len(ln) > 80] == []
@@ -40,7 +40,7 @@ wheels = [
40
40
 
41
41
  [[package]]
42
42
  name = "git-worktrees"
43
- version = "0.6.0"
43
+ version = "0.6.1"
44
44
  source = { editable = "." }
45
45
 
46
46
  [package.optional-dependencies]
@@ -3,7 +3,7 @@
3
3
  #
4
4
  # Everything else here is a console script and needs nothing: gws, gwp, gwbc
5
5
  # and gwrot answer the same from a prompt and from a script. A binary cannot
6
- # cd its caller, and that is the only reason this file exists.
6
+ # cd its caller, and that is the reason this file exists.
7
7
  #
8
8
  # $PATH is the package's business. `uv tool install` puts the binaries in
9
9
  # ~/.local/bin; nothing here adds a directory to it.
@@ -13,3 +13,8 @@
13
13
 
14
14
  fpath+=("${0:h}/functions" "${0:h}/completions")
15
15
  autoload -Uz gwa gwl gwm gwr 'gwr!'
16
+
17
+ # The release this plugin came from, which `gwh` and `gw version` compare
18
+ # with their own. Exported, because the environment is all a binary can see
19
+ # of the shell that started it.
20
+ export WORKTREES_PLUGIN_VERSION=0.6.1
@@ -1,3 +0,0 @@
1
- # Nothing to initialise. Fisher autoloads functions/ and completions/ on
2
- # demand, and the binaries come from `uv tool install` rather than from this
3
- # package.
@@ -1,87 +0,0 @@
1
- """The version is written once, and `gw version` reads that one."""
2
-
3
- from __future__ import annotations
4
-
5
- import subprocess
6
- import sys
7
- import tomllib
8
- from pathlib import Path
9
-
10
- ROOT = Path(__file__).resolve().parents[1]
11
- sys.path.insert(0, str(ROOT))
12
-
13
-
14
- def test_the_package_declares_its_version_nowhere() -> None:
15
- """A second copy is a second thing to bump, and `uv version` bumps one.
16
-
17
- `uv version 0.2.0` rewrites pyproject.toml and uv.lock. It does not touch
18
- Python source, so a literal here would survive a release and every
19
- console script would report the version before it.
20
- """
21
- source = (ROOT / "src" / "worktrees" / "__init__.py").read_text()
22
- declared = tomllib.loads((ROOT / "pyproject.toml").read_text())["project"][
23
- "version"
24
- ]
25
- assert declared not in source, (
26
- f"{declared} is written into __init__.py as well as pyproject.toml"
27
- )
28
- assert 'version("git-worktrees")' in source
29
-
30
-
31
- def test_version_matches_the_distribution() -> None:
32
- from worktrees import __version__
33
-
34
- declared = tomllib.loads((ROOT / "pyproject.toml").read_text())["project"][
35
- "version"
36
- ]
37
- assert __version__ == declared
38
-
39
-
40
- def test_both_names_that_take_a_subcommand_report_it() -> None:
41
- """Driven, because the console script is what a person runs."""
42
- declared = tomllib.loads((ROOT / "pyproject.toml").read_text())["project"][
43
- "version"
44
- ]
45
- for entry in ("gw", "main"):
46
- proc = subprocess.run(
47
- [
48
- sys.executable,
49
- "-c",
50
- f"from worktrees.cli import {entry}; raise SystemExit({entry}())",
51
- "version",
52
- ],
53
- capture_output=True,
54
- text=True,
55
- check=False,
56
- )
57
- assert proc.returncode == 0, proc.stderr
58
- assert proc.stdout.strip() == declared, entry
59
-
60
-
61
- def test_the_build_hook_writes_and_ships_the_version(tmp_path) -> None:
62
- """Both halves, because either one alone fails in silence.
63
-
64
- `.gitignore` keeps the generated file out of the repository, and hatchling
65
- reads `.gitignore`, so a hook that only writes it builds a wheel without
66
- it. Every install then falls back to the metadata lookup and nothing says
67
- so.
68
- """
69
- import hatch_build
70
-
71
- assert hatch_build.GENERATED.as_posix() in (ROOT / ".gitignore").read_text()
72
-
73
- (tmp_path / hatch_build.GENERATED.parent).mkdir(parents=True)
74
- build_data: dict[str, list[str]] = {}
75
- written = hatch_build.generate(tmp_path, "9.9.9", build_data)
76
-
77
- namespace: dict[str, str] = {}
78
- exec(written.read_text(), namespace)
79
- assert namespace["__version__"] == "9.9.9"
80
- assert f"/{hatch_build.GENERATED.as_posix()}" in build_data["artifacts"]
81
-
82
-
83
- def test_a_checkout_falls_back_to_the_metadata_lookup() -> None:
84
- """No generated file is the normal state of a clone, and it still answers."""
85
- source = (ROOT / "src" / "worktrees" / "__init__.py").read_text()
86
- assert "from ._version import __version__" in source
87
- assert "except ImportError" in source
File without changes
File without changes
File without changes