@osdlc/repo-cleaner 0.9.2 → 0.10.1
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 +55 -0
- package/README.md +89 -62
- package/dist/apply.d.ts +45 -7
- package/dist/apply.js +118 -14
- package/dist/branches.d.ts +12 -2
- package/dist/branches.js +34 -8
- package/dist/cli.d.ts +26 -7
- package/dist/cli.js +472 -184
- package/dist/devlink.d.ts +5 -0
- package/dist/devlink.js +3 -1
- package/dist/index.d.ts +5 -6
- package/dist/index.js +3 -4
- package/dist/plan.d.ts +7 -3
- package/dist/plan.js +24 -11
- package/dist/receipt.d.ts +6 -0
- package/dist/receipt.js +11 -2
- package/dist/render.d.ts +33 -1
- package/dist/render.js +134 -1
- package/dist/status-line.d.ts +24 -0
- package/dist/status-line.js +54 -0
- package/dist/sweep.d.ts +8 -1
- package/dist/sweep.js +17 -4
- package/dist/types.d.ts +15 -0
- package/dist/types.js +3 -1
- package/dist/version.generated.d.ts +1 -1
- package/dist/version.generated.js +1 -1
- package/dist/worktrees.d.ts +1 -1
- package/dist/worktrees.js +21 -5
- package/package.json +7 -7
- package/dist/config.d.ts +0 -53
- package/dist/config.js +0 -67
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({
|
|
@@ -95,10 +103,11 @@ export function remoteDeleteCommand(remote, name, sha) {
|
|
|
95
103
|
]);
|
|
96
104
|
}
|
|
97
105
|
// ── local branches ─────────────────────────────────────────────────────────
|
|
98
|
-
export function gatherLocalBranches(repo, prs, worktreeBranches, git, only) {
|
|
106
|
+
export function gatherLocalBranches(repo, prs, worktreeBranches, git, only, tick) {
|
|
99
107
|
const rows = git.at(repo.root).forEachRef('refs/heads/', { format: LOCAL_FORMAT });
|
|
100
108
|
const items = [];
|
|
101
|
-
for (const line of rows) {
|
|
109
|
+
for (const [i, line] of rows.entries()) {
|
|
110
|
+
tick?.(i, rows.length);
|
|
102
111
|
const parts = line.split('\t');
|
|
103
112
|
if (parts.length !== 5)
|
|
104
113
|
continue;
|
|
@@ -140,10 +149,24 @@ export function classifyLocalBranch(opts) {
|
|
|
140
149
|
const pinned = (reason) => keep(reason, { deletable: false });
|
|
141
150
|
if (name === repo.mainBranch)
|
|
142
151
|
return pinned('default branch');
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
152
|
+
const held = name === repo.currentBranch
|
|
153
|
+
? 'current HEAD'
|
|
154
|
+
: worktreeBranches.has(name)
|
|
155
|
+
? 'checked out in a worktree (handle via that worktree)'
|
|
156
|
+
: null;
|
|
157
|
+
if (held !== null) {
|
|
158
|
+
// Never deletable here, but still counted as merged or not: by ancestry or
|
|
159
|
+
// an exact-head merged PR, without the patch checks a deletable row gets.
|
|
160
|
+
// `soft`: a missing default ref reads as not merged.
|
|
161
|
+
const pr = prs.get(name);
|
|
162
|
+
const { landed } = recordPr(facts, pr, sha, repo);
|
|
163
|
+
facts['merged'] = mergedFact({
|
|
164
|
+
landed,
|
|
165
|
+
ancestor: git.at(repo.root).soft.isAncestor(sha, landedRef(repo)),
|
|
166
|
+
patchLanded: false,
|
|
167
|
+
});
|
|
168
|
+
return pinned(held);
|
|
169
|
+
}
|
|
147
170
|
// Work merged only into an unpushed local default has not landed. A missing
|
|
148
171
|
// remote-tracking ref exits 128: ancestry unknown (null).
|
|
149
172
|
const compareRef = landedRef(repo);
|
|
@@ -161,6 +184,7 @@ export function classifyLocalBranch(opts) {
|
|
|
161
184
|
recordLanding(facts, patch);
|
|
162
185
|
const patchLanded = patch !== null && patch.check !== null;
|
|
163
186
|
const unmerged = !landed && !isAncestor && !landingConfirmed(patch);
|
|
187
|
+
facts['merged'] = mergedFact({ landed, ancestor: isAncestor, patchLanded });
|
|
164
188
|
if (!landed && !isAncestor) {
|
|
165
189
|
facts['unique_commits'] = git.at(repo.root).aheadBehind(repo.mainBranch, sha).ahead;
|
|
166
190
|
}
|
|
@@ -230,7 +254,7 @@ export function classifyLocalBranch(opts) {
|
|
|
230
254
|
});
|
|
231
255
|
}
|
|
232
256
|
// ── remote branches ────────────────────────────────────────────────────────
|
|
233
|
-
export function gatherRemoteBranches(repo, prs, localBranchNames, git, only) {
|
|
257
|
+
export function gatherRemoteBranches(repo, prs, localBranchNames, git, only, tick) {
|
|
234
258
|
if (repo.remote === null)
|
|
235
259
|
return [];
|
|
236
260
|
// Default door: an unreachable remote throws rather than rendering as "the
|
|
@@ -238,7 +262,8 @@ export function gatherRemoteBranches(repo, prs, localBranchNames, git, only) {
|
|
|
238
262
|
const pattern = only === undefined ? undefined : `refs/heads/${only}`;
|
|
239
263
|
const rows = git.at(repo.root).lsRemote(repo.remote, pattern, { heads: true });
|
|
240
264
|
const items = [];
|
|
241
|
-
for (const { sha, ref } of rows) {
|
|
265
|
+
for (const [i, { sha, ref }] of rows.entries()) {
|
|
266
|
+
tick?.(i, rows.length);
|
|
242
267
|
if (!ref.startsWith('refs/heads/'))
|
|
243
268
|
continue;
|
|
244
269
|
const name = ref.slice('refs/heads/'.length);
|
|
@@ -283,6 +308,7 @@ export function classifyRemoteBranch(opts) {
|
|
|
283
308
|
recordLanding(facts, patch);
|
|
284
309
|
const patchLanded = patch !== null && patch.check !== null;
|
|
285
310
|
const unmerged = !landed && !isAncestor && !landingConfirmed(patch);
|
|
311
|
+
facts['merged'] = mergedFact({ landed, ancestor: isAncestor, patchLanded });
|
|
286
312
|
if (!landed && !isAncestor) {
|
|
287
313
|
// Unknown (null) when the clone lacks `sha` or `remoteMain`, as above.
|
|
288
314
|
const counted = git.at(repo.root).try.aheadBehind(remoteMain, sha);
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,19 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `repo-cleaner` — what has been merged, and what can go away.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Four verbs over one set of sweep filters:
|
|
5
|
+
*
|
|
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
|
|
8
|
+
* plan write the deletable rows as a plan, to stdout or a file
|
|
9
|
+
* apply re-check, show the steps, ask, then delete
|
|
10
|
+
*
|
|
11
|
+
* `apply` works from a saved plan (`--plan`) or sweeps first. Either way it
|
|
12
|
+
* re-checks every row, prints the steps it would run and asks before deleting;
|
|
13
|
+
* `--yes` skips the question. Without a terminal and without `--yes` it
|
|
14
|
+
* refuses rather than deleting. Scope is opt-in: the sweep covers the
|
|
15
|
+
* repository you are standing in unless a path says otherwise.
|
|
9
16
|
*
|
|
10
17
|
* Exit codes:
|
|
11
|
-
* 0 swept; or
|
|
18
|
+
* 0 swept; or apply ran, was declined, or had nothing to run
|
|
12
19
|
* 1 failed against the state it found
|
|
13
20
|
* 2 cannot run as typed
|
|
14
21
|
* 10 nothing to sweep: no entry named a repository
|
|
15
22
|
* 11 apply stopped at a failed step; the receipt says which
|
|
16
23
|
*/
|
|
17
24
|
import { type CliContext } from '@osdlc/cli-tool';
|
|
25
|
+
/**
|
|
26
|
+
* Asks a yes/no question, or returns null when there is no one to ask. `main`
|
|
27
|
+
* takes it as a parameter so a test can answer.
|
|
28
|
+
*/
|
|
29
|
+
export type Confirm = (question: string) => boolean | null;
|
|
30
|
+
export interface MainDeps {
|
|
31
|
+
confirm?: Confirm;
|
|
32
|
+
/** Reads `--plan -`. Defaults to stdin. */
|
|
33
|
+
readStdin?: () => string;
|
|
34
|
+
/** Whether stderr is a terminal, so a status line may be drawn. Defaults to `isatty(2)`. */
|
|
35
|
+
stderrIsTerminal?: boolean;
|
|
36
|
+
}
|
|
18
37
|
/** `main(argv, ctx)`: returns an exit code, never exits. */
|
|
19
|
-
export declare function main(argv: readonly string[], ctx: CliContext): number;
|
|
38
|
+
export declare function main(argv: readonly string[], ctx: CliContext, deps?: MainDeps): number;
|