@osdlc/repo-cleaner 0.15.0 → 0.17.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 +19 -0
- package/README.md +10 -5
- package/dist/apply.d.ts +2 -1
- package/dist/apply.js +6 -3
- package/dist/branches.d.ts +5 -3
- package/dist/branches.js +6 -4
- package/dist/cli.js +10 -19
- package/dist/hooks.d.ts +4 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/sweep.d.ts +13 -2
- package/dist/sweep.js +42 -3
- package/dist/types.d.ts +1 -1
- package/dist/version.generated.d.ts +1 -1
- package/dist/version.generated.js +1 -1
- package/package.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# @osdlc/repo-cleaner
|
|
2
2
|
|
|
3
|
+
## 0.17.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [4f764b7]
|
|
8
|
+
- @osdlc/typed-gh@0.15.0
|
|
9
|
+
|
|
10
|
+
## 0.17.0
|
|
11
|
+
|
|
12
|
+
### Minor Changes
|
|
13
|
+
|
|
14
|
+
- b036603: `holdFreedBranches(items)` keeps a branch freed for its worktree's removal when that worktree no longer goes, such as when a host's `annotate` holds the worktree back. `applyPlan` runs it after `annotate`. A host should run it on its own annotated rows too, so the counts and the plan match what `apply` will do.
|
|
15
|
+
|
|
16
|
+
## 0.16.0
|
|
17
|
+
|
|
18
|
+
### Minor Changes
|
|
19
|
+
|
|
20
|
+
- db33c17: **Breaking (CLI):** `--remote <name>` is now `--measure-remote <name>`, so it is no longer one letter away from `--remotes`. Typing `--remote` when you meant `--remotes` used to fail with "argument missing". `--no-gh` is gone; use `--provider none`, which does the same. The help no longer says the tool asks `gh`, since the provider can be GitLab.
|
|
21
|
+
|
|
3
22
|
## 0.15.0
|
|
4
23
|
|
|
5
24
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -386,10 +386,9 @@ Every command that sweeps takes these:
|
|
|
386
386
|
| `--only <list>` | `worktrees`, `branches`, `remotes` (default: `worktrees,branches`) |
|
|
387
387
|
| `--remotes` | Also sweep remote branches. Slower: each one is compared with the default branch |
|
|
388
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
|
-
| `--remote <name>` |
|
|
389
|
+
| `--measure-remote <name>` | The remote whose default branch work is measured against (default `origin`) |
|
|
390
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
|
|
392
|
-
| `--no-gh` | With `--remotes`: classify on git evidence alone; the same as `--provider none`. Without `--remotes`, no provider is asked |
|
|
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 |
|
|
393
392
|
| `--no-fetch` | With `--remotes`: skip the fetch; the header then says `not fetched`. Without `--remotes`, nothing is fetched |
|
|
394
393
|
|
|
395
394
|
`status` also takes:
|
|
@@ -530,8 +529,14 @@ so the rows it checks again are the rows the host planned from.
|
|
|
530
529
|
|
|
531
530
|
This works because of how the ladder is ordered. Every verdict the sweep
|
|
532
531
|
reaches on its own evidence outranks anything a host could contribute, so a
|
|
533
|
-
host
|
|
534
|
-
or `needs-investigation`.
|
|
532
|
+
host rarely has to veto one. It mostly sharpens the rows left at
|
|
533
|
+
`safe-to-keep` or `needs-investigation`.
|
|
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.
|
|
535
540
|
|
|
536
541
|
An issue tracker's linkage, applied this way, is a host's own layer over the
|
|
537
542
|
sweep and `applyPlan`.
|
package/dist/apply.d.ts
CHANGED
|
@@ -55,7 +55,8 @@ export interface ApplyOptions extends HostHooks {
|
|
|
55
55
|
safeOnly?: boolean | undefined;
|
|
56
56
|
/**
|
|
57
57
|
* Items to re-check against instead of sweeping again, keyed by repository
|
|
58
|
-
* root, already annotated: the hook is not applied to them again
|
|
58
|
+
* root, already annotated: the hook is not applied to them again, though
|
|
59
|
+
* `holdFreedBranches` is, so a freed branch matches its worktree. For a
|
|
59
60
|
* preview straight after a sweep; the run that deletes should always sweep
|
|
60
61
|
* again.
|
|
61
62
|
*/
|
package/dist/apply.js
CHANGED
|
@@ -22,7 +22,7 @@ import { localDeleteCommand, remoteDeleteCommand } from "./branches.js";
|
|
|
22
22
|
import { busyHolders, defaultProcessProbe, parseLockOwner } from "./processes.js";
|
|
23
23
|
import { PLAN_CATEGORIES, fingerprint, isPlanCategory, sameFingerprint, } from "./plan.js";
|
|
24
24
|
import { RECEIPT_VERSION } from "./receipt.js";
|
|
25
|
-
import { sweepRepo } from "./sweep.js";
|
|
25
|
+
import { holdFreedBranches, sweepRepo } from "./sweep.js";
|
|
26
26
|
import { AUTHORIZATIONS, } from "./types.js";
|
|
27
27
|
import { worktreePruneCommand, worktreeRemoveCommand, worktreeUnlockCommand } from "./worktrees.js";
|
|
28
28
|
import { writeJsonAtomic } from "./write-json.js";
|
|
@@ -89,9 +89,10 @@ const SCOPE_OF = {
|
|
|
89
89
|
};
|
|
90
90
|
/** The plan repository's rows as they stand now, with the host's overlay applied. */
|
|
91
91
|
function resweep(repo, scope, opts) {
|
|
92
|
+
const branchScoped = repo.branch != null;
|
|
92
93
|
const given = opts.swept?.get(repo.root);
|
|
93
94
|
if (given !== undefined) {
|
|
94
|
-
return [...given];
|
|
95
|
+
return holdFreedBranches([...given], { branchScoped });
|
|
95
96
|
}
|
|
96
97
|
const sweep = sweepRepo(repo.root, {
|
|
97
98
|
remote: repo.remote,
|
|
@@ -110,7 +111,9 @@ function resweep(repo, scope, opts) {
|
|
|
110
111
|
if (sweep.repo === null || sweep.error !== null) {
|
|
111
112
|
throw new Error(sweep.error ?? `could not read repository: ${repo.root}`);
|
|
112
113
|
}
|
|
113
|
-
return opts.annotate === undefined
|
|
114
|
+
return opts.annotate === undefined
|
|
115
|
+
? sweep.items
|
|
116
|
+
: holdFreedBranches(opts.annotate(sweep.repo, sweep.items), { branchScoped });
|
|
114
117
|
}
|
|
115
118
|
/** Re-sweep, then split the selection into rows to run and rows dropped. */
|
|
116
119
|
function recheck(selected, kept, grants, implicit, opts) {
|
package/dist/branches.d.ts
CHANGED
|
@@ -41,11 +41,13 @@ export declare function mergedFact(e: {
|
|
|
41
41
|
}): boolean;
|
|
42
42
|
/** `HEAD` as the branch checked out at `git`'s directory; any other name as given. */
|
|
43
43
|
export declare function resolveBranch(value: string, git: Git): string;
|
|
44
|
+
/** Why a branch checked out in a worktree is kept. */
|
|
45
|
+
export declare const HELD_BY_WORKTREE = "checked out in a worktree (handle via that worktree)";
|
|
44
46
|
/**
|
|
45
47
|
* Delete local branch `name` only while it still points at `sha` (the full
|
|
46
|
-
* sha). Same caveats as `deleteRef` in `@osdlc/typed-git`.
|
|
47
|
-
*
|
|
48
|
-
*
|
|
48
|
+
* sha). Same caveats as `deleteRef` in `@osdlc/typed-git`. A sweep proposes a
|
|
49
|
+
* branch checked out in a worktree only alongside that worktree's removal,
|
|
50
|
+
* which must run first.
|
|
49
51
|
*/
|
|
50
52
|
export declare function localDeleteCommand(name: string, sha: string): string;
|
|
51
53
|
/** Delete `remote`'s branch `name` only while it still points at `sha`. */
|
package/dist/branches.js
CHANGED
|
@@ -90,11 +90,13 @@ export function resolveBranch(value, git) {
|
|
|
90
90
|
throw new Error(`--branch HEAD: no branch is checked out at ${git.cwd}`);
|
|
91
91
|
return current;
|
|
92
92
|
}
|
|
93
|
+
/** Why a branch checked out in a worktree is kept. */
|
|
94
|
+
export const HELD_BY_WORKTREE = 'checked out in a worktree (handle via that worktree)';
|
|
93
95
|
/**
|
|
94
96
|
* Delete local branch `name` only while it still points at `sha` (the full
|
|
95
|
-
* sha). Same caveats as `deleteRef` in `@osdlc/typed-git`.
|
|
96
|
-
*
|
|
97
|
-
*
|
|
97
|
+
* sha). Same caveats as `deleteRef` in `@osdlc/typed-git`. A sweep proposes a
|
|
98
|
+
* branch checked out in a worktree only alongside that worktree's removal,
|
|
99
|
+
* which must run first.
|
|
98
100
|
*/
|
|
99
101
|
export function localDeleteCommand(name, sha) {
|
|
100
102
|
return shellJoin(['git', 'update-ref', '-d', `refs/heads/${name}`, sha]);
|
|
@@ -159,7 +161,7 @@ export function classifyLocalBranch(opts) {
|
|
|
159
161
|
const held = name === repo.currentBranch
|
|
160
162
|
? 'current HEAD'
|
|
161
163
|
: worktreeBranches.has(name)
|
|
162
|
-
?
|
|
164
|
+
? HELD_BY_WORKTREE
|
|
163
165
|
: null;
|
|
164
166
|
if (held !== null) {
|
|
165
167
|
// Never deletable here, but still counted as merged or not: by ancestry or
|
package/dist/cli.js
CHANGED
|
@@ -65,10 +65,10 @@ const SWEEP_FLAGS = {
|
|
|
65
65
|
valueName: 'name|HEAD',
|
|
66
66
|
help: 'only this branch of this repository: its worktree and local branch, and its remote branch with --remotes',
|
|
67
67
|
},
|
|
68
|
-
|
|
68
|
+
measureRemote: {
|
|
69
69
|
kind: 'string',
|
|
70
70
|
valueName: 'name',
|
|
71
|
-
help: 'remote
|
|
71
|
+
help: 'the remote whose default branch work is measured against (default: origin)',
|
|
72
72
|
},
|
|
73
73
|
remoteUrl: {
|
|
74
74
|
kind: 'list',
|
|
@@ -77,16 +77,11 @@ const SWEEP_FLAGS = {
|
|
|
77
77
|
commaSeparated: false,
|
|
78
78
|
help: 'sweep the branches of this remote with no checkout of your own, through a clone kept in the cache directory; repeatable',
|
|
79
79
|
},
|
|
80
|
-
gh: {
|
|
81
|
-
kind: 'boolean',
|
|
82
|
-
negatable: true,
|
|
83
|
-
help: 'with --remotes: classify on git evidence alone, the same as --provider none (without --remotes no provider is asked)',
|
|
84
|
-
},
|
|
85
80
|
provider: {
|
|
86
81
|
kind: 'enum',
|
|
87
82
|
choices: PROVIDER_CHOICES,
|
|
88
83
|
valueName: 'name',
|
|
89
|
-
help: 'with --remotes: where pull requests come from: auto (by remote host), github (gh), gitlab (glab), none',
|
|
84
|
+
help: 'with --remotes: where pull requests come from: auto (by remote host), github (gh), gitlab (glab), or none to classify on git evidence alone',
|
|
90
85
|
},
|
|
91
86
|
fetch: {
|
|
92
87
|
kind: 'boolean',
|
|
@@ -117,7 +112,7 @@ const statusCli = () => defineCli({
|
|
|
117
112
|
name: NAME,
|
|
118
113
|
summary: 'Inventory local branches and worktrees (remote branches too with --remotes), and say ' +
|
|
119
114
|
'which have been merged into the default branch. Offline unless --remotes, which fetches ' +
|
|
120
|
-
'each remote first and asks
|
|
115
|
+
'each remote first and asks the remote host about pull requests; deletes nothing.',
|
|
121
116
|
positionals: PATHS,
|
|
122
117
|
flags: {
|
|
123
118
|
...SWEEP_FLAGS,
|
|
@@ -276,7 +271,7 @@ function remotesNote(swept, style) {
|
|
|
276
271
|
return '';
|
|
277
272
|
if (!swept.result.repos.some((r) => r.repo?.remote != null))
|
|
278
273
|
return '';
|
|
279
|
-
return (style.muted('Local only: nothing fetched, no
|
|
274
|
+
return (style.muted('Local only: nothing fetched, no pull requests looked up, remote branches not checked; add --remotes for all three.') + '\n');
|
|
280
275
|
}
|
|
281
276
|
/** Where `--remote-url` keeps its clones. */
|
|
282
277
|
function mirrorCache(ctx) {
|
|
@@ -322,10 +317,6 @@ function sweep(name, v, paths, ctx, status) {
|
|
|
322
317
|
if (conflict !== null)
|
|
323
318
|
return { kind: 'exit', code: usageError(ctx, name, conflict) };
|
|
324
319
|
}
|
|
325
|
-
if (!v.gh && v.provider !== undefined && v.provider !== 'none') {
|
|
326
|
-
const message = `--no-gh asks no provider; drop it or --provider ${v.provider}`;
|
|
327
|
-
return { kind: 'exit', code: usageError(ctx, name, message) };
|
|
328
|
-
}
|
|
329
320
|
let branch;
|
|
330
321
|
try {
|
|
331
322
|
branch = v.branch === undefined ? undefined : resolveBranch(v.branch, new Git(ctx.cwd));
|
|
@@ -338,8 +329,8 @@ function sweep(name, v, paths, ctx, status) {
|
|
|
338
329
|
const options = {
|
|
339
330
|
baseDir: ctx.cwd,
|
|
340
331
|
cwd: ctx.cwd,
|
|
341
|
-
// Local by default means offline: the fetch and
|
|
342
|
-
noProvider: !
|
|
332
|
+
// Local by default means offline: the fetch and the pull-request lookup go with remote branches.
|
|
333
|
+
noProvider: !online || v.provider === 'none',
|
|
343
334
|
fetch: online && v.fetch,
|
|
344
335
|
provider: v.provider,
|
|
345
336
|
branch,
|
|
@@ -348,7 +339,7 @@ function sweep(name, v, paths, ctx, status) {
|
|
|
348
339
|
onWarn: (message) => ctx.io.stderr(`${NAME}: ${message}\n`),
|
|
349
340
|
...(v.progress ? { onProgress: (p) => status.update(describeSweep(p)) } : {}),
|
|
350
341
|
scope,
|
|
351
|
-
...(v.
|
|
342
|
+
...(v.measureRemote === undefined ? {} : { remote: v.measureRemote }),
|
|
352
343
|
};
|
|
353
344
|
let entries = paths.length > 0 ? [...paths] : [ctx.cwd];
|
|
354
345
|
if (urls.length > 0) {
|
|
@@ -393,7 +384,7 @@ function reportUnreadable(ctx, name, result) {
|
|
|
393
384
|
}
|
|
394
385
|
function planOf(swept, now) {
|
|
395
386
|
const opts = {
|
|
396
|
-
remote: swept.values.
|
|
387
|
+
remote: swept.values.measureRemote ?? null,
|
|
397
388
|
noProvider: swept.noProvider,
|
|
398
389
|
provider: swept.values.provider,
|
|
399
390
|
branch: swept.branch ?? null,
|
|
@@ -539,7 +530,7 @@ function applyConflict(v, paths) {
|
|
|
539
530
|
paths.length > 0 ? 'paths' : null,
|
|
540
531
|
v.only !== undefined ? '--only' : null,
|
|
541
532
|
v.branch !== undefined ? '--branch' : null,
|
|
542
|
-
v.
|
|
533
|
+
v.measureRemote !== undefined ? '--measure-remote' : null,
|
|
543
534
|
v.remoteUrl.length > 0 ? '--remote-url' : null,
|
|
544
535
|
].filter((f) => f !== null);
|
|
545
536
|
if (sweepFlags.length > 0)
|
package/dist/hooks.d.ts
CHANGED
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
import type { RepoInfo, SweepItem } from './types.ts';
|
|
8
8
|
/**
|
|
9
9
|
* Re-rank a repository's swept rows with what the host knows. Runs after the
|
|
10
|
-
* sweep, on its output; it never reaches into classification.
|
|
10
|
+
* sweep, on its output; it never reaches into classification. A host that
|
|
11
|
+
* holds back or demotes a worktree passes its own annotated rows through
|
|
12
|
+
* `holdFreedBranches`, as `applyPlan` does, so that worktree's branch is kept
|
|
13
|
+
* too.
|
|
11
14
|
*/
|
|
12
15
|
export type Annotate = (repo: RepoInfo, items: SweepItem[]) => SweepItem[];
|
|
13
16
|
export interface HostHooks {
|
package/dist/index.d.ts
CHANGED
|
@@ -26,6 +26,6 @@ export { defaultProcessProbe, isIdleShell, parseLockOwner, parseLsofCwd, type De
|
|
|
26
26
|
export { fetchPrs } from './prs.ts';
|
|
27
27
|
export { PROVIDER_CHOICES, PROVIDER_NAMES, chooseProvider, forgeTarget, githubProvider, gitlabProvider, probeProvider, providers, type ForgeTarget, type PrProvider, type ProviderChoice, type ProviderName, type ProviderStatus, } from './providers/index.ts';
|
|
28
28
|
export { expandTilde, isCheckoutRoot, resolveEntries, type Resolution, type ResolveOptions, } from './resolve.ts';
|
|
29
|
-
export { sweepRepo, sweepRepos, type SweepOptions, type SweepReposOptions } from './sweep.ts';
|
|
29
|
+
export { holdFreedBranches, sweepRepo, sweepRepos, type SweepOptions, type SweepReposOptions, } from './sweep.ts';
|
|
30
30
|
export { proposedCommands, renderJson, renderApplySummary, renderReceipt, summarizeReceipt, type ApplySummary, renderRepo, renderScan, renderSummary, renderText, requiresNote, } from './render.ts';
|
|
31
31
|
export { ALL_SCOPES, AUTHORIZATIONS, authorizationsFor, emptyBucket, emptyScan, findDuplicateCheckouts, isUnfetched, makeItem, summarize, type Authorization, type Category, type Classification, type DuplicateCheckout, type PRInfo, type RepoInfo, type RepoSweep, type ScanStats, type Scope, type SummaryBucket, type SweepItem, type SweepProgress, type SweepResult, type UnresolvedEntry, } from './types.ts';
|
package/dist/index.js
CHANGED
|
@@ -25,6 +25,6 @@ export { defaultProcessProbe, isIdleShell, parseLockOwner, parseLsofCwd, } from
|
|
|
25
25
|
export { fetchPrs } from "./prs.js";
|
|
26
26
|
export { PROVIDER_CHOICES, PROVIDER_NAMES, chooseProvider, forgeTarget, githubProvider, gitlabProvider, probeProvider, providers, } from "./providers/index.js";
|
|
27
27
|
export { expandTilde, isCheckoutRoot, resolveEntries, } from "./resolve.js";
|
|
28
|
-
export { sweepRepo, sweepRepos } from "./sweep.js";
|
|
28
|
+
export { holdFreedBranches, sweepRepo, sweepRepos, } from "./sweep.js";
|
|
29
29
|
export { proposedCommands, renderJson, renderApplySummary, renderReceipt, summarizeReceipt, renderRepo, renderScan, renderSummary, renderText, requiresNote, } from "./render.js";
|
|
30
30
|
export { ALL_SCOPES, AUTHORIZATIONS, authorizationsFor, emptyBucket, emptyScan, findDuplicateCheckouts, isUnfetched, makeItem, summarize, } from "./types.js";
|
package/dist/sweep.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import type { CommandRunner } from '@osdlc/command-seam';
|
|
9
9
|
import { type ProcessProbe } from './processes.ts';
|
|
10
10
|
import { type ProviderChoice } from './providers/index.ts';
|
|
11
|
-
import { type RepoSweep, type Scope, type SweepProgress, type SweepResult } from './types.ts';
|
|
11
|
+
import { type RepoSweep, type Scope, type SweepItem, type SweepProgress, type SweepResult } from './types.ts';
|
|
12
12
|
export interface SweepOptions {
|
|
13
13
|
/** Which categories to gather. Defaults to all of them. */
|
|
14
14
|
scope?: Iterable<Scope | string>;
|
|
@@ -37,7 +37,8 @@ export interface SweepOptions {
|
|
|
37
37
|
* its remote branch. The default branch is refused. `cwd` is ignored, so the
|
|
38
38
|
* worktree is judged on its facts, and the local branch is not pinned for
|
|
39
39
|
* being checked out there: apply removes the worktree first. It stays pinned
|
|
40
|
-
* when that worktree is blocked or cannot be removed
|
|
40
|
+
* when that worktree is blocked or cannot be removed, or when `scope` leaves
|
|
41
|
+
* worktrees out.
|
|
41
42
|
*/
|
|
42
43
|
branch?: string | undefined;
|
|
43
44
|
/** Injected runners. Defaults spawn the real `git`, `gh` and `glab`. */
|
|
@@ -80,3 +81,13 @@ export interface SweepReposOptions extends SweepOptions {
|
|
|
80
81
|
* row and the sweep continues.
|
|
81
82
|
*/
|
|
82
83
|
export declare function sweepRepos(entries: readonly string[], opts?: SweepReposOptions): SweepResult;
|
|
84
|
+
/**
|
|
85
|
+
* Keep again each branch a sweep freed for its worktree's removal when that
|
|
86
|
+
* worktree no longer frees it by the sweep's own rule: a host's `annotate`
|
|
87
|
+
* held it back or demoted it, or dropped its row. Run it on annotated rows,
|
|
88
|
+
* with `branchScoped` set for a `branch` sweep, so the counts and the plan
|
|
89
|
+
* match what `applyPlan` will do. Running it twice changes nothing.
|
|
90
|
+
*/
|
|
91
|
+
export declare function holdFreedBranches(items: SweepItem[], opts?: {
|
|
92
|
+
branchScoped?: boolean;
|
|
93
|
+
}): SweepItem[];
|
package/dist/sweep.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { Git } from '@osdlc/typed-git';
|
|
9
9
|
import { Gh } from '@osdlc/typed-gh';
|
|
10
|
-
import { gatherLocalBranches, gatherRemoteBranches } from "./branches.js";
|
|
10
|
+
import { HELD_BY_WORKTREE, gatherLocalBranches, gatherRemoteBranches } from "./branches.js";
|
|
11
11
|
import { DiscoveryFailed, discoverRepo } from "./discover.js";
|
|
12
12
|
import { defaultProcessProbe } from "./processes.js";
|
|
13
13
|
import { fetchPrs } from "./prs.js";
|
|
@@ -107,9 +107,9 @@ export function sweepRepo(path, opts = {}) {
|
|
|
107
107
|
if (scope.has('worktrees')) {
|
|
108
108
|
for (const item of worktreeItems) {
|
|
109
109
|
const branch = item.facts['branch'];
|
|
110
|
-
|
|
111
|
-
if (goes && typeof branch === 'string')
|
|
110
|
+
if (freesBranch(item, only !== undefined) && typeof branch === 'string') {
|
|
112
111
|
freed.set(branch, item.id);
|
|
112
|
+
}
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
115
|
const pinned = new Set([...worktreeBranches].filter((name) => !freed.has(name)));
|
|
@@ -161,3 +161,42 @@ export function sweepRepos(entries, opts = {}) {
|
|
|
161
161
|
duplicates: findDuplicateCheckouts(repos.map((sweep) => sweep.repo)),
|
|
162
162
|
};
|
|
163
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Whether worktree row `item` frees its branch to be judged on its own. A full
|
|
166
|
+
* sweep frees it only for a safe-to-delete worktree; a branch-scoped sweep,
|
|
167
|
+
* where the caller named the branch, for any deletable one.
|
|
168
|
+
*/
|
|
169
|
+
function freesBranch(item, branchScoped) {
|
|
170
|
+
return item.deletable && (branchScoped || item.classification === 'safe-to-delete');
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Keep again each branch a sweep freed for its worktree's removal when that
|
|
174
|
+
* worktree no longer frees it by the sweep's own rule: a host's `annotate`
|
|
175
|
+
* held it back or demoted it, or dropped its row. Run it on annotated rows,
|
|
176
|
+
* with `branchScoped` set for a `branch` sweep, so the counts and the plan
|
|
177
|
+
* match what `applyPlan` will do. Running it twice changes nothing.
|
|
178
|
+
*/
|
|
179
|
+
export function holdFreedBranches(items, opts = {}) {
|
|
180
|
+
const going = new Set(items
|
|
181
|
+
.filter((it) => it.category === 'worktree' && freesBranch(it, opts.branchScoped === true))
|
|
182
|
+
.map((it) => it.id));
|
|
183
|
+
return items.map((item) => {
|
|
184
|
+
const worktree = item.facts['worktree'];
|
|
185
|
+
if (item.category !== 'local_branch' || typeof worktree !== 'string')
|
|
186
|
+
return item;
|
|
187
|
+
if (going.has(worktree))
|
|
188
|
+
return item;
|
|
189
|
+
const { worktree: _, ...facts } = item.facts;
|
|
190
|
+
return {
|
|
191
|
+
...item,
|
|
192
|
+
classification: 'safe-to-keep',
|
|
193
|
+
action: 'keep',
|
|
194
|
+
reason: HELD_BY_WORKTREE,
|
|
195
|
+
facts,
|
|
196
|
+
commands: [],
|
|
197
|
+
investigationParts: [],
|
|
198
|
+
requires: [],
|
|
199
|
+
deletable: false,
|
|
200
|
+
};
|
|
201
|
+
});
|
|
202
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -53,7 +53,7 @@ export interface RepoInfo {
|
|
|
53
53
|
/**
|
|
54
54
|
* The provider asked for pull requests and what its probe found: whether
|
|
55
55
|
* its CLI is installed and signed in. Null when none was asked
|
|
56
|
-
* (`--provider none`,
|
|
56
|
+
* (`--provider none`, or a sweep without `--remotes`).
|
|
57
57
|
*/
|
|
58
58
|
provider: ProviderStatus | null;
|
|
59
59
|
/** True when the sweep fetched the remote before classifying. */
|
|
@@ -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.17.1";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@osdlc/repo-cleaner",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.1",
|
|
4
4
|
"description": "What has been merged into the default branch, and what can go away. Inventories local branches, worktrees and remote branches across one repository or many, classifying each with the evidence behind the verdict and the command that would act on it. Plans and applies approved deletes, compare-and-swap.",
|
|
5
5
|
"license": "MIT OR Apache-2.0",
|
|
6
6
|
"author": "sksizer",
|
|
@@ -50,11 +50,11 @@
|
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"zod": "^4.6.6",
|
|
52
52
|
"@osdlc/cli-tool": "^0.16.0",
|
|
53
|
+
"@osdlc/typed-gh": "^0.15.0",
|
|
53
54
|
"@osdlc/command-seam": "^0.14.0",
|
|
54
|
-
"@osdlc/typed-gh": "^0.14.1",
|
|
55
55
|
"@osdlc/typed-git": "^0.12.0",
|
|
56
|
-
"@osdlc/
|
|
57
|
-
"@osdlc/
|
|
56
|
+
"@osdlc/ux": "^0.1.1",
|
|
57
|
+
"@osdlc/path-guard": "^0.3.1"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@types/node": "^26.1.0",
|
|
@@ -63,8 +63,8 @@
|
|
|
63
63
|
"prettier": "3.9.6",
|
|
64
64
|
"typescript": "~6.0.3",
|
|
65
65
|
"vitest": "^5.0.5",
|
|
66
|
-
"@osdlc/
|
|
67
|
-
"@osdlc/
|
|
66
|
+
"@osdlc/tsconfig": "^0.4.1",
|
|
67
|
+
"@osdlc/prettier-config": "^0.3.1"
|
|
68
68
|
},
|
|
69
69
|
"scripts": {
|
|
70
70
|
"build": "cli-tool-stamp-version && tsc -p tsconfig.build.json",
|