@osdlc/repo-cleaner 0.17.2 → 0.18.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 +18 -0
- package/README.md +497 -329
- package/dist/branches.d.ts +9 -36
- package/dist/branches.js +92 -172
- package/dist/clean.d.ts +162 -0
- package/dist/clean.js +755 -0
- package/dist/cli.d.ts +12 -16
- package/dist/cli.js +340 -479
- 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/receipt.d.ts +44 -57
- package/dist/receipt.js +46 -28
- package/dist/reference.d.ts +12 -0
- package/dist/reference.js +148 -0
- package/dist/remote-mirror.d.ts +8 -0
- package/dist/remote-mirror.js +18 -0
- package/dist/render.d.ts +61 -61
- package/dist/render.js +195 -254
- package/dist/resolve.js +1 -9
- 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 +97 -67
- package/dist/sweep.js +272 -118
- package/dist/types.d.ts +276 -105
- package/dist/types.js +100 -54
- package/dist/util.d.ts +7 -0
- package/dist/util.js +20 -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 +4 -4
- 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,279 @@ 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 sweeps the checkout you are in. A bare invocation never fans out.
|
|
117
|
+
What a target names is read off the disk rather than from how it is written:
|
|
118
|
+
a path that is itself a checkout names that repository, and a path that is
|
|
119
|
+
not is searched for the checkouts under it, descending through directories
|
|
120
|
+
that are not checkouts and stopping at the ones that are. A directory holding
|
|
121
|
+
a checkout and three of its worktrees sweeps one repository, not four; the
|
|
122
|
+
worktrees appear as rows of that repository.
|
|
123
|
+
|
|
124
|
+
The report ends with what the search cost — `searched 1 target: 214
|
|
125
|
+
directories read, 12 checkouts found.` — because those numbers are how a
|
|
126
|
+
target pointing at the wrong directory gets caught. A search that passes
|
|
127
|
+
10,000 directories says so on stderr while it is still running. A target that
|
|
128
|
+
resolves to nothing is reported rather than dropped.
|
|
129
|
+
|
|
130
|
+
### Sweep options
|
|
131
|
+
|
|
132
|
+
The report, `plan` and `clean` take these:
|
|
133
|
+
|
|
134
|
+
| Option | Meaning |
|
|
135
|
+
| --- | --- |
|
|
136
|
+
| `--remote` | Also judge remote branches. Turns on the network: fetches each remote first and asks GitHub or GitLab about pull requests. Slower. |
|
|
137
|
+
| `--offline` | With `--remote`: do not fetch; judge against the remote-tracking refs already on disk. |
|
|
138
|
+
| `--only <kinds>` | Only these kinds of row: `worktree`, `branch`, `remote`, comma-separated. Default: `worktree,branch`, plus `remote` with `--remote`. Naming `remote` implies `--remote`. |
|
|
139
|
+
| `--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. |
|
|
140
|
+
| `--against <remote>` | The remote whose default branch counts as "merged into". Default: `origin`, then the first remote. |
|
|
141
|
+
| `--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. |
|
|
142
|
+
| `--no-progress` | No progress line on stderr. |
|
|
143
|
+
|
|
144
|
+
### The report
|
|
145
|
+
|
|
146
|
+
`repo-cleaner [target...]` lists each row with its verdict, why, the risks it
|
|
147
|
+
carries, and the command that would delete it.
|
|
148
|
+
|
|
149
|
+
| Option | Meaning |
|
|
150
|
+
| --- | --- |
|
|
151
|
+
| `--all` | Also show `keep` rows. By default only `delete`, `review` and `blocked` rows are listed. Only with `--format text`. |
|
|
152
|
+
| `--format text` | The report. The default. |
|
|
153
|
+
| `--format summary` | Counts per repository: total, merged, delete, prune, review, blocked, keep. |
|
|
154
|
+
| `--format commands` | Only the commands of `delete` rows that carry no risk, ready to copy. |
|
|
155
|
+
| `--format json` | Everything, evidence included, for scripts. |
|
|
156
|
+
|
|
157
|
+
```console
|
|
158
|
+
$ repo-cleaner --format summary --remote
|
|
159
|
+
/Users/me/work/api (acme/api, default main, origin)
|
|
160
|
+
total merged delete prune review blocked keep
|
|
161
|
+
local branches 12 8 6 0 1 0 5
|
|
162
|
+
worktrees 4 3 1 1 0 1 1
|
|
163
|
+
remote branches 40 31 30 0 6 0 4
|
|
164
|
+
|
|
165
|
+
37 to delete, 1 to prune, 7 to review, 1 blocked, across 1 repository.
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`prune` counts worktree rows whose directory is gone, or is no longer a
|
|
169
|
+
checkout: deleting one drops the stale record, with `git worktree remove
|
|
170
|
+
<path>` for a directory that is gone (that worktree's record alone) and the
|
|
171
|
+
repository-wide `git worktree prune` for one left without its `.git`, and a
|
|
172
|
+
directory that is still there stays. `merged` counts rows whose work is on
|
|
173
|
+
the default branch, by ancestry, by a merged PR at the branch's head, or by a
|
|
174
|
+
patch match; the default branch and the primary worktree are counted in
|
|
175
|
+
`total` but not judged.
|
|
176
|
+
|
|
177
|
+
### `repo-cleaner plan`
|
|
178
|
+
|
|
179
|
+
Sweeps and saves every row, each with a fingerprint of the evidence behind its
|
|
180
|
+
verdict, so it can be reviewed now and run later with
|
|
181
|
+
`clean --plan`. `-o, --out <file>` sets where it goes; the default, `-`, is
|
|
182
|
+
stdout, and the totals line goes to stderr.
|
|
183
|
+
|
|
184
|
+
### `repo-cleaner clean`
|
|
185
|
+
|
|
186
|
+
Sweeps (or reads `--plan`), checks every row again, shows the steps, asks,
|
|
187
|
+
then deletes, compare-and-swap, verifying each step and stopping at the first
|
|
188
|
+
failure. Without `--pick` it runs every row judged `delete`.
|
|
189
|
+
|
|
190
|
+
| Option | Meaning |
|
|
191
|
+
| --- | --- |
|
|
192
|
+
| `--pick <row>` | Work on exactly these rows. See [Pick and spare](#pick-and-spare). Repeatable. |
|
|
193
|
+
| `--spare <row>` | Leave this row alone, and make sure it is what you saw. Repeatable. |
|
|
194
|
+
| `--allow <risk>` | Accept this risk for every row that carries it. |
|
|
195
|
+
| `--allow <risk>=<row>` | Accept it for that one row only. |
|
|
196
|
+
| `--plan <file\|->` | Run a saved plan instead of sweeping now (`-` reads stdin). Rows whose evidence changed since the plan are dropped, not deleted. |
|
|
197
|
+
| `-y, --yes` | Do not ask. Without it, and with no terminal to ask on, `clean` refuses. |
|
|
198
|
+
| `--dry-run` | Check and show the steps, then stop. |
|
|
199
|
+
| `--receipt <file>` | Write a record of every step, rewritten after each one. Default: next to a `--plan` file; none otherwise. |
|
|
200
|
+
| `--format text\|json` | How the steps and the outcome are shown. |
|
|
201
|
+
|
|
202
|
+
```text
|
|
203
|
+
/Users/me/work/api
|
|
204
|
+
delete branch:fix/login-redirect
|
|
205
|
+
git update-ref -d refs/heads/fix/login-redirect 3f9c2e1d…
|
|
206
|
+
prune worktree:/Users/me/work/wt/old
|
|
207
|
+
git -C /Users/me/work/api worktree remove /Users/me/work/wt/old
|
|
208
|
+
skip worktree:/Users/me/work/wt/spike
|
|
209
|
+
needs-pick: review: clean but no clear disposability signal; pick it to run it
|
|
210
|
+
|
|
211
|
+
would delete: 1 local branch, 1 stale worktree pruned
|
|
212
|
+
skipped 1, not deleted: 1 needs-pick (name with --pick)
|
|
213
|
+
Delete 2 items? [y/N]
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
`clean` throws away anything typed while the sweep ran, so a key pressed
|
|
217
|
+
early cannot answer the question, and says so when it did.
|
|
218
|
+
|
|
219
|
+
### Pick and spare
|
|
220
|
+
|
|
221
|
+
**`--pick <row>`** means "work on exactly these rows". It does two things:
|
|
222
|
+
|
|
223
|
+
1. **It narrows the run.** Only the picked rows are considered; everything
|
|
224
|
+
else is left alone.
|
|
225
|
+
2. **It opts in rows that never run otherwise.** Without `--pick`, `clean`
|
|
226
|
+
deletes only rows judged `delete`. A `review` row, or a `keep` row with an
|
|
227
|
+
action (a branch with an open pull request), is deleted only when it is
|
|
228
|
+
named, so a borderline row never goes as part of a batch.
|
|
229
|
+
|
|
230
|
+
Picking does not approve anything: a picked row that carries a risk still
|
|
231
|
+
needs `--allow` for it. A `blocked` row, or a `keep` row with no action, is
|
|
232
|
+
skipped and shown with the reason.
|
|
233
|
+
|
|
234
|
+
**`--spare <row>`** means "leave this row alone, and make sure it is what I
|
|
235
|
+
saw". The usual case: delete a merged worktree and its local branch, but keep
|
|
236
|
+
the branch on the remote as a backup.
|
|
237
|
+
|
|
238
|
+
- A spared row is not deleted, even if it is judged `delete`.
|
|
239
|
+
- A spared remote branch is still checked. If it is gone or its tip moved
|
|
240
|
+
since the plan, the run stops before deleting anything: you spared it for
|
|
241
|
+
what it held, so if that changed, the rest of the plan rests on a wrong
|
|
242
|
+
assumption. The run's first step checks the tip again with `git ls-remote`,
|
|
243
|
+
so one that moves after the check stops the run too.
|
|
244
|
+
- Sparing a worktree also spares its branch, which was only free to delete
|
|
245
|
+
because the worktree was going.
|
|
246
|
+
|
|
247
|
+
### Rows, verdicts and risks
|
|
248
|
+
|
|
249
|
+
A **row** is one thing that could be deleted, named `<kind>:<name>`. A bare
|
|
250
|
+
name or path works when only one row has it.
|
|
251
|
+
|
|
252
|
+
| Row | What it is | Deleted with |
|
|
253
|
+
| --- | --- | --- |
|
|
254
|
+
| `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` |
|
|
255
|
+
| `branch:<name>` | a local branch | `git update-ref -d refs/heads/<name> <sha>` |
|
|
256
|
+
| `remote:<remote>/<name>` | a branch on a remote | `git push --force-with-lease=…:<sha> <remote> :refs/heads/<name>` |
|
|
257
|
+
|
|
258
|
+
A **verdict** is what the sweep concluded about a row:
|
|
259
|
+
|
|
260
|
+
| Verdict | Meaning | What `clean` does |
|
|
261
|
+
| --- | --- | --- |
|
|
262
|
+
| `delete` | Its work is on the default branch, or it is otherwise safe to lose | Deletes it, once its risks are allowed |
|
|
263
|
+
| `review` | The signals are mixed, or it holds work | Deletes it only when picked and its risks are allowed |
|
|
264
|
+
| `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 |
|
|
265
|
+
| `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 |
|
|
266
|
+
|
|
267
|
+
The report shows `prune` for a `delete` row whose worktree directory is
|
|
268
|
+
already gone: only the stale record goes.
|
|
269
|
+
|
|
270
|
+
A **risk** is what deleting a row would lose. Each row lists its risks, and
|
|
271
|
+
you approve them by name with `--allow`:
|
|
272
|
+
|
|
273
|
+
| Risk | Meaning |
|
|
274
|
+
| --- | --- |
|
|
275
|
+
| `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. |
|
|
276
|
+
| `discard-changes` | Uncommitted or untracked files in the worktree or any of its submodules. |
|
|
277
|
+
| `confirm-open-pr` | The branch has an open pull request; deleting the remote branch closes it. |
|
|
278
|
+
|
|
279
|
+
A worktree's submodules are part of it: one `--allow` on the worktree row
|
|
280
|
+
covers the same risk in every submodule. A stash in the worktree itself is
|
|
281
|
+
shared with the main checkout and survives, so it carries no risk.
|
|
282
|
+
|
|
283
|
+
A branch whose worktree is judged `delete` is not held by it: the sweep judges
|
|
284
|
+
the branch on its own and records the worktree's path in `evidence.worktree`,
|
|
285
|
+
so one run removes the worktree and then deletes the branch. `clean` deletes
|
|
286
|
+
such a branch only when the same run removes its worktree, and drops it as
|
|
287
|
+
`held` otherwise. With `--only branch` no worktree is swept, so the branch
|
|
288
|
+
stays held.
|
|
289
|
+
|
|
290
|
+
### Refused combinations
|
|
291
|
+
|
|
292
|
+
A combination of options is refused when it contradicts itself, or when one of
|
|
293
|
+
its options would have no effect: a flag that is silently ignored leaves you
|
|
294
|
+
believing something that is not true. repo-cleaner never guesses, and no
|
|
295
|
+
option silently overrides another.
|
|
296
|
+
|
|
297
|
+
Refused before anything is deleted, with exit 2. Most are refused before
|
|
298
|
+
anything is swept; a row named two ways (a bare name and its id), and an
|
|
299
|
+
`--allow` for a row, are checked against the plan, after the sweep:
|
|
300
|
+
|
|
301
|
+
| Combination | Why |
|
|
302
|
+
| --- | --- |
|
|
303
|
+
| The same row in `--pick` and `--spare` | Asks for the row to be deleted and kept |
|
|
304
|
+
| `--allow <risk>=<row>` for a spared row, or for a row outside `--pick` | Approves deleting something that will not be deleted |
|
|
305
|
+
| A picked or spared row of a kind `--only` leaves out | The row cannot be in the run |
|
|
306
|
+
| A picked or spared row that is not one of `--branch`'s rows | The same |
|
|
307
|
+
| `--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 |
|
|
308
|
+
| `--plan` with targets or sweep options | The plan already fixed what is swept |
|
|
309
|
+
| `--plan -` without `--yes` or `--dry-run` | stdin carries the plan, so nothing can ask |
|
|
310
|
+
| `--yes` with `--dry-run` | One deletes without asking, the other deletes nothing |
|
|
311
|
+
| `--receipt` with `--dry-run` | Nothing runs, so there is nothing to record |
|
|
312
|
+
| `--offline` or `--provider` without `--remote` | Nothing is fetched or looked up without it |
|
|
313
|
+
| `--remote` with an `--only` that leaves `remote` out | They ask for opposite things |
|
|
314
|
+
| 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 |
|
|
315
|
+
| `--all` with any `--format` but `text` | `--all` shows keep rows in the text report only |
|
|
316
|
+
|
|
317
|
+
Refused after the sweep, before deleting, with exit 1:
|
|
318
|
+
|
|
319
|
+
- a picked, spared or allowed row the plan does not have;
|
|
320
|
+
- a row name that matches rows in more than one repository: sweep or plan
|
|
321
|
+
only the repository it is in (a bare name can also be given as its full id,
|
|
322
|
+
when the matches are of different kinds);
|
|
323
|
+
- a spared remote branch that is gone or moved since the plan.
|
|
324
|
+
|
|
325
|
+
Allowed, because nothing contradicts:
|
|
326
|
+
|
|
327
|
+
| Combination | What happens |
|
|
328
|
+
| --- | --- |
|
|
329
|
+
| The same row picked twice | It runs once |
|
|
330
|
+
| `--allow <risk>` with `--allow <risk>=<row>` | The broader one covers it |
|
|
331
|
+
| `--allow` for a risk no row that runs carries | Runs; the preview lists it as `unused` |
|
|
332
|
+
| Picking a `blocked` or `keep` row | It is skipped and shown with the reason; the rest runs |
|
|
333
|
+
| `--only remote` without `--remote` | Asking for remote branches implies the network |
|
|
334
|
+
|
|
335
|
+
### A remote with no checkout
|
|
336
|
+
|
|
337
|
+
```bash
|
|
338
|
+
repo-cleaner git@github.com:acme/api.git
|
|
339
|
+
repo-cleaner clean git@github.com:acme/api.git
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
A remote URL target sweeps a remote's branches when you have no checkout of
|
|
343
|
+
it. It clones the URL once, with `--no-checkout`, into
|
|
344
|
+
`repo-cleaner/remotes/` under the cache directory (`~/Library/Caches` on
|
|
345
|
+
macOS, `$XDG_CACHE_HOME` or `~/.cache` on Linux, `REPO_CLEANER_CACHE_DIR` to
|
|
346
|
+
move it), and sweeps that clone. Later runs reuse the clone and fetch. Only
|
|
347
|
+
remote branches are swept: the clone's own branches and worktree are not
|
|
348
|
+
yours. Deletes are pushed to the URL from the clone, compare-and-swap as
|
|
349
|
+
always. A local bare repository is named as `file:///path/to/repo.git`.
|
|
350
|
+
|
|
351
|
+
The clone is kept, not deleted after the run, so a plan of a remote URL can
|
|
352
|
+
run later from anywhere: the plan names the clone's directory.
|
|
353
|
+
`repo-cleaner cache` lists the clones with their URL and size;
|
|
354
|
+
`repo-cleaner cache remove <url>` drops one, and `cache remove --all` drops
|
|
355
|
+
every one, a clone left by a stopped run included. A plan whose clone was
|
|
356
|
+
removed cannot run; sweep again. The clone is a full one, not
|
|
357
|
+
`--filter=blob:none`: the patch check reads file contents.
|
|
358
|
+
|
|
359
|
+
A clone is reused only while its `origin` fetches from that URL and pushes to
|
|
360
|
+
it alone; `clean --plan` checks the same of a plan's clone before deleting.
|
|
361
|
+
The clone's directory name and every message carry the URL with its
|
|
362
|
+
credentials, query and fragment removed.
|
|
363
|
+
|
|
93
364
|
## How it decides
|
|
94
365
|
|
|
95
366
|
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 +375,11 @@ Ancestry is measured against `<remote>/<default>`, so work merged only into an
|
|
|
104
375
|
unpushed local default branch does not count. Without a remote, the local
|
|
105
376
|
default branch is used.
|
|
106
377
|
|
|
107
|
-
A merged PR that fails the head or base check is kept in `
|
|
108
|
-
`
|
|
378
|
+
A merged PR that fails the head or base check is kept in `evidence.pr` with
|
|
379
|
+
`proof` set to `head-moved` or `other-base`. Fork PRs are ignored.
|
|
109
380
|
|
|
110
381
|
Everything else is held back. A branch that is neither merged nor an ancestor
|
|
111
|
-
is `
|
|
382
|
+
is `review` with the reasons spelled out, not a guess. A worktree
|
|
112
383
|
holding uncommitted or unpushed work is never proposed for removal whatever
|
|
113
384
|
else is true of it — losing an hour of someone's work costs more than any
|
|
114
385
|
amount of clutter.
|
|
@@ -117,24 +388,73 @@ A clean worktree whose HEAD is merged by ancestry alone, with no merged PR
|
|
|
117
388
|
or patch match, needs one more sign, because a worktree that `git worktree
|
|
118
389
|
add -b` just made from the default branch is merged that way too, and
|
|
119
390
|
removing it would lose the ignored files of work about to start. The sign is
|
|
120
|
-
in `
|
|
391
|
+
in `evidence.branchUsed`: `commits` when the branch's reflog records a commit,
|
|
121
392
|
cherry-pick, `am`, or a merge or pull that made a merge commit (a
|
|
122
393
|
fast-forward only moves the branch), and `upstream` when the branch
|
|
123
394
|
tracks a branch on a remote, other than the default branch (a branch made
|
|
124
395
|
with `--track` from `main` or `origin/main` does not count). With either, the worktree
|
|
125
|
-
is `
|
|
396
|
+
is `delete`; with neither (`null`) it stays `review`.
|
|
126
397
|
|
|
127
|
-
Some worktrees are held back whatever their branch says
|
|
128
|
-
`
|
|
398
|
+
Some worktrees are held back whatever their branch says and whatever is
|
|
399
|
+
allowed. Each lands in `row.blockers`, the row's verdict is `blocked`, and it
|
|
400
|
+
carries no command and no action:
|
|
129
401
|
|
|
130
402
|
| Blocker | Why |
|
|
131
403
|
| --- | --- |
|
|
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. `
|
|
404
|
+
| `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). |
|
|
405
|
+
| `live-process` | A process other than an idle shell has its working directory in the worktree (`evidence.liveProcesses`), lock or not. |
|
|
406
|
+
| `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
407
|
| `hidden-index-flags` | Files marked skip-worktree or assume-unchanged hide edits from `status`. |
|
|
136
408
|
| `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 (`
|
|
409
|
+
| `branch-in-other-worktree` | Another worktree has the same branch checked out (`evidence.otherWorktrees`). |
|
|
410
|
+
| `submodule-unreadable` | A submodule could not be read, so nobody can say it is safe to lose (see [Submodules](#submodules)). |
|
|
411
|
+
|
|
412
|
+
### Submodules
|
|
413
|
+
|
|
414
|
+
A linked worktree's submodules keep their git dirs inside the worktree's own
|
|
415
|
+
git dir (`.git/worktrees/<id>/modules/<name>`), not the main checkout's. Removing
|
|
416
|
+
the worktree, or pruning it once its directory is gone, deletes them: a commit
|
|
417
|
+
made in a submodule and never pushed goes with it, and so does a stash or a
|
|
418
|
+
local branch. Plain `git worktree remove` refuses every worktree with a
|
|
419
|
+
checked-out submodule, or with a submodule git dir a `git submodule deinit`
|
|
420
|
+
left behind, whatever state it is in. It takes `--force`, and `--force` also
|
|
421
|
+
skips git's own check for modified and untracked files.
|
|
422
|
+
|
|
423
|
+
So the sweep reads every submodule itself, nested ones through their parents,
|
|
424
|
+
and every left-over submodule git dir, and records what it found in
|
|
425
|
+
`evidence.submodules`. A submodule is judged as part of its worktree: the same
|
|
426
|
+
risks apply wherever they are, and one `--allow` on a worktree row covers its
|
|
427
|
+
submodules too.
|
|
428
|
+
|
|
429
|
+
| Found in a submodule | Effect on the worktree row |
|
|
430
|
+
| --- | --- |
|
|
431
|
+
| 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 |
|
|
432
|
+
| Modified, staged or untracked files | `review`, carries `discard-changes`; `evidence.submoduleDirtyFiles` counts them |
|
|
433
|
+
| Skip-worktree or assume-unchanged files, nested repositories | `hidden-index-flags`, `nested-repository`, with the submodule's path in front |
|
|
434
|
+
| A git call that failed | `submodule-unreadable` blocker |
|
|
435
|
+
|
|
436
|
+
A worktree whose submodules are all clean and pushed is judged as any other,
|
|
437
|
+
and when it is `delete` its command is `git worktree remove --force`
|
|
438
|
+
(`evidence.forcedForSubmodules` is `true`, and the reason says why). Just before that
|
|
439
|
+
remove, `clean` checks the worktree's own files and every submodule again,
|
|
440
|
+
since git will not, and refuses the step if it would lose anything not allowed. A submodule's
|
|
441
|
+
state is part of the plan fingerprint, so a submodule that moved since the plan
|
|
442
|
+
drops the row first.
|
|
443
|
+
|
|
444
|
+
A worktree whose directory is gone is read through its git dir: unpushed work
|
|
445
|
+
in a left-over submodule git dir makes the prune row a `review` that carries
|
|
446
|
+
`abandon-unmerged`.
|
|
447
|
+
A worktree whose directory is gone is removed by its own record. One whose
|
|
448
|
+
directory is left without its `.git` takes the repository-wide `git worktree
|
|
449
|
+
prune`, which drops every gone worktree, not only the planned one. So such a
|
|
450
|
+
row runs last, and only while every other gone worktree that prune would drop
|
|
451
|
+
goes in the same run; otherwise `clean` drops it (`blocked`, with the reason).
|
|
452
|
+
A run never drops the record of a gone worktree it did not select.
|
|
453
|
+
|
|
454
|
+
Commits count as pushed only when a remote-tracking ref of the submodule holds
|
|
455
|
+
them. A superproject that records a submodule commit reachable from no branch
|
|
456
|
+
on the submodule's remote (fetched by sha, or on a branch since deleted) carries
|
|
457
|
+
`abandon-unmerged` until a fetch brings it under one.
|
|
138
458
|
|
|
139
459
|
### Patch equivalence
|
|
140
460
|
|
|
@@ -149,16 +469,16 @@ branch as safe as an ancestor. A second, independent check must agree:
|
|
|
149
469
|
`git merge-tree --write-tree <default> <tip>` must come out clean with exactly
|
|
150
470
|
the default branch's tree, which shows the branch's content is already there. It
|
|
151
471
|
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
|
-
|
|
472
|
+
agreed. The row is `delete` either way, but only an agreed check drops the
|
|
473
|
+
`abandon-unmerged` risk. Without it the row keeps carrying that risk, and a
|
|
474
|
+
worktree with unpushed commits stays `review`. The row carries three pieces of
|
|
475
|
+
evidence, and its reason says which case applied:
|
|
156
476
|
|
|
157
|
-
|
|
|
477
|
+
| Evidence | Value |
|
|
158
478
|
| --- | --- |
|
|
159
|
-
| `
|
|
160
|
-
| `
|
|
161
|
-
| `
|
|
479
|
+
| `evidence.landing` | `patch-equivalent` |
|
|
480
|
+
| `evidence.landingCheck` | which check passed, below |
|
|
481
|
+
| `evidence.landingContentConfirmed` | `true` when the merge-tree check agreed, `false` otherwise |
|
|
162
482
|
|
|
163
483
|
| `landing_check` | Meaning |
|
|
164
484
|
| --- | --- |
|
|
@@ -168,7 +488,7 @@ carries three facts, and its reason says which case applied:
|
|
|
168
488
|
|
|
169
489
|
`squashed` accepts any one commit on the default branch with the same net
|
|
170
490
|
patch, not necessarily one that came from this branch. That is acceptable only
|
|
171
|
-
because the content check, or
|
|
491
|
+
because the content check, or `--allow abandon-unmerged`, stands behind it.
|
|
172
492
|
|
|
173
493
|
The squashed check builds one commit object with `git commit-tree` and no ref.
|
|
174
494
|
It leaves the index, the working tree and every ref alone, and needs no git
|
|
@@ -179,7 +499,7 @@ The check decides what gets deleted, so a doubt is never evidence. None of
|
|
|
179
499
|
these count as landed:
|
|
180
500
|
|
|
181
501
|
- a commit that is not on the default branch, however many others are (the
|
|
182
|
-
commits that are missing are listed in `
|
|
502
|
+
commits that are missing are listed in `evidence.unmatchedCommits` and in the
|
|
183
503
|
investigation text when part of the work did land);
|
|
184
504
|
- a git call that fails, or output that cannot be read;
|
|
185
505
|
- a shallow clone, a missing merge base, a ref that does not resolve;
|
|
@@ -193,7 +513,7 @@ A patch id says two changes are the same, not that they come from the same
|
|
|
193
513
|
branch, and it ignores whitespace. A one-line change that the default branch
|
|
194
514
|
also made independently, an indentation-only difference, or work that landed
|
|
195
515
|
and was then reverted all match by patch id; the content check does not agree
|
|
196
|
-
with them, so they keep needing
|
|
516
|
+
with them, so they keep needing `--allow abandon-unmerged`.
|
|
197
517
|
|
|
198
518
|
### Locks and running processes
|
|
199
519
|
|
|
@@ -202,9 +522,9 @@ token from the lock reason, `pid <N>`, optionally followed by `start <time>`,
|
|
|
202
522
|
and asks whether that process is still running. It reads nothing else of the
|
|
203
523
|
reason, so a free-form reason such as `keep, build running` always blocks.
|
|
204
524
|
|
|
205
|
-
- The owner is dead: the lock is not a blocker. `
|
|
206
|
-
`
|
|
207
|
-
with `git worktree unlock`. `
|
|
525
|
+
- The owner is dead: the lock is not a blocker. `evidence.staleLock` is `true`,
|
|
526
|
+
`evidence.lockOwner` holds the pid and start time, and the row's commands begin
|
|
527
|
+
with `git worktree unlock`. `clean` unlocks, removes and verifies it, and the
|
|
208
528
|
receipt records the `unlock-worktree` step.
|
|
209
529
|
- The owner is alive, or cannot be ruled out: `locked-worktree`.
|
|
210
530
|
- A lock writes `pid <N>` and, to guard against pid reuse, `start <time>`. The
|
|
@@ -220,7 +540,7 @@ reason, so a free-form reason such as `keep, build running` always blocks.
|
|
|
220
540
|
- The `pid` token is trusted as a local pid. A pid from another container or
|
|
221
541
|
host that shares the filesystem is looked up here, will not be found, and
|
|
222
542
|
reads as dead; do not sweep a repository whose locks were written elsewhere.
|
|
223
|
-
- Before the unlock and again before the remove, `
|
|
543
|
+
- Before the unlock and again before the remove, `clean` asks again, and reads
|
|
224
544
|
the process table afresh. If the owner is alive by then, or a process other
|
|
225
545
|
than an idle shell works in the directory, or the table cannot be read, the
|
|
226
546
|
step fails and the run stops. A row dropped at the re-sweep touches nothing;
|
|
@@ -233,16 +553,16 @@ reason, so a free-form reason such as `keep, build running` always blocks.
|
|
|
233
553
|
- Separately, any process whose working directory is inside the worktree blocks
|
|
234
554
|
it as `live-process`, lock or not. A shell (`sh`, `bash`, `zsh`, `fish`) is
|
|
235
555
|
idle only when it has no child processes; it is then listed in
|
|
236
|
-
`
|
|
556
|
+
`evidence.idleShells` and does not block. A shell with a child, or whose
|
|
237
557
|
children are unknown, counts as a live process.
|
|
238
558
|
|
|
239
559
|
Both questions go to a `ProcessProbe` (`alive(pid, startedAt?)` and
|
|
240
|
-
`holders(path, { fresh? })`), passed as `probe` in the options of `
|
|
241
|
-
`
|
|
560
|
+
`holders(path, { fresh? })`), passed as `probe` in the options of `sweep`,
|
|
561
|
+
`check` and `run`. The default, `defaultProcessProbe()`, runs
|
|
242
562
|
`ps -o lstart=`, `ps -A -o pid=,ppid=` and `lsof -d cwd` through
|
|
243
563
|
`@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
|
-
|
|
564
|
+
process table once per probe and reuses it for every worktree: `sweep` makes
|
|
565
|
+
one probe per repository, and `run` uses one probe for its re-sweep and
|
|
246
566
|
reads the table afresh for each pre-unlock and pre-remove check. Where the
|
|
247
567
|
table cannot be read the worktree is held back as `process-probe-failed`. A
|
|
248
568
|
host or test supplies its own probe.
|
|
@@ -255,230 +575,48 @@ says so. See [Providers (prototype)](#providers-prototype).
|
|
|
255
575
|
Every branch delete names the full sha the sweep judged. `update-ref -d` and
|
|
256
576
|
the pinned `--force-with-lease` push both refuse when the branch has moved
|
|
257
577
|
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
|
-
|
|
261
|
-
Every row names, in `requires`, the risks its delete carries, so a person
|
|
262
|
-
approves them by name:
|
|
263
|
-
|
|
264
|
-
| Authorization | When a row needs it |
|
|
265
|
-
| --- | --- |
|
|
266
|
-
| `abandon-unmerged` | Commits not on the default branch and not captured by an exact-head merged PR; a worktree's unpushed commits. `facts.unique_commits` counts them. |
|
|
267
|
-
| `discard-changes` | A worktree has modified, staged or untracked files. |
|
|
268
|
-
| `confirm-open-pr` | The branch has an open PR. |
|
|
269
|
-
|
|
270
|
-
A row kept for structure (default branch, current HEAD, a branch held by a
|
|
271
|
-
worktree, the primary or caller's worktree) has `deletable: false`.
|
|
272
|
-
|
|
273
|
-
A branch whose worktree is `safe-to-delete` is not held by it: the sweep judges
|
|
274
|
-
the branch on its own and records the worktree's path in `facts.worktree`, so
|
|
275
|
-
one run removes the worktree and then deletes the branch. `apply` deletes such
|
|
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.
|
|
278
|
-
|
|
279
|
-
## Commands
|
|
280
|
-
|
|
281
|
-
```bash
|
|
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
|
|
287
|
-
```
|
|
288
|
-
|
|
289
|
-
The first four take the same paths and sweep options. A checkout directory
|
|
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.
|
|
295
|
-
|
|
296
|
-
```console
|
|
297
|
-
$ repo-cleaner summary --remotes
|
|
298
|
-
/Users/me/work/api (acme/api, default main, origin)
|
|
299
|
-
total merged delete prune review keep
|
|
300
|
-
local branches 12 8 6 0 1 5
|
|
301
|
-
worktrees 4 3 1 1 1 1
|
|
302
|
-
remote branches 40 31 30 0 6 4
|
|
303
|
-
|
|
304
|
-
37 to delete, 1 to prune, 8 to review, across 1 repository.
|
|
305
|
-
```
|
|
306
|
-
|
|
307
|
-
`prune` counts worktree rows whose directory is gone, or is no longer a
|
|
308
|
-
checkout: deleting one runs `git worktree prune` to drop the stale link, and
|
|
309
|
-
a directory that is still there stays. The report labels those rows `prune`
|
|
310
|
-
rather than `delete`. In `--output json` they are `safe-to-delete` with
|
|
311
|
-
`action: "prune"`, and each summary bucket has a `prune` count, a part of its
|
|
312
|
-
`safe-to-delete` count.
|
|
313
|
-
|
|
314
|
-
`merged` counts rows whose work is on the default branch: by ancestry, by a
|
|
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`.
|
|
319
|
-
|
|
320
|
-
## Scope is opt-in
|
|
321
|
-
|
|
322
|
-
```bash
|
|
323
|
-
repo-cleaner # the repository you are standing in
|
|
324
|
-
repo-cleaner ~/work/api ~/work/web # named repositories
|
|
325
|
-
repo-cleaner ~/Developer # every checkout under it
|
|
326
|
-
```
|
|
327
|
-
|
|
328
|
-
A bare invocation never fans out. What an argument names is read off the disk
|
|
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.
|
|
333
|
-
|
|
334
|
-
A linked worktree collapses into the repository it belongs to, so a directory
|
|
335
|
-
holding a checkout and three of its worktrees sweeps one repository, not four.
|
|
336
|
-
Those worktrees still appear — as worktree rows in that repository's own
|
|
337
|
-
inventory.
|
|
338
578
|
|
|
339
|
-
|
|
340
|
-
directories read, 12 checkouts found.` — because those numbers are how an
|
|
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.
|
|
344
|
-
|
|
345
|
-
An entry that resolves to nothing is reported rather than dropped. A worklist
|
|
346
|
-
that silently sweeps four of the five repositories it was given is worse than
|
|
347
|
-
one that says which it could not find.
|
|
348
|
-
|
|
349
|
-
### A remote with no checkout
|
|
350
|
-
|
|
351
|
-
```bash
|
|
352
|
-
repo-cleaner --remote-url git@github.com:acme/api.git
|
|
353
|
-
repo-cleaner apply --remote-url git@github.com:acme/api.git
|
|
354
|
-
```
|
|
355
|
-
|
|
356
|
-
`--remote-url <url>` sweeps a remote's branches when you have no checkout of
|
|
357
|
-
it. It clones the URL once, with `--no-checkout`, into
|
|
358
|
-
`repo-cleaner/remotes/` under the cache directory (`~/Library/Caches` on
|
|
359
|
-
macOS, `$XDG_CACHE_HOME` or `~/.cache` on Linux, `REPO_CLEANER_CACHE_DIR`
|
|
360
|
-
to move it), and sweeps that clone. Later runs reuse the clone and fetch. Only remote branches
|
|
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.
|
|
372
|
-
|
|
373
|
-
The flag repeats for several remotes. It does not combine with paths,
|
|
374
|
-
`--branch` or `--plan`, `--only` may only name `remotes`, and an empty URL is
|
|
375
|
-
refused. A clone is reused only while its `origin` fetches from that URL and
|
|
376
|
-
pushes to it alone; `apply --plan` checks the same of a plan's clone before
|
|
377
|
-
deleting. The clone's directory name and every message carry the URL with
|
|
378
|
-
its credentials, query and fragment removed.
|
|
379
|
-
|
|
380
|
-
## Options
|
|
381
|
-
|
|
382
|
-
Every command that sweeps takes these:
|
|
383
|
-
|
|
384
|
-
| Flag | Effect |
|
|
385
|
-
| --- | --- |
|
|
386
|
-
| `--only <list>` | `worktrees`, `branches`, `remotes` (default: `worktrees,branches`) |
|
|
387
|
-
| `--remotes` | Also sweep remote branches. Slower: each one is compared with the default branch |
|
|
388
|
-
| `--branch <name\|HEAD>` | One branch of this repository: its worktree and local branch, and its remote branch with `--remotes`. Refuses the default branch |
|
|
389
|
-
| `--measure-remote <name>` | The remote whose default branch work is measured against (default `origin`) |
|
|
390
|
-
| `--remote-url <url>` | Sweep this remote's branches with no checkout of your own (see [A remote with no checkout](#a-remote-with-no-checkout)); repeatable |
|
|
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 |
|
|
393
|
-
|
|
394
|
-
`status` also takes:
|
|
395
|
-
|
|
396
|
-
| Flag | Effect |
|
|
397
|
-
| --- | --- |
|
|
398
|
-
| `--keep` | Include `safe-to-keep` rows in the report |
|
|
399
|
-
| `--commands` | Print the proposed commands of rows that need no authorization, and nothing else |
|
|
400
|
-
| `--output json` | The full inventory, facts included |
|
|
401
|
-
|
|
402
|
-
`--version` prints `repo-cleaner <version>` and exits; it must be the first
|
|
403
|
-
argument. A checkout directory named like a command (`status`, `summary`,
|
|
404
|
-
`plan`, `apply`, `dev`) is swept as `./status` and so on: `dev` runs the tool from a
|
|
405
|
-
checkout (see Running from a checkout).
|
|
406
|
-
|
|
407
|
-
## Plan and apply
|
|
579
|
+
## Plan and clean
|
|
408
580
|
|
|
409
581
|
```bash
|
|
410
|
-
repo-cleaner
|
|
411
|
-
repo-cleaner
|
|
412
|
-
repo-cleaner plan -o plan.json
|
|
413
|
-
repo-cleaner
|
|
414
|
-
repo-cleaner plan | repo-cleaner
|
|
582
|
+
repo-cleaner clean # sweep, show the steps, ask, delete
|
|
583
|
+
repo-cleaner clean --only worktree --yes # no question
|
|
584
|
+
repo-cleaner plan -o plan.json # save the plan to review or edit
|
|
585
|
+
repo-cleaner clean --plan plan.json --pick branch:feat/x
|
|
586
|
+
repo-cleaner plan | repo-cleaner clean --plan - --yes
|
|
415
587
|
```
|
|
416
588
|
|
|
417
|
-
A plan holds
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
needs-review: clean but no clear disposability signal; name it with --row to apply it
|
|
438
|
-
|
|
439
|
-
would delete: 1 local branch
|
|
440
|
-
skipped 1, not deleted: 1 needs-review (name with --row)
|
|
441
|
-
Delete 2 items? [y/N]
|
|
442
|
-
```
|
|
443
|
-
|
|
444
|
-
The re-check reads only the categories the selected rows are in, so applying
|
|
445
|
-
a plan of local rows does not compare every remote branch again. A plan
|
|
446
|
-
records in each repository's `fetch` whether its sweep fetched; the re-check
|
|
447
|
-
fetches too when it did, so it judges against the same, current default
|
|
448
|
-
branch. A plan written before that field re-checks without fetching.
|
|
449
|
-
|
|
450
|
-
`--keep remote_branch:<remote>/<branch>` keeps a remote branch while the
|
|
451
|
-
rest of its rows go: say, delete the worktree and local branch but leave
|
|
452
|
-
the branch on the remote. A kept row is checked, not skipped. The re-check
|
|
453
|
-
refuses the run if the branch is gone or its tip moved since the plan, and
|
|
454
|
-
the run's first step checks the tip again with `git ls-remote`, so one that
|
|
455
|
-
moves after the re-check stops the run before anything is deleted. Only the
|
|
456
|
-
tip counts: a PR that closed since the plan does not refuse it. Another
|
|
457
|
-
clone of the same remote does not delete a kept branch either; its row is
|
|
458
|
-
dropped as `kept`. The row id is the one the plan lists,
|
|
459
|
-
`<remote>/<branch>`, e.g. `origin/feat/x`.
|
|
460
|
-
`--yes` skips the question. With no terminal to ask on and no `--yes`, it
|
|
461
|
-
refuses with exit 2 rather than deleting. `--plan -` needs `--yes` or
|
|
462
|
-
`--dry-run`, because stdin carries the plan. `--dry-run` prints the steps and
|
|
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:
|
|
589
|
+
A plan holds every row with a fingerprint of the evidence behind its verdict,
|
|
590
|
+
and the sweep options it was made with. Without `--pick` only the rows that
|
|
591
|
+
could be deleted run; the blocked and structural rows are there so a pick of
|
|
592
|
+
one is skipped with its reason. `clean`
|
|
593
|
+
either sweeps and plans in memory, or reads a plan with `--plan`. A plan of an
|
|
594
|
+
older version is refused, not translated: sweep again.
|
|
595
|
+
|
|
596
|
+
Before it deletes anything, `clean` sweeps again, with the plan's options, and
|
|
597
|
+
drops a row that is gone (`gone`), that can no longer be deleted (`blocked`,
|
|
598
|
+
`keep`, or `held` for a branch whose worktree this run does not remove), that
|
|
599
|
+
was not picked and is no longer `delete` (`needs-pick`), whose evidence moved
|
|
600
|
+
(`changed`), or that carries a risk you did not `--allow` (`not-allowed`). The
|
|
601
|
+
re-check reads only the kinds the selected rows are in, so running a plan of
|
|
602
|
+
local rows does not compare every remote branch again, and it fetches when the
|
|
603
|
+
plan's sweep did, so it judges against the same, current default branch.
|
|
604
|
+
|
|
605
|
+
On yes it runs only the steps it showed: spared remote tips first, then
|
|
606
|
+
worktrees, local branches and remote branches, in that order. Branch
|
|
607
|
+
deletes are compare-and-swap, every step is verified, and the first failure
|
|
608
|
+
stops the run. Every run ends with what it did:
|
|
470
609
|
|
|
471
610
|
```text
|
|
472
611
|
deleted: 1 worktree, 3 local branches, 2 remote branches, 1 stale worktree pruned
|
|
473
|
-
skipped 2, not deleted: 1 needs-
|
|
612
|
+
skipped 2, not deleted: 1 needs-pick (name with --pick), 1 duplicate
|
|
474
613
|
status: complete
|
|
475
614
|
```
|
|
476
615
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
compared by the push URL git uses, with `url.*.insteadOf` applied.
|
|
616
|
+
Two clones of one remote list the same remote branches; `clean` deletes each
|
|
617
|
+
once and drops the other clone's row as `duplicate`, or as `spared` when the
|
|
618
|
+
branch is spared through the other clone. Remotes are compared by the push URL
|
|
619
|
+
git uses, with `url.*.insteadOf` applied.
|
|
482
620
|
|
|
483
621
|
A receipt records every step and is rewritten after each one. With
|
|
484
622
|
`--plan plan.json` it goes beside the plan, as
|
|
@@ -490,56 +628,86 @@ that swept, or read its plan from stdin, writes a receipt only when
|
|
|
490
628
|
|
|
491
629
|
| Code | Meaning |
|
|
492
630
|
| --- | --- |
|
|
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` | `
|
|
631
|
+
| `0` | Swept; or `clean` ran, was declined, or had nothing to do |
|
|
632
|
+
| `1` | Failed against the state it found: an unreadable plan, a row not in it, a spared branch that moved |
|
|
633
|
+
| `2` | Cannot run as typed: an unknown flag, a malformed row, a refused combination, `clean` with nobody to ask and no `--yes` |
|
|
634
|
+
| `10` | Nothing to sweep: no target named a repository |
|
|
635
|
+
| `11` | `clean` stopped at a failed step; the receipt says which |
|
|
498
636
|
|
|
499
637
|
## As a library
|
|
500
638
|
|
|
501
639
|
```ts
|
|
502
|
-
import {
|
|
640
|
+
import { check, plan, render, run, sweep } from '@osdlc/repo-cleaner'
|
|
503
641
|
|
|
504
|
-
const
|
|
505
|
-
for (const
|
|
506
|
-
console.log(
|
|
642
|
+
const result = sweep(['/Users/me/work/api'], { remote: true })
|
|
643
|
+
for (const row of result.repos[0]?.rows ?? []) {
|
|
644
|
+
console.log(row.id, row.verdict, row.why, row.risks)
|
|
507
645
|
}
|
|
508
646
|
|
|
509
|
-
const
|
|
647
|
+
const preview = check(plan(result), {
|
|
648
|
+
pick: ['branch:spike/cache'],
|
|
649
|
+
allow: [{ risk: 'abandon-unmerged', row: 'branch:spike/cache' }],
|
|
650
|
+
})
|
|
651
|
+
process.stdout.write(render.preview(preview))
|
|
652
|
+
const receipt = run(preview)
|
|
510
653
|
```
|
|
511
654
|
|
|
512
|
-
|
|
513
|
-
|
|
655
|
+
| Export | What it does |
|
|
656
|
+
| --- | --- |
|
|
657
|
+
| `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` |
|
|
658
|
+
| `plan(sweep)` | Every row, fingerprinted, with the sweep's options |
|
|
659
|
+
| `check(plan, approval, opts?)` | Resolve `{ pick, spare, allow }` against the plan, sweep again, and return a `Preview`: the steps, the rows dropped and why, and the allows no row needed. Deletes nothing |
|
|
660
|
+
| `run(preview, opts?)` | Sweep once more and take the preview's steps, writing a `Receipt` (to `receiptPath` after every step, when given) |
|
|
661
|
+
| `inspectWorktree(path, opts?)` | One worktree judged as a sweep judges it, submodules included |
|
|
662
|
+
| `readPlan`, `writePlan` | Plans on disk |
|
|
663
|
+
| `render` | The command line's formats: `text`, `json`, `summary`, `commands`, `preview`, `receipt` |
|
|
664
|
+
| `defaultProcessProbe`, `providers` | The defaults a host can wrap or replace |
|
|
665
|
+
| `RepoCleanerError` | Every refusal, with a `code`: `conflict`, `bad-input`, `not-found`, `ambiguous`, `spared-changed`, `bad-plan` |
|
|
666
|
+
|
|
667
|
+
A row is a `WorktreeRow`, `BranchRow` or `RemoteRow`, told apart by `kind`:
|
|
514
668
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
669
|
+
```ts
|
|
670
|
+
interface Row {
|
|
671
|
+
kind: 'worktree' | 'branch' | 'remote'
|
|
672
|
+
id: RowId // 'worktree:/p', 'branch:feat/x', 'remote:origin/feat/x'
|
|
673
|
+
name: string // the path, the branch, `<remote>/<branch>`
|
|
674
|
+
verdict: 'delete' | 'review' | 'keep' | 'blocked'
|
|
675
|
+
action: 'remove' | 'prune' | 'delete' | null // null: never deleted
|
|
676
|
+
why: string
|
|
677
|
+
risks: Risk[] // gathered over the row's submodules
|
|
678
|
+
blockers: Blocker[] // non-empty exactly when verdict is 'blocked'
|
|
679
|
+
commands: string[]
|
|
680
|
+
notes: string[] // what is unclear, or what to do about a blocker
|
|
681
|
+
evidence: WorktreeEvidence | BranchEvidence | RemoteEvidence
|
|
682
|
+
}
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
`evidence` is typed for each kind: a worktree's `path`, `branch`, `head`,
|
|
686
|
+
`dirtyFiles`, `unpushedCommits`, lock and process evidence, and `submodules`;
|
|
687
|
+
a branch's `sha`, `upstream`, `track`; every kind's `merged`, `pr`,
|
|
688
|
+
`uniqueCommits` and landing evidence.
|
|
689
|
+
|
|
690
|
+
`sweep` never throws for a repository it cannot read: that one comes back
|
|
691
|
+
with `error` set, because a fan-out must not end on its first bad entry. It
|
|
692
|
+
throws a `RepoCleanerError` for options that contradict each other, as the
|
|
693
|
+
command line refuses them, before reading anything. `check` throws for an
|
|
694
|
+
approval that contradicts itself, names a row the plan does not have, or
|
|
695
|
+
spares a remote branch that moved; nothing has been touched when it does.
|
|
520
696
|
|
|
521
697
|
### Adding knowledge the library does not have
|
|
522
698
|
|
|
523
|
-
Every row carries the
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
699
|
+
Every row carries the evidence behind its verdict, and `notes` is a list of
|
|
700
|
+
sentences rather than a finished string. That is deliberate: a host that knows
|
|
701
|
+
something git does not — an issue tracker, a task corpus — passes `annotate`
|
|
702
|
+
to `sweep`, `check` and `run`, and re-ranks the rows or appends its own
|
|
703
|
+
sentence, instead of reaching into classification. Because the same hook runs
|
|
704
|
+
in every re-check, the rows a run checks are the rows the host planned from.
|
|
529
705
|
|
|
530
706
|
This works because of how the ladder is ordered. Every verdict the sweep
|
|
531
707
|
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`.
|
|
708
|
+
host rarely has to veto one. It mostly sharpens the rows left at `keep` or
|
|
709
|
+
`review`. A worktree the host holds back keeps its branch: the sweep applies
|
|
710
|
+
that rule after the hook.
|
|
543
711
|
|
|
544
712
|
## Providers (prototype)
|
|
545
713
|
|
|
@@ -552,11 +720,11 @@ branch.
|
|
|
552
720
|
| `github` | `gh` | `gh pr list --state all`, up to 300 |
|
|
553
721
|
| `gitlab` | `glab` | `glab api projects/<path>/merge_requests?state=all`, 100 a page, up to 300 |
|
|
554
722
|
|
|
555
|
-
`--provider auto` (the default) reads the remote's host: a host with `gitlab`
|
|
723
|
+
With `--remote`, `--provider auto` (the default) reads the remote's host: a host with `gitlab`
|
|
556
724
|
in its name is GitLab, any other is GitHub, which is what the tool did before
|
|
557
725
|
providers existed. Name one with `--provider github|gitlab` for a host the
|
|
558
726
|
name does not give away, or `--provider none` to skip pull requests. A plan
|
|
559
|
-
records the choice, so `
|
|
727
|
+
records the choice, so `clean` re-checks with the same provider.
|
|
560
728
|
|
|
561
729
|
GitLab maps onto the same evidence: `opened` and `locked` are `OPEN`, `merged`
|
|
562
730
|
is `MERGED`, `closed` is `CLOSED`, `sha` is the head commit and `target_branch`
|