git-worktrees 0.1.0__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. {git_worktrees-0.1.0 → git_worktrees-0.2.0}/.github/workflows/publish.yml +2 -1
  2. {git_worktrees-0.1.0 → git_worktrees-0.2.0}/AGENTS.md +62 -11
  3. git_worktrees-0.2.0/CHANGELOG.md +172 -0
  4. git_worktrees-0.2.0/PKG-INFO +485 -0
  5. git_worktrees-0.2.0/README.md +460 -0
  6. git_worktrees-0.2.0/completions/gw.fish +23 -0
  7. git_worktrees-0.2.0/completions/gwa.fish +13 -0
  8. git_worktrees-0.2.0/completions/gwh.fish +6 -0
  9. git_worktrees-0.2.0/completions/gwl.fish +15 -0
  10. git_worktrees-0.2.0/completions/gwm.fish +11 -0
  11. git_worktrees-0.2.0/completions/gwnb.fish +13 -0
  12. git_worktrees-0.2.0/completions/gwp.fish +17 -0
  13. git_worktrees-0.2.0/completions/gwr.fish +18 -0
  14. git_worktrees-0.2.0/completions/gwrot.fish +13 -0
  15. git_worktrees-0.2.0/completions/gws.fish +16 -0
  16. git_worktrees-0.2.0/conf.d/worktrees.fish +3 -0
  17. {git_worktrees-0.1.0 → git_worktrees-0.2.0}/docs/DEVELOPMENT.md +9 -7
  18. {git_worktrees-0.1.0 → git_worktrees-0.2.0}/pyproject.toml +3 -2
  19. {git_worktrees-0.1.0 → git_worktrees-0.2.0}/src/worktrees/cli.py +376 -87
  20. {git_worktrees-0.1.0 → git_worktrees-0.2.0}/src/worktrees/git.py +22 -0
  21. {git_worktrees-0.1.0 → git_worktrees-0.2.0}/src/worktrees/pick.py +6 -1
  22. {git_worktrees-0.1.0 → git_worktrees-0.2.0}/src/worktrees/prune.py +20 -15
  23. git_worktrees-0.2.0/src/worktrees/render.py +88 -0
  24. {git_worktrees-0.1.0 → git_worktrees-0.2.0}/src/worktrees/repo.py +13 -2
  25. {git_worktrees-0.1.0 → git_worktrees-0.2.0}/src/worktrees/verdicts.py +12 -2
  26. {git_worktrees-0.1.0 → git_worktrees-0.2.0}/src/worktrees/worktree.py +36 -7
  27. {git_worktrees-0.1.0 → git_worktrees-0.2.0}/tests/test_cli.py +230 -9
  28. git_worktrees-0.2.0/tests/test_completions.py +189 -0
  29. {git_worktrees-0.1.0 → git_worktrees-0.2.0}/tests/test_gates.py +62 -1
  30. git_worktrees-0.2.0/tests/test_render.py +75 -0
  31. {git_worktrees-0.1.0 → git_worktrees-0.2.0}/tests/test_worktree.py +139 -2
  32. {git_worktrees-0.1.0 → git_worktrees-0.2.0}/uv.lock +1 -1
  33. git_worktrees-0.2.0/zsh/plugins/worktrees/completions/_gw +54 -0
  34. git_worktrees-0.2.0/zsh/plugins/worktrees/completions/_gwa +12 -0
  35. git_worktrees-0.2.0/zsh/plugins/worktrees/completions/_gwh +7 -0
  36. git_worktrees-0.2.0/zsh/plugins/worktrees/completions/_gwl +26 -0
  37. git_worktrees-0.2.0/zsh/plugins/worktrees/completions/_gwm +11 -0
  38. git_worktrees-0.2.0/zsh/plugins/worktrees/completions/_gwnb +12 -0
  39. git_worktrees-0.2.0/zsh/plugins/worktrees/completions/_gwp +16 -0
  40. git_worktrees-0.2.0/zsh/plugins/worktrees/completions/_gwr +29 -0
  41. git_worktrees-0.2.0/zsh/plugins/worktrees/completions/_gwrot +12 -0
  42. git_worktrees-0.2.0/zsh/plugins/worktrees/completions/_gws +15 -0
  43. {git_worktrees-0.1.0 → git_worktrees-0.2.0}/zsh/plugins/worktrees/worktrees.plugin.zsh +1 -1
  44. git_worktrees-0.1.0/CHANGELOG.md +0 -70
  45. git_worktrees-0.1.0/PKG-INFO +0 -335
  46. git_worktrees-0.1.0/README.md +0 -310
  47. git_worktrees-0.1.0/conf.d/worktrees.fish +0 -2
  48. {git_worktrees-0.1.0 → git_worktrees-0.2.0}/.agents/skills/release/SKILL.md +0 -0
  49. {git_worktrees-0.1.0 → git_worktrees-0.2.0}/.claude/settings.json +0 -0
  50. {git_worktrees-0.1.0 → git_worktrees-0.2.0}/.github/workflows/tests.yml +0 -0
  51. {git_worktrees-0.1.0 → git_worktrees-0.2.0}/.gitignore +0 -0
  52. {git_worktrees-0.1.0 → git_worktrees-0.2.0}/.pre-commit-config.yaml +0 -0
  53. {git_worktrees-0.1.0 → git_worktrees-0.2.0}/.python-version +0 -0
  54. {git_worktrees-0.1.0 → git_worktrees-0.2.0}/LICENSE +0 -0
  55. {git_worktrees-0.1.0 → git_worktrees-0.2.0}/functions/gwa.fish +0 -0
  56. {git_worktrees-0.1.0 → git_worktrees-0.2.0}/functions/gwl.fish +0 -0
  57. {git_worktrees-0.1.0 → git_worktrees-0.2.0}/functions/gwm.fish +0 -0
  58. {git_worktrees-0.1.0 → git_worktrees-0.2.0}/functions/gwr.fish +0 -0
  59. {git_worktrees-0.1.0 → git_worktrees-0.2.0}/renovate.json +0 -0
  60. {git_worktrees-0.1.0 → git_worktrees-0.2.0}/scripts/verify-publish.bash +0 -0
  61. {git_worktrees-0.1.0 → git_worktrees-0.2.0}/src/worktrees/__init__.py +0 -0
  62. {git_worktrees-0.1.0 → git_worktrees-0.2.0}/src/worktrees/forge.py +0 -0
  63. {git_worktrees-0.1.0 → git_worktrees-0.2.0}/src/worktrees/layout.py +0 -0
  64. {git_worktrees-0.1.0 → git_worktrees-0.2.0}/src/worktrees/merged.py +0 -0
  65. {git_worktrees-0.1.0 → git_worktrees-0.2.0}/src/worktrees/new_branch.py +0 -0
  66. {git_worktrees-0.1.0 → git_worktrees-0.2.0}/src/worktrees/rotate.py +0 -0
  67. {git_worktrees-0.1.0 → git_worktrees-0.2.0}/tests/conftest.py +0 -0
  68. {git_worktrees-0.1.0 → git_worktrees-0.2.0}/tests/test_forge.py +0 -0
  69. {git_worktrees-0.1.0 → git_worktrees-0.2.0}/tests/test_new_branch.py +0 -0
  70. {git_worktrees-0.1.0 → git_worktrees-0.2.0}/tests/test_rotate.py +0 -0
  71. {git_worktrees-0.1.0 → git_worktrees-0.2.0}/tests/test_shims.py +0 -0
  72. {git_worktrees-0.1.0 → git_worktrees-0.2.0}/tests/test_spec.py +0 -0
  73. {git_worktrees-0.1.0 → git_worktrees-0.2.0}/tests/test_version.py +0 -0
  74. {git_worktrees-0.1.0 → git_worktrees-0.2.0}/zsh/plugins/worktrees/functions/gwa +0 -0
  75. {git_worktrees-0.1.0 → git_worktrees-0.2.0}/zsh/plugins/worktrees/functions/gwl +0 -0
  76. {git_worktrees-0.1.0 → git_worktrees-0.2.0}/zsh/plugins/worktrees/functions/gwm +0 -0
  77. {git_worktrees-0.1.0 → git_worktrees-0.2.0}/zsh/plugins/worktrees/functions/gwr +0 -0
@@ -14,7 +14,8 @@ name: Publish
14
14
  # or replaced, so if the rehearsal fails there is nothing to pin against and
15
15
  # the run stops there.
16
16
  #
17
- # No API token is stored anywhere. See README, "Publishing a version".
17
+ # No API token is stored anywhere. See docs/DEVELOPMENT.md, "Publishing a
18
+ # version".
18
19
 
19
20
  on:
20
21
  push:
@@ -23,7 +23,7 @@ call `command <name>` and `cd` to what that prints.**
23
23
 
24
24
  | | changes directory | ships as |
25
25
  | --- | --- | --- |
26
- | `gws`, `gwp`, `gwnb`, `gwrot` | no | a console script, and nothing else |
26
+ | `gws`, `gwp`, `gwnb`, `gwrot`, `gwh` | no | a console script, and nothing else |
27
27
  | a command that lands you somewhere | yes | a console script, plus a function of the same name |
28
28
 
29
29
  A binary cannot `cd` its caller. That is the only thing shell code is here for,
@@ -85,9 +85,16 @@ Nothing installs this from a checkout on the user's disk. It is consumed as a
85
85
  third-party plugin and a third-party tool, so anything that assumes a local
86
86
  clone at a known path is wrong here.
87
87
 
88
- A completion asks the CLI for its candidates rather than deriving them. The
89
- ranking is tested in Python; a shell file that reimplements it is a second
90
- answer to the same question.
88
+ A completion asks the CLI for its candidates rather than deriving them, and
89
+ `--complete` is how: `gw --complete` prints every subcommand and shorthand,
90
+ `gwl --complete` prints every worktree, both as name and description separated
91
+ by a tab, which fish reads directly and zsh splits for `_describe`. Hidden from
92
+ `--help`, because a person has `gwh` and `--list` for the same answers.
93
+
94
+ No shell file holds a candidate or a rank. What each file does carry is its own
95
+ command's flags, and `tests/test_completions.py` diffs those against `--help`
96
+ in both directions for both shells: a renamed flag fails there rather than
97
+ going quiet in a completion nobody is watching.
91
98
 
92
99
  ## The picker is Python's, and there is no fzf
93
100
 
@@ -101,9 +108,9 @@ number of linked worktrees in one is four. A fuzzy finder is the wrong
101
108
  instrument at that size, and a numbered prompt reads faster:
102
109
 
103
110
  ```
104
- 1 fix-parser ~/git/.worktrees/fix-parser/repo
105
- 2 add-tests ~/git/.worktrees/add-tests/repo
106
- >
111
+ 1 fix-parser /home/you/git/.worktrees/fix-parser/repo
112
+ 2 add-tests /home/you/git/.worktrees/add-tests/repo
113
+ which? [1-2, or blank to cancel]
107
114
  ```
108
115
 
109
116
  Dropping `fzf` is less code, not more. It removes the spawn, the tty rules
@@ -123,12 +130,57 @@ tightly they cluster. About twenty lines, `difflib` not required.
123
130
  Two rules, both the shape `gwp`'s prompt already has:
124
131
 
125
132
  - One match takes it outright, with no prompt at all.
126
- - No terminal, no prompt. Refuse at exit 2 and name the flag that answers
133
+ - No terminal, no prompt. Refuse at exit 3 and name the flag that answers
127
134
  without one, rather than blocking on something nothing can drive.
128
135
 
136
+ Two more about what is in the list at all:
137
+
138
+ - The main checkout is a destination. It is the one that is always there and
139
+ the one a finished branch leaves you in, so leaving it out leaves out the
140
+ only answer that is always right. `main` finds it whatever branch it stands
141
+ 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.
146
+
129
147
  `--json` and `--list` answer the same question without any of this, and an
130
148
  agent uses those.
131
149
 
150
+ ## One row per command, under every name it answers to
151
+
152
+ `_COMMANDS` in `cli.py` is the table. A row carries the function, the flags,
153
+ the one-line description, the console script, the argument shape, the
154
+ shorthands and whether the command changes the caller's directory. `gws`,
155
+ `gw status`, `gw st` and `gw s` reach the same function because that row says
156
+ so, and `gwh` prints the table rather than a second list that can disagree
157
+ with it.
158
+
159
+ `_canonical()` raises at import when two rows answer to one name. A dict
160
+ comprehension would keep the last one and leave a command reachable under a
161
+ name that runs a different one.
162
+
163
+ A shorthand is for typing, so it is short and it is not clever: the first
164
+ letters of the word, or the two-letter name the same job has in git and in a
165
+ shell. `rm` and `mv` mean what they mean everywhere else.
166
+
167
+ ## Colour is a property of the stream, not of the program
168
+
169
+ `render.py` holds both halves of it: the codes, and a table whose columns line
170
+ up. `render.setup()` runs once per invocation and decides per stream.
171
+
172
+ Padding runs on the text and painting after it. The other order counts an
173
+ escape sequence as width, and every column under a coloured cell sits crooked
174
+ by exactly the length of the code.
175
+
176
+ stdout carries the path `cd $(gwa x)` reads and the JSON `jq` parses, so a
177
+ redirect, a pipe, a non-empty `NO_COLOR`, or `TERM=dumb` mean the bytes go out
178
+ as they would have without the module. `NO_COLOR=` is not a request to turn it
179
+ off: that is the no-color.org rule, and it is why `supported` tests the value
180
+ and not the key. `--json` is never painted at all:
181
+ the colour is applied at the call site that formats a table, and the JSON
182
+ paths do not pass through one.
183
+
132
184
  ## Every git command is one spec
133
185
 
134
186
  The spec is the command as you would type it, with `$name` where a value goes.
@@ -186,9 +238,8 @@ no call site can assemble its way past it. `reset --hard`, a forced `checkout`
186
238
  or `switch`, `clean -f`, a bare `push --force`, `worktree remove --force` and
187
239
  `branch -D` are refused absolutely. There is no flag, and `--yes` least of all.
188
240
 
189
- `verdicts.py` declares no mutating command. `prune.py` declares the six that
190
- mutate. A read-only command may not call one: a test reads the verbose log and
191
- asserts it.
241
+ `verdicts.py` declares no mutating command. A read-only command may not call
242
+ one: a test reads the verbose log and asserts it.
192
243
 
193
244
  ## Content cannot settle a stacked branch
194
245
 
@@ -0,0 +1,172 @@
1
+ # Changelog
2
+
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/).
7
+
8
+ An entry earns its place by being observable. If running a command produces
9
+ no different result, no different output and no different exit code, it is
10
+ not in here, whatever it cost to build.
11
+
12
+ A release closes with a `### Choices` section when a decision in it is worth
13
+ the reader's time: what was chosen, and what the alternative failed to do.
14
+
15
+ ## 0.2.0 - 2026-09-11
16
+
17
+ A squash-merged branch goes with its checkout, every removal names the
18
+ ignored files it deletes, `gwl` finds every worktree and `gwp` says why it
19
+ removed nothing. Every command is also reachable by a short name, output takes
20
+ colour, and both shells complete every command.
21
+
22
+ A branch the probe proved squash-merged is deleted with its worktree. It used
23
+ to survive: the verdict came from comparing content, and the delete then went
24
+ through `git branch -d`, which reads history and refuses exactly that case, so
25
+ `gwp` removed the checkout and left the branch behind with git's refusal
26
+ printed under it. The delete is now `git update-ref -d <ref> <sha>` against the
27
+ sha the verdict was formed on, so a commit landing between the verdict and the
28
+ removal fails it rather than going with it. `git branch -D` stays refused, and
29
+ so does `update-ref -d` with no sha, which is the same thing spelled longer.
30
+
31
+ Every removal names the ignored files it is about to delete, not just the ones
32
+ under `--delete-ignored`. `gwr --force` used to print a worktree, remove it,
33
+ and say nothing about the `.env` inside it; `git worktree remove` takes the
34
+ whole directory whatever flag got it there, so `--delete-ignored` decides
35
+ consent and never decides what is deleted.
36
+
37
+ `gwr` no longer calls a finished branch unfinished. A worktree holding ignored
38
+ files was refused with `is not finished: squash-merged, but holds 6 ignored
39
+ path(s)`, contradicting itself in one line, and the flag it offered was
40
+ `--force`, which keeps a branch whose work already landed and deletes those
41
+ files anyway. It now says `is finished` and names `--delete-ignored` alone.
42
+
43
+ `gwl` offers the main checkout, and never the worktree you are standing in.
44
+ In a repository with one linked worktree, standing in it, `gwl` printed
45
+ nothing and exited 0: the main checkout was filtered out of the candidates,
46
+ leaving one, which the picker took outright and handed back the path you were
47
+ already at. `gwl main` now finds the main checkout whatever branch it stands
48
+ on, two worktrees and no query go to the other one, and asking for the one
49
+ you are in says `already in <branch>` and stays put. `--list` shows every
50
+ worktree and marks that one. A repository with no linked worktrees says so
51
+ and names `gwa`, where it used to answer as though nothing were there.
52
+
53
+ `gws --json` carries `ignored` and `sha` on every verdict. The count of
54
+ ignored paths reads as a number rather than as a phrase inside `why`, and the
55
+ sha is the commit the verdict was formed against, which is the value a caller
56
+ would otherwise have to go and read for itself.
57
+
58
+ `gwp` with nothing removable prints the verdict table. It used to print
59
+ `nothing to remove; gws says why`, so the reason each worktree stayed cost a
60
+ second command to read.
61
+
62
+ `gwh` is the help, so nothing needs `gw --help` typed out. It joins `gwa`,
63
+ `gwl`, `gwm`, `gwr`, `gws`, `gwp`, `gwnb`, `gwrot` and `gw` on `$PATH`, and
64
+ `gw` still takes the same set as subcommands. `gw` with no subcommand prints
65
+ that table too, where it used to print argparse's usage. `gw` has its own
66
+ entry point now, so a usage error from it names `gw` rather than `worktrees`.
67
+
68
+ Every subcommand answers to a shorthand as well as its name: `s` and `st` for
69
+ `status`, `p` for `prune`, `a` for `add`, `l` and `ls` for `list`, `m` and
70
+ `mv` for `move`, `rm` for `remove`, `nb` and `new` for `new-branch`, `rot` for
71
+ `rotate`, `h` for `help`. One table in the CLI carries every name, so what
72
+ `gwh` prints, what the completions offer and what `gw` accepts cannot
73
+ disagree, and it refuses to import if two commands ever claim one name.
74
+
75
+ Output is coloured, and `NO_COLOR` turns it off. The check reads the value
76
+ rather than the key, which is the no-color.org rule: `NO_COLOR=` unsets the
77
+ request instead of making it. A redirect or a pipe turns colour off as well,
78
+ because `cd $(gwa x)` and `jq` read that output and neither wants escapes.
79
+
80
+ Completion covers every command in fish and in zsh, ten files per shell. The
81
+ candidates come from the CLI and never from a shell file: `gw --complete`
82
+ prints every subcommand and shorthand, `gwl --complete` and `gwr --complete`
83
+ print worktrees, each as a name and a description separated by a tab, which
84
+ fish reads directly and zsh splits for `_describe`. A command added to the
85
+ table needs no edit in either shell.
86
+
87
+ `gwl` and `gwr` do not offer the same set. `gwr` cannot remove the main
88
+ checkout, so offering it would complete to "no worktree matches"; for `gwl`
89
+ it is the one destination always there.
90
+
91
+ `gwa` says when it leaves something behind. It creates the directories before
92
+ git is asked, so a refusal used to strand an empty one silently.
93
+
94
+ The worktree picker prints destinations in full rather than abbreviating them
95
+ to `~/git/...`. The abbreviation was not what got printed and not what you
96
+ could paste.
97
+
98
+ ### Choices
99
+
100
+ A branch is deleted by `update-ref -d <ref> <sha>` rather than by `branch -D`.
101
+ Both force the delete; only one names what it expects to find, which turns a
102
+ concurrent commit into a failure instead of a loss. The guard reads that last
103
+ argument rather than counting the arguments: git takes `""` as "no old value"
104
+ and deletes the branch at exit 0, so a rule checking the shape would have
105
+ passed the one spelling that matters. `branch -d` was the
106
+ original choice and it inverts on the case this tool exists for: it reads
107
+ history, a squash merge leaves none, and deferring to it meant the content
108
+ probe bought the checkout and never the branch.
109
+
110
+ Completion flags live in the shell files, because that is the half that rots
111
+ without failing: rename a flag and nothing breaks, the candidate just stops
112
+ being offered. `test_completions.py` diffs both shells against `--help` in
113
+ both directions rather than trusting either copy, and drives every candidate
114
+ `gwr` offers back through `gwr` rather than comparing against a second copy
115
+ of the filter.
116
+
117
+ ## 0.1.0 - 2026-09-11
118
+
119
+ The first release. Eight commands for git worktrees, four of which answer a
120
+ question and print it, and four of which land you somewhere.
121
+
122
+ `gws` says which of a repository's worktrees are finished and why, in three
123
+ verdicts. `remove` was proved finished, `keep` has a reason not to be, and
124
+ `unknown` could not tell. It removes nothing and has no flag that could.
125
+
126
+ `gwp` removes the ones `gws` marks `remove`, under the same flags, printing
127
+ what goes and the command that puts it back before asking. `-y` skips the
128
+ question, and a run whose stdin is not a terminal refuses rather than
129
+ blocking.
130
+
131
+ Two failures in git are why those two exist. A squash merge makes
132
+ `git branch -d` report "not fully merged" and point at `-D`, which deletes
133
+ anything; the probe replays the branch's tree as one commit on the merge
134
+ base and asks `git cherry` whether that patch is already upstream. And
135
+ `git worktree remove` deletes `.env` and `node_modules/` at exit 0 with no
136
+ `--force` and no word about it, so ignored paths are counted and the
137
+ worktree is kept until you pass `--delete-ignored`.
138
+
139
+ Where content cannot tell, the forge is asked. A branch merged as part of a
140
+ stack is that case: its changes reach the head branch across several
141
+ squashes, so a diff cannot separate it from a branch with work left. A
142
+ merged pull request settles it, cross-checked against the commits an
143
+ upstream has not got. `--no-forge` decides from git alone.
144
+
145
+ `gwa NAME` creates a worktree at `<PARENT>/.worktrees/<NAME>/<REPO>` and
146
+ prints where it is. `gwl [QUERY]` picks one of the existing ones, taking a
147
+ single match outright and asking when there are several. `gwm NEW` renames
148
+ this worktree's branch and moves the checkout to match. `gwr` removes one
149
+ whose branch is finished, and the branch with it.
150
+
151
+ Those four change your shell's directory, which a binary cannot do, so each
152
+ ships a shell function of the same name for fish and for zsh. The function
153
+ reads the path the binary printed and does the `cd`, and carries nothing
154
+ else.
155
+
156
+ `gwnb NAME` starts a branch off the head branch as the remote has it after a
157
+ fetch. `gwrot` starts the next branch in a series, named
158
+ `<stem>-YYYY-MM-DD_NNN` at the first number free today. **Both are alpha and
159
+ may go**: they start branches rather than worktrees, `origin new-branch` and
160
+ `origin rotate` already do that, and only one of the two sets survives.
161
+
162
+ Six git commands are refused wherever they appear and whatever flags are
163
+ passed: `reset --hard`, a forced `checkout` or `switch`, `clean -f`, a bare
164
+ `push --force`, `worktree remove --force`, and `branch -D`. A branch is
165
+ deleted on git's own proof of merge or not at all.
166
+
167
+ Every command takes `--json`, so an agent reads verdicts as data rather than
168
+ parsing a table. Data goes to stdout and every diagnostic to stderr, the
169
+ prompts included, so `--json` parses in every mode. `--version` reports the
170
+ version of the installed distribution.
171
+
172
+ Python 3.11 or newer, and no dependencies.