@osdlc/repo-cleaner 0.10.0 → 0.11.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 +56 -6
- package/dist/apply.d.ts +23 -2
- package/dist/apply.js +75 -2
- package/dist/branches.d.ts +10 -0
- package/dist/branches.js +15 -5
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +218 -109
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/receipt.d.ts +4 -0
- package/dist/receipt.js +11 -2
- package/dist/remote-mirror.d.ts +55 -0
- package/dist/remote-mirror.js +123 -0
- package/dist/render.d.ts +25 -2
- package/dist/render.js +92 -13
- package/dist/types.d.ts +4 -0
- package/dist/types.js +14 -1
- package/dist/version.generated.d.ts +1 -1
- package/dist/version.generated.js +1 -1
- package/dist/worktrees.js +8 -6
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# @osdlc/repo-cleaner
|
|
2
2
|
|
|
3
|
+
## 0.11.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 14d4351: A worktree row whose directory is gone, or is no longer a checkout, now reads `prune` instead of `delete` in the report, and `summary` counts those rows in a `prune` column (and a `prune` count in each JSON bucket, a part of `safe-to-delete`). The row's `action` is `prune`.
|
|
8
|
+
|
|
9
|
+
New `--remote-url <url>` on every sweeping command: sweep a remote's branches with no checkout of your own. The URL is cloned once (`--no-checkout`) under the cache directory and reused; deletes are pushed from that clone, so a plan written with `--remote-url` can be applied later from anywhere.
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Updated dependencies [14d4351]
|
|
14
|
+
- Updated dependencies [dd2265e]
|
|
15
|
+
- @osdlc/cli-tool@0.13.0
|
|
16
|
+
|
|
17
|
+
## 0.10.1
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- cde11b5: `apply` fixes:
|
|
22
|
+
|
|
23
|
+
- The run a confirmation approves keeps the safe-to-delete guard: a row that turned into a review row between the preview and the yes is dropped as `needs-review`, not run.
|
|
24
|
+
- Two clones of one remote no longer delete its branches twice. The second row is dropped as the new reason `duplicate`, naming the clone that deletes it; before, the second push was rejected and the run stopped with exit 11. Remotes are compared by the push URLs git uses (`url.*.insteadOf` applied), a network URL in any form (`git@host:o/r`, `ssh://`, `https://`) as `host/path`, and a local path as written.
|
|
25
|
+
- A swept `apply` re-checks once before asking, from the sweep it just ran, instead of sweeping again; the run that deletes still sweeps afresh.
|
|
26
|
+
- A run that deletes without a receipt file says so on stderr.
|
|
27
|
+
- Every run ends with a summary: `deleted: 1 worktree, 3 local branches, 2 remote branches`, the dropped rows by reason, and how many rows a failed run did not reach. A preview says `would delete:`. `--output json` adds a `summary` object.
|
|
28
|
+
- The prompt's `Delete N items?` counts rows, not steps: a stale-locked worktree is one item, not two.
|
|
29
|
+
- `--repo-root` naming no repository in the plan exits 1 instead of reporting nothing to delete.
|
|
30
|
+
- An empty plan's JSON receipt says `complete`, not `dry-run`, unless the run was `--dry-run`.
|
|
31
|
+
|
|
32
|
+
Library: `ApplyOptions.safeOnly` and `ApplyOptions.swept`, the `duplicate` drop reason, `RECEIPT_VERSION`, and `summarizeReceipt`, `renderApplySummary` and `ApplySummary`.
|
|
33
|
+
|
|
3
34
|
## 0.10.0
|
|
4
35
|
|
|
5
36
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -205,7 +205,7 @@ worktree, the primary or caller's worktree) has `deletable: false`.
|
|
|
205
205
|
|
|
206
206
|
```bash
|
|
207
207
|
repo-cleaner [status] # the report: what is merged, what can go (the default)
|
|
208
|
-
repo-cleaner summary # counts per repository: total, merged, delete, review, keep
|
|
208
|
+
repo-cleaner summary # counts per repository: total, merged, delete, prune, review, keep
|
|
209
209
|
repo-cleaner plan # the deletable rows as a plan, to stdout or -o <file>
|
|
210
210
|
repo-cleaner apply # re-check, show the steps, ask, delete
|
|
211
211
|
```
|
|
@@ -215,14 +215,21 @@ All four take the same paths and sweep options.
|
|
|
215
215
|
```console
|
|
216
216
|
$ repo-cleaner summary
|
|
217
217
|
/Users/me/work/api (acme/api, default main, origin)
|
|
218
|
-
total merged delete review keep
|
|
219
|
-
local branches 12 8 6 1 5
|
|
220
|
-
worktrees 4 3
|
|
221
|
-
remote branches 40 31 30 6 4
|
|
218
|
+
total merged delete prune review keep
|
|
219
|
+
local branches 12 8 6 0 1 5
|
|
220
|
+
worktrees 4 3 1 1 1 1
|
|
221
|
+
remote branches 40 31 30 0 6 4
|
|
222
222
|
|
|
223
|
-
|
|
223
|
+
37 to delete, 1 to prune, 8 to review, across 1 repository.
|
|
224
224
|
```
|
|
225
225
|
|
|
226
|
+
`prune` counts worktree rows whose directory is gone, or is no longer a
|
|
227
|
+
checkout: deleting one runs `git worktree prune` to drop the stale link, and
|
|
228
|
+
a directory that is still there stays. The report labels those rows `prune`
|
|
229
|
+
rather than `delete`. In `--output json` they are `safe-to-delete` with
|
|
230
|
+
`action: "prune"`, and each summary bucket has a `prune` count, a part of its
|
|
231
|
+
`safe-to-delete` count.
|
|
232
|
+
|
|
226
233
|
`merged` counts rows whose work is on the default branch: by ancestry, by a
|
|
227
234
|
merged PR at the branch's head, or by a patch match. A branch checked out in a
|
|
228
235
|
worktree counts as merged by ancestry or PR alone. The default branch and the
|
|
@@ -258,6 +265,34 @@ An entry that resolves to nothing is reported rather than dropped. A worklist
|
|
|
258
265
|
that silently sweeps four of the five repositories it was given is worse than
|
|
259
266
|
one that says which it could not find.
|
|
260
267
|
|
|
268
|
+
### A remote with no checkout
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
repo-cleaner --remote-url git@github.com:acme/api.git
|
|
272
|
+
repo-cleaner apply --remote-url git@github.com:acme/api.git
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
`--remote-url <url>` sweeps a remote's branches when you have no checkout of
|
|
276
|
+
it. It clones the URL once, with `--no-checkout`, into
|
|
277
|
+
`repo-cleaner/remotes/` under the cache directory (`~/Library/Caches` on
|
|
278
|
+
macOS, `$XDG_CACHE_HOME` or `~/.cache` on Linux, `REPO_CLEANER_CACHE_DIR`
|
|
279
|
+
to move it), and sweeps that clone. Later runs reuse the clone and fetch. Only remote branches
|
|
280
|
+
are swept: the clone's own branches and worktree are not yours. Deletes are
|
|
281
|
+
pushed to the URL from the clone, compare-and-swap as always.
|
|
282
|
+
|
|
283
|
+
The clone is kept, not deleted after the run, so a plan written with
|
|
284
|
+
`--remote-url` can be applied later from anywhere: the plan names the clone's
|
|
285
|
+
directory. Remove the directory to drop it. The clone is a full one, not
|
|
286
|
+
`--filter=blob:none`: the patch check reads file contents, and a partial
|
|
287
|
+
clone fetches them one branch at a time.
|
|
288
|
+
|
|
289
|
+
The flag repeats for several remotes. It does not combine with paths,
|
|
290
|
+
`--branch` or `--plan`, `--only` may only name `remotes`, and an empty URL is
|
|
291
|
+
refused. A clone is reused only while its `origin` fetches from that URL and
|
|
292
|
+
pushes to it alone; `apply --plan` checks the same of a plan's clone before
|
|
293
|
+
deleting. The clone's directory name and every message carry the URL with
|
|
294
|
+
its credentials, query and fragment removed.
|
|
295
|
+
|
|
261
296
|
## Options
|
|
262
297
|
|
|
263
298
|
Every command that sweeps takes these:
|
|
@@ -267,6 +302,7 @@ Every command that sweeps takes these:
|
|
|
267
302
|
| `--only <list>` | `worktrees`, `branches`, `remotes` (default: all three) |
|
|
268
303
|
| `--branch <name\|HEAD>` | One branch of this repository: its worktree, local and remote branch. Refuses the default branch |
|
|
269
304
|
| `--remote <name>` | Remote to measure against (default `origin`) |
|
|
305
|
+
| `--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 |
|
|
270
306
|
| `--no-gh` | Classify on git evidence alone |
|
|
271
307
|
| `--no-fetch` | Skip the fetch; the header then says `not fetched` |
|
|
272
308
|
|
|
@@ -312,6 +348,20 @@ On yes it runs only the steps it showed: worktrees first, then local branches,
|
|
|
312
348
|
remote branches and a prune, in that order. Branch deletes are
|
|
313
349
|
compare-and-swap, every step is verified, and the first failure stops the run.
|
|
314
350
|
|
|
351
|
+
Every run ends with what it did:
|
|
352
|
+
|
|
353
|
+
```text
|
|
354
|
+
deleted: 1 worktree, 3 local branches, 2 remote branches, 1 stale worktree pruned
|
|
355
|
+
dropped 2: 1 needs-review, 1 duplicate
|
|
356
|
+
status: complete
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
The preview says `would delete:`, and a run that stops at a failed step says
|
|
360
|
+
how many rows it did not reach. `--output json` carries the same counts as
|
|
361
|
+
`summary`. Two clones of one remote list the same remote branches; `apply`
|
|
362
|
+
deletes each once and drops the other clone's row as `duplicate`. Remotes are
|
|
363
|
+
compared by the push URL git uses, with `url.*.insteadOf` applied.
|
|
364
|
+
|
|
315
365
|
A receipt records every step and is rewritten after each one. With
|
|
316
366
|
`--plan plan.json` it goes beside the plan, as
|
|
317
367
|
`plan.receipt-<timestamp>.json`. `--receipt <file>` puts it anywhere. A run
|
package/dist/apply.d.ts
CHANGED
|
@@ -18,8 +18,8 @@ import type { CommandRunner } from '@osdlc/command-seam';
|
|
|
18
18
|
import type { HostHooks } from './hooks.ts';
|
|
19
19
|
import { type ProcessProbe } from './processes.ts';
|
|
20
20
|
import { type Plan } from './plan.ts';
|
|
21
|
-
import type
|
|
22
|
-
import { type Authorization, type SweepProgress } from './types.ts';
|
|
21
|
+
import { type Dropped, type Receipt, type RowRef, type Step } from './receipt.ts';
|
|
22
|
+
import { type Authorization, type SweepItem, type SweepProgress } from './types.ts';
|
|
23
23
|
/** An authorization the caller gave, for one row or (`row: null`) for all. */
|
|
24
24
|
export interface Grant {
|
|
25
25
|
name: Authorization;
|
|
@@ -45,6 +45,20 @@ export interface ApplyOptions extends HostHooks {
|
|
|
45
45
|
/** Limit the run to the plan repository rooted here. */
|
|
46
46
|
repoRoot?: string | undefined;
|
|
47
47
|
now?: () => Date;
|
|
48
|
+
/**
|
|
49
|
+
* Drop each selected row the re-check does not judge `safe-to-delete`.
|
|
50
|
+
* Defaults to true when no rows are requested, so a row to review runs only
|
|
51
|
+
* when it is named. Pass it to keep that guard on a run whose rows came
|
|
52
|
+
* from such a selection, such as the run a preview confirmed.
|
|
53
|
+
*/
|
|
54
|
+
safeOnly?: boolean | undefined;
|
|
55
|
+
/**
|
|
56
|
+
* Items to re-check against instead of sweeping again, keyed by repository
|
|
57
|
+
* root, already annotated: the hook is not applied to them again. For a
|
|
58
|
+
* preview straight after a sweep; the run that deletes should always sweep
|
|
59
|
+
* again.
|
|
60
|
+
*/
|
|
61
|
+
swept?: ReadonlyMap<string, readonly SweepItem[]> | undefined;
|
|
48
62
|
/** Rows dropped before this run, such as by a preview; recorded first in the receipt. */
|
|
49
63
|
dropped?: readonly Dropped[] | undefined;
|
|
50
64
|
/** Passed to the re-sweep's `onProgress`, for a status line. */
|
|
@@ -66,6 +80,13 @@ export declare function parseGrant(text: string): Grant;
|
|
|
66
80
|
export type RowSelection = RowRef & {
|
|
67
81
|
repo?: string;
|
|
68
82
|
};
|
|
83
|
+
/**
|
|
84
|
+
* A network URL as `host[:port]/path`, the same for `git@host:o/r.git`,
|
|
85
|
+
* `ssh://git@host/o/r` and `https://host/o/r/`; the whole path is kept, so
|
|
86
|
+
* nested groups stay apart. Null for a path on this machine, including
|
|
87
|
+
* `file://` and a Windows drive path.
|
|
88
|
+
*/
|
|
89
|
+
export declare function networkRemoteKey(url: string): string | null;
|
|
69
90
|
/**
|
|
70
91
|
* Apply the `requested` rows of `plan` under `grants`. An empty `requested`
|
|
71
92
|
* selects every row of the plan (of `repoRoot`'s repository, when given) and
|
package/dist/apply.js
CHANGED
|
@@ -20,6 +20,7 @@ import { CommandFailed, Git, PUSH_REJECTIONS } from '@osdlc/typed-git';
|
|
|
20
20
|
import { localDeleteCommand, remoteDeleteCommand } from "./branches.js";
|
|
21
21
|
import { busyHolders, defaultProcessProbe, parseLockOwner } from "./processes.js";
|
|
22
22
|
import { PLAN_CATEGORIES, fingerprint, isPlanCategory, sameFingerprint, } from "./plan.js";
|
|
23
|
+
import { RECEIPT_VERSION } from "./receipt.js";
|
|
23
24
|
import { sweepRepo } from "./sweep.js";
|
|
24
25
|
import { AUTHORIZATIONS } from "./types.js";
|
|
25
26
|
import { worktreePruneCommand, worktreeRemoveCommand, worktreeUnlockCommand } from "./worktrees.js";
|
|
@@ -78,6 +79,10 @@ function granted(grants, ref, name) {
|
|
|
78
79
|
}
|
|
79
80
|
/** The plan repository's rows as they stand now, with the host's overlay applied. */
|
|
80
81
|
function resweep(repo, opts) {
|
|
82
|
+
const given = opts.swept?.get(repo.root);
|
|
83
|
+
if (given !== undefined) {
|
|
84
|
+
return [...given];
|
|
85
|
+
}
|
|
81
86
|
const sweep = sweepRepo(repo.root, {
|
|
82
87
|
remote: repo.remote,
|
|
83
88
|
noGh: repo.no_gh,
|
|
@@ -257,6 +262,73 @@ function execute(step, row, git, probe) {
|
|
|
257
262
|
}
|
|
258
263
|
}
|
|
259
264
|
}
|
|
265
|
+
/**
|
|
266
|
+
* A network URL as `host[:port]/path`, the same for `git@host:o/r.git`,
|
|
267
|
+
* `ssh://git@host/o/r` and `https://host/o/r/`; the whole path is kept, so
|
|
268
|
+
* nested groups stay apart. Null for a path on this machine, including
|
|
269
|
+
* `file://` and a Windows drive path.
|
|
270
|
+
*/
|
|
271
|
+
export function networkRemoteKey(url) {
|
|
272
|
+
const u = url.trim();
|
|
273
|
+
if (/^[a-z]:[\\/]/i.test(u) || /^file:/i.test(u))
|
|
274
|
+
return null;
|
|
275
|
+
const uri = /^[a-z][a-z0-9+.-]*:\/\/(?:[^@/]+@)?([^/:]+)(?::(\d+))?(\/.*)?$/i.exec(u);
|
|
276
|
+
const scp = uri === null ? /^(?:[^@/:]+@)?([^/:]+):(?!\/\/)(.+)$/.exec(u) : null;
|
|
277
|
+
const host = uri?.[1] ?? scp?.[1];
|
|
278
|
+
if (host === undefined)
|
|
279
|
+
return null;
|
|
280
|
+
const port = uri?.[2] === undefined ? '' : `:${uri[2]}`;
|
|
281
|
+
const path = (uri?.[3] ?? scp?.[2] ?? '').replace(/^\/+|\/+$/g, '').replace(/\.git$/, '');
|
|
282
|
+
return `${host.toLowerCase()}${port}/${path}`;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* One string per remote repository, so two clones that push to the same one
|
|
286
|
+
* agree. Built from the push URLs git would use (`remote get-url --push
|
|
287
|
+
* --all`, which applies `url.*.insteadOf` and `pushInsteadOf`): a network URL
|
|
288
|
+
* by {@link networkRemoteKey}, a local path resolved against the repository
|
|
289
|
+
* and kept as written, `.git` and all. Null when git reports no URL; such a
|
|
290
|
+
* row is never treated as a duplicate.
|
|
291
|
+
*/
|
|
292
|
+
function remoteKey(git, repo, remote) {
|
|
293
|
+
const urls = git.at(repo).try.remoteGetUrls(remote, { push: true });
|
|
294
|
+
if (!urls.ok || urls.value === null || urls.value.length === 0)
|
|
295
|
+
return null;
|
|
296
|
+
const keys = urls.value.map((url) => networkRemoteKey(url) ?? resolve(repo, url.trim().replace(/^file:\/\/(localhost)?/i, '')));
|
|
297
|
+
return [...new Set(keys)].sort().join('\n');
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Keep the first row for each remote branch. Two clones of one remote each
|
|
301
|
+
* list its branches; deleting one twice fails the second push and stops the
|
|
302
|
+
* run, so the later row is dropped as `duplicate`.
|
|
303
|
+
*/
|
|
304
|
+
function onePerRemoteBranch({ accepted, dropped }, git) {
|
|
305
|
+
const seen = new Map();
|
|
306
|
+
const kept = [];
|
|
307
|
+
const extra = [];
|
|
308
|
+
for (const row of accepted) {
|
|
309
|
+
if (row.item.category !== 'remote_branch') {
|
|
310
|
+
kept.push(row);
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
const remote = remoteKey(git, row.repo, String(row.item.facts['remote']));
|
|
314
|
+
const key = remote === null ? null : `${remote}\0${String(row.item.facts['branch'])}`;
|
|
315
|
+
const first = key === null ? undefined : seen.get(key);
|
|
316
|
+
if (first !== undefined) {
|
|
317
|
+
extra.push({
|
|
318
|
+
repo: row.repo,
|
|
319
|
+
category: row.item.category,
|
|
320
|
+
id: row.item.id,
|
|
321
|
+
reason: 'duplicate',
|
|
322
|
+
detail: `the same remote branch is deleted from ${first}`,
|
|
323
|
+
});
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (key !== null)
|
|
327
|
+
seen.set(key, row.repo);
|
|
328
|
+
kept.push(row);
|
|
329
|
+
}
|
|
330
|
+
return { accepted: kept, dropped: [...dropped, ...extra] };
|
|
331
|
+
}
|
|
260
332
|
/**
|
|
261
333
|
* Apply the `requested` rows of `plan` under `grants`. An empty `requested`
|
|
262
334
|
* selects every row of the plan (of `repoRoot`'s repository, when given) and
|
|
@@ -286,7 +358,7 @@ export function applyPlan(plan, requested, grants, opts) {
|
|
|
286
358
|
}
|
|
287
359
|
}
|
|
288
360
|
const receipt = {
|
|
289
|
-
version:
|
|
361
|
+
version: RECEIPT_VERSION,
|
|
290
362
|
plan: opts.planPath,
|
|
291
363
|
status: opts.dryRun ? 'dry-run' : 'applying',
|
|
292
364
|
started_at: now().toISOString(),
|
|
@@ -294,7 +366,8 @@ export function applyPlan(plan, requested, grants, opts) {
|
|
|
294
366
|
dropped: [],
|
|
295
367
|
steps: [],
|
|
296
368
|
};
|
|
297
|
-
const
|
|
369
|
+
const rechecked = recheck(selected, grants, opts.safeOnly ?? requested.length === 0, opts);
|
|
370
|
+
const { accepted, dropped } = onePerRemoteBranch(rechecked, new Git(opts.callerCwd, { runner: opts.git }));
|
|
298
371
|
receipt.dropped = [...(opts.dropped ?? []), ...dropped];
|
|
299
372
|
const ordered = PLAN_CATEGORIES.flatMap((category) => accepted.filter((row) => row.item.category === category));
|
|
300
373
|
const plannedSteps = ordered.flatMap((row) => stepsFor(row).map((step) => [step, row]));
|
package/dist/branches.d.ts
CHANGED
|
@@ -27,6 +27,16 @@ export declare function recordPr(facts: Record<string, unknown>, pr: PRInfo | un
|
|
|
27
27
|
};
|
|
28
28
|
/** One sentence on why a merged PR did not count, for `investigationParts`. */
|
|
29
29
|
export declare function gapSentence(pr: PRInfo, gap: PrEvidenceGap, repo: RepoInfo): string;
|
|
30
|
+
/**
|
|
31
|
+
* `facts.merged`: the work is on the default branch, by an exact-head merged
|
|
32
|
+
* PR, by ancestry or by a patch match. Every category records it this way, so
|
|
33
|
+
* `summary` counts one definition.
|
|
34
|
+
*/
|
|
35
|
+
export declare function mergedFact(e: {
|
|
36
|
+
landed: boolean;
|
|
37
|
+
ancestor: boolean;
|
|
38
|
+
patchLanded: boolean;
|
|
39
|
+
}): boolean;
|
|
30
40
|
/** `HEAD` as the branch checked out at `git`'s directory; any other name as given. */
|
|
31
41
|
export declare function resolveBranch(value: string, git: Git): string;
|
|
32
42
|
/**
|
package/dist/branches.js
CHANGED
|
@@ -59,6 +59,14 @@ export function gapSentence(pr, gap, repo) {
|
|
|
59
59
|
? `PR #${pr.number} merged into \`${pr.baseRef}\`, not \`${repo.mainBranch}\`, so it is not landing proof (other-base).`
|
|
60
60
|
: `PR #${pr.number} merged at head ${pr.headOid ?? '(unknown)'}, which is not this tip, so it is not landing proof (head-moved).`;
|
|
61
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* `facts.merged`: the work is on the default branch, by an exact-head merged
|
|
64
|
+
* PR, by ancestry or by a patch match. Every category records it this way, so
|
|
65
|
+
* `summary` counts one definition.
|
|
66
|
+
*/
|
|
67
|
+
export function mergedFact(e) {
|
|
68
|
+
return e.landed || e.ancestor || e.patchLanded;
|
|
69
|
+
}
|
|
62
70
|
/** What deleting a branch needs: its unlanded commits, and its open PR. */
|
|
63
71
|
function branchRequires(unmerged, pr) {
|
|
64
72
|
return authorizationsFor({
|
|
@@ -152,7 +160,11 @@ export function classifyLocalBranch(opts) {
|
|
|
152
160
|
// `soft`: a missing default ref reads as not merged.
|
|
153
161
|
const pr = prs.get(name);
|
|
154
162
|
const { landed } = recordPr(facts, pr, sha, repo);
|
|
155
|
-
facts['merged'] =
|
|
163
|
+
facts['merged'] = mergedFact({
|
|
164
|
+
landed,
|
|
165
|
+
ancestor: git.at(repo.root).soft.isAncestor(sha, landedRef(repo)),
|
|
166
|
+
patchLanded: false,
|
|
167
|
+
});
|
|
156
168
|
return pinned(held);
|
|
157
169
|
}
|
|
158
170
|
// Work merged only into an unpushed local default has not landed. A missing
|
|
@@ -172,8 +184,7 @@ export function classifyLocalBranch(opts) {
|
|
|
172
184
|
recordLanding(facts, patch);
|
|
173
185
|
const patchLanded = patch !== null && patch.check !== null;
|
|
174
186
|
const unmerged = !landed && !isAncestor && !landingConfirmed(patch);
|
|
175
|
-
|
|
176
|
-
facts['merged'] = landed || isAncestor || patchLanded;
|
|
187
|
+
facts['merged'] = mergedFact({ landed, ancestor: isAncestor, patchLanded });
|
|
177
188
|
if (!landed && !isAncestor) {
|
|
178
189
|
facts['unique_commits'] = git.at(repo.root).aheadBehind(repo.mainBranch, sha).ahead;
|
|
179
190
|
}
|
|
@@ -297,8 +308,7 @@ export function classifyRemoteBranch(opts) {
|
|
|
297
308
|
recordLanding(facts, patch);
|
|
298
309
|
const patchLanded = patch !== null && patch.check !== null;
|
|
299
310
|
const unmerged = !landed && !isAncestor && !landingConfirmed(patch);
|
|
300
|
-
|
|
301
|
-
facts['merged'] = landed || isAncestor || patchLanded;
|
|
311
|
+
facts['merged'] = mergedFact({ landed, ancestor: isAncestor, patchLanded });
|
|
302
312
|
if (!landed && !isAncestor) {
|
|
303
313
|
// Unknown (null) when the clone lacks `sha` or `remoteMain`, as above.
|
|
304
314
|
const counted = git.at(repo.root).try.aheadBehind(remoteMain, sha);
|
package/dist/cli.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Four verbs over one set of sweep filters:
|
|
5
5
|
*
|
|
6
6
|
* status (the default) inventory and classify; read-only but for the fetch
|
|
7
|
-
* summary the same sweep as counts: totals, merged, delete, review, keep
|
|
7
|
+
* summary the same sweep as counts: totals, merged, delete, prune, review, keep
|
|
8
8
|
* plan write the deletable rows as a plan, to stdout or a file
|
|
9
9
|
* apply re-check, show the steps, ask, then delete
|
|
10
10
|
*
|