@osdlc/repo-cleaner 0.17.2 → 0.19.0
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.
- package/CHANGELOG.md +31 -0
- package/README.md +691 -305
- package/dist/branches.d.ts +37 -36
- package/dist/branches.js +199 -196
- package/dist/clean.d.ts +179 -0
- package/dist/clean.js +824 -0
- package/dist/cli.d.ts +12 -16
- package/dist/cli.js +367 -478
- package/dist/hooks.d.ts +8 -12
- package/dist/hooks.js +2 -2
- package/dist/index.d.ts +18 -25
- package/dist/index.js +16 -23
- package/dist/landing.d.ts +3 -3
- package/dist/landing.js +7 -7
- package/dist/plan.d.ts +76 -70
- package/dist/plan.js +82 -77
- package/dist/providers/github.d.ts +2 -1
- package/dist/providers/github.js +2 -1
- package/dist/providers/gitlab.d.ts +3 -2
- package/dist/providers/gitlab.js +14 -8
- package/dist/providers/index.d.ts +5 -4
- package/dist/providers/index.js +8 -5
- package/dist/providers/types.d.ts +8 -2
- package/dist/prs.d.ts +35 -7
- package/dist/prs.js +45 -12
- package/dist/receipt.d.ts +46 -57
- package/dist/receipt.js +52 -28
- package/dist/reference.d.ts +12 -0
- package/dist/reference.js +152 -0
- package/dist/remote-mirror.d.ts +9 -1
- package/dist/remote-mirror.js +19 -1
- package/dist/render.d.ts +66 -61
- package/dist/render.js +206 -255
- package/dist/resolve.d.ts +3 -3
- package/dist/resolve.js +4 -12
- package/dist/status-line.d.ts +1 -1
- package/dist/status-line.js +5 -5
- package/dist/step-board.d.ts +1 -1
- package/dist/step-board.js +2 -5
- package/dist/submodules.d.ts +120 -0
- package/dist/submodules.js +292 -0
- package/dist/sweep.d.ts +107 -64
- package/dist/sweep.js +333 -119
- package/dist/types.d.ts +294 -105
- package/dist/types.js +106 -54
- package/dist/util.d.ts +13 -0
- package/dist/util.js +36 -0
- package/dist/version.generated.d.ts +1 -1
- package/dist/version.generated.js +1 -1
- package/dist/worktrees.d.ts +21 -37
- package/dist/worktrees.js +198 -165
- package/package.json +5 -5
- package/dist/apply.d.ts +0 -131
- package/dist/apply.js +0 -541
package/README.md
CHANGED
|
@@ -2,64 +2,62 @@
|
|
|
2
2
|
|
|
3
3
|
What has been merged into the default branch, and what can go away.
|
|
4
4
|
|
|
5
|
-
Point it at a repository and it
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
Point it at a repository and it lists the worktrees and local branches, and
|
|
6
|
+
with `--remote` the remote branches too, and judges each one: `delete`,
|
|
7
|
+
`review`, `keep` or `blocked`, with the evidence behind the verdict, the risks
|
|
8
|
+
deleting it carries, and the literal command that would delete it.
|
|
9
9
|
|
|
10
|
-
`repo-cleaner`
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
`repo-cleaner` never deletes anything; it prints the commands that would.
|
|
11
|
+
`repo-cleaner clean` deletes: it checks each row again, shows you what it
|
|
12
|
+
will delete and skip, and asks before it runs.
|
|
13
13
|
|
|
14
14
|
By default it works on your own machine only: worktrees and local branches,
|
|
15
|
-
nothing fetched and no
|
|
16
|
-
last fetched (the header says `not fetched`).
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
Without
|
|
24
|
-
and a clean worktree merged by ancestry is `delete` once
|
|
25
|
-
it was worked on (see [How it decides](#how-it-decides)); one
|
|
26
|
-
freshly made stays `review`.
|
|
27
|
-
|
|
28
|
-
On a terminal, a long sweep step draws a progress bar on stderr, and
|
|
29
|
-
`apply` draws its steps as a list that updates in place: each row has its
|
|
30
|
-
own bar, empty while it waits, half full while it runs and full with a `✓`
|
|
31
|
-
(or `✗`) when it is done, with a bar for the whole run underneath:
|
|
15
|
+
nothing fetched and no pull-request lookup, compared with the
|
|
16
|
+
`origin/<default>` you last fetched (the header says `not fetched`).
|
|
17
|
+
`--remote` turns on all three things that need the network: it fetches each
|
|
18
|
+
remote first, asks GitHub or GitLab about pull requests, and judges remote
|
|
19
|
+
branches. Remote branches are shared, often number in the hundreds, and each
|
|
20
|
+
one is compared with the default branch, so they make a sweep much slower. A
|
|
21
|
+
local report ends with a line saying what it left out.
|
|
22
|
+
|
|
23
|
+
Without a pull-request lookup, a squash-merged branch is still found by
|
|
24
|
+
comparing patches, and a clean worktree merged by ancestry is `delete` once
|
|
25
|
+
its branch shows it was worked on (see [How it decides](#how-it-decides)); one
|
|
26
|
+
that looks freshly made stays `review`.
|
|
32
27
|
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
28
|
+
```console
|
|
29
|
+
$ repo-cleaner --remote
|
|
30
|
+
/Users/me/work/api (acme/api, default main, origin)
|
|
31
|
+
branch fix/login-redirect delete PR #412 merged
|
|
32
|
+
git update-ref -d refs/heads/fix/login-redirect 3f9c2e1d8b7a6c5d4e3f2a1b0c9d8e7f6a5b4c3d
|
|
33
|
+
branch spike/cache review mixed or absent signals; needs --allow abandon-unmerged (drops 2 commits)
|
|
34
|
+
git update-ref -d refs/heads/spike/cache 9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b
|
|
35
|
+
worktree /Users/me/work/wt/412 delete clean; PR #412 merged
|
|
36
|
+
git -C /Users/me/work/api worktree remove /Users/me/work/wt/412
|
|
37
|
+
remote origin/fix/login-… delete PR #412 merged (auto-delete didn't fire)
|
|
38
|
+
git push --force-with-lease=refs/heads/fix/login-redirect:3f9c2e1d8b7a6c5d4e3f2a1b0c9d8e7f6a5b4c3d origin :refs/heads/fix/login-redirect
|
|
39
|
+
|
|
40
|
+
3 to delete, 1 to review, across 1 repository.
|
|
39
41
|
```
|
|
40
42
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
`FORCE_COLOR` asks for it.
|
|
43
|
+
On a terminal, the report and the preview are colored: red for a delete,
|
|
44
|
+
blue for a prune, yellow for a review, a blocked or a skipped row, green for a
|
|
45
|
+
keep. `NO_COLOR` turns this off; a pipe or a file gets no color unless
|
|
46
|
+
`FORCE_COLOR` asks for it. A long sweep step draws a progress bar on stderr,
|
|
47
|
+
and `clean` draws its steps as a list that updates in place, with a bar for the
|
|
48
|
+
whole run underneath:
|
|
48
49
|
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
worktree wt/412 delete clean; PR #412 merged
|
|
56
|
-
git -C /Users/me/work/api worktree remove wt/412
|
|
57
|
-
remote_branch origin/fix/login-… delete PR #412 merged (auto-delete didn't fire)
|
|
58
|
-
git push --force-with-lease=refs/heads/fix/login-redirect:3f9c2e1d8b7a6c5d4e3f2a1b0c9d8e7f6a5b4c3d origin :refs/heads/fix/login-redirect
|
|
59
|
-
|
|
60
|
-
1 to delete, 1 to review, across 1 repository.
|
|
50
|
+
```text
|
|
51
|
+
██████████ ✓ delete branch:feat/a
|
|
52
|
+
█████░░░░░ › delete branch:feat/b
|
|
53
|
+
░░░░░░░░░░ delete remote:origin/feat/b
|
|
54
|
+
░░░░░░░░░░ prune worktree:/Users/me/work/wt/old
|
|
55
|
+
████████░░░░░░░░░░░░░░░░░░░░░░ 1/4
|
|
61
56
|
```
|
|
62
57
|
|
|
58
|
+
A list taller than the terminal scrolls to keep the running step in view, and
|
|
59
|
+
a failed step's error is printed in full under the list.
|
|
60
|
+
|
|
63
61
|
## Install
|
|
64
62
|
|
|
65
63
|
```sh
|
|
@@ -90,6 +88,285 @@ nvm users in fish can use a fish port such as `nvm.fish`; fnm has
|
|
|
90
88
|
`fnm env --use-on-cd --shell fish | source`. Then open a new shell and check
|
|
91
89
|
with `type repo-cleaner`.
|
|
92
90
|
|
|
91
|
+
|
|
92
|
+
## Commands
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
repo-cleaner [target...] # the report: what is merged, what can go; deletes nothing
|
|
96
|
+
repo-cleaner plan [target...] # save the sweep's rows and their evidence, to run later (-o <file>)
|
|
97
|
+
repo-cleaner clean [target...] # check again, show the steps, ask, delete
|
|
98
|
+
repo-cleaner cache # list or remove the clones kept for remote URL targets
|
|
99
|
+
repo-cleaner dev # run repo-cleaner from a development checkout
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Every command takes `--help`, and `--help --extended` for the full reference:
|
|
103
|
+
what each option means, the words the report uses, and which combinations are
|
|
104
|
+
refused. A checkout directory named like a command is swept as `./<name>`.
|
|
105
|
+
|
|
106
|
+
### Targets
|
|
107
|
+
|
|
108
|
+
A target is what to sweep:
|
|
109
|
+
|
|
110
|
+
| Target | Swept |
|
|
111
|
+
| --- | --- |
|
|
112
|
+
| a checkout | that repository; a linked worktree sweeps the repository it belongs to |
|
|
113
|
+
| a directory | every checkout found under it, three levels deep at most |
|
|
114
|
+
| a remote URL (`https://`, `ssh://`, `file://`, `user@host:path`) | its remote branches only, through a clone kept in the cache directory (see [A remote with no checkout](#a-remote-with-no-checkout)) |
|
|
115
|
+
|
|
116
|
+
No target is the same as naming the current directory: at a checkout's root
|
|
117
|
+
it sweeps that repository, and in a directory of checkouts it sweeps each
|
|
118
|
+
one. In a subdirectory of a checkout it searches below that subdirectory,
|
|
119
|
+
like any other directory, so name the checkout instead.
|
|
120
|
+
What a target names is read off the disk rather than from how it is written:
|
|
121
|
+
a path that is itself a checkout names that repository, and a path that is
|
|
122
|
+
not is searched for the checkouts under it, descending through directories
|
|
123
|
+
that are not checkouts and stopping at the ones that are. A directory holding
|
|
124
|
+
a checkout and three of its worktrees sweeps one repository, not four; the
|
|
125
|
+
worktrees appear as rows of that repository.
|
|
126
|
+
|
|
127
|
+
The report ends with what the search cost — `searched 1 target: 214
|
|
128
|
+
directories read, 12 checkouts found.` — because those numbers are how a
|
|
129
|
+
target pointing at the wrong directory gets caught. A search that passes
|
|
130
|
+
10,000 directories says so on stderr while it is still running. A target that
|
|
131
|
+
resolves to nothing is reported rather than dropped.
|
|
132
|
+
|
|
133
|
+
### Sweep options
|
|
134
|
+
|
|
135
|
+
The report, `plan` and `clean` take these:
|
|
136
|
+
|
|
137
|
+
| Option | Meaning |
|
|
138
|
+
| --- | --- |
|
|
139
|
+
| `--remote` | Also judge remote branches. Turns on the network: fetches each remote first and asks GitHub or GitLab about pull requests. Slower. |
|
|
140
|
+
| `--offline` | With `--remote`: do not fetch. Judge against the remote-tracking refs already on disk, remote branches included, so judging needs no access to the remote. Pull requests are still looked up; add `--provider none` for no network at all. The refs can be stale: see [`--offline` reads what is already on disk](#--offline-reads-what-is-already-on-disk). |
|
|
141
|
+
| `--only <kinds>` | Only these kinds of row: `worktree`, `branch`, `remote`, comma-separated. Default: `worktree,branch`, plus `remote` with `--remote`. Naming `remote` implies `--remote`. |
|
|
142
|
+
| `--branch <name\|HEAD>` | Only the rows of one branch: its worktree, its local branch, and its remote branch with `--remote`. `HEAD` is the branch checked out at the target. Never the default branch. |
|
|
143
|
+
| `--against <remote>` | The remote whose default branch counts as "merged into". Default: `origin`, then the first remote. |
|
|
144
|
+
| `--provider <name>` | With `--remote`: where pull requests come from. `auto` (by the remote's host), `github`, `gitlab`, or `none` to judge on git evidence alone. |
|
|
145
|
+
| `--no-progress` | No progress line on stderr. |
|
|
146
|
+
|
|
147
|
+
### The report
|
|
148
|
+
|
|
149
|
+
`repo-cleaner [target...]` lists each row with its verdict, why, the risks it
|
|
150
|
+
carries, and the command that would delete it.
|
|
151
|
+
|
|
152
|
+
| Option | Meaning |
|
|
153
|
+
| --- | --- |
|
|
154
|
+
| `--all` | Also show `keep` rows. By default only `delete`, `review` and `blocked` rows are listed. Only with `--format text`. |
|
|
155
|
+
| `--format text` | The report. The default. |
|
|
156
|
+
| `--format summary` | Counts per repository: total, merged, delete, prune, review, blocked, keep. |
|
|
157
|
+
| `--format commands` | Only the commands of `delete` rows that carry no risk, ready to copy. |
|
|
158
|
+
| `--format json` | Everything, evidence included, for scripts. |
|
|
159
|
+
|
|
160
|
+
```console
|
|
161
|
+
$ repo-cleaner --format summary --remote
|
|
162
|
+
/Users/me/work/api (acme/api, default main, origin)
|
|
163
|
+
total merged delete prune review blocked keep
|
|
164
|
+
local branches 12 8 6 0 1 0 5
|
|
165
|
+
worktrees 4 3 1 1 0 1 1
|
|
166
|
+
remote branches 40 31 30 0 6 0 4
|
|
167
|
+
|
|
168
|
+
37 to delete, 1 to prune, 7 to review, 1 blocked, across 1 repository.
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
`prune` counts worktree rows whose directory is gone, or is no longer a
|
|
172
|
+
checkout: deleting one drops the stale record, with `git worktree remove
|
|
173
|
+
<path>` for a directory that is gone (that worktree's record alone) and the
|
|
174
|
+
repository-wide `git worktree prune` for one left without its `.git`, and a
|
|
175
|
+
directory that is still there stays. `merged` counts rows whose work is on
|
|
176
|
+
the default branch, by ancestry, by a merged PR at the branch's head, or by a
|
|
177
|
+
patch match; the default branch and the primary worktree are counted in
|
|
178
|
+
`total` but not judged.
|
|
179
|
+
|
|
180
|
+
### `repo-cleaner plan`
|
|
181
|
+
|
|
182
|
+
Sweeps and saves every row, each with a fingerprint of the evidence behind its
|
|
183
|
+
verdict, so it can be reviewed now and run later with
|
|
184
|
+
`clean --plan`. `-o, --out <file>` sets where it goes; the default, `-`, is
|
|
185
|
+
stdout, and the totals line goes to stderr.
|
|
186
|
+
|
|
187
|
+
### `repo-cleaner clean`
|
|
188
|
+
|
|
189
|
+
Sweeps (or reads `--plan`), checks every row again, shows the steps, asks,
|
|
190
|
+
then deletes, compare-and-swap, verifying each step and stopping at the first
|
|
191
|
+
failure. Without `--pick` it runs every row judged `delete`.
|
|
192
|
+
|
|
193
|
+
| Option | Meaning |
|
|
194
|
+
| --- | --- |
|
|
195
|
+
| `--pick <row>` | Work on exactly these rows. See [Pick and spare](#pick-and-spare). Repeatable. |
|
|
196
|
+
| `--spare <row>` | Leave this row alone, and make sure it is what you saw. Repeatable. |
|
|
197
|
+
| `--allow <risk>` | Accept this risk for every row that carries it. |
|
|
198
|
+
| `--allow <risk>=<row>` | Accept it for that one row only. |
|
|
199
|
+
| `--plan <file\|->` | Run a saved plan instead of sweeping now (`-` reads stdin). Rows whose evidence changed since the plan are dropped, not deleted. |
|
|
200
|
+
| `-y, --yes` | Do not ask. Without it, and with no terminal to ask on, `clean` refuses. |
|
|
201
|
+
| `--dry-run` | Check and show the steps, then stop. |
|
|
202
|
+
| `--receipt <file>` | Write a record of every step, rewritten after each one. Default: next to a `--plan` file; none otherwise. |
|
|
203
|
+
| `--format text\|json` | How the steps and the outcome are shown. |
|
|
204
|
+
|
|
205
|
+
```text
|
|
206
|
+
/Users/me/work/api
|
|
207
|
+
delete branch:fix/login-redirect
|
|
208
|
+
git update-ref -d refs/heads/fix/login-redirect 3f9c2e1d…
|
|
209
|
+
prune worktree:/Users/me/work/wt/old
|
|
210
|
+
git -C /Users/me/work/api worktree remove /Users/me/work/wt/old
|
|
211
|
+
skip worktree:/Users/me/work/wt/spike
|
|
212
|
+
needs-pick: review: clean but no clear disposability signal; pick it to run it
|
|
213
|
+
|
|
214
|
+
would delete: 1 local branch, 1 stale worktree pruned
|
|
215
|
+
skipped 1, not deleted: 1 needs-pick (name with --pick)
|
|
216
|
+
Delete 2 items? [y/N]
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
`clean` throws away anything typed while the sweep ran, so a key pressed
|
|
220
|
+
early cannot answer the question, and says so when it did.
|
|
221
|
+
|
|
222
|
+
### Pick and spare
|
|
223
|
+
|
|
224
|
+
**`--pick <row>`** means "work on exactly these rows". It does two things:
|
|
225
|
+
|
|
226
|
+
1. **It narrows the run.** Only the picked rows are considered; everything
|
|
227
|
+
else is left alone.
|
|
228
|
+
2. **It opts in rows that never run otherwise.** Without `--pick`, `clean`
|
|
229
|
+
deletes only rows judged `delete`. A `review` row, or a `keep` row with an
|
|
230
|
+
action (a branch with an open pull request), is deleted only when it is
|
|
231
|
+
named, so a borderline row never goes as part of a batch.
|
|
232
|
+
|
|
233
|
+
Picking does not approve anything: a picked row that carries a risk still
|
|
234
|
+
needs `--allow` for it. A `blocked` row, or a `keep` row with no action, is
|
|
235
|
+
skipped and shown with the reason.
|
|
236
|
+
|
|
237
|
+
**`--spare <row>`** means "leave this row alone, and make sure it is what I
|
|
238
|
+
saw". The usual case: delete a merged worktree and its local branch, but keep
|
|
239
|
+
the branch on the remote as a backup.
|
|
240
|
+
|
|
241
|
+
- A spared row is not deleted, even if it is judged `delete`.
|
|
242
|
+
- A spared remote branch is still checked. If it is gone or its tip moved
|
|
243
|
+
since the plan, the run stops before deleting anything: you spared it for
|
|
244
|
+
what it held, so if that changed, the rest of the plan rests on a wrong
|
|
245
|
+
assumption. The run's first step checks the tip again with `git ls-remote`,
|
|
246
|
+
so one that moves after the check stops the run too. This holds under
|
|
247
|
+
`--offline` too: `clean`'s re-check asks the remote, so a spared branch the
|
|
248
|
+
remote deleted refuses the run, whatever the tracking refs on disk say.
|
|
249
|
+
- Sparing a worktree also spares its branch, which was only free to delete
|
|
250
|
+
because the worktree was going.
|
|
251
|
+
|
|
252
|
+
### Rows, verdicts and risks
|
|
253
|
+
|
|
254
|
+
A **row** is one thing that could be deleted, named `<kind>:<name>`. A bare
|
|
255
|
+
name or path works when only one row has it.
|
|
256
|
+
|
|
257
|
+
| Row | What it is | Deleted with |
|
|
258
|
+
| --- | --- | --- |
|
|
259
|
+
| `worktree:<path>` | a linked worktree | `git worktree remove`; when its directory is already gone, the same on its record alone, or `git worktree prune` when the directory is left without its `.git` |
|
|
260
|
+
| `branch:<name>` | a local branch | `git update-ref -d refs/heads/<name> <sha>` |
|
|
261
|
+
| `remote:<remote>/<name>` | a branch on a remote | `git push --force-with-lease=…:<sha> <remote> :refs/heads/<name>` |
|
|
262
|
+
|
|
263
|
+
A **verdict** is what the sweep concluded about a row:
|
|
264
|
+
|
|
265
|
+
| Verdict | Meaning | What `clean` does |
|
|
266
|
+
| --- | --- | --- |
|
|
267
|
+
| `delete` | Its work is on the default branch, or it is otherwise safe to lose | Deletes it, once its risks are allowed |
|
|
268
|
+
| `review` | The signals are mixed, or it holds work | Deletes it only when picked and its risks are allowed |
|
|
269
|
+
| `blocked` | Something rules removal out, such as a lock, a running process, or a submodule that cannot be read | Never deletes it; fix the cause and sweep again |
|
|
270
|
+
| `keep` | Structural (the default branch, the current checkout, the primary worktree), or in use (an open pull request) | Never deletes a structural row; deletes one in use only when picked and `confirm-open-pr` is allowed |
|
|
271
|
+
|
|
272
|
+
The report shows `prune` for a `delete` row whose worktree directory is
|
|
273
|
+
already gone: only the stale record goes.
|
|
274
|
+
|
|
275
|
+
A **risk** is what deleting a row would lose. Each row lists its risks, and
|
|
276
|
+
you approve them by name with `--allow`:
|
|
277
|
+
|
|
278
|
+
| Risk | Meaning |
|
|
279
|
+
| --- | --- |
|
|
280
|
+
| `abandon-unmerged` | Commits not on the default branch and not captured by an exact-head merged PR, or a worktree's (or its submodules') commits and stashes that no remote has. `evidence.uniqueCommits` counts the commits. |
|
|
281
|
+
| `discard-changes` | Uncommitted or untracked files in the worktree or any of its submodules. |
|
|
282
|
+
| `confirm-open-pr` | The branch has an open pull request, or open pull requests could not be checked (the CLI is missing or signed out, or the listing failed) so one cannot be ruled out; deleting the remote branch closes it. |
|
|
283
|
+
|
|
284
|
+
A worktree's submodules are part of it: one `--allow` on the worktree row
|
|
285
|
+
covers the same risk in every submodule. A stash in the worktree itself is
|
|
286
|
+
shared with the main checkout and survives, so it carries no risk.
|
|
287
|
+
|
|
288
|
+
A branch whose worktree is judged `delete` is not held by it: the sweep judges
|
|
289
|
+
the branch on its own and records the worktree's path in `evidence.worktree`,
|
|
290
|
+
so one run removes the worktree and then deletes the branch. `clean` deletes
|
|
291
|
+
such a branch only when the same run removes its worktree, and drops it as
|
|
292
|
+
`held` otherwise. With `--only branch` no worktree is swept, so the branch
|
|
293
|
+
stays held.
|
|
294
|
+
|
|
295
|
+
### Refused combinations
|
|
296
|
+
|
|
297
|
+
A combination of options is refused when it contradicts itself, or when one of
|
|
298
|
+
its options would have no effect: a flag that is silently ignored leaves you
|
|
299
|
+
believing something that is not true. repo-cleaner never guesses, and no
|
|
300
|
+
option silently overrides another.
|
|
301
|
+
|
|
302
|
+
Refused before anything is deleted, with exit 2. Most are refused before
|
|
303
|
+
anything is swept; a row named two ways (a bare name and its id), and an
|
|
304
|
+
`--allow` for a row, are checked against the plan, after the sweep:
|
|
305
|
+
|
|
306
|
+
| Combination | Why |
|
|
307
|
+
| --- | --- |
|
|
308
|
+
| The same row in `--pick` and `--spare` | Asks for the row to be deleted and kept |
|
|
309
|
+
| `--allow <risk>=<row>` for a spared row, or for a row outside `--pick` | Approves deleting something that will not be deleted |
|
|
310
|
+
| A picked or spared row of a kind `--only` leaves out | The row cannot be in the run |
|
|
311
|
+
| A picked or spared row that is not one of `--branch`'s rows | The same |
|
|
312
|
+
| `--branch` with more than one target, with a remote URL, or naming the default branch | `--branch` means one branch of one checkout, and the default branch is never a candidate |
|
|
313
|
+
| `--plan` with targets or sweep options | The plan already fixed what is swept |
|
|
314
|
+
| `--plan -` without `--yes` or `--dry-run` | stdin carries the plan, so nothing can ask |
|
|
315
|
+
| `--yes` with `--dry-run` | One deletes without asking, the other deletes nothing |
|
|
316
|
+
| `--receipt` with `--dry-run` | Nothing runs, so there is nothing to record |
|
|
317
|
+
| `--offline` or `--provider` without `--remote` | Nothing is fetched or looked up without it |
|
|
318
|
+
| `--remote` with an `--only` that leaves `remote` out | They ask for opposite things |
|
|
319
|
+
| A remote URL target with `--only worktree` or `--only branch`, with `--offline`, or mixed with checkout targets | A remote URL has only remote branches, and it is fetched to be swept |
|
|
320
|
+
| `--all` with any `--format` but `text` | `--all` shows keep rows in the text report only |
|
|
321
|
+
|
|
322
|
+
Refused after the sweep, before deleting, with exit 1:
|
|
323
|
+
|
|
324
|
+
- a picked, spared or allowed row the plan does not have;
|
|
325
|
+
- a row name that matches rows in more than one repository: sweep or plan
|
|
326
|
+
only the repository it is in (a bare name can also be given as its full id,
|
|
327
|
+
when the matches are of different kinds);
|
|
328
|
+
- a spared remote branch that moved since the plan, or that the re-check no
|
|
329
|
+
longer finds.
|
|
330
|
+
|
|
331
|
+
Allowed, because nothing contradicts:
|
|
332
|
+
|
|
333
|
+
| Combination | What happens |
|
|
334
|
+
| --- | --- |
|
|
335
|
+
| The same row picked twice | It runs once |
|
|
336
|
+
| `--allow <risk>` with `--allow <risk>=<row>` | The broader one covers it |
|
|
337
|
+
| `--allow` for a risk no row that runs carries | Runs; the preview lists it as `unused` |
|
|
338
|
+
| Picking a `blocked` or `keep` row | It is skipped and shown with the reason; the rest runs |
|
|
339
|
+
| `--only remote` without `--remote` | Asking for remote branches implies the network |
|
|
340
|
+
|
|
341
|
+
### A remote with no checkout
|
|
342
|
+
|
|
343
|
+
```bash
|
|
344
|
+
repo-cleaner git@github.com:acme/api.git
|
|
345
|
+
repo-cleaner clean git@github.com:acme/api.git
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
A remote URL target sweeps a remote's branches when you have no checkout of
|
|
349
|
+
it. It clones the URL once, with `--no-checkout`, into
|
|
350
|
+
`repo-cleaner/remotes/` under the cache directory (`~/Library/Caches` on
|
|
351
|
+
macOS, `$XDG_CACHE_HOME` or `~/.cache` on Linux, `REPO_CLEANER_CACHE_DIR` to
|
|
352
|
+
move it), and sweeps that clone. Later runs reuse the clone and fetch. Only
|
|
353
|
+
remote branches are swept: the clone's own branches and worktree are not
|
|
354
|
+
yours. Deletes are pushed to the URL from the clone, compare-and-swap as
|
|
355
|
+
always. A local bare repository is named as `file:///path/to/repo.git`.
|
|
356
|
+
|
|
357
|
+
The clone is kept, not deleted after the run, so a plan of a remote URL can
|
|
358
|
+
run later from anywhere: the plan names the clone's directory.
|
|
359
|
+
`repo-cleaner cache` lists the clones with their URL and size;
|
|
360
|
+
`repo-cleaner cache remove <url>` drops one, and `cache remove --all` drops
|
|
361
|
+
every one, a clone left by a stopped run included. A plan whose clone was
|
|
362
|
+
removed cannot run; sweep again. The clone is a full one, not
|
|
363
|
+
`--filter=blob:none`: the patch check reads file contents.
|
|
364
|
+
|
|
365
|
+
A clone is reused only while its `origin` fetches from that URL and pushes to
|
|
366
|
+
it alone; `clean --plan` checks the same of a plan's clone before deleting.
|
|
367
|
+
The clone's directory name and every message carry the URL with its
|
|
368
|
+
credentials, query and fragment removed.
|
|
369
|
+
|
|
93
370
|
## How it decides
|
|
94
371
|
|
|
95
372
|
Three signals settle most items. The first two are enough alone; the third needs a content check too (see [Patch equivalence](#patch-equivalence)):
|
|
@@ -104,11 +381,11 @@ Ancestry is measured against `<remote>/<default>`, so work merged only into an
|
|
|
104
381
|
unpushed local default branch does not count. Without a remote, the local
|
|
105
382
|
default branch is used.
|
|
106
383
|
|
|
107
|
-
A merged PR that fails the head or base check is kept in `
|
|
108
|
-
`
|
|
384
|
+
A merged PR that fails the head or base check is kept in `evidence.pr` with
|
|
385
|
+
`proof` set to `head-moved` or `other-base`. Fork PRs are ignored.
|
|
109
386
|
|
|
110
387
|
Everything else is held back. A branch that is neither merged nor an ancestor
|
|
111
|
-
is `
|
|
388
|
+
is `review` with the reasons spelled out, not a guess. A worktree
|
|
112
389
|
holding uncommitted or unpushed work is never proposed for removal whatever
|
|
113
390
|
else is true of it — losing an hour of someone's work costs more than any
|
|
114
391
|
amount of clutter.
|
|
@@ -117,24 +394,73 @@ A clean worktree whose HEAD is merged by ancestry alone, with no merged PR
|
|
|
117
394
|
or patch match, needs one more sign, because a worktree that `git worktree
|
|
118
395
|
add -b` just made from the default branch is merged that way too, and
|
|
119
396
|
removing it would lose the ignored files of work about to start. The sign is
|
|
120
|
-
in `
|
|
397
|
+
in `evidence.branchUsed`: `commits` when the branch's reflog records a commit,
|
|
121
398
|
cherry-pick, `am`, or a merge or pull that made a merge commit (a
|
|
122
399
|
fast-forward only moves the branch), and `upstream` when the branch
|
|
123
400
|
tracks a branch on a remote, other than the default branch (a branch made
|
|
124
401
|
with `--track` from `main` or `origin/main` does not count). With either, the worktree
|
|
125
|
-
is `
|
|
402
|
+
is `delete`; with neither (`null`) it stays `review`.
|
|
126
403
|
|
|
127
|
-
Some worktrees are held back whatever their branch says
|
|
128
|
-
`
|
|
404
|
+
Some worktrees are held back whatever their branch says and whatever is
|
|
405
|
+
allowed. Each lands in `row.blockers`, the row's verdict is `blocked`, and it
|
|
406
|
+
carries no command and no action:
|
|
129
407
|
|
|
130
408
|
| Blocker | Why |
|
|
131
409
|
| --- | --- |
|
|
132
|
-
| `locked-worktree` | Someone ran `git worktree lock` and the lock's owner is alive or unknown; the reason is in `
|
|
133
|
-
| `live-process` | A process other than an idle shell has its working directory in the worktree (`
|
|
134
|
-
| `process-probe-failed` | The process table could not be read (no `lsof`, a timeout, an `lsof` that failed outright), so nobody can say the directory is idle. `
|
|
410
|
+
| `locked-worktree` | Someone ran `git worktree lock` and the lock's owner is alive or unknown; the reason is in `evidence.lockReason`. A lock whose owner is dead is not a blocker (see below). |
|
|
411
|
+
| `live-process` | A process other than an idle shell has its working directory in the worktree (`evidence.liveProcesses`), lock or not. |
|
|
412
|
+
| `process-probe-failed` | The process table could not be read (no `lsof`, a timeout, an `lsof` that failed outright), so nobody can say the directory is idle. `evidence.processProbeError` has the message. |
|
|
135
413
|
| `hidden-index-flags` | Files marked skip-worktree or assume-unchanged hide edits from `status`. |
|
|
136
414
|
| `nested-repository` | An untracked or ignored directory holds a repository with its own history. |
|
|
137
|
-
| `branch-in-other-worktree` | Another worktree has the same branch checked out (`
|
|
415
|
+
| `branch-in-other-worktree` | Another worktree has the same branch checked out (`evidence.otherWorktrees`). |
|
|
416
|
+
| `submodule-unreadable` | A submodule could not be read, so nobody can say it is safe to lose (see [Submodules](#submodules)). |
|
|
417
|
+
|
|
418
|
+
### Submodules
|
|
419
|
+
|
|
420
|
+
A linked worktree's submodules keep their git dirs inside the worktree's own
|
|
421
|
+
git dir (`.git/worktrees/<id>/modules/<name>`), not the main checkout's. Removing
|
|
422
|
+
the worktree, or pruning it once its directory is gone, deletes them: a commit
|
|
423
|
+
made in a submodule and never pushed goes with it, and so does a stash or a
|
|
424
|
+
local branch. Plain `git worktree remove` refuses every worktree with a
|
|
425
|
+
checked-out submodule, or with a submodule git dir a `git submodule deinit`
|
|
426
|
+
left behind, whatever state it is in. It takes `--force`, and `--force` also
|
|
427
|
+
skips git's own check for modified and untracked files.
|
|
428
|
+
|
|
429
|
+
So the sweep reads every submodule itself, nested ones through their parents,
|
|
430
|
+
and every left-over submodule git dir, and records what it found in
|
|
431
|
+
`evidence.submodules`. A submodule is judged as part of its worktree: the same
|
|
432
|
+
risks apply wherever they are, and one `--allow` on a worktree row covers its
|
|
433
|
+
submodules too.
|
|
434
|
+
|
|
435
|
+
| Found in a submodule | Effect on the worktree row |
|
|
436
|
+
| --- | --- |
|
|
437
|
+
| Commits on HEAD or a local branch that no remote-tracking ref holds, or a stash | `review`, carries `abandon-unmerged`; the reason names each submodule and what it holds |
|
|
438
|
+
| Modified, staged or untracked files | `review`, carries `discard-changes`; `evidence.submoduleDirtyFiles` counts them |
|
|
439
|
+
| Skip-worktree or assume-unchanged files, nested repositories | `hidden-index-flags`, `nested-repository`, with the submodule's path in front |
|
|
440
|
+
| A git call that failed | `submodule-unreadable` blocker |
|
|
441
|
+
|
|
442
|
+
A worktree whose submodules are all clean and pushed is judged as any other,
|
|
443
|
+
and when it is `delete` its command is `git worktree remove --force`
|
|
444
|
+
(`evidence.forcedForSubmodules` is `true`, and the reason says why). Just before that
|
|
445
|
+
remove, `clean` checks the worktree's own files and every submodule again,
|
|
446
|
+
since git will not, and refuses the step if it would lose anything not allowed. A submodule's
|
|
447
|
+
state is part of the plan fingerprint, so a submodule that moved since the plan
|
|
448
|
+
drops the row first.
|
|
449
|
+
|
|
450
|
+
A worktree whose directory is gone is read through its git dir: unpushed work
|
|
451
|
+
in a left-over submodule git dir makes the prune row a `review` that carries
|
|
452
|
+
`abandon-unmerged`.
|
|
453
|
+
A worktree whose directory is gone is removed by its own record. One whose
|
|
454
|
+
directory is left without its `.git` takes the repository-wide `git worktree
|
|
455
|
+
prune`, which drops every gone worktree, not only the planned one. So such a
|
|
456
|
+
row runs last, and only while every other gone worktree that prune would drop
|
|
457
|
+
goes in the same run; otherwise `clean` drops it (`blocked`, with the reason).
|
|
458
|
+
A run never drops the record of a gone worktree it did not select.
|
|
459
|
+
|
|
460
|
+
Commits count as pushed only when a remote-tracking ref of the submodule holds
|
|
461
|
+
them. A superproject that records a submodule commit reachable from no branch
|
|
462
|
+
on the submodule's remote (fetched by sha, or on a branch since deleted) carries
|
|
463
|
+
`abandon-unmerged` until a fetch brings it under one.
|
|
138
464
|
|
|
139
465
|
### Patch equivalence
|
|
140
466
|
|
|
@@ -149,16 +475,16 @@ branch as safe as an ancestor. A second, independent check must agree:
|
|
|
149
475
|
`git merge-tree --write-tree <default> <tip>` must come out clean with exactly
|
|
150
476
|
the default branch's tree, which shows the branch's content is already there. It
|
|
151
477
|
needs git 2.38 or later; an older git, a conflict or any failure counts as not
|
|
152
|
-
agreed. The row is `
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
478
|
+
agreed. The row is `delete` either way, but only an agreed check drops the
|
|
479
|
+
`abandon-unmerged` risk. Without it the row keeps carrying that risk, and a
|
|
480
|
+
worktree with unpushed commits stays `review`. The row carries three pieces of
|
|
481
|
+
evidence, and its reason says which case applied:
|
|
156
482
|
|
|
157
|
-
|
|
|
483
|
+
| Evidence | Value |
|
|
158
484
|
| --- | --- |
|
|
159
|
-
| `
|
|
160
|
-
| `
|
|
161
|
-
| `
|
|
485
|
+
| `evidence.landing` | `patch-equivalent` |
|
|
486
|
+
| `evidence.landingCheck` | which check passed, below |
|
|
487
|
+
| `evidence.landingContentConfirmed` | `true` when the merge-tree check agreed, `false` otherwise |
|
|
162
488
|
|
|
163
489
|
| `landing_check` | Meaning |
|
|
164
490
|
| --- | --- |
|
|
@@ -168,7 +494,7 @@ carries three facts, and its reason says which case applied:
|
|
|
168
494
|
|
|
169
495
|
`squashed` accepts any one commit on the default branch with the same net
|
|
170
496
|
patch, not necessarily one that came from this branch. That is acceptable only
|
|
171
|
-
because the content check, or
|
|
497
|
+
because the content check, or `--allow abandon-unmerged`, stands behind it.
|
|
172
498
|
|
|
173
499
|
The squashed check builds one commit object with `git commit-tree` and no ref.
|
|
174
500
|
It leaves the index, the working tree and every ref alone, and needs no git
|
|
@@ -179,7 +505,7 @@ The check decides what gets deleted, so a doubt is never evidence. None of
|
|
|
179
505
|
these count as landed:
|
|
180
506
|
|
|
181
507
|
- a commit that is not on the default branch, however many others are (the
|
|
182
|
-
commits that are missing are listed in `
|
|
508
|
+
commits that are missing are listed in `evidence.unmatchedCommits` and in the
|
|
183
509
|
investigation text when part of the work did land);
|
|
184
510
|
- a git call that fails, or output that cannot be read;
|
|
185
511
|
- a shallow clone, a missing merge base, a ref that does not resolve;
|
|
@@ -193,7 +519,7 @@ A patch id says two changes are the same, not that they come from the same
|
|
|
193
519
|
branch, and it ignores whitespace. A one-line change that the default branch
|
|
194
520
|
also made independently, an indentation-only difference, or work that landed
|
|
195
521
|
and was then reverted all match by patch id; the content check does not agree
|
|
196
|
-
with them, so they keep needing
|
|
522
|
+
with them, so they keep needing `--allow abandon-unmerged`.
|
|
197
523
|
|
|
198
524
|
### Locks and running processes
|
|
199
525
|
|
|
@@ -202,9 +528,9 @@ token from the lock reason, `pid <N>`, optionally followed by `start <time>`,
|
|
|
202
528
|
and asks whether that process is still running. It reads nothing else of the
|
|
203
529
|
reason, so a free-form reason such as `keep, build running` always blocks.
|
|
204
530
|
|
|
205
|
-
- The owner is dead: the lock is not a blocker. `
|
|
206
|
-
`
|
|
207
|
-
with `git worktree unlock`. `
|
|
531
|
+
- The owner is dead: the lock is not a blocker. `evidence.staleLock` is `true`,
|
|
532
|
+
`evidence.lockOwner` holds the pid and start time, and the row's commands begin
|
|
533
|
+
with `git worktree unlock`. `clean` unlocks, removes and verifies it, and the
|
|
208
534
|
receipt records the `unlock-worktree` step.
|
|
209
535
|
- The owner is alive, or cannot be ruled out: `locked-worktree`.
|
|
210
536
|
- A lock writes `pid <N>` and, to guard against pid reuse, `start <time>`. The
|
|
@@ -220,7 +546,7 @@ reason, so a free-form reason such as `keep, build running` always blocks.
|
|
|
220
546
|
- The `pid` token is trusted as a local pid. A pid from another container or
|
|
221
547
|
host that shares the filesystem is looked up here, will not be found, and
|
|
222
548
|
reads as dead; do not sweep a repository whose locks were written elsewhere.
|
|
223
|
-
- Before the unlock and again before the remove, `
|
|
549
|
+
- Before the unlock and again before the remove, `clean` asks again, and reads
|
|
224
550
|
the process table afresh. If the owner is alive by then, or a process other
|
|
225
551
|
than an idle shell works in the directory, or the table cannot be read, the
|
|
226
552
|
step fails and the run stops. A row dropped at the re-sweep touches nothing;
|
|
@@ -233,252 +559,272 @@ reason, so a free-form reason such as `keep, build running` always blocks.
|
|
|
233
559
|
- Separately, any process whose working directory is inside the worktree blocks
|
|
234
560
|
it as `live-process`, lock or not. A shell (`sh`, `bash`, `zsh`, `fish`) is
|
|
235
561
|
idle only when it has no child processes; it is then listed in
|
|
236
|
-
`
|
|
562
|
+
`evidence.idleShells` and does not block. A shell with a child, or whose
|
|
237
563
|
children are unknown, counts as a live process.
|
|
238
564
|
|
|
239
565
|
Both questions go to a `ProcessProbe` (`alive(pid, startedAt?)` and
|
|
240
|
-
`holders(path, { fresh? })`), passed as `probe` in the options of `
|
|
241
|
-
`
|
|
566
|
+
`holders(path, { fresh? })`), passed as `probe` in the options of `sweep`,
|
|
567
|
+
`check` and `run`. The default, `defaultProcessProbe()`, runs
|
|
242
568
|
`ps -o lstart=`, `ps -A -o pid=,ppid=` and `lsof -d cwd` through
|
|
243
569
|
`@osdlc/command-seam`, and needs `ps` and `lsof` on `PATH`. It reads the
|
|
244
|
-
process table once per probe and reuses it for every worktree: `
|
|
245
|
-
|
|
570
|
+
process table once per probe and reuses it for every worktree: `sweep` makes
|
|
571
|
+
one probe per repository, and `run` uses one probe for its re-sweep and
|
|
246
572
|
reads the table afresh for each pre-unlock and pre-remove check. Where the
|
|
247
573
|
table cannot be read the worktree is held back as `process-probe-failed`. A
|
|
248
574
|
host or test supplies its own probe.
|
|
249
575
|
Nothing is ever killed.
|
|
250
576
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
577
|
+
When the provider's CLI is missing or signed out, or a lookup fails, the
|
|
578
|
+
sweep keeps going on what git alone can tell it: see
|
|
579
|
+
[When a lookup fails](#when-a-lookup-fails).
|
|
254
580
|
|
|
255
581
|
Every branch delete names the full sha the sweep judged. `update-ref -d` and
|
|
256
582
|
the pinned `--force-with-lease` push both refuse when the branch has moved
|
|
257
583
|
since, so a commit pushed while you read the plan survives it.
|
|
258
|
-
`localDeleteCommand` and `remoteDeleteCommand` build them for hosts that add
|
|
259
|
-
their own rows.
|
|
260
584
|
|
|
261
|
-
|
|
262
|
-
approves them by name:
|
|
585
|
+
## When a lookup fails
|
|
263
586
|
|
|
264
|
-
|
|
587
|
+
A sweep asks three things of the outside world: `git fetch` for the remote's
|
|
588
|
+
current state, `git ls-remote` for its branches, and `gh` or `glab` for pull
|
|
589
|
+
requests. Any of them can fail: the network is down, the repository was
|
|
590
|
+
renamed or deleted, the signed-in account cannot see it, a rate limit.
|
|
591
|
+
|
|
592
|
+
The rule is that a failed lookup costs only what it would have told you. The
|
|
593
|
+
rows that did not need it are judged as usual, the rows that did are judged
|
|
594
|
+
without it, and the repository carries a `note` saying what was missing. Only
|
|
595
|
+
a failed fetch costs the whole repository.
|
|
596
|
+
|
|
597
|
+
| What failed | What you get |
|
|
265
598
|
| --- | --- |
|
|
266
|
-
| `
|
|
267
|
-
| `
|
|
268
|
-
| `
|
|
599
|
+
| `gh pr list` / `glab api` (the PR listing) | Every row is judged on git evidence. Remote branches with no known PR need `--allow confirm-open-pr` to delete. One note on the repository. |
|
|
600
|
+
| `gh` / `glab` not installed or not signed in | The provider is never asked, so this is treated like a failed listing: every row is judged on git evidence, remote branches with no known PR need `--allow confirm-open-pr` to delete, and a note on the repository says why. Pass `--provider none` if the remote has no forge, so there is nothing to check. |
|
|
601
|
+
| `git ls-remote`, after a fetch that worked | Remote branches are read from the refs that fetch just wrote. A note on the repository and on each remote row. |
|
|
602
|
+
| `git ls-remote`, in `clean`'s re-check of an `--offline` plan | The run is refused before anything runs. The refs on disk are what the re-check is there to replace, and `clean` does not delete on a remote it cannot ask. |
|
|
603
|
+
| `git fetch` | The repository is an error with no rows. Every row would otherwise be judged against an older default branch than the one you asked for. |
|
|
269
604
|
|
|
270
|
-
|
|
271
|
-
worktree, the primary or caller's worktree) has `deletable: false`.
|
|
605
|
+
### Example: a repository `gh` cannot see
|
|
272
606
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
a branch only when the same run removes its worktree, and skips it otherwise.
|
|
277
|
-
With `--only branches` no worktree is swept, so the branch stays held.
|
|
607
|
+
The remote names a repository that was deleted, or that your account cannot
|
|
608
|
+
read. In 0.18 and earlier the repository came back as an error, and none of
|
|
609
|
+
it was judged:
|
|
278
610
|
|
|
279
|
-
|
|
611
|
+
```console
|
|
612
|
+
$ repo-cleaner --remote --offline
|
|
613
|
+
~/work/app (acme/app, default main, origin, not fetched)
|
|
614
|
+
error: GitHub has no such pull request, issue or repository, or you cannot see it.
|
|
280
615
|
|
|
281
|
-
|
|
282
|
-
repo-cleaner [status] # the report: what is merged, what can go (the default)
|
|
283
|
-
repo-cleaner summary # counts per repository: total, merged, delete, prune, review, keep
|
|
284
|
-
repo-cleaner plan # the deletable rows as a plan, to stdout or -o <file>
|
|
285
|
-
repo-cleaner apply # re-check, show the steps, ask, delete
|
|
286
|
-
repo-cleaner cache # list or remove the clones --remote-url keeps
|
|
616
|
+
0 to delete, 0 to review, across 1 repository (1 unreadable).
|
|
287
617
|
```
|
|
288
618
|
|
|
289
|
-
|
|
290
|
-
named like a command is swept as `./<name>`.
|
|
291
|
-
|
|
292
|
-
`apply` asks before deleting, and throws away anything typed while the sweep
|
|
293
|
-
ran, so a key pressed early cannot answer the question. It says so when it
|
|
294
|
-
did.
|
|
619
|
+
Now the merged branch is still found, and the note says what was missing:
|
|
295
620
|
|
|
296
621
|
```console
|
|
297
|
-
$ repo-cleaner
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
remote
|
|
303
|
-
|
|
304
|
-
|
|
622
|
+
$ repo-cleaner --remote --offline
|
|
623
|
+
~/work/app (acme/app, default main, origin, not fetched)
|
|
624
|
+
note: Listing PRs with `gh pr list` failed (GitHub has no such pull request, issue or repository, or you cannot see it.); judged on git evidence alone.
|
|
625
|
+
branch feat/login delete fully merged into origin/main
|
|
626
|
+
git update-ref -d refs/heads/feat/login 44d2db93…
|
|
627
|
+
remote origin/feat/login delete fully merged into origin/main; needs --allow confirm-open-pr
|
|
628
|
+
git push --force-with-lease=refs/heads/feat/login:44d2db93… origin :refs/heads/feat/login
|
|
629
|
+
|
|
630
|
+
2 to delete, 0 to review, across 1 repository.
|
|
305
631
|
```
|
|
306
632
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
`action: "prune"`, and each summary bucket has a `prune` count, a part of its
|
|
312
|
-
`safe-to-delete` count.
|
|
633
|
+
When the remote may not be on a forge at all, the note also suggests
|
|
634
|
+
`--provider none`: for a host `auto` could only guess was GitHub, or a local
|
|
635
|
+
path swept with `--provider github`. Under `auto`, a local path gets no
|
|
636
|
+
provider in the first place.
|
|
313
637
|
|
|
314
|
-
|
|
315
|
-
merged PR at the branch's head, or by a patch match. A branch checked out in a
|
|
316
|
-
worktree counts as merged by ancestry or PR alone. The default branch and the
|
|
317
|
-
primary worktree are counted in `total` but not judged. Each row's verdict is
|
|
318
|
-
in its `facts.merged`.
|
|
638
|
+
### Why the remote branch needs `confirm-open-pr`
|
|
319
639
|
|
|
320
|
-
|
|
640
|
+
Deleting the head branch of an open pull request closes that pull request.
|
|
641
|
+
When the listing works, `repo-cleaner` knows which branches have one. When it
|
|
642
|
+
failed, or could not run because `gh` is missing or signed out, it cannot
|
|
643
|
+
tell, so it does not delete a remote branch on its own say:
|
|
321
644
|
|
|
322
|
-
```
|
|
323
|
-
repo-cleaner
|
|
324
|
-
repo-cleaner
|
|
325
|
-
|
|
645
|
+
```console
|
|
646
|
+
$ repo-cleaner clean --remote --offline --yes
|
|
647
|
+
repo-cleaner clean: ~/work/app: Listing PRs with `gh pr list` failed (GitHub has no such pull request, …); judged on git evidence alone.
|
|
648
|
+
skipped remote:origin/feat/login — not-allowed (needs confirm-open-pr)
|
|
649
|
+
ok branch:feat/login — git update-ref -d refs/heads/feat/login 44d2db93… (ref does not resolve)
|
|
650
|
+
|
|
651
|
+
deleted: 1 local branch
|
|
652
|
+
skipped 1, not deleted: 1 not-allowed (--allow confirm-open-pr)
|
|
653
|
+
status: complete
|
|
326
654
|
```
|
|
327
655
|
|
|
328
|
-
|
|
329
|
-
rather than from how it is written: a path that is itself a checkout names
|
|
330
|
-
that repository, and a path that is not is searched for the checkouts under
|
|
331
|
-
it, descending through directories that are not checkouts — an owner
|
|
332
|
-
directory, a folder per client — and stopping at the ones that are.
|
|
656
|
+
The local branch went; the remote branch stayed. To go on, pick one:
|
|
333
657
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
658
|
+
- Fix the lookup (install `gh`, sign in, check the remote URL) and run again.
|
|
659
|
+
- Pass `--provider none` if this remote has no forge at all, so there is no
|
|
660
|
+
pull request to lose.
|
|
661
|
+
- Check for open pull requests yourself, then pass `--allow confirm-open-pr`.
|
|
338
662
|
|
|
339
|
-
The
|
|
340
|
-
|
|
341
|
-
entry pointing at the wrong directory gets caught. A search that passes
|
|
342
|
-
10,000 directories says so on stderr while it is still running, and the walk
|
|
343
|
-
stops descending three levels below the entry.
|
|
663
|
+
The same happens when `gh` is not installed or not signed in. Nothing was
|
|
664
|
+
asked, so nothing can rule out an open pull request:
|
|
344
665
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
666
|
+
```console
|
|
667
|
+
$ repo-cleaner --remote --offline
|
|
668
|
+
~/work/app (acme/app, default main, origin, not fetched)
|
|
669
|
+
note: Could not check for open PRs: `gh` is not installed, so deleting a remote branch needs confirm-open-pr. Install `gh`, or pass `--allow confirm-open-pr` once you have checked.
|
|
670
|
+
branch feat/login delete fully merged into origin/main
|
|
671
|
+
git update-ref -d refs/heads/feat/login 255acd56…
|
|
672
|
+
remote origin/feat/login delete fully merged into origin/main; needs --allow confirm-open-pr
|
|
673
|
+
git push --force-with-lease=refs/heads/feat/login:255acd56… origin :refs/heads/feat/login
|
|
674
|
+
|
|
675
|
+
2 to delete, 0 to review, across 1 repository.
|
|
676
|
+
```
|
|
348
677
|
|
|
349
|
-
|
|
678
|
+
`--provider none` is how you say the remote has no forge: then there is no
|
|
679
|
+
lookup, no note and no gate.
|
|
350
680
|
|
|
351
|
-
|
|
352
|
-
repo-cleaner --remote-url git@github.com:acme/api.git
|
|
353
|
-
repo-cleaner apply --remote-url git@github.com:acme/api.git
|
|
354
|
-
```
|
|
681
|
+
### `--offline` reads what is already on disk
|
|
355
682
|
|
|
356
|
-
`--
|
|
357
|
-
|
|
358
|
-
`
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
are swept: the clone's own branches and worktree are not yours. Deletes are
|
|
362
|
-
pushed to the URL from the clone, compare-and-swap as always.
|
|
363
|
-
|
|
364
|
-
The clone is kept, not deleted after the run, so a plan written with
|
|
365
|
-
`--remote-url` can be applied later from anywhere: the plan names the clone's
|
|
366
|
-
directory. `repo-cleaner cache` lists the clones with their URL and size;
|
|
367
|
-
`repo-cleaner cache remove <url>` drops one, and `cache remove --all` drops
|
|
368
|
-
every one, a clone left by a stopped run included. A plan whose clone was
|
|
369
|
-
removed cannot be applied; sweep again. The clone is a full one, not
|
|
370
|
-
`--filter=blob:none`: the patch check reads file contents, and a partial
|
|
371
|
-
clone fetches them one branch at a time.
|
|
683
|
+
`--offline` does not fetch, and reads remote branches from the
|
|
684
|
+
remote-tracking refs (`refs/remotes/<remote>/`) instead of asking the remote
|
|
685
|
+
with `git ls-remote`. So judging works with no network and with a remote that
|
|
686
|
+
no longer exists. In 0.18 and earlier it still called `git ls-remote`, and a
|
|
687
|
+
remote that could not be read failed the whole repository:
|
|
372
688
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
its credentials, query and fragment removed.
|
|
689
|
+
```console
|
|
690
|
+
$ repo-cleaner --remote --offline --provider none # 0.18 and earlier
|
|
691
|
+
~/work/api (default main, origin, not fetched)
|
|
692
|
+
error: git ls-remote: The remote '/srv/git/api.git' does not exist or is not a git repository. …
|
|
693
|
+
```
|
|
379
694
|
|
|
380
|
-
|
|
695
|
+
```console
|
|
696
|
+
$ repo-cleaner --remote --offline --provider none # now
|
|
697
|
+
~/work/api (default main, origin, not fetched)
|
|
698
|
+
branch feat/login delete fully merged into origin/main
|
|
699
|
+
git update-ref -d refs/heads/feat/login 255acd56…
|
|
700
|
+
remote origin/feat/login delete fully merged into origin/main
|
|
701
|
+
git push --force-with-lease=refs/heads/feat/login:255acd56… origin :refs/heads/feat/login
|
|
702
|
+
|
|
703
|
+
2 to delete, 0 to review, across 1 repository.
|
|
704
|
+
```
|
|
381
705
|
|
|
382
|
-
|
|
706
|
+
`--offline` still asks `gh` or `glab` about pull requests. Add
|
|
707
|
+
`--provider none` to judge with no network at all.
|
|
383
708
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
| `--provider <name>` | With `--remotes`: where pull requests come from: `auto` (default, by the remote's host), `github`, `gitlab`, or `none` to classify on git evidence alone |
|
|
392
|
-
| `--no-fetch` | With `--remotes`: skip the fetch; the header then says `not fetched`. Without `--remotes`, nothing is fetched |
|
|
709
|
+
The refs on disk can be stale. A remote-tracking ref stays until the next
|
|
710
|
+
`git fetch --prune`, so a branch the forge deleted after its pull request
|
|
711
|
+
merged still shows in the report, and a branch someone pushed to since shows
|
|
712
|
+
its old tip. That is fine for a report, but `clean` deletes on the remote, so
|
|
713
|
+
before it does, its re-check asks the remote itself with `git ls-remote`. A
|
|
714
|
+
branch the remote no longer has is skipped as gone, and one whose tip moved
|
|
715
|
+
is skipped too:
|
|
393
716
|
|
|
394
|
-
|
|
717
|
+
```console
|
|
718
|
+
$ repo-cleaner clean --remote --offline --provider none --only remote --yes
|
|
719
|
+
skipped remote:origin/feat/search — gone (no longer in the sweep)
|
|
720
|
+
ok remote:origin/feat/cart — git push … origin :refs/heads/feat/cart (ls-remote lists nothing)
|
|
395
721
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
| `--output json` | The full inventory, facts included |
|
|
722
|
+
deleted: 1 remote branch
|
|
723
|
+
skipped 1, not deleted: 1 gone
|
|
724
|
+
status: complete
|
|
725
|
+
```
|
|
401
726
|
|
|
402
|
-
`--
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
727
|
+
The preview does the same, so `--dry-run` shows what the run will do. If the
|
|
728
|
+
re-check's `git ls-remote` fails, nothing runs, `--dry-run` included.
|
|
729
|
+
A remote branch you `--spare` (see [Pick and spare](#pick-and-spare)) that is
|
|
730
|
+
gone from the remote refuses the run instead, because you spared it for what
|
|
731
|
+
it held. `git fetch --prune` drops the stale tracking refs, so the report
|
|
732
|
+
stops showing them.
|
|
733
|
+
|
|
734
|
+
### In the library
|
|
735
|
+
|
|
736
|
+
The same information is on the result, for a host to show or act on:
|
|
737
|
+
|
|
738
|
+
- `RepoSweep.notes`: one sentence for each lookup the repository was judged
|
|
739
|
+
without. Empty when everything answered.
|
|
740
|
+
- `RepoInfo.provider.listError`: the provider's own reason, when its listing
|
|
741
|
+
failed.
|
|
742
|
+
- `Preview.notes` (`check`) and `RunOptions.onNotes` (`run`): the same notes
|
|
743
|
+
from their re-checks. A re-check that sweeps again can meet a failure the
|
|
744
|
+
first sweep did not. `check` given the sweep (`CheckOptions.sweep`) reports
|
|
745
|
+
that sweep's notes, plus those of each repository it sweeps again because
|
|
746
|
+
the sweep read its remote branches from disk.
|
|
747
|
+
- `Step.gone` on a receipt step, and `RunSummary.gone`: a remote branch
|
|
748
|
+
someone deleted between the re-check and the run's push, counted apart from
|
|
749
|
+
what the run deleted.
|
|
406
750
|
|
|
407
|
-
|
|
751
|
+
```ts
|
|
752
|
+
import { sweep } from '@osdlc/repo-cleaner'
|
|
753
|
+
|
|
754
|
+
const result = sweep(['.'], { remote: true, offline: true, provider: 'github' })
|
|
755
|
+
for (const repo of result.repos) {
|
|
756
|
+
if (repo.error !== null) {
|
|
757
|
+
console.log(`${repo.path}: could not be swept: ${repo.error}`)
|
|
758
|
+
continue
|
|
759
|
+
}
|
|
760
|
+
for (const note of repo.notes) console.log(`note: ${note}`)
|
|
761
|
+
console.log(`listError: ${repo.repo?.provider?.listError ?? '(none)'}`)
|
|
762
|
+
for (const row of repo.rows) console.log(row.id, row.verdict, row.risks.join(', ') || '-')
|
|
763
|
+
}
|
|
764
|
+
```
|
|
408
765
|
|
|
409
|
-
```
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
766
|
+
```text
|
|
767
|
+
note: Listing PRs with `gh pr list` failed (GitHub has no such pull request, …); judged on git evidence alone.
|
|
768
|
+
listError: GitHub has no such pull request, issue or repository, or you cannot see it.
|
|
769
|
+
worktree:/Users/me/work/app keep -
|
|
770
|
+
branch:feat/login delete -
|
|
771
|
+
branch:main keep -
|
|
772
|
+
remote:origin/feat/login delete confirm-open-pr
|
|
773
|
+
remote:origin/main keep -
|
|
415
774
|
```
|
|
416
775
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
`safe-to-delete` and drops each row to review as `needs-review`. Name a row
|
|
421
|
-
with `--row <category>:<id>` to apply it, review rows included.
|
|
776
|
+
The local branch needs no grant: deleting it closes no pull request. The
|
|
777
|
+
remote branch carries `confirm-open-pr`, so `check` drops it unless that risk
|
|
778
|
+
is allowed.
|
|
422
779
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
(one row). It then shows, per repository, each row it will delete or prune
|
|
427
|
-
with the command under it, each row it will skip and why, and the counts, and
|
|
428
|
-
asks `Delete N items? [y/N]`:
|
|
780
|
+
A `PrProvider` you write reports a failure by returning `null` or by
|
|
781
|
+
throwing; either becomes `listError` and a note. Return an empty list only
|
|
782
|
+
when the forge answered and there are no pull requests.
|
|
429
783
|
|
|
430
|
-
|
|
431
|
-
/
|
|
432
|
-
delete local_branch fix/login-redirect
|
|
433
|
-
git update-ref -d refs/heads/fix/login-redirect 3f9c2e1d…
|
|
434
|
-
prune worktree records
|
|
435
|
-
git -C /Users/me/work/api worktree prune
|
|
436
|
-
skip worktree /Users/me/work/api/wt/spike
|
|
437
|
-
needs-review: clean but no clear disposability signal; name it with --row to apply it
|
|
784
|
+
The decision behind this is
|
|
785
|
+
[D-UVUH](docs/planning/decisions/D-UVUH-failed-lookups.md).
|
|
438
786
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
787
|
+
## Plan and clean
|
|
788
|
+
|
|
789
|
+
```bash
|
|
790
|
+
repo-cleaner clean # sweep, show the steps, ask, delete
|
|
791
|
+
repo-cleaner clean --only worktree --yes # no question
|
|
792
|
+
repo-cleaner plan -o plan.json # save the plan to review or edit
|
|
793
|
+
repo-cleaner clean --plan plan.json --pick branch:feat/x
|
|
794
|
+
repo-cleaner plan | repo-cleaner clean --plan - --yes
|
|
442
795
|
```
|
|
443
796
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
stops.
|
|
464
|
-
|
|
465
|
-
On yes it runs only the steps it showed: worktrees first, then local branches,
|
|
466
|
-
remote branches and a prune, in that order. Branch deletes are
|
|
467
|
-
compare-and-swap, every step is verified, and the first failure stops the run.
|
|
468
|
-
|
|
469
|
-
Every run ends with what it did:
|
|
797
|
+
A plan holds every row with a fingerprint of the evidence behind its verdict,
|
|
798
|
+
and the sweep options it was made with. Without `--pick` only the rows that
|
|
799
|
+
could be deleted run; the blocked and structural rows are there so a pick of
|
|
800
|
+
one is skipped with its reason. `clean`
|
|
801
|
+
either sweeps and plans in memory, or reads a plan with `--plan`. A plan of an
|
|
802
|
+
older version is refused, not translated: sweep again.
|
|
803
|
+
|
|
804
|
+
Before it deletes anything, `clean` sweeps again, with the plan's options, and
|
|
805
|
+
drops a row that is gone (`gone`), that can no longer be deleted (`blocked`,
|
|
806
|
+
`keep`, or `held` for a branch whose worktree this run does not remove), that
|
|
807
|
+
was not picked and is no longer `delete` (`needs-pick`), whose evidence moved
|
|
808
|
+
(`changed`), or that carries a risk you did not `--allow` (`not-allowed`). The
|
|
809
|
+
re-check reads only the kinds the selected rows are in, so running a plan of
|
|
810
|
+
local rows does not compare every remote branch again, and it fetches when the
|
|
811
|
+
plan's sweep did, so it judges against the same, current default branch.
|
|
812
|
+
|
|
813
|
+
On yes it runs only the steps it showed: spared remote tips first, then
|
|
814
|
+
worktrees, local branches and remote branches, in that order. Branch
|
|
815
|
+
deletes are compare-and-swap, every step is verified, and the first failure
|
|
816
|
+
stops the run. Every run ends with what it did:
|
|
470
817
|
|
|
471
818
|
```text
|
|
472
819
|
deleted: 1 worktree, 3 local branches, 2 remote branches, 1 stale worktree pruned
|
|
473
|
-
skipped 2, not deleted: 1 needs-
|
|
820
|
+
skipped 2, not deleted: 1 needs-pick (name with --pick), 1 duplicate
|
|
474
821
|
status: complete
|
|
475
822
|
```
|
|
476
823
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
compared by the push URL git uses, with `url.*.insteadOf` applied.
|
|
824
|
+
Two clones of one remote list the same remote branches; `clean` deletes each
|
|
825
|
+
once and drops the other clone's row as `duplicate`, or as `spared` when the
|
|
826
|
+
branch is spared through the other clone. Remotes are compared by the push URL
|
|
827
|
+
git uses, with `url.*.insteadOf` applied.
|
|
482
828
|
|
|
483
829
|
A receipt records every step and is rewritten after each one. With
|
|
484
830
|
`--plan plan.json` it goes beside the plan, as
|
|
@@ -490,56 +836,89 @@ that swept, or read its plan from stdin, writes a receipt only when
|
|
|
490
836
|
|
|
491
837
|
| Code | Meaning |
|
|
492
838
|
| --- | --- |
|
|
493
|
-
| `0` | Swept; or `
|
|
494
|
-
| `1` | Failed against the state it found: an unreadable plan, a row not in
|
|
495
|
-
| `2` | Cannot run as typed: an unknown flag, a malformed
|
|
496
|
-
| `10` | Nothing to sweep: no
|
|
497
|
-
| `11` | `
|
|
839
|
+
| `0` | Swept; or `clean` ran, was declined, or had nothing to do |
|
|
840
|
+
| `1` | Failed against the state it found: an unreadable plan, a row not in it, a spared branch that moved |
|
|
841
|
+
| `2` | Cannot run as typed: an unknown flag, a malformed row, a refused combination, `clean` with nobody to ask and no `--yes` |
|
|
842
|
+
| `10` | Nothing to sweep: no target named a repository |
|
|
843
|
+
| `11` | `clean` stopped at a failed step; the receipt says which |
|
|
498
844
|
|
|
499
845
|
## As a library
|
|
500
846
|
|
|
501
847
|
```ts
|
|
502
|
-
import {
|
|
848
|
+
import { check, plan, render, run, sweep } from '@osdlc/repo-cleaner'
|
|
503
849
|
|
|
504
|
-
const
|
|
505
|
-
for (const
|
|
506
|
-
console.log(
|
|
850
|
+
const result = sweep(['/Users/me/work/api'], { remote: true })
|
|
851
|
+
for (const row of result.repos[0]?.rows ?? []) {
|
|
852
|
+
console.log(row.id, row.verdict, row.why, row.risks)
|
|
507
853
|
}
|
|
508
854
|
|
|
509
|
-
const
|
|
855
|
+
const preview = check(plan(result), {
|
|
856
|
+
pick: ['branch:spike/cache'],
|
|
857
|
+
allow: [{ risk: 'abandon-unmerged', row: 'branch:spike/cache' }],
|
|
858
|
+
})
|
|
859
|
+
process.stdout.write(render.preview(preview))
|
|
860
|
+
const receipt = run(preview)
|
|
510
861
|
```
|
|
511
862
|
|
|
512
|
-
|
|
513
|
-
|
|
863
|
+
| Export | What it does |
|
|
864
|
+
| --- | --- |
|
|
865
|
+
| `sweep(targets, opts?)` | Resolve the targets and judge every row. Options as on the command line: `only`, `remote`, `offline`, `against`, `provider`, `branch`, plus `cwd`, `cacheDir`, `annotate`, `onProgress`, `onWarn`, and the injectable `git`, `gh`, `glab` runners and `probe` |
|
|
866
|
+
| `plan(sweep)` | Every row, fingerprinted, with the sweep's options |
|
|
867
|
+
| `check(plan, approval, opts?)` | Resolve `{ pick, spare, allow }` against the plan, sweep again, and return a `Preview`: the steps, the rows dropped and why, the allows no row needed, and `notes`, what the re-check judged without. Deletes nothing |
|
|
868
|
+
| `run(preview, opts?)` | Sweep once more and take the preview's steps, writing a `Receipt` (to `receiptPath` after every step, when given). `onNotes` hears what its own re-check judged without |
|
|
869
|
+
| `inspectWorktree(path, opts?)` | One worktree judged as a sweep judges it, submodules included |
|
|
870
|
+
| `readPlan`, `writePlan` | Plans on disk |
|
|
871
|
+
| `render` | The command line's formats: `text`, `json`, `summary`, `commands`, `preview`, `receipt` |
|
|
872
|
+
| `defaultProcessProbe`, `providers` | The defaults a host can wrap or replace |
|
|
873
|
+
| `RepoCleanerError` | Every refusal, with a `code`: `conflict`, `bad-input`, `not-found`, `ambiguous`, `spared-changed`, `bad-plan` |
|
|
874
|
+
|
|
875
|
+
A row is a `WorktreeRow`, `BranchRow` or `RemoteRow`, told apart by `kind`:
|
|
876
|
+
|
|
877
|
+
```ts
|
|
878
|
+
interface Row {
|
|
879
|
+
kind: 'worktree' | 'branch' | 'remote'
|
|
880
|
+
id: RowId // 'worktree:/p', 'branch:feat/x', 'remote:origin/feat/x'
|
|
881
|
+
name: string // the path, the branch, `<remote>/<branch>`
|
|
882
|
+
verdict: 'delete' | 'review' | 'keep' | 'blocked'
|
|
883
|
+
action: 'remove' | 'prune' | 'delete' | null // null: never deleted
|
|
884
|
+
why: string
|
|
885
|
+
risks: Risk[] // gathered over the row's submodules
|
|
886
|
+
blockers: Blocker[] // non-empty exactly when verdict is 'blocked'
|
|
887
|
+
commands: string[]
|
|
888
|
+
notes: string[] // what is unclear, or what to do about a blocker
|
|
889
|
+
evidence: WorktreeEvidence | BranchEvidence | RemoteEvidence
|
|
890
|
+
}
|
|
891
|
+
```
|
|
514
892
|
|
|
515
|
-
`
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
`
|
|
519
|
-
|
|
893
|
+
`evidence` is typed for each kind: a worktree's `path`, `branch`, `head`,
|
|
894
|
+
`dirtyFiles`, `unpushedCommits`, lock and process evidence, and `submodules`;
|
|
895
|
+
a branch's `sha`, `upstream`, `track`; every kind's `merged`, `pr`,
|
|
896
|
+
`uniqueCommits` and landing evidence.
|
|
897
|
+
|
|
898
|
+
`sweep` never throws for a repository it cannot read: that one comes back
|
|
899
|
+
with `error` set, because a fan-out must not end on its first bad entry. A
|
|
900
|
+
lookup that only adds evidence (a pull request listing, or `ls-remote` after
|
|
901
|
+
the fetch) costs that evidence and not the repository: its rows are judged
|
|
902
|
+
without it, and the repository's `notes` say what was missing. It
|
|
903
|
+
throws a `RepoCleanerError` for options that contradict each other, as the
|
|
904
|
+
command line refuses them, before reading anything. `check` throws for an
|
|
905
|
+
approval that contradicts itself, names a row the plan does not have, or
|
|
906
|
+
spares a remote branch that moved; nothing has been touched when it does.
|
|
520
907
|
|
|
521
908
|
### Adding knowledge the library does not have
|
|
522
909
|
|
|
523
|
-
Every row carries the
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
910
|
+
Every row carries the evidence behind its verdict, and `notes` is a list of
|
|
911
|
+
sentences rather than a finished string. That is deliberate: a host that knows
|
|
912
|
+
something git does not — an issue tracker, a task corpus — passes `annotate`
|
|
913
|
+
to `sweep`, `check` and `run`, and re-ranks the rows or appends its own
|
|
914
|
+
sentence, instead of reaching into classification. Because the same hook runs
|
|
915
|
+
in every re-check, the rows a run checks are the rows the host planned from.
|
|
529
916
|
|
|
530
917
|
This works because of how the ladder is ordered. Every verdict the sweep
|
|
531
918
|
reaches on its own evidence outranks anything a host could contribute, so a
|
|
532
|
-
host rarely has to veto one. It mostly sharpens the rows left at
|
|
533
|
-
`
|
|
534
|
-
|
|
535
|
-
A host that does hold back or demote a worktree row must also keep that
|
|
536
|
-
worktree's branch, which the sweep freed because the worktree was going. Pass
|
|
537
|
-
the annotated rows through `holdFreedBranches(items, { branchScoped })`, with
|
|
538
|
-
`branchScoped` set for a `branch` sweep. `applyPlan` does the same after its
|
|
539
|
-
`annotate` hook, so the plan and the run agree.
|
|
540
|
-
|
|
541
|
-
An issue tracker's linkage, applied this way, is a host's own layer over the
|
|
542
|
-
sweep and `applyPlan`.
|
|
919
|
+
host rarely has to veto one. It mostly sharpens the rows left at `keep` or
|
|
920
|
+
`review`. A worktree the host holds back keeps its branch: the sweep applies
|
|
921
|
+
that rule after the hook.
|
|
543
922
|
|
|
544
923
|
## Providers (prototype)
|
|
545
924
|
|
|
@@ -552,11 +931,12 @@ branch.
|
|
|
552
931
|
| `github` | `gh` | `gh pr list --state all`, up to 300 |
|
|
553
932
|
| `gitlab` | `glab` | `glab api projects/<path>/merge_requests?state=all`, 100 a page, up to 300 |
|
|
554
933
|
|
|
555
|
-
`--provider auto` (the default) reads the remote's host: a
|
|
556
|
-
|
|
557
|
-
|
|
934
|
+
With `--remote`, `--provider auto` (the default) reads the remote's host: a
|
|
935
|
+
remote that names no host (a local path or `file://`) gets no provider, since
|
|
936
|
+
it can have no pull requests; a host with `gitlab` in its name is GitLab; any
|
|
937
|
+
other is GitHub, which is what the tool did before providers existed. Name one with `--provider github|gitlab` for a host the
|
|
558
938
|
name does not give away, or `--provider none` to skip pull requests. A plan
|
|
559
|
-
records the choice, so `
|
|
939
|
+
records the choice, so `clean` re-checks with the same provider.
|
|
560
940
|
|
|
561
941
|
GitLab maps onto the same evidence: `opened` and `locked` are `OPEN`, `merged`
|
|
562
942
|
is `MERGED`, `closed` is `CLOSED`, `sha` is the head commit and `target_branch`
|
|
@@ -570,20 +950,26 @@ Not done yet:
|
|
|
570
950
|
- Gitea, Forgejo and Bitbucket.
|
|
571
951
|
- Mapping a self-hosted host to a provider in config; today it is the host
|
|
572
952
|
name or `--provider`.
|
|
573
|
-
- Auth edge cases
|
|
574
|
-
|
|
953
|
+
- Auth edge cases. A token without API scope, or a rate limit, is a failed
|
|
954
|
+
listing: the rows are judged on git evidence with a note (see
|
|
955
|
+
[When a lookup fails](#when-a-lookup-fails)). `glab` signed in only to a
|
|
956
|
+
different host than the remote's reads as not signed in for that host,
|
|
957
|
+
which gates remote deletes the same way.
|
|
575
958
|
- Squash metadata. GitLab reports `squash_commit_sha`; it is not used, so a
|
|
576
959
|
squashed MR proves landing only by its head commit, as on GitHub.
|
|
577
960
|
- GitLab under a sub-path (`https://host/gitlab/group/proj`). The URL does not
|
|
578
961
|
say where the install ends and the project begins, so the whole path is read
|
|
579
962
|
as the project, `glab` is asked for a project that does not exist, and the
|
|
580
|
-
listing fails.
|
|
581
|
-
|
|
963
|
+
listing fails. The rows are judged on git evidence with a note, and a remote
|
|
964
|
+
branch needs `--allow confirm-open-pr` to delete.
|
|
582
965
|
|
|
583
966
|
## Runtime dependencies
|
|
584
967
|
|
|
585
|
-
`git` on `PATH`, and `gh` (or `glab` for GitLab)
|
|
586
|
-
|
|
968
|
+
`git` on `PATH`, and `gh` (or `glab` for GitLab) for pull request evidence.
|
|
969
|
+
Without it, deleting a remote branch needs `--allow confirm-open-pr`, or
|
|
970
|
+
`--provider none` for a remote with no forge (see
|
|
971
|
+
[When a lookup fails](#when-a-lookup-fails)). Authentication is the CLI's
|
|
972
|
+
own — this package reads no token.
|
|
587
973
|
|
|
588
974
|
## Running from a checkout
|
|
589
975
|
|