git-worktrees 0.3.0__tar.gz → 0.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/AGENTS.md +8 -4
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/CHANGELOG.md +32 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/PKG-INFO +16 -7
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/README.md +15 -6
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/pyproject.toml +1 -1
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/src/worktrees/cli.py +22 -9
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/src/worktrees/pick.py +17 -1
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/src/worktrees/verdicts.py +23 -1
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/tests/test_cli.py +73 -10
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/uv.lock +1 -1
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/.agents/skills/release/SKILL.md +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/.claude/settings.json +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/.github/workflows/publish.yml +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/.github/workflows/testpypi.yml +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/.github/workflows/tests.yml +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/.gitignore +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/.pre-commit-config.yaml +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/.python-version +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/LICENSE +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/completions/gw.fish +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/completions/gwa.fish +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/completions/gwh.fish +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/completions/gwl.fish +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/completions/gwm.fish +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/completions/gwnb.fish +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/completions/gwp.fish +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/completions/gwr.fish +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/completions/gwrot.fish +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/completions/gws.fish +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/conf.d/worktrees.fish +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/docs/BENCHMARK.md +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/docs/DEVELOPMENT.md +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/functions/gwa.fish +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/functions/gwl.fish +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/functions/gwm.fish +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/functions/gwr.fish +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/hatch_build.py +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/renovate.json +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/scripts/README.md +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/scripts/benchmark.py +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/scripts/verify-publish.bash +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/src/worktrees/__init__.py +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/src/worktrees/errors.py +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/src/worktrees/forge.py +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/src/worktrees/git.py +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/src/worktrees/layout.py +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/src/worktrees/merged.py +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/src/worktrees/new_branch.py +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/src/worktrees/prune.py +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/src/worktrees/render.py +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/src/worktrees/repo.py +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/src/worktrees/rotate.py +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/src/worktrees/worktree.py +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/tests/conftest.py +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/tests/test_benchmark.py +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/tests/test_completions.py +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/tests/test_fixture.py +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/tests/test_forge.py +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/tests/test_gates.py +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/tests/test_new_branch.py +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/tests/test_render.py +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/tests/test_rotate.py +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/tests/test_shims.py +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/tests/test_spec.py +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/tests/test_version.py +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/tests/test_worktree.py +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/zsh/plugins/worktrees/completions/_gw +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/zsh/plugins/worktrees/completions/_gwa +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/zsh/plugins/worktrees/completions/_gwh +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/zsh/plugins/worktrees/completions/_gwl +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/zsh/plugins/worktrees/completions/_gwm +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/zsh/plugins/worktrees/completions/_gwnb +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/zsh/plugins/worktrees/completions/_gwp +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/zsh/plugins/worktrees/completions/_gwr +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/zsh/plugins/worktrees/completions/_gwrot +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/zsh/plugins/worktrees/completions/_gws +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/zsh/plugins/worktrees/functions/gwa +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/zsh/plugins/worktrees/functions/gwl +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/zsh/plugins/worktrees/functions/gwm +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/zsh/plugins/worktrees/functions/gwr +0 -0
- {git_worktrees-0.3.0 → git_worktrees-0.3.1}/zsh/plugins/worktrees/worktrees.plugin.zsh +0 -0
|
@@ -156,10 +156,14 @@ Two more about what is in the list at all:
|
|
|
156
156
|
the one a finished branch leaves you in, so leaving it out leaves out the
|
|
157
157
|
only answer that is always right. `main` finds it whatever branch it stands
|
|
158
158
|
on, because its label carries the word, once.
|
|
159
|
-
- The worktree you are standing in is
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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.
|
|
163
167
|
|
|
164
168
|
`--json` and `--list` answer the same question without any of this, and an
|
|
165
169
|
agent uses those.
|
|
@@ -17,6 +17,38 @@ 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.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
|
+
|
|
20
52
|
## 0.3.0 - 2026-09-20
|
|
21
53
|
|
|
22
54
|
`gwl` with no query asks which worktree rather than moving you to one.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: git-worktrees
|
|
3
|
-
Version: 0.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
|
|
@@ -168,17 +168,21 @@ separate.
|
|
|
168
168
|
```console
|
|
169
169
|
$ gws
|
|
170
170
|
VERDICT BRANCH WHY PATH
|
|
171
|
+
keep main it is the main checkout, and never removable /home/you/git/repo
|
|
171
172
|
keep dirty-work it has uncommitted changes /home/you/git/.worktrees/dirty-work/repo
|
|
172
173
|
keep holds-secrets merged, but holds 2 ignored path(s); pass --delete-ignored /home/you/git/.worktrees/holds-secrets/repo
|
|
173
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
|
|
174
175
|
remove squash-merged squash-merged /home/you/git/.worktrees/squash-merged/repo
|
|
175
176
|
|
|
176
|
-
1 removable,
|
|
177
|
+
1 removable, 3 kept, 1 unclear
|
|
177
178
|
unclear, and gwp does not touch these: never-pushed
|
|
178
179
|
gwp removes the 1 marked removable
|
|
179
180
|
```
|
|
180
181
|
|
|
181
|
-
Rows come in `git worktree list` order
|
|
182
|
+
Rows come in `git worktree list` order, the main checkout first. It is there
|
|
183
|
+
so the table shows what `git worktree list` shows; nothing proposes it, and
|
|
184
|
+
`git worktree remove` answers `fatal: '<path>' is a main working tree`
|
|
185
|
+
whatever flag it is given. Paths are absolute wherever this
|
|
182
186
|
prints one, so a row can be pasted into the next command. The table and the
|
|
183
187
|
count are on stdout; the two lines after them are on stderr.
|
|
184
188
|
|
|
@@ -326,13 +330,18 @@ show me the options, and being moved without being asked is not that. So it
|
|
|
326
330
|
asks, numbered, however many there are:
|
|
327
331
|
|
|
328
332
|
```console
|
|
329
|
-
$ gwl # standing in
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
|
333
338
|
which? [1-3, or blank to cancel]
|
|
334
339
|
```
|
|
335
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
|
+
|
|
336
345
|
Matching is substring first and then subsequence, so `tst` finds `add-tests`,
|
|
337
346
|
and a substring hit always outranks a loose one. Without a terminal `gwl`
|
|
338
347
|
refuses at exit 3 and names `--list` and `--json`.
|
|
@@ -142,17 +142,21 @@ separate.
|
|
|
142
142
|
```console
|
|
143
143
|
$ gws
|
|
144
144
|
VERDICT BRANCH WHY PATH
|
|
145
|
+
keep main it is the main checkout, and never removable /home/you/git/repo
|
|
145
146
|
keep dirty-work it has uncommitted changes /home/you/git/.worktrees/dirty-work/repo
|
|
146
147
|
keep holds-secrets merged, but holds 2 ignored path(s); pass --delete-ignored /home/you/git/.worktrees/holds-secrets/repo
|
|
147
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
|
|
148
149
|
remove squash-merged squash-merged /home/you/git/.worktrees/squash-merged/repo
|
|
149
150
|
|
|
150
|
-
1 removable,
|
|
151
|
+
1 removable, 3 kept, 1 unclear
|
|
151
152
|
unclear, and gwp does not touch these: never-pushed
|
|
152
153
|
gwp removes the 1 marked removable
|
|
153
154
|
```
|
|
154
155
|
|
|
155
|
-
Rows come in `git worktree list` order
|
|
156
|
+
Rows come in `git worktree list` order, the main checkout first. It is there
|
|
157
|
+
so the table shows what `git worktree list` shows; nothing proposes it, and
|
|
158
|
+
`git worktree remove` answers `fatal: '<path>' is a main working tree`
|
|
159
|
+
whatever flag it is given. Paths are absolute wherever this
|
|
156
160
|
prints one, so a row can be pasted into the next command. The table and the
|
|
157
161
|
count are on stdout; the two lines after them are on stderr.
|
|
158
162
|
|
|
@@ -300,13 +304,18 @@ show me the options, and being moved without being asked is not that. So it
|
|
|
300
304
|
asks, numbered, however many there are:
|
|
301
305
|
|
|
302
306
|
```console
|
|
303
|
-
$ gwl # standing in
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
|
307
312
|
which? [1-3, or blank to cancel]
|
|
308
313
|
```
|
|
309
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
|
+
|
|
310
319
|
Matching is substring first and then subsequence, so `tst` finds `add-tests`,
|
|
311
320
|
and a substring hit always outranks a loose one. Without a terminal `gwl`
|
|
312
321
|
refuses at exit 3 and names `--list` and `--json`.
|
|
@@ -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.3.
|
|
12
|
+
version = "0.3.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.
|
|
@@ -193,7 +193,15 @@ def _assess(
|
|
|
193
193
|
judge(only, head, head_branch, ignored, ask_forge=ask_forge)
|
|
194
194
|
if judge
|
|
195
195
|
else verdicts.assess(
|
|
196
|
-
records,
|
|
196
|
+
records,
|
|
197
|
+
only,
|
|
198
|
+
head,
|
|
199
|
+
head_branch,
|
|
200
|
+
ignored,
|
|
201
|
+
ask_forge=ask_forge,
|
|
202
|
+
# `gws` and `gwp` report; `gwr` acts, and takes this list as its
|
|
203
|
+
# candidates, so it asks through `judge` and gets it without.
|
|
204
|
+
include_main=True,
|
|
197
205
|
)
|
|
198
206
|
)
|
|
199
207
|
return rows, verdicts.stale(records), head, head_branch
|
|
@@ -247,16 +255,15 @@ def run_status(args: argparse.Namespace) -> int:
|
|
|
247
255
|
)
|
|
248
256
|
return 0
|
|
249
257
|
|
|
250
|
-
|
|
251
|
-
print("no worktrees besides the main checkout")
|
|
252
|
-
else:
|
|
253
|
-
print(_verdict_table(rows))
|
|
258
|
+
print(_verdict_table(rows))
|
|
254
259
|
|
|
255
260
|
if args.quiet:
|
|
256
261
|
return 0
|
|
257
262
|
|
|
258
263
|
print()
|
|
259
264
|
print(_counted(rows))
|
|
265
|
+
if len(rows) < 2:
|
|
266
|
+
_err("no worktrees besides the main checkout; gwa NAME makes one")
|
|
260
267
|
if stale:
|
|
261
268
|
_err(
|
|
262
269
|
f"{len(stale)} stale record(s) for directories that are gone; "
|
|
@@ -300,13 +307,13 @@ def run_prune(args: argparse.Namespace) -> int:
|
|
|
300
307
|
# command that would have printed it.
|
|
301
308
|
if args.json:
|
|
302
309
|
print(json.dumps({"removed": [], "failed": 0}, indent=2))
|
|
303
|
-
elif not rows:
|
|
304
|
-
print("no worktrees besides the main checkout")
|
|
305
310
|
else:
|
|
306
311
|
print(_verdict_table(rows))
|
|
307
312
|
if not args.quiet:
|
|
308
313
|
print()
|
|
309
314
|
print(f"nothing to remove; {_counted(rows)}")
|
|
315
|
+
if len(rows) < 2:
|
|
316
|
+
_err("no worktrees besides the main checkout; gwa NAME makes one")
|
|
310
317
|
return 0
|
|
311
318
|
|
|
312
319
|
if unknown and not args.quiet:
|
|
@@ -663,8 +670,14 @@ def run_list(args: argparse.Namespace) -> int:
|
|
|
663
670
|
return 0
|
|
664
671
|
|
|
665
672
|
# A query that narrows to one has said which. No query has not, even
|
|
666
|
-
# when the repository holds exactly one other worktree.
|
|
667
|
-
|
|
673
|
+
# when the repository holds exactly one other worktree. The one you are
|
|
674
|
+
# standing in is shown above them, marked and unnumbered.
|
|
675
|
+
chosen = pick.choose(
|
|
676
|
+
elsewhere,
|
|
677
|
+
_err,
|
|
678
|
+
outright=bool(args.query),
|
|
679
|
+
standing_in=next((w for w in found if _same(w.path, here)), None),
|
|
680
|
+
)
|
|
668
681
|
if chosen is None:
|
|
669
682
|
_err("nothing picked")
|
|
670
683
|
return 0
|
|
@@ -69,6 +69,7 @@ def choose(
|
|
|
69
69
|
show: Callable[[str], None],
|
|
70
70
|
ask: Callable[[str], str] | None = None,
|
|
71
71
|
outright: bool = True,
|
|
72
|
+
standing_in: Worktree | None = None,
|
|
72
73
|
) -> Worktree | None:
|
|
73
74
|
"""Ask which one, and None means cancelled.
|
|
74
75
|
|
|
@@ -77,6 +78,11 @@ def choose(
|
|
|
77
78
|
a request to be shown the options, and being moved without being asked
|
|
78
79
|
because there happened to be one other worktree is not that.
|
|
79
80
|
|
|
81
|
+
`standing_in` is shown above the numbered ones, marked `*` and carrying
|
|
82
|
+
no number. It is not somewhere to go, and a list that leaves it out shows
|
|
83
|
+
one row where `git worktree list` shows two, which reads as though
|
|
84
|
+
something went missing rather than as where you already are.
|
|
85
|
+
|
|
80
86
|
A run whose stdin is not a terminal is refused rather than left to block:
|
|
81
87
|
an agent or a pipe reaching a prompt would hang, and --json answers the
|
|
82
88
|
same question without one.
|
|
@@ -96,7 +102,17 @@ def choose(
|
|
|
96
102
|
)
|
|
97
103
|
ask = _prompt
|
|
98
104
|
|
|
99
|
-
|
|
105
|
+
listed = [*([standing_in] if standing_in is not None else []), *candidates]
|
|
106
|
+
width = max(len(wt.label) for wt in listed)
|
|
107
|
+
if standing_in is not None:
|
|
108
|
+
# Padded before it is painted, or the escape codes count as width and
|
|
109
|
+
# every column under it sits crooked.
|
|
110
|
+
mark = f"{'*':>3}"
|
|
111
|
+
show(
|
|
112
|
+
f"{render.err(mark, render.DIM)} "
|
|
113
|
+
f"{render.err(standing_in.label.ljust(width), render.DIM)} "
|
|
114
|
+
f"{render.err(standing_in.path, render.DIM)}"
|
|
115
|
+
)
|
|
100
116
|
for i, wt in enumerate(candidates, 1):
|
|
101
117
|
show(
|
|
102
118
|
f"{render.err(f'{i:>3}', render.DIM)} "
|
|
@@ -74,6 +74,7 @@ def assess(
|
|
|
74
74
|
repo: str | os.PathLike[str] | None = None,
|
|
75
75
|
here: str | None = None,
|
|
76
76
|
ask_forge: bool = False,
|
|
77
|
+
include_main: bool = False,
|
|
77
78
|
) -> list[Verdict]:
|
|
78
79
|
"""Every worktree, in the order `git worktree remove` would refuse them.
|
|
79
80
|
|
|
@@ -83,6 +84,11 @@ def assess(
|
|
|
83
84
|
`here` is the checkout the caller is standing in, which is kept rather
|
|
84
85
|
than proposed. Pass "" to judge it like any other: `gwr` is asked for one
|
|
85
86
|
by name and steps out of it first, where a listing has no way to.
|
|
87
|
+
|
|
88
|
+
`include_main` puts the main checkout in the list, kept and never
|
|
89
|
+
proposed. A listing leaving it out shows one row where `git worktree
|
|
90
|
+
list` shows two, and reads as though something went missing. `gwr` takes
|
|
91
|
+
the list as its candidates, so it asks for the list without it.
|
|
86
92
|
"""
|
|
87
93
|
main = R.main_worktree(repo)
|
|
88
94
|
if here is None:
|
|
@@ -91,10 +97,26 @@ def assess(
|
|
|
91
97
|
|
|
92
98
|
out: list[Verdict] = []
|
|
93
99
|
for wt in records:
|
|
94
|
-
if "bare" in wt.flags or "prunable" in wt.flags
|
|
100
|
+
if "bare" in wt.flags or "prunable" in wt.flags:
|
|
95
101
|
continue
|
|
96
102
|
if only and wt.branch != only:
|
|
97
103
|
continue
|
|
104
|
+
if wt.path == main:
|
|
105
|
+
# Said before the standing-in rule below, because which one is
|
|
106
|
+
# the main checkout is the more useful of the two facts and the
|
|
107
|
+
# only one that is true from anywhere.
|
|
108
|
+
if include_main:
|
|
109
|
+
out.append(
|
|
110
|
+
Verdict(
|
|
111
|
+
KEEP,
|
|
112
|
+
wt.branch,
|
|
113
|
+
wt.path,
|
|
114
|
+
"it is the main checkout, and never removable",
|
|
115
|
+
0,
|
|
116
|
+
wt.sha,
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
continue
|
|
98
120
|
|
|
99
121
|
def say(verdict: str, why: str, ignored: int = 0, wt: R.Worktree = wt) -> None:
|
|
100
122
|
out.append(Verdict(verdict, wt.branch, wt.path, why, ignored, wt.sha))
|
|
@@ -68,8 +68,21 @@ def run_with_a_terminal_on_stdout(
|
|
|
68
68
|
return proc.returncode, b"".join(chunks).decode().replace("\r\n", "\n")
|
|
69
69
|
|
|
70
70
|
|
|
71
|
+
def run_on_a_terminal_at(
|
|
72
|
+
world, at: Path, entry: str, *args: str, answer: str
|
|
73
|
+
) -> tuple[int, str]:
|
|
74
|
+
"""The same, from a directory that is not the main checkout."""
|
|
75
|
+
return _on_a_terminal(world, at, entry, *args, answer=answer)
|
|
76
|
+
|
|
77
|
+
|
|
71
78
|
def run_on_a_terminal(world, entry: str, *args: str, answer: str) -> tuple[int, str]:
|
|
72
79
|
"""The same, with a real tty on stdin, so the prompt is reachable."""
|
|
80
|
+
return _on_a_terminal(world, world.repo, entry, *args, answer=answer)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _on_a_terminal(
|
|
84
|
+
world, at: Path, entry: str, *args: str, answer: str
|
|
85
|
+
) -> tuple[int, str]:
|
|
73
86
|
parent, child = pty.openpty()
|
|
74
87
|
proc = subprocess.Popen(
|
|
75
88
|
[
|
|
@@ -78,7 +91,7 @@ def run_on_a_terminal(world, entry: str, *args: str, answer: str) -> tuple[int,
|
|
|
78
91
|
f"from worktrees.cli import {entry}; raise SystemExit({entry}())",
|
|
79
92
|
*args,
|
|
80
93
|
],
|
|
81
|
-
cwd=str(
|
|
94
|
+
cwd=str(at),
|
|
82
95
|
stdin=child,
|
|
83
96
|
stdout=subprocess.PIPE,
|
|
84
97
|
stderr=subprocess.STDOUT,
|
|
@@ -113,16 +126,19 @@ def test_status_leaves_a_removable_worktree_alone(world) -> None:
|
|
|
113
126
|
p = run(world, "gws", "--no-fetch")
|
|
114
127
|
assert p.returncode == 0, p.stderr
|
|
115
128
|
assert "remove" in p.stdout
|
|
116
|
-
assert "1 removable,
|
|
129
|
+
assert "1 removable, 1 kept, 0 unclear" in p.stdout # the main checkout
|
|
117
130
|
assert "gwp removes the 1 marked removable" in p.stderr
|
|
118
131
|
assert wt.exists()
|
|
119
132
|
assert world.git("rev-parse", "--verify", "refs/heads/done")
|
|
120
133
|
|
|
121
134
|
|
|
122
135
|
def test_status_says_when_there_is_nothing(world) -> None:
|
|
136
|
+
"""The main checkout is still a row, so the table shows what
|
|
137
|
+
`git worktree list` shows and the hint says what is missing."""
|
|
123
138
|
p = run(world, "gws", "--no-fetch")
|
|
124
139
|
assert p.returncode == 0, p.stderr
|
|
125
|
-
assert "
|
|
140
|
+
assert "it is the main checkout" in p.stdout
|
|
141
|
+
assert "no worktrees besides the main checkout; gwa NAME makes one" in p.stderr
|
|
126
142
|
|
|
127
143
|
|
|
128
144
|
def test_status_json_is_data_on_stdout(world) -> None:
|
|
@@ -131,7 +147,9 @@ def test_status_json_is_data_on_stdout(world) -> None:
|
|
|
131
147
|
assert p.returncode == 0, p.stderr
|
|
132
148
|
payload = json.loads(p.stdout)
|
|
133
149
|
assert payload["head"] == "refs/heads/main"
|
|
134
|
-
|
|
150
|
+
rows = {v["branch"]: v for v in payload["verdicts"]}
|
|
151
|
+
assert rows["done"]["verdict"] == "remove"
|
|
152
|
+
assert rows["main"]["why"] == "it is the main checkout, and never removable"
|
|
135
153
|
assert payload["stale"] == []
|
|
136
154
|
|
|
137
155
|
|
|
@@ -147,9 +165,11 @@ def test_json_carries_the_ignored_count_as_a_number(world) -> None:
|
|
|
147
165
|
|
|
148
166
|
p = run(world, "gws", "--no-fetch", "--no-forge", "--json")
|
|
149
167
|
assert p.returncode == 0, p.stderr
|
|
150
|
-
|
|
168
|
+
rows = {v["branch"]: v for v in json.loads(p.stdout)["verdicts"]}
|
|
169
|
+
row = rows["holds-secrets"]
|
|
151
170
|
assert row["ignored"] == 1
|
|
152
171
|
assert row["verdict"] == "keep"
|
|
172
|
+
assert rows["main"]["ignored"] == 0
|
|
153
173
|
# and the sentence still says it, for the person reading the table
|
|
154
174
|
assert "1 ignored path(s)" in row["why"]
|
|
155
175
|
|
|
@@ -312,7 +332,8 @@ def test_prune_clears_a_stale_record(world) -> None:
|
|
|
312
332
|
def test_prune_says_so_when_there_is_nothing(world) -> None:
|
|
313
333
|
p = run(world, "gwp", "--no-fetch", "--yes")
|
|
314
334
|
assert p.returncode == 0, p.stderr
|
|
315
|
-
assert "
|
|
335
|
+
assert "it is the main checkout" in p.stdout
|
|
336
|
+
assert "no worktrees besides the main checkout; gwa NAME makes one" in p.stderr
|
|
316
337
|
|
|
317
338
|
|
|
318
339
|
def test_prune_prints_the_reason_rather_than_naming_the_command(world) -> None:
|
|
@@ -324,7 +345,7 @@ def test_prune_prints_the_reason_rather_than_naming_the_command(world) -> None:
|
|
|
324
345
|
assert p.returncode == 0, p.stderr
|
|
325
346
|
assert "busy" in p.stdout
|
|
326
347
|
assert "it has uncommitted changes" in p.stdout
|
|
327
|
-
assert "nothing to remove; 0 removable,
|
|
348
|
+
assert "nothing to remove; 0 removable, 2 kept, 0 unclear" in p.stdout
|
|
328
349
|
assert "gws" not in p.stdout
|
|
329
350
|
|
|
330
351
|
|
|
@@ -390,7 +411,7 @@ def test_the_cli_name_takes_a_subcommand(world) -> None:
|
|
|
390
411
|
world.worktree("done")
|
|
391
412
|
p = run(world, "main", "status", "--no-fetch", "--json")
|
|
392
413
|
assert p.returncode == 0, p.stderr
|
|
393
|
-
assert json.loads(p.stdout)["verdicts"]
|
|
414
|
+
assert "done" in {v["branch"] for v in json.loads(p.stdout)["verdicts"]}
|
|
394
415
|
|
|
395
416
|
|
|
396
417
|
def test_the_cli_name_with_no_argument_prints_help(world) -> None:
|
|
@@ -437,7 +458,7 @@ def test_every_shorthand_reaches_the_same_command(world, spelling: str) -> None:
|
|
|
437
458
|
world.worktree("done")
|
|
438
459
|
p = run(world, "main", spelling, "--no-fetch", "--json")
|
|
439
460
|
assert p.returncode == 0, p.stderr
|
|
440
|
-
assert json.loads(p.stdout)["verdicts"]
|
|
461
|
+
assert "done" in {v["branch"] for v in json.loads(p.stdout)["verdicts"]}
|
|
441
462
|
|
|
442
463
|
|
|
443
464
|
@pytest.mark.parametrize(
|
|
@@ -560,7 +581,7 @@ def test_json_is_never_coloured_even_on_a_terminal(world) -> None:
|
|
|
560
581
|
code, out = run_with_a_terminal_on_stdout(world, "gws", "--no-fetch", "--json")
|
|
561
582
|
assert code == 0, out
|
|
562
583
|
assert "[" not in out
|
|
563
|
-
assert json.loads(out)["verdicts"]
|
|
584
|
+
assert "done" in {v["branch"] for v in json.loads(out)["verdicts"]}
|
|
564
585
|
|
|
565
586
|
|
|
566
587
|
def test_no_color_turns_it_off_on_a_terminal(world) -> None:
|
|
@@ -696,3 +717,45 @@ def test_list_with_no_query_takes_blank_as_cancelled(world) -> None:
|
|
|
696
717
|
code, out = run_on_a_terminal(world, "gwl", answer="\n")
|
|
697
718
|
assert code == 0, out
|
|
698
719
|
assert "nothing picked" in out
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
def test_status_lists_the_main_checkout_and_never_proposes_it(world) -> None:
|
|
723
|
+
"""A listing that leaves it out shows one row where `git worktree list`
|
|
724
|
+
shows two, which reads as though something went missing."""
|
|
725
|
+
world.worktree("done")
|
|
726
|
+
p = run(world, "gws", "--no-fetch", "--no-forge")
|
|
727
|
+
assert p.returncode == 0, p.stderr
|
|
728
|
+
assert "it is the main checkout, and never removable" in p.stdout
|
|
729
|
+
assert p.stdout.count("keep") == 1
|
|
730
|
+
assert "1 removable, 1 kept, 0 unclear" in p.stdout
|
|
731
|
+
rows = {
|
|
732
|
+
v["branch"]: v
|
|
733
|
+
for v in json.loads(
|
|
734
|
+
run(world, "gws", "--no-fetch", "--no-forge", "--json").stdout
|
|
735
|
+
)["verdicts"]
|
|
736
|
+
}
|
|
737
|
+
assert set(rows) == {"main", "done"}
|
|
738
|
+
assert rows["main"]["verdict"] == "keep"
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
def test_remove_is_never_offered_the_main_checkout(world) -> None:
|
|
742
|
+
"""`gwr` takes the same list as its candidates, and `git worktree remove`
|
|
743
|
+
answers `fatal: '<path>' is a main working tree` whatever flag it is
|
|
744
|
+
given, so offering it would propose something certain to be refused.
|
|
745
|
+
"""
|
|
746
|
+
world.worktree("done")
|
|
747
|
+
p = run(world, "gwr", "--no-fetch", "--complete")
|
|
748
|
+
assert p.returncode == 0, p.stderr
|
|
749
|
+
assert "main" not in p.stdout
|
|
750
|
+
assert "done" in p.stdout
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
def test_list_shows_the_worktree_you_are_standing_in_unnumbered(world) -> None:
|
|
754
|
+
"""Marked, above the ones you can go to, and carrying no number: it is
|
|
755
|
+
where you are rather than somewhere to go."""
|
|
756
|
+
wt = world.worktree("cleanup")
|
|
757
|
+
code, out = run_on_a_terminal_at(world, wt, "gwl", answer="\n")
|
|
758
|
+
assert code == 0, out
|
|
759
|
+
assert " * cleanup" in out
|
|
760
|
+
assert " 1 main" in out
|
|
761
|
+
assert "which? [1-1, or blank to cancel]" in out
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|