@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/dist/types.d.ts
CHANGED
|
@@ -1,43 +1,219 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The inventory a sweep produces, and the vocabulary it is described in.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* A sweep returns rows. Each row is one thing that could be deleted — a
|
|
5
|
+
* worktree, a local branch, a remote branch — with the sweep's verdict on it,
|
|
6
|
+
* the action that would delete it, the risks that action carries, and the
|
|
7
|
+
* evidence behind it all. A host that knows more than git does rewrites rows
|
|
8
|
+
* through the `annotate` hook rather than reaching into classification.
|
|
7
9
|
*/
|
|
10
|
+
import type { LandingCheck } from './landing.ts';
|
|
11
|
+
import type { Plan } from './plan.ts';
|
|
12
|
+
import type { ProcessHolder } from './processes.ts';
|
|
8
13
|
import type { ProviderStatus } from './providers/types.ts';
|
|
9
|
-
/**
|
|
10
|
-
export
|
|
11
|
-
/**
|
|
12
|
-
export type
|
|
14
|
+
/** The kinds of row, in the order a run acts on them. */
|
|
15
|
+
export declare const KINDS: readonly ["worktree", "branch", "remote"];
|
|
16
|
+
/** A worktree, a local branch, or a branch on a remote. */
|
|
17
|
+
export type Kind = (typeof KINDS)[number];
|
|
13
18
|
/**
|
|
14
|
-
*
|
|
15
|
-
* `
|
|
16
|
-
* count items within a step that walks many.
|
|
19
|
+
* A row's name with its kind in front: `worktree:<path>`, `branch:<name>`,
|
|
20
|
+
* `remote:<remote>/<name>`. Unique within a repository.
|
|
17
21
|
*/
|
|
18
|
-
export
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
22
|
+
export type RowId = `${Kind}:${string}`;
|
|
23
|
+
/** The row id of `name` in `kind`. */
|
|
24
|
+
export declare function rowId(kind: Kind, name: string): RowId;
|
|
25
|
+
/**
|
|
26
|
+
* `<kind>:<name>` split, or null when `text` has no kind in front. A
|
|
27
|
+
* one-letter prefix is a Windows drive (`C:\\…`), not a kind.
|
|
28
|
+
*/
|
|
29
|
+
export declare function parseRowId(text: string): {
|
|
30
|
+
kind: Kind;
|
|
31
|
+
name: string;
|
|
32
|
+
} | null;
|
|
33
|
+
/**
|
|
34
|
+
* What the sweep concluded about a row.
|
|
35
|
+
*
|
|
36
|
+
* `delete` its work is on the default branch, or it is otherwise safe to lose
|
|
37
|
+
* `review` the signals are mixed, or it holds work; deleted only when picked
|
|
38
|
+
* `keep` structural (the default branch, the current checkout), or in use
|
|
39
|
+
* (an open pull request); a kept row with an action is deleted only
|
|
40
|
+
* when picked
|
|
41
|
+
* `blocked` something rules removal out; never deleted, whatever is granted
|
|
42
|
+
*/
|
|
43
|
+
export declare const VERDICTS: readonly ["delete", "review", "keep", "blocked"];
|
|
44
|
+
export type Verdict = (typeof VERDICTS)[number];
|
|
45
|
+
/**
|
|
46
|
+
* What deleting a row does: `remove` a worktree, `prune` the stale record of
|
|
47
|
+
* a worktree whose directory is gone, `delete` a branch. Null on a row that
|
|
48
|
+
* can never be deleted.
|
|
49
|
+
*/
|
|
50
|
+
export declare const ACTIONS: readonly ["remove", "prune", "delete"];
|
|
51
|
+
export type Action = (typeof ACTIONS)[number];
|
|
31
52
|
/**
|
|
32
|
-
* A risk
|
|
33
|
-
* `abandon-unmerged` drops commits not on the default branch or a
|
|
34
|
-
*
|
|
35
|
-
* `
|
|
53
|
+
* A risk an action carries, which a person approves by name:
|
|
54
|
+
* `abandon-unmerged` drops commits not on the default branch, or a worktree's
|
|
55
|
+
* (or its submodules') commits and stashes no remote has
|
|
56
|
+
* `discard-changes` drops modified, staged or untracked files in a worktree
|
|
57
|
+
* or any of its submodules
|
|
58
|
+
* `confirm-open-pr` the branch has an open pull request; deleting the remote
|
|
59
|
+
* branch closes it
|
|
36
60
|
*/
|
|
37
|
-
export declare const
|
|
38
|
-
export type
|
|
39
|
-
/** The
|
|
40
|
-
export declare function
|
|
61
|
+
export declare const RISKS: readonly ["abandon-unmerged", "discard-changes", "confirm-open-pr"];
|
|
62
|
+
export type Risk = (typeof RISKS)[number];
|
|
63
|
+
/** The risks whose flag is set, in canonical order. */
|
|
64
|
+
export declare function risksFor(flags: Partial<Record<Risk, boolean>>): Risk[];
|
|
65
|
+
/** A condition that rules out removing a worktree, whatever is granted. */
|
|
66
|
+
export type Blocker = 'locked-worktree' | 'live-process' | 'process-probe-failed' | 'hidden-index-flags' | 'nested-repository' | 'branch-in-other-worktree' | 'submodule-unreadable';
|
|
67
|
+
/** Why a merged PR does not prove that a tip landed. */
|
|
68
|
+
export type PrEvidenceGap = 'other-base' | 'head-moved';
|
|
69
|
+
/** A pull request, reduced to what classification consults. */
|
|
70
|
+
export interface PRInfo {
|
|
71
|
+
number: number;
|
|
72
|
+
state: string;
|
|
73
|
+
mergedAt: string | null;
|
|
74
|
+
headRef: string;
|
|
75
|
+
/** The PR's head commit; null when the provider did not report one. */
|
|
76
|
+
headOid: string | null;
|
|
77
|
+
/** The branch the PR targets. */
|
|
78
|
+
baseRef: string;
|
|
79
|
+
title: string;
|
|
80
|
+
url: string;
|
|
81
|
+
}
|
|
82
|
+
/** A row's pull request, as evidence: the PR without its head branch, which the row names already. */
|
|
83
|
+
export type PrEvidence = Omit<PRInfo, 'headRef'> & {
|
|
84
|
+
/** For a merged PR: `exact-head` when it proves the tip landed, otherwise the gap. */
|
|
85
|
+
proof?: 'exact-head' | PrEvidenceGap;
|
|
86
|
+
};
|
|
87
|
+
/** Evidence every kind of row records about whether its work landed. */
|
|
88
|
+
export interface LandingEvidence {
|
|
89
|
+
pr?: PrEvidence;
|
|
90
|
+
/** The work is on the default branch: by ancestry, an exact-head merged PR, or a patch match. Absent on a row not judged. */
|
|
91
|
+
merged?: boolean;
|
|
92
|
+
/** Commits not on the default branch; null when they could not be counted. */
|
|
93
|
+
uniqueCommits?: number | null;
|
|
94
|
+
/** Set when the work landed under other commit ids. */
|
|
95
|
+
landing?: 'patch-equivalent';
|
|
96
|
+
/** Which patch check passed. */
|
|
97
|
+
landingCheck?: LandingCheck;
|
|
98
|
+
/** True when `git merge-tree` agreed the content is on the default branch. */
|
|
99
|
+
landingContentConfirmed?: boolean;
|
|
100
|
+
/** Commits of a partly landed branch that have no patch-equivalent commit. */
|
|
101
|
+
unmatchedCommits?: string[];
|
|
102
|
+
}
|
|
103
|
+
/** How a branch shows it was worked on, for a worktree merged by ancestry alone. */
|
|
104
|
+
export type BranchUse = 'commits' | 'upstream';
|
|
105
|
+
/** One submodule of a worktree, as evidence. */
|
|
106
|
+
export interface SubmoduleEvidence {
|
|
107
|
+
path: string;
|
|
108
|
+
populated: boolean;
|
|
109
|
+
head: string | null;
|
|
110
|
+
recorded: string | null;
|
|
111
|
+
dirtyFiles: number;
|
|
112
|
+
unpushedCommits: number;
|
|
113
|
+
stashes: number;
|
|
114
|
+
error?: string;
|
|
115
|
+
}
|
|
116
|
+
export interface WorktreeEvidence extends LandingEvidence {
|
|
117
|
+
path: string;
|
|
118
|
+
branch: string | null;
|
|
119
|
+
head: string | null;
|
|
120
|
+
bare: boolean;
|
|
121
|
+
detached: boolean;
|
|
122
|
+
primary: boolean;
|
|
123
|
+
/** The directory is there and is still a checkout. */
|
|
124
|
+
onDisk?: boolean;
|
|
125
|
+
/** The directory is there but is no longer a checkout (its `.git` is gone). */
|
|
126
|
+
directoryLeft?: boolean;
|
|
127
|
+
dirtyFiles?: number;
|
|
128
|
+
unpushedCommits?: number;
|
|
129
|
+
/** What `unpushedCommits` was counted against: the upstream, or the default branch. */
|
|
130
|
+
compareAgainst?: string;
|
|
131
|
+
branchExists?: boolean;
|
|
132
|
+
/** Set for a worktree merged by ancestry alone; null when its branch looks freshly made. */
|
|
133
|
+
branchUsed?: BranchUse | null;
|
|
134
|
+
lockReason?: string | null;
|
|
135
|
+
/** The lock's owner is dead, so the lock is cleared before the remove. */
|
|
136
|
+
staleLock?: boolean;
|
|
137
|
+
lockOwner?: {
|
|
138
|
+
pid: number;
|
|
139
|
+
startedAt: string | null;
|
|
140
|
+
};
|
|
141
|
+
liveProcesses?: ProcessHolder[];
|
|
142
|
+
idleShells?: ProcessHolder[];
|
|
143
|
+
processProbeError?: string;
|
|
144
|
+
/** Count of skip-worktree and assume-unchanged files. */
|
|
145
|
+
hiddenIndexFlags?: number;
|
|
146
|
+
/** The first ten of them. */
|
|
147
|
+
hiddenIndexPaths?: string[];
|
|
148
|
+
nestedRepositories?: string[];
|
|
149
|
+
otherWorktrees?: string[];
|
|
150
|
+
submodules?: SubmoduleEvidence[];
|
|
151
|
+
submoduleDirtyFiles?: number;
|
|
152
|
+
/** Its submodules make plain `git worktree remove` refuse, so the remove is forced. */
|
|
153
|
+
forcedForSubmodules?: boolean;
|
|
154
|
+
}
|
|
155
|
+
export interface BranchEvidence extends LandingEvidence {
|
|
156
|
+
branch: string;
|
|
157
|
+
upstream: string | null;
|
|
158
|
+
track: string | null;
|
|
159
|
+
lastCommit: string;
|
|
160
|
+
sha: string;
|
|
161
|
+
compareAgainst?: string;
|
|
162
|
+
/** Null when the remote default branch was not found. */
|
|
163
|
+
ancestorOfRemoteMain?: boolean | null;
|
|
164
|
+
/** The worktree whose removal frees this branch; it is deleted only when that worktree goes in the same run. */
|
|
165
|
+
worktree?: string;
|
|
166
|
+
}
|
|
167
|
+
export interface RemoteEvidence extends LandingEvidence {
|
|
168
|
+
branch: string;
|
|
169
|
+
remote: string;
|
|
170
|
+
sha: string;
|
|
171
|
+
/** A local branch has the same name. */
|
|
172
|
+
hasLocal: boolean;
|
|
173
|
+
ancestorOfRemoteMain?: boolean;
|
|
174
|
+
}
|
|
175
|
+
interface RowCommon {
|
|
176
|
+
id: RowId;
|
|
177
|
+
/** The row's name within its kind: a worktree's path, a branch name, `<remote>/<branch>`. */
|
|
178
|
+
name: string;
|
|
179
|
+
verdict: Verdict;
|
|
180
|
+
/** What deleting it does; null when it can never be deleted. */
|
|
181
|
+
action: Action | null;
|
|
182
|
+
/** One line, already phrased for a report. */
|
|
183
|
+
why: string;
|
|
184
|
+
/** What its action would lose, which a person must allow by name. */
|
|
185
|
+
risks: Risk[];
|
|
186
|
+
/** Non-empty exactly when `verdict` is `blocked`. */
|
|
187
|
+
blockers: Blocker[];
|
|
188
|
+
/** The literal commands that would carry out `action`. Never run by the sweep. */
|
|
189
|
+
commands: string[];
|
|
190
|
+
/**
|
|
191
|
+
* Sentences on what is unclear or what to do about a blocker. A list, so a
|
|
192
|
+
* host appends its own before anything is rendered.
|
|
193
|
+
*/
|
|
194
|
+
notes: string[];
|
|
195
|
+
}
|
|
196
|
+
export interface WorktreeRow extends RowCommon {
|
|
197
|
+
kind: 'worktree';
|
|
198
|
+
evidence: WorktreeEvidence;
|
|
199
|
+
}
|
|
200
|
+
export interface BranchRow extends RowCommon {
|
|
201
|
+
kind: 'branch';
|
|
202
|
+
evidence: BranchEvidence;
|
|
203
|
+
}
|
|
204
|
+
export interface RemoteRow extends RowCommon {
|
|
205
|
+
kind: 'remote';
|
|
206
|
+
evidence: RemoteEvidence;
|
|
207
|
+
}
|
|
208
|
+
/** One thing that could be deleted, and the sweep's verdict on it. */
|
|
209
|
+
export type Row = WorktreeRow | BranchRow | RemoteRow;
|
|
210
|
+
/** A row whose deletion only prunes a stale worktree record. */
|
|
211
|
+
export declare function isPrune(row: Pick<Row, 'verdict' | 'action'>): boolean;
|
|
212
|
+
type Defaulted = 'commands' | 'notes' | 'risks' | 'blockers';
|
|
213
|
+
/** The fields a classifier supplies for a row of type `R`; kind, name and evidence it fixes once. */
|
|
214
|
+
export type RowFields<R extends Row> = Omit<R, Defaulted | 'id' | 'kind' | 'name' | 'evidence'> & Partial<Pick<R, Defaulted>>;
|
|
215
|
+
/** Build a row, defaulting its lists to empty. */
|
|
216
|
+
export declare function makeRow<R extends Row>(partial: Omit<R, Defaulted | 'id'> & Partial<Pick<R, Defaulted>>): R;
|
|
41
217
|
/** What the sweep learned about a repository before inventorying it. */
|
|
42
218
|
export interface RepoInfo {
|
|
43
219
|
/** The main checkout's root, even when the sweep was pointed at a worktree. */
|
|
@@ -51,9 +227,8 @@ export interface RepoInfo {
|
|
|
51
227
|
remote: string | null;
|
|
52
228
|
currentBranch: string;
|
|
53
229
|
/**
|
|
54
|
-
* The provider asked for pull requests and what its probe found
|
|
55
|
-
*
|
|
56
|
-
* (`--provider none`, or a sweep without `--remotes`).
|
|
230
|
+
* The provider asked for pull requests and what its probe found. Null when
|
|
231
|
+
* none was asked (`provider: 'none'`, or a sweep without remote branches).
|
|
57
232
|
*/
|
|
58
233
|
provider: ProviderStatus | null;
|
|
59
234
|
/** True when the sweep fetched the remote before classifying. */
|
|
@@ -63,70 +238,48 @@ export interface RepoInfo {
|
|
|
63
238
|
export declare function landedRef(repo: RepoInfo): string;
|
|
64
239
|
/** True when the repository has a remote the sweep did not fetch, so its refs may be stale. */
|
|
65
240
|
export declare function isUnfetched(repo: RepoInfo): boolean;
|
|
66
|
-
/**
|
|
67
|
-
export
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
title: string;
|
|
79
|
-
url: string;
|
|
80
|
-
}
|
|
81
|
-
export interface SweepItem {
|
|
82
|
-
category: Category;
|
|
83
|
-
id: string;
|
|
84
|
-
classification: Classification;
|
|
85
|
-
action: string;
|
|
86
|
-
/** One line, already phrased for a report. */
|
|
87
|
-
reason: string;
|
|
88
|
-
/** The evidence behind the verdict. A host re-ranking a row reads these. */
|
|
89
|
-
facts: Record<string, unknown>;
|
|
90
|
-
/** Literal commands that would carry out `action`. Never run by this library. */
|
|
91
|
-
commands: string[];
|
|
92
|
-
/**
|
|
93
|
-
* Sentences explaining what is unclear, for a `needs-investigation` row.
|
|
94
|
-
*
|
|
95
|
-
* Parts rather than one string: a host with knowledge this library does not
|
|
96
|
-
* have appends its own sentence before anything is rendered. Empty for a row
|
|
97
|
-
* that needs no investigation.
|
|
98
|
-
*/
|
|
99
|
-
investigationParts: string[];
|
|
100
|
-
/** What deleting or removing this row would need approved. */
|
|
101
|
-
requires: Authorization[];
|
|
102
|
-
/**
|
|
103
|
-
* False for a row kept for structure, such as the default branch, and for a
|
|
104
|
-
* blocked worktree. Never offered for deletion.
|
|
105
|
-
*/
|
|
106
|
-
deletable: boolean;
|
|
241
|
+
/** Rows of one kind, counted by what the sweep concluded. */
|
|
242
|
+
export interface Bucket {
|
|
243
|
+
total: number;
|
|
244
|
+
/** Rows whose work is on the default branch. Rows kept for structure are not judged. */
|
|
245
|
+
merged: number;
|
|
246
|
+
/** `delete` rows that delete something. */
|
|
247
|
+
delete: number;
|
|
248
|
+
/** `delete` rows that only prune a stale worktree record. */
|
|
249
|
+
prune: number;
|
|
250
|
+
review: number;
|
|
251
|
+
blocked: number;
|
|
252
|
+
keep: number;
|
|
107
253
|
}
|
|
108
|
-
|
|
254
|
+
export type Counts = Record<Kind, Bucket>;
|
|
255
|
+
export declare function emptyBucket(): Bucket;
|
|
256
|
+
/** Per-kind counts over `rows`, every kind present. */
|
|
257
|
+
export declare function countRows(rows: readonly Row[]): Counts;
|
|
258
|
+
/** `counts` summed across kinds. */
|
|
259
|
+
export declare function totalOf(counts: Counts): Bucket;
|
|
260
|
+
/** One repository's rows, or the reason there are none. */
|
|
109
261
|
export interface RepoSweep {
|
|
110
|
-
/** Present when the repository could be read. */
|
|
111
|
-
repo: RepoInfo | null;
|
|
112
262
|
/** The path the sweep was pointed at, whether or not it could be read. */
|
|
113
263
|
path: string;
|
|
114
|
-
|
|
115
|
-
|
|
264
|
+
/** Present when the repository could be read. */
|
|
265
|
+
repo: RepoInfo | null;
|
|
266
|
+
rows: Row[];
|
|
267
|
+
counts: Counts;
|
|
268
|
+
/** Why this repository produced no rows. Null on success. */
|
|
116
269
|
error: string | null;
|
|
117
270
|
}
|
|
118
|
-
/**
|
|
271
|
+
/** A target that named no repository. */
|
|
119
272
|
export interface UnresolvedEntry {
|
|
120
273
|
entry: string;
|
|
121
274
|
reason: string;
|
|
122
275
|
}
|
|
123
276
|
/**
|
|
124
|
-
* What resolving the
|
|
125
|
-
* nothing to find; a
|
|
126
|
-
* wrong
|
|
277
|
+
* What resolving the targets cost. A target that is itself a checkout costs
|
|
278
|
+
* nothing to find; a directory is walked, and a person who points one at the
|
|
279
|
+
* wrong place deserves to be told how much was read.
|
|
127
280
|
*/
|
|
128
281
|
export interface ScanStats {
|
|
129
|
-
/**
|
|
282
|
+
/** Targets that had to be searched, rather than naming a checkout outright. */
|
|
130
283
|
containers: number;
|
|
131
284
|
/** Directories read across those searches. */
|
|
132
285
|
directoriesScanned: number;
|
|
@@ -141,12 +294,18 @@ export interface DuplicateCheckout {
|
|
|
141
294
|
/** Main-checkout paths, sorted. */
|
|
142
295
|
roots: string[];
|
|
143
296
|
}
|
|
144
|
-
/**
|
|
145
|
-
export
|
|
297
|
+
/** The options a sweep ran with, as a plan records them so a re-check runs the same sweep. */
|
|
298
|
+
export type SweptWith = Plan['options'];
|
|
299
|
+
/** Every repository swept in one run, plus the targets that named none. */
|
|
300
|
+
export interface Sweep {
|
|
146
301
|
repos: RepoSweep[];
|
|
147
302
|
unresolved: UnresolvedEntry[];
|
|
148
303
|
scan: ScanStats;
|
|
149
304
|
duplicates: DuplicateCheckout[];
|
|
305
|
+
/** Every repository's counts, summed per kind. */
|
|
306
|
+
counts: Counts;
|
|
307
|
+
/** What was swept, for a plan. */
|
|
308
|
+
options: SweptWith;
|
|
150
309
|
}
|
|
151
310
|
/**
|
|
152
311
|
* Slugs shared by two or more of `repos`, roots sorted. A null entry or a
|
|
@@ -157,22 +316,34 @@ export declare function findDuplicateCheckouts(repos: readonly ({
|
|
|
157
316
|
slug: string | null;
|
|
158
317
|
} | null)[]): DuplicateCheckout[];
|
|
159
318
|
export declare function emptyScan(): ScanStats;
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
319
|
+
/**
|
|
320
|
+
* What a sweep is doing, for a status line. `repo` is the path being swept;
|
|
321
|
+
* `index` and `of` place it among the run's repositories. `done` and `total`
|
|
322
|
+
* count rows within a step that walks many.
|
|
323
|
+
*/
|
|
324
|
+
export interface SweepProgress {
|
|
325
|
+
repo: string;
|
|
326
|
+
index: number;
|
|
327
|
+
of: number;
|
|
328
|
+
step: 'discover' | 'fetch' | 'prs' | Kind;
|
|
329
|
+
done?: number;
|
|
330
|
+
total?: number;
|
|
331
|
+
/** The CLI asked during the `prs` step: `gh`, `glab`. */
|
|
332
|
+
tool?: string;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Why the library refused. Nothing has been touched when it throws.
|
|
336
|
+
*
|
|
337
|
+
* `conflict` options that contradict each other, or one that would have no effect
|
|
338
|
+
* `bad-input` a malformed row id or an unknown risk
|
|
339
|
+
* `not-found` a picked, spared or allowed row the plan does not have
|
|
340
|
+
* `ambiguous` a row id that matches rows in more than one repository
|
|
341
|
+
* `spared-changed` a spared remote branch is gone or moved since the plan
|
|
342
|
+
* `bad-plan` a plan that cannot be read, or one from another plan version
|
|
343
|
+
*/
|
|
344
|
+
export type ErrorCode = 'conflict' | 'bad-input' | 'not-found' | 'ambiguous' | 'spared-changed' | 'bad-plan';
|
|
345
|
+
export declare class RepoCleanerError extends Error {
|
|
346
|
+
readonly code: ErrorCode;
|
|
347
|
+
constructor(code: ErrorCode, message: string);
|
|
348
|
+
}
|
|
178
349
|
export {};
|
package/dist/types.js
CHANGED
|
@@ -1,22 +1,76 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The inventory a sweep produces, and the vocabulary it is described in.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* A sweep returns rows. Each row is one thing that could be deleted — a
|
|
5
|
+
* worktree, a local branch, a remote branch — with the sweep's verdict on it,
|
|
6
|
+
* the action that would delete it, the risks that action carries, and the
|
|
7
|
+
* evidence behind it all. A host that knows more than git does rewrites rows
|
|
8
|
+
* through the `annotate` hook rather than reaching into classification.
|
|
7
9
|
*/
|
|
8
|
-
/**
|
|
9
|
-
export const
|
|
10
|
+
/** The kinds of row, in the order a run acts on them. */
|
|
11
|
+
export const KINDS = ['worktree', 'branch', 'remote'];
|
|
12
|
+
/** The row id of `name` in `kind`. */
|
|
13
|
+
export function rowId(kind, name) {
|
|
14
|
+
return `${kind}:${name}`;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* `<kind>:<name>` split, or null when `text` has no kind in front. A
|
|
18
|
+
* one-letter prefix is a Windows drive (`C:\\…`), not a kind.
|
|
19
|
+
*/
|
|
20
|
+
export function parseRowId(text) {
|
|
21
|
+
const colon = text.indexOf(':');
|
|
22
|
+
if (colon <= 1)
|
|
23
|
+
return null;
|
|
24
|
+
const kind = text.slice(0, colon);
|
|
25
|
+
if (!KINDS.includes(kind))
|
|
26
|
+
return null;
|
|
27
|
+
return { kind: kind, name: text.slice(colon + 1) };
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* What the sweep concluded about a row.
|
|
31
|
+
*
|
|
32
|
+
* `delete` its work is on the default branch, or it is otherwise safe to lose
|
|
33
|
+
* `review` the signals are mixed, or it holds work; deleted only when picked
|
|
34
|
+
* `keep` structural (the default branch, the current checkout), or in use
|
|
35
|
+
* (an open pull request); a kept row with an action is deleted only
|
|
36
|
+
* when picked
|
|
37
|
+
* `blocked` something rules removal out; never deleted, whatever is granted
|
|
38
|
+
*/
|
|
39
|
+
export const VERDICTS = ['delete', 'review', 'keep', 'blocked'];
|
|
10
40
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* `confirm-open-pr` the branch has an open PR; deleting the remote branch closes it
|
|
41
|
+
* What deleting a row does: `remove` a worktree, `prune` the stale record of
|
|
42
|
+
* a worktree whose directory is gone, `delete` a branch. Null on a row that
|
|
43
|
+
* can never be deleted.
|
|
15
44
|
*/
|
|
16
|
-
export const
|
|
17
|
-
/**
|
|
18
|
-
|
|
19
|
-
|
|
45
|
+
export const ACTIONS = ['remove', 'prune', 'delete'];
|
|
46
|
+
/**
|
|
47
|
+
* A risk an action carries, which a person approves by name:
|
|
48
|
+
* `abandon-unmerged` drops commits not on the default branch, or a worktree's
|
|
49
|
+
* (or its submodules') commits and stashes no remote has
|
|
50
|
+
* `discard-changes` drops modified, staged or untracked files in a worktree
|
|
51
|
+
* or any of its submodules
|
|
52
|
+
* `confirm-open-pr` the branch has an open pull request; deleting the remote
|
|
53
|
+
* branch closes it
|
|
54
|
+
*/
|
|
55
|
+
export const RISKS = ['abandon-unmerged', 'discard-changes', 'confirm-open-pr'];
|
|
56
|
+
/** The risks whose flag is set, in canonical order. */
|
|
57
|
+
export function risksFor(flags) {
|
|
58
|
+
return RISKS.filter((name) => flags[name] === true);
|
|
59
|
+
}
|
|
60
|
+
/** A row whose deletion only prunes a stale worktree record. */
|
|
61
|
+
export function isPrune(row) {
|
|
62
|
+
return row.verdict === 'delete' && row.action === 'prune';
|
|
63
|
+
}
|
|
64
|
+
/** Build a row, defaulting its lists to empty. */
|
|
65
|
+
export function makeRow(partial) {
|
|
66
|
+
return {
|
|
67
|
+
...partial,
|
|
68
|
+
id: rowId(partial.kind, partial.name),
|
|
69
|
+
commands: partial.commands ?? [],
|
|
70
|
+
notes: partial.notes ?? [],
|
|
71
|
+
risks: partial.risks ?? [],
|
|
72
|
+
blockers: partial.blockers ?? [],
|
|
73
|
+
};
|
|
20
74
|
}
|
|
21
75
|
/** The ref a branch must reach to count as landed: `<remote>/<default>`, or the local default without a remote. */
|
|
22
76
|
export function landedRef(repo) {
|
|
@@ -26,6 +80,33 @@ export function landedRef(repo) {
|
|
|
26
80
|
export function isUnfetched(repo) {
|
|
27
81
|
return repo.remote !== null && !repo.fetched;
|
|
28
82
|
}
|
|
83
|
+
export function emptyBucket() {
|
|
84
|
+
return { total: 0, merged: 0, delete: 0, prune: 0, review: 0, blocked: 0, keep: 0 };
|
|
85
|
+
}
|
|
86
|
+
/** Per-kind counts over `rows`, every kind present. */
|
|
87
|
+
export function countRows(rows) {
|
|
88
|
+
const out = Object.fromEntries(KINDS.map((k) => [k, emptyBucket()]));
|
|
89
|
+
for (const row of rows) {
|
|
90
|
+
const bucket = out[row.kind];
|
|
91
|
+
bucket.total += 1;
|
|
92
|
+
if (row.evidence.merged === true)
|
|
93
|
+
bucket.merged += 1;
|
|
94
|
+
if (isPrune(row))
|
|
95
|
+
bucket.prune += 1;
|
|
96
|
+
else
|
|
97
|
+
bucket[row.verdict] += 1;
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
/** `counts` summed across kinds. */
|
|
102
|
+
export function totalOf(counts) {
|
|
103
|
+
const out = emptyBucket();
|
|
104
|
+
for (const kind of KINDS) {
|
|
105
|
+
for (const key of Object.keys(out))
|
|
106
|
+
out[key] += counts[kind][key];
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
29
110
|
/**
|
|
30
111
|
* Slugs shared by two or more of `repos`, roots sorted. A null entry or a
|
|
31
112
|
* repository without a slug is left out.
|
|
@@ -47,46 +128,11 @@ export function findDuplicateCheckouts(repos) {
|
|
|
47
128
|
export function emptyScan() {
|
|
48
129
|
return { containers: 0, directoriesScanned: 0, depthLimited: 0, checkoutsFound: 0 };
|
|
49
130
|
}
|
|
50
|
-
export
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
'
|
|
55
|
-
|
|
56
|
-
'safe-to-keep': 0,
|
|
57
|
-
'needs-investigation': 0,
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
/** Per-category counts over `items`. */
|
|
61
|
-
export function summarize(items) {
|
|
62
|
-
const out = {};
|
|
63
|
-
for (const item of items) {
|
|
64
|
-
const bucket = (out[item.category] ??= emptyBucket());
|
|
65
|
-
bucket.total += 1;
|
|
66
|
-
if (item.facts['merged'] === true)
|
|
67
|
-
bucket.merged += 1;
|
|
68
|
-
if (isPrune(item))
|
|
69
|
-
bucket.prune += 1;
|
|
70
|
-
bucket[item.classification] += 1;
|
|
131
|
+
export class RepoCleanerError extends Error {
|
|
132
|
+
code;
|
|
133
|
+
constructor(code, message) {
|
|
134
|
+
super(message);
|
|
135
|
+
this.name = 'RepoCleanerError';
|
|
136
|
+
this.code = code;
|
|
71
137
|
}
|
|
72
|
-
return out;
|
|
73
|
-
}
|
|
74
|
-
/** A row whose deletion only prunes a stale worktree link. */
|
|
75
|
-
export function isPrune(item) {
|
|
76
|
-
return item.classification === 'safe-to-delete' && item.action === 'prune';
|
|
77
|
-
}
|
|
78
|
-
/** Build an item, defaulting the collection fields and `deletable: true`. */
|
|
79
|
-
export function makeItem(partial) {
|
|
80
|
-
return {
|
|
81
|
-
category: partial.category,
|
|
82
|
-
id: partial.id,
|
|
83
|
-
classification: partial.classification,
|
|
84
|
-
action: partial.action,
|
|
85
|
-
reason: partial.reason,
|
|
86
|
-
facts: partial.facts ?? {},
|
|
87
|
-
commands: partial.commands ?? [],
|
|
88
|
-
investigationParts: partial.investigationParts ?? [],
|
|
89
|
-
requires: partial.requires ?? [],
|
|
90
|
-
deletable: partial.deletable ?? true,
|
|
91
|
-
};
|
|
92
138
|
}
|
package/dist/util.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Small helpers every module of the package shares. */
|
|
2
|
+
/** True when `path` is a directory; false for anything else, or when it cannot be read. */
|
|
3
|
+
export declare function isDir(path: string): boolean;
|
|
4
|
+
/** An error as one line for a person: a failed git call's own output, or the message. */
|
|
5
|
+
export declare function errorText(err: unknown): string;
|
|
6
|
+
/** `1 commit`, `3 commits`. */
|
|
7
|
+
export declare const plural: (n: number, one: string, many: string) => string;
|
package/dist/util.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Small helpers every module of the package shares. */
|
|
2
|
+
import { statSync } from 'node:fs';
|
|
3
|
+
import { CommandFailed } from '@osdlc/typed-git';
|
|
4
|
+
/** True when `path` is a directory; false for anything else, or when it cannot be read. */
|
|
5
|
+
export function isDir(path) {
|
|
6
|
+
try {
|
|
7
|
+
return statSync(path).isDirectory();
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** An error as one line for a person: a failed git call's own output, or the message. */
|
|
14
|
+
export function errorText(err) {
|
|
15
|
+
if (err instanceof CommandFailed)
|
|
16
|
+
return err.detail.trim() || err.message;
|
|
17
|
+
return err instanceof Error ? err.message : String(err);
|
|
18
|
+
}
|
|
19
|
+
/** `1 commit`, `3 commits`. */
|
|
20
|
+
export const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** This package's version, from its package.json when it was built. */
|
|
2
|
-
export declare const VERSION = "0.
|
|
2
|
+
export declare const VERSION = "0.18.0";
|