@codyswann/lisa 4.10.14 → 4.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/all/copy-overwrite/scripts/lisa-hooks/block-no-verify.sh +4 -1
- package/all/copy-overwrite/scripts/lisa-hooks/parity-safety-net.sh +132 -43
- package/dist/cli/doctor-worktree-hygiene.d.ts +5 -2
- package/dist/cli/doctor-worktree-hygiene.d.ts.map +1 -1
- package/dist/cli/doctor-worktree-hygiene.js +12 -6
- package/dist/cli/doctor-worktree-hygiene.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/prune-commands.d.ts +52 -0
- package/dist/cli/prune-commands.d.ts.map +1 -0
- package/dist/cli/prune-commands.js +158 -0
- package/dist/cli/prune-commands.js.map +1 -0
- package/dist/cli/prune-report.d.ts +40 -0
- package/dist/cli/prune-report.d.ts.map +1 -0
- package/dist/cli/prune-report.js +81 -0
- package/dist/cli/prune-report.js.map +1 -0
- package/dist/cli/stash-prune-policy.d.ts +72 -0
- package/dist/cli/stash-prune-policy.d.ts.map +1 -0
- package/dist/cli/stash-prune-policy.js +90 -0
- package/dist/cli/stash-prune-policy.js.map +1 -0
- package/dist/cli/stash-prune.d.ts +104 -0
- package/dist/cli/stash-prune.d.ts.map +1 -0
- package/dist/cli/stash-prune.js +206 -0
- package/dist/cli/stash-prune.js.map +1 -0
- package/dist/cli/worktree-inventory.d.ts +82 -0
- package/dist/cli/worktree-inventory.d.ts.map +1 -0
- package/dist/cli/worktree-inventory.js +195 -0
- package/dist/cli/worktree-inventory.js.map +1 -0
- package/dist/cli/worktree-liveness.d.ts +52 -0
- package/dist/cli/worktree-liveness.d.ts.map +1 -0
- package/dist/cli/worktree-liveness.js +124 -0
- package/dist/cli/worktree-liveness.js.map +1 -0
- package/dist/cli/worktree-ownership.d.ts +65 -0
- package/dist/cli/worktree-ownership.d.ts.map +1 -0
- package/dist/cli/worktree-ownership.js +142 -0
- package/dist/cli/worktree-ownership.js.map +1 -0
- package/dist/cli/worktree-prune-policy.d.ts +76 -0
- package/dist/cli/worktree-prune-policy.d.ts.map +1 -0
- package/dist/cli/worktree-prune-policy.js +114 -0
- package/dist/cli/worktree-prune-policy.js.map +1 -0
- package/dist/cli/worktree-prune.d.ts +96 -0
- package/dist/cli/worktree-prune.d.ts.map +1 -0
- package/dist/cli/worktree-prune.js +153 -0
- package/dist/cli/worktree-prune.js.map +1 -0
- package/dist/codex/scripts/block-no-verify.sh +4 -1
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +4 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +23 -6
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/opencode/plugin-templates/block-no-verify.sh +4 -1
- package/dist/opencode/plugin-templates/parity-safety-net.sh +132 -43
- package/package.json +1 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/hooks/block-no-verify.agy.sh +4 -1
- package/plugins/lisa/hooks/block-no-verify.sh +4 -1
- package/plugins/lisa/hooks/parity-safety-net.sh +132 -43
- package/plugins/lisa/rules/reference/base-rules.md +1 -1
- package/plugins/lisa-agy/hooks/block-no-verify.agy.sh +4 -1
- package/plugins/lisa-agy/hooks/parity-safety-net.sh +132 -43
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/hooks/block-no-verify.sh +4 -1
- package/plugins/lisa-copilot/hooks/parity-safety-net.sh +132 -43
- package/plugins/lisa-copilot/rules/reference/base-rules.md +1 -1
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/block-no-verify.sh +4 -1
- package/plugins/lisa-cursor/hooks/parity-safety-net.sh +132 -43
- package/plugins/lisa-cursor/rules/base-rules-reference.mdc +1 -1
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/hooks/block-no-verify.agy.sh +4 -1
- package/plugins/src/base/hooks/block-no-verify.sh +4 -1
- package/plugins/src/base/hooks/parity-safety-net.sh +132 -43
- package/plugins/src/base/rules/reference/base-rules.md +1 -1
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Operator-readable reports for the prune verbs.
|
|
3
|
+
*
|
|
4
|
+
* Every candidate appears in the output, kept ones included, with the reason it
|
|
5
|
+
* was kept. A cleaner that silently leaves things behind is as hard to trust as
|
|
6
|
+
* one that silently takes them: the operator cannot tell "nothing was eligible"
|
|
7
|
+
* from "the check never ran". Nothing here truncates the list for that reason.
|
|
8
|
+
* @module cli/prune-report
|
|
9
|
+
*/
|
|
10
|
+
import { STASH_BLOCKER_EXPLANATIONS, } from "./stash-prune-policy.js";
|
|
11
|
+
import { BLOCKER_EXPLANATIONS, } from "./worktree-prune-policy.js";
|
|
12
|
+
/**
|
|
13
|
+
* Describe one worktree verdict in a single line.
|
|
14
|
+
* @param verdict - Verdict for one worktree
|
|
15
|
+
* @returns Report line
|
|
16
|
+
*/
|
|
17
|
+
export function describeWorktreeVerdict(verdict) {
|
|
18
|
+
return verdict.eligible
|
|
19
|
+
? ` REMOVE ${verdict.path}`
|
|
20
|
+
: ` KEEP ${verdict.path} — ${verdict.blockers
|
|
21
|
+
.map(blocker => BLOCKER_EXPLANATIONS[blocker])
|
|
22
|
+
.join("; ")}`;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Describe one stash verdict in a single line.
|
|
26
|
+
* @param verdict - Verdict for one stash entry
|
|
27
|
+
* @returns Report line
|
|
28
|
+
*/
|
|
29
|
+
export function describeStashVerdict(verdict) {
|
|
30
|
+
const label = `stash@{${verdict.index}} ${verdict.sha.slice(0, 8)}`;
|
|
31
|
+
return verdict.eligible
|
|
32
|
+
? ` DROP ${label} — ${verdict.redundancy ?? "redundant"}`
|
|
33
|
+
: ` KEEP ${label} — ${verdict.blockers
|
|
34
|
+
.map(blocker => STASH_BLOCKER_EXPLANATIONS[blocker])
|
|
35
|
+
.join("; ")}`;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Render the worktree plan for a human.
|
|
39
|
+
* @param plan - Plan produced by the prune verb
|
|
40
|
+
* @param applied - Whether removals were actually performed
|
|
41
|
+
* @returns Report lines
|
|
42
|
+
*/
|
|
43
|
+
export function renderWorktreePlan(plan, applied) {
|
|
44
|
+
const eligible = plan.verdicts.filter(verdict => verdict.eligible).length;
|
|
45
|
+
return [
|
|
46
|
+
`${plan.verdicts.length} worktree(s) registered for ${plan.repoPath}.`,
|
|
47
|
+
...(plan.livenessAvailable
|
|
48
|
+
? []
|
|
49
|
+
: [
|
|
50
|
+
"The live-process probe could not run, so NOTHING is eligible. This " +
|
|
51
|
+
"is unassessed, not clean: without it a worktree another agent is " +
|
|
52
|
+
"working in right now is indistinguishable from an abandoned one.",
|
|
53
|
+
]),
|
|
54
|
+
...plan.verdicts.map(describeWorktreeVerdict),
|
|
55
|
+
...(plan.prunableRegistrations.length > 0
|
|
56
|
+
? [
|
|
57
|
+
`${plan.prunableRegistrations.length} registration(s) point at a directory that is already gone and will be forgotten.`,
|
|
58
|
+
]
|
|
59
|
+
: []),
|
|
60
|
+
applied
|
|
61
|
+
? `${eligible} worktree(s) removed.`
|
|
62
|
+
: `${eligible} worktree(s) would be removed. This was a dry run — re-run with --apply to perform it.`,
|
|
63
|
+
];
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Render the stash plan for a human.
|
|
67
|
+
* @param plan - Plan produced by the stash prune verb
|
|
68
|
+
* @param applied - Whether drops were actually performed
|
|
69
|
+
* @returns Report lines
|
|
70
|
+
*/
|
|
71
|
+
export function renderStashPlan(plan, applied) {
|
|
72
|
+
const eligible = plan.verdicts.filter(verdict => verdict.eligible).length;
|
|
73
|
+
return [
|
|
74
|
+
`${plan.verdicts.length} stash entr(ies) in ${plan.repoPath}.`,
|
|
75
|
+
...plan.verdicts.map(describeStashVerdict),
|
|
76
|
+
applied
|
|
77
|
+
? `${eligible} entr(ies) dropped. Each one was first anchored under refs/lisa/pruned-stashes/<sha> and can be restored with \`git stash apply <ref>\`.`
|
|
78
|
+
: `${eligible} entr(ies) would be dropped. This was a dry run — re-run with --apply to perform it.`,
|
|
79
|
+
];
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=prune-report.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prune-report.js","sourceRoot":"","sources":["../../src/cli/prune-report.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EACL,0BAA0B,GAE3B,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EACL,oBAAoB,GAErB,MAAM,4BAA4B,CAAC;AAEpC;;;;GAIG;AACH,MAAM,UAAU,uBAAuB,CAAC,OAAwB;IAC9D,OAAO,OAAO,CAAC,QAAQ;QACrB,CAAC,CAAC,aAAa,OAAO,CAAC,IAAI,EAAE;QAC7B,CAAC,CAAC,aAAa,OAAO,CAAC,IAAI,MAAM,OAAO,CAAC,QAAQ;aAC5C,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,oBAAoB,CAAC,OAAO,CAAC,CAAC;aAC7C,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;AACtB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,OAAqB;IACxD,MAAM,KAAK,GAAG,UAAU,OAAO,CAAC,KAAK,KAAK,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;IACpE,OAAO,OAAO,CAAC,QAAQ;QACrB,CAAC,CAAC,aAAa,KAAK,MAAM,OAAO,CAAC,UAAU,IAAI,WAAW,EAAE;QAC7D,CAAC,CAAC,aAAa,KAAK,MAAM,OAAO,CAAC,QAAQ;aACrC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,0BAA0B,CAAC,OAAO,CAAC,CAAC;aACnD,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;AACtB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAChC,IAAuB,EACvB,OAAgB;IAEhB,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC;IAC1E,OAAO;QACL,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,+BAA+B,IAAI,CAAC,QAAQ,GAAG;QACtE,GAAG,CAAC,IAAI,CAAC,iBAAiB;YACxB,CAAC,CAAC,EAAE;YACJ,CAAC,CAAC;gBACE,qEAAqE;oBACnE,mEAAmE;oBACnE,kEAAkE;aACrE,CAAC;QACN,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,uBAAuB,CAAC;QAC7C,GAAG,CAAC,IAAI,CAAC,qBAAqB,CAAC,MAAM,GAAG,CAAC;YACvC,CAAC,CAAC;gBACE,GAAG,IAAI,CAAC,qBAAqB,CAAC,MAAM,mFAAmF;aACxH;YACH,CAAC,CAAC,EAAE,CAAC;QACP,OAAO;YACL,CAAC,CAAC,GAAG,QAAQ,uBAAuB;YACpC,CAAC,CAAC,GAAG,QAAQ,wFAAwF;KACxG,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAC7B,IAAoB,EACpB,OAAgB;IAEhB,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC;IAC1E,OAAO;QACL,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,uBAAuB,IAAI,CAAC,QAAQ,GAAG;QAC9D,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,oBAAoB,CAAC;QAC1C,OAAO;YACL,CAAC,CAAC,GAAG,QAAQ,0IAA0I;YACvJ,CAAC,CAAC,GAAG,QAAQ,sFAAsF;KACtG,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The entitlement rule for dropping a stash entry.
|
|
3
|
+
*
|
|
4
|
+
* Stashes are harder to own than worktrees: `refs/stash` is a single reflog
|
|
5
|
+
* shared by every worktree of a repository, so an entry an agent finds may have
|
|
6
|
+
* been pushed a minute ago by a sibling working three directories away. There
|
|
7
|
+
* is no path, no branch, and no author to scope by. The only defensible
|
|
8
|
+
* entitlement is therefore not "mine" but **provably redundant** — an entry
|
|
9
|
+
* whose content is already preserved somewhere else, or machine-generated
|
|
10
|
+
* debris whose producing run is provably long dead.
|
|
11
|
+
*
|
|
12
|
+
* Everything else is kept and reported. A stash that cannot be proven redundant
|
|
13
|
+
* is precisely the case the standing block on `git stash drop` exists for.
|
|
14
|
+
* @module cli/stash-prune-policy
|
|
15
|
+
*/
|
|
16
|
+
/** Why one stash entry was kept. */
|
|
17
|
+
export type StashBlocker = "not-provably-redundant" | "too-recent" | "unreadable";
|
|
18
|
+
/** Why one stash entry may be dropped. */
|
|
19
|
+
export type StashRedundancy = "empty-and-published" | "machine-debris";
|
|
20
|
+
/** Everything the rule is allowed to consider about one stash entry. */
|
|
21
|
+
export interface StashFacts {
|
|
22
|
+
/** Position in `git stash list` at plan time. */
|
|
23
|
+
readonly index: number;
|
|
24
|
+
/** Commit id of the stash entry — the identity that survives renumbering. */
|
|
25
|
+
readonly sha: string;
|
|
26
|
+
/** Reflog subject, e.g. `WIP on main: …` or the lint-staged backup message. */
|
|
27
|
+
readonly subject: string;
|
|
28
|
+
/** Age of the entry in seconds. */
|
|
29
|
+
readonly ageSeconds: number;
|
|
30
|
+
/** Whether every fact below could be established. */
|
|
31
|
+
readonly readable: boolean;
|
|
32
|
+
/** Whether the entry records no change at all against the commit it came from. */
|
|
33
|
+
readonly recordsNoChange: boolean;
|
|
34
|
+
/** Whether the commit the entry came from is reachable from a remote. */
|
|
35
|
+
readonly baseOnRemote: boolean;
|
|
36
|
+
}
|
|
37
|
+
/** Tunable part of the rule. */
|
|
38
|
+
export interface StashPrunePolicy {
|
|
39
|
+
/** Age a machine-generated backup must reach before it is debris. */
|
|
40
|
+
readonly minDebrisAgeSeconds: number;
|
|
41
|
+
}
|
|
42
|
+
/** Result of applying the rule to one stash entry. */
|
|
43
|
+
export interface StashVerdict {
|
|
44
|
+
/** Position in `git stash list` at plan time. */
|
|
45
|
+
readonly index: number;
|
|
46
|
+
/** Commit id of the stash entry. */
|
|
47
|
+
readonly sha: string;
|
|
48
|
+
/** Whether the entry may be dropped. */
|
|
49
|
+
readonly eligible: boolean;
|
|
50
|
+
/** Why it may be dropped, when it may. */
|
|
51
|
+
readonly redundancy?: StashRedundancy;
|
|
52
|
+
/** Why it was kept, when it was. */
|
|
53
|
+
readonly blockers: readonly StashBlocker[];
|
|
54
|
+
}
|
|
55
|
+
/** Default debris age: a lint-staged run that old is not coming back. */
|
|
56
|
+
export declare const DEFAULT_MIN_DEBRIS_AGE_SECONDS: number;
|
|
57
|
+
/** Operator-readable explanation for each blocker. */
|
|
58
|
+
export declare const STASH_BLOCKER_EXPLANATIONS: Readonly<Record<StashBlocker, string>>;
|
|
59
|
+
/**
|
|
60
|
+
* Report whether a reflog subject is lint-staged's automatic backup.
|
|
61
|
+
* @param subject - Reflog subject of the stash entry
|
|
62
|
+
* @returns True when the entry is machine-generated backup debris
|
|
63
|
+
*/
|
|
64
|
+
export declare function isLintStagedBackup(subject: string): boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Decide whether one stash entry may be dropped, and say why not when it may not.
|
|
67
|
+
* @param facts - Gathered facts about the entry
|
|
68
|
+
* @param policy - Tunable thresholds
|
|
69
|
+
* @returns Verdict with the reason it may be dropped, or every reason it may not
|
|
70
|
+
*/
|
|
71
|
+
export declare function classifyStash(facts: StashFacts, policy: StashPrunePolicy): StashVerdict;
|
|
72
|
+
//# sourceMappingURL=stash-prune-policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stash-prune-policy.d.ts","sourceRoot":"","sources":["../../src/cli/stash-prune-policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,oCAAoC;AACpC,MAAM,MAAM,YAAY,GACpB,wBAAwB,GACxB,YAAY,GACZ,YAAY,CAAC;AAEjB,0CAA0C;AAC1C,MAAM,MAAM,eAAe,GAAG,qBAAqB,GAAG,gBAAgB,CAAC;AAEvE,wEAAwE;AACxE,MAAM,WAAW,UAAU;IACzB,iDAAiD;IACjD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,6EAA6E;IAC7E,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,+EAA+E;IAC/E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,mCAAmC;IACnC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,qDAAqD;IACrD,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,kFAAkF;IAClF,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;IAClC,yEAAyE;IACzE,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;CAChC;AAED,gCAAgC;AAChC,MAAM,WAAW,gBAAgB;IAC/B,qEAAqE;IACrE,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;CACtC;AAED,sDAAsD;AACtD,MAAM,WAAW,YAAY;IAC3B,iDAAiD;IACjD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,oCAAoC;IACpC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,wCAAwC;IACxC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,0CAA0C;IAC1C,QAAQ,CAAC,UAAU,CAAC,EAAE,eAAe,CAAC;IACtC,oCAAoC;IACpC,QAAQ,CAAC,QAAQ,EAAE,SAAS,YAAY,EAAE,CAAC;CAC5C;AAED,yEAAyE;AACzE,eAAO,MAAM,8BAA8B,QAAe,CAAC;AAW3D,sDAAsD;AACtD,eAAO,MAAM,0BAA0B,EAAE,QAAQ,CAC/C,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAO5B,CAAC;AAEH;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAE3D;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,UAAU,EACjB,MAAM,EAAE,gBAAgB,GACvB,YAAY,CA6Bd"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The entitlement rule for dropping a stash entry.
|
|
3
|
+
*
|
|
4
|
+
* Stashes are harder to own than worktrees: `refs/stash` is a single reflog
|
|
5
|
+
* shared by every worktree of a repository, so an entry an agent finds may have
|
|
6
|
+
* been pushed a minute ago by a sibling working three directories away. There
|
|
7
|
+
* is no path, no branch, and no author to scope by. The only defensible
|
|
8
|
+
* entitlement is therefore not "mine" but **provably redundant** — an entry
|
|
9
|
+
* whose content is already preserved somewhere else, or machine-generated
|
|
10
|
+
* debris whose producing run is provably long dead.
|
|
11
|
+
*
|
|
12
|
+
* Everything else is kept and reported. A stash that cannot be proven redundant
|
|
13
|
+
* is precisely the case the standing block on `git stash drop` exists for.
|
|
14
|
+
* @module cli/stash-prune-policy
|
|
15
|
+
*/
|
|
16
|
+
/** Default debris age: a lint-staged run that old is not coming back. */
|
|
17
|
+
export const DEFAULT_MIN_DEBRIS_AGE_SECONDS = 24 * 60 * 60;
|
|
18
|
+
/**
|
|
19
|
+
* The exact message lint-staged gives its automatic backup stash.
|
|
20
|
+
*
|
|
21
|
+
* Matched exactly, and with the `On <branch>: ` prefix git adds when the entry
|
|
22
|
+
* is stored through `git stash push`, because a substring match would sweep a
|
|
23
|
+
* human stash whose message merely mentions lint-staged.
|
|
24
|
+
*/
|
|
25
|
+
const LINT_STAGED_BACKUP = /^(?:On [^:]+: )?lint-staged automatic backup$/;
|
|
26
|
+
/** Operator-readable explanation for each blocker. */
|
|
27
|
+
export const STASH_BLOCKER_EXPLANATIONS = Object.freeze({
|
|
28
|
+
"not-provably-redundant": "its content is not provably preserved anywhere else, so dropping it could destroy the only copy",
|
|
29
|
+
"too-recent": "it is machine-generated backup debris, but recent enough that the run which created it may still be alive",
|
|
30
|
+
unreadable: "git could not report on it, so nothing about it was proven",
|
|
31
|
+
});
|
|
32
|
+
/**
|
|
33
|
+
* Report whether a reflog subject is lint-staged's automatic backup.
|
|
34
|
+
* @param subject - Reflog subject of the stash entry
|
|
35
|
+
* @returns True when the entry is machine-generated backup debris
|
|
36
|
+
*/
|
|
37
|
+
export function isLintStagedBackup(subject) {
|
|
38
|
+
return LINT_STAGED_BACKUP.test(subject.trim());
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Decide whether one stash entry may be dropped, and say why not when it may not.
|
|
42
|
+
* @param facts - Gathered facts about the entry
|
|
43
|
+
* @param policy - Tunable thresholds
|
|
44
|
+
* @returns Verdict with the reason it may be dropped, or every reason it may not
|
|
45
|
+
*/
|
|
46
|
+
export function classifyStash(facts, policy) {
|
|
47
|
+
if (!facts.readable) {
|
|
48
|
+
return {
|
|
49
|
+
index: facts.index,
|
|
50
|
+
sha: facts.sha,
|
|
51
|
+
eligible: false,
|
|
52
|
+
blockers: ["unreadable"],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const redundancy = findRedundancy(facts, policy);
|
|
56
|
+
if (redundancy !== undefined) {
|
|
57
|
+
return {
|
|
58
|
+
index: facts.index,
|
|
59
|
+
sha: facts.sha,
|
|
60
|
+
eligible: true,
|
|
61
|
+
redundancy,
|
|
62
|
+
blockers: [],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
index: facts.index,
|
|
67
|
+
sha: facts.sha,
|
|
68
|
+
eligible: false,
|
|
69
|
+
blockers: [
|
|
70
|
+
isLintStagedBackup(facts.subject)
|
|
71
|
+
? "too-recent"
|
|
72
|
+
: "not-provably-redundant",
|
|
73
|
+
],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Identify which redundancy proof, if any, one entry satisfies.
|
|
78
|
+
* @param facts - Gathered facts about the entry
|
|
79
|
+
* @param policy - Tunable thresholds
|
|
80
|
+
* @returns The satisfied proof, or undefined when none is
|
|
81
|
+
*/
|
|
82
|
+
function findRedundancy(facts, policy) {
|
|
83
|
+
if (facts.recordsNoChange && facts.baseOnRemote)
|
|
84
|
+
return "empty-and-published";
|
|
85
|
+
return isLintStagedBackup(facts.subject) &&
|
|
86
|
+
facts.ageSeconds >= policy.minDebrisAgeSeconds
|
|
87
|
+
? "machine-debris"
|
|
88
|
+
: undefined;
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=stash-prune-policy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stash-prune-policy.js","sourceRoot":"","sources":["../../src/cli/stash-prune-policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAiDH,yEAAyE;AACzE,MAAM,CAAC,MAAM,8BAA8B,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC;AAE3D;;;;;;GAMG;AACH,MAAM,kBAAkB,GAAG,+CAA+C,CAAC;AAE3E,sDAAsD;AACtD,MAAM,CAAC,MAAM,0BAA0B,GAEnC,MAAM,CAAC,MAAM,CAAC;IAChB,wBAAwB,EACtB,iGAAiG;IACnG,YAAY,EACV,2GAA2G;IAC7G,UAAU,EAAE,4DAA4D;CACzE,CAAC,CAAC;AAEH;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAAe;IAChD,OAAO,kBAAkB,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;AACjD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAC3B,KAAiB,EACjB,MAAwB;IAExB,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;QACpB,OAAO;YACL,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,GAAG,EAAE,KAAK,CAAC,GAAG;YACd,QAAQ,EAAE,KAAK;YACf,QAAQ,EAAE,CAAC,YAAY,CAAC;SACzB,CAAC;IACJ,CAAC;IACD,MAAM,UAAU,GAAG,cAAc,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;IACjD,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;QAC7B,OAAO;YACL,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,GAAG,EAAE,KAAK,CAAC,GAAG;YACd,QAAQ,EAAE,IAAI;YACd,UAAU;YACV,QAAQ,EAAE,EAAE;SACb,CAAC;IACJ,CAAC;IACD,OAAO;QACL,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,GAAG,EAAE,KAAK,CAAC,GAAG;QACd,QAAQ,EAAE,KAAK;QACf,QAAQ,EAAE;YACR,kBAAkB,CAAC,KAAK,CAAC,OAAO,CAAC;gBAC/B,CAAC,CAAC,YAAY;gBACd,CAAC,CAAC,wBAAwB;SAC7B;KACF,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,SAAS,cAAc,CACrB,KAAiB,EACjB,MAAwB;IAExB,IAAI,KAAK,CAAC,eAAe,IAAI,KAAK,CAAC,YAAY;QAAE,OAAO,qBAAqB,CAAC;IAC9E,OAAO,kBAAkB,CAAC,KAAK,CAAC,OAAO,CAAC;QACtC,KAAK,CAAC,UAAU,IAAI,MAAM,CAAC,mBAAmB;QAC9C,CAAC,CAAC,gBAAgB;QAClB,CAAC,CAAC,SAAS,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `lisa stash prune` — the vetted verb for clearing redundant stash entries.
|
|
3
|
+
*
|
|
4
|
+
* The guard blocks `git stash drop` and `git stash clear` because they destroy
|
|
5
|
+
* stashed work. That block stays. This command answers it rather than relaxing
|
|
6
|
+
* it: every entry it drops is first re-anchored under
|
|
7
|
+
* `refs/lisa/pruned-stashes/`, so the stash commit — trees, index state, and
|
|
8
|
+
* the untracked-files parent alike — stays in the object database and is
|
|
9
|
+
* recoverable with a single `git stash apply <ref>`. Nothing is destroyed; the
|
|
10
|
+
* reflog entry is what goes away.
|
|
11
|
+
*
|
|
12
|
+
* Dropping is index-shifting on a ref that every worktree of the repository
|
|
13
|
+
* shares, so entries are addressed by commit id and re-located immediately
|
|
14
|
+
* before each drop. An entry whose id no longer sits where it was is skipped,
|
|
15
|
+
* never dropped by position — that is the difference between a cleaner and a
|
|
16
|
+
* race that eats a sibling's stash.
|
|
17
|
+
* @module cli/stash-prune
|
|
18
|
+
*/
|
|
19
|
+
import { type StashFacts, type StashVerdict } from "./stash-prune-policy.js";
|
|
20
|
+
/** Namespace the preserved stash commits are anchored under. */
|
|
21
|
+
export declare const PRESERVED_STASH_REF_PREFIX = "refs/lisa/pruned-stashes";
|
|
22
|
+
/** Command-line options for the stash prune verb. */
|
|
23
|
+
export interface StashPruneOptions {
|
|
24
|
+
/** Perform the drops instead of reporting them. */
|
|
25
|
+
readonly apply?: boolean;
|
|
26
|
+
/** Age in hours a machine backup must reach before it counts as debris. */
|
|
27
|
+
readonly olderThanHours?: string;
|
|
28
|
+
/** Emit machine-readable output. */
|
|
29
|
+
readonly json?: boolean;
|
|
30
|
+
}
|
|
31
|
+
/** One entry as the listing reports it. */
|
|
32
|
+
export interface StashRecord {
|
|
33
|
+
/** Commit id of the entry. */
|
|
34
|
+
readonly sha: string;
|
|
35
|
+
/** Reflog subject. */
|
|
36
|
+
readonly subject: string;
|
|
37
|
+
/** Commit timestamp in epoch seconds. */
|
|
38
|
+
readonly timestamp: number;
|
|
39
|
+
}
|
|
40
|
+
/** What one run decided, before anything is dropped. */
|
|
41
|
+
export interface StashPrunePlan {
|
|
42
|
+
/** Repository the plan was computed for. */
|
|
43
|
+
readonly repoPath: string;
|
|
44
|
+
/** Verdict for every stash entry. */
|
|
45
|
+
readonly verdicts: readonly StashVerdict[];
|
|
46
|
+
/** Facts behind each verdict, in the same order. */
|
|
47
|
+
readonly facts: readonly StashFacts[];
|
|
48
|
+
}
|
|
49
|
+
/** Outcome of dropping one stash entry. */
|
|
50
|
+
export interface StashDropOutcome {
|
|
51
|
+
/** Commit id of the entry. */
|
|
52
|
+
readonly sha: string;
|
|
53
|
+
/** Whether the entry was dropped. */
|
|
54
|
+
readonly dropped: boolean;
|
|
55
|
+
/** Ref the stash commit was anchored under before the drop. */
|
|
56
|
+
readonly preservedRef?: string;
|
|
57
|
+
/** Why the drop did not happen. */
|
|
58
|
+
readonly error?: string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Resolve the debris age threshold from the command line.
|
|
62
|
+
* @param olderThanHours - Raw `--older-than-hours` value
|
|
63
|
+
* @returns Threshold in seconds
|
|
64
|
+
*/
|
|
65
|
+
export declare function resolveMinDebrisAgeSeconds(olderThanHours: string | undefined): number;
|
|
66
|
+
/**
|
|
67
|
+
* Parse the formatted stash listing into its raw fields.
|
|
68
|
+
* @param stdout - Output of the formatted `git stash list`
|
|
69
|
+
* @returns One record per entry, in list order
|
|
70
|
+
*/
|
|
71
|
+
export declare function parseStashList(stdout: string): readonly StashRecord[];
|
|
72
|
+
/**
|
|
73
|
+
* The ref one stash commit is preserved under.
|
|
74
|
+
* @param sha - Commit id of the stash entry
|
|
75
|
+
* @returns Fully-qualified ref name
|
|
76
|
+
*/
|
|
77
|
+
export declare function preservedStashRef(sha: string): string;
|
|
78
|
+
/**
|
|
79
|
+
* Compute what a run would do, without dropping anything.
|
|
80
|
+
* @param repoPath - Any path inside the repository
|
|
81
|
+
* @param options - Command-line options
|
|
82
|
+
* @param now - Clock in epoch milliseconds
|
|
83
|
+
* @returns The plan
|
|
84
|
+
*/
|
|
85
|
+
export declare function planStashPrune(repoPath: string, options: StashPruneOptions, now?: () => number): Promise<StashPrunePlan>;
|
|
86
|
+
/**
|
|
87
|
+
* Locate a stash entry by commit id, as `git stash list` currently numbers it.
|
|
88
|
+
*
|
|
89
|
+
* Re-derived immediately before every drop. Positions shift under a concurrent
|
|
90
|
+
* push or drop from any worktree of the repository, and a stale position is how
|
|
91
|
+
* a cleaner destroys the entry after the one it meant to.
|
|
92
|
+
* @param repoPath - Any path inside the repository
|
|
93
|
+
* @param sha - Commit id of the entry
|
|
94
|
+
* @returns Current `stash@{n}` reference, or undefined when the entry is gone
|
|
95
|
+
*/
|
|
96
|
+
export declare function locateStash(repoPath: string, sha: string): Promise<string | undefined>;
|
|
97
|
+
/**
|
|
98
|
+
* Preserve and drop every eligible stash entry in a plan.
|
|
99
|
+
* @param plan - Plan produced by {@link planStashPrune}
|
|
100
|
+
* @param index - Index of the eligible entry to drop next
|
|
101
|
+
* @returns One outcome per eligible entry
|
|
102
|
+
*/
|
|
103
|
+
export declare function applyStashPrune(plan: StashPrunePlan, index?: number): Promise<readonly StashDropOutcome[]>;
|
|
104
|
+
//# sourceMappingURL=stash-prune.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stash-prune.d.ts","sourceRoot":"","sources":["../../src/cli/stash-prune.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EAGL,KAAK,UAAU,EACf,KAAK,YAAY,EAClB,MAAM,yBAAyB,CAAC;AASjC,gEAAgE;AAChE,eAAO,MAAM,0BAA0B,6BAA6B,CAAC;AAErE,qDAAqD;AACrD,MAAM,WAAW,iBAAiB;IAChC,mDAAmD;IACnD,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,2EAA2E;IAC3E,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,oCAAoC;IACpC,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,2CAA2C;AAC3C,MAAM,WAAW,WAAW;IAC1B,8BAA8B;IAC9B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,sBAAsB;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,yCAAyC;IACzC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED,wDAAwD;AACxD,MAAM,WAAW,cAAc;IAC7B,4CAA4C;IAC5C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,qCAAqC;IACrC,QAAQ,CAAC,QAAQ,EAAE,SAAS,YAAY,EAAE,CAAC;IAC3C,oDAAoD;IACpD,QAAQ,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,CAAC;CACvC;AAED,2CAA2C;AAC3C,MAAM,WAAW,gBAAgB;IAC/B,8BAA8B;IAC9B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,qCAAqC;IACrC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,+DAA+D;IAC/D,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,mCAAmC;IACnC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CACxC,cAAc,EAAE,MAAM,GAAG,SAAS,GACjC,MAAM,CAOR;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,WAAW,EAAE,CAWrE;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAErD;AAkFD;;;;;;GAMG;AACH,wBAAsB,cAAc,CAClC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,iBAAiB,EAC1B,GAAG,GAAE,MAAM,MAAyB,GACnC,OAAO,CAAC,cAAc,CAAC,CAkBzB;AAED;;;;;;;;;GASG;AACH,wBAAsB,WAAW,CAC/B,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAM7B;AA+CD;;;;;GAKG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,cAAc,EACpB,KAAK,SAAI,GACR,OAAO,CAAC,SAAS,gBAAgB,EAAE,CAAC,CAQtC"}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `lisa stash prune` — the vetted verb for clearing redundant stash entries.
|
|
3
|
+
*
|
|
4
|
+
* The guard blocks `git stash drop` and `git stash clear` because they destroy
|
|
5
|
+
* stashed work. That block stays. This command answers it rather than relaxing
|
|
6
|
+
* it: every entry it drops is first re-anchored under
|
|
7
|
+
* `refs/lisa/pruned-stashes/`, so the stash commit — trees, index state, and
|
|
8
|
+
* the untracked-files parent alike — stays in the object database and is
|
|
9
|
+
* recoverable with a single `git stash apply <ref>`. Nothing is destroyed; the
|
|
10
|
+
* reflog entry is what goes away.
|
|
11
|
+
*
|
|
12
|
+
* Dropping is index-shifting on a ref that every worktree of the repository
|
|
13
|
+
* shares, so entries are addressed by commit id and re-located immediately
|
|
14
|
+
* before each drop. An entry whose id no longer sits where it was is skipped,
|
|
15
|
+
* never dropped by position — that is the difference between a cleaner and a
|
|
16
|
+
* race that eats a sibling's stash.
|
|
17
|
+
* @module cli/stash-prune
|
|
18
|
+
*/
|
|
19
|
+
import { classifyStash, DEFAULT_MIN_DEBRIS_AGE_SECONDS, } from "./stash-prune-policy.js";
|
|
20
|
+
import { git } from "./worktree-inventory.js";
|
|
21
|
+
/** Unit-separator field delimiter, matching the `%x1f` in the list format. */
|
|
22
|
+
const FIELD = "\u001F";
|
|
23
|
+
/** Format that makes `git stash list` machine-readable. */
|
|
24
|
+
const STASH_LIST_FORMAT = "--format=%H%x1f%gs%x1f%ct";
|
|
25
|
+
/** Namespace the preserved stash commits are anchored under. */
|
|
26
|
+
export const PRESERVED_STASH_REF_PREFIX = "refs/lisa/pruned-stashes";
|
|
27
|
+
/**
|
|
28
|
+
* Resolve the debris age threshold from the command line.
|
|
29
|
+
* @param olderThanHours - Raw `--older-than-hours` value
|
|
30
|
+
* @returns Threshold in seconds
|
|
31
|
+
*/
|
|
32
|
+
export function resolveMinDebrisAgeSeconds(olderThanHours) {
|
|
33
|
+
if (olderThanHours === undefined)
|
|
34
|
+
return DEFAULT_MIN_DEBRIS_AGE_SECONDS;
|
|
35
|
+
const parsed = Number.parseFloat(olderThanHours);
|
|
36
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
37
|
+
throw new Error("--older-than-hours must be a non-negative number");
|
|
38
|
+
}
|
|
39
|
+
return Math.floor(parsed * 3600);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Parse the formatted stash listing into its raw fields.
|
|
43
|
+
* @param stdout - Output of the formatted `git stash list`
|
|
44
|
+
* @returns One record per entry, in list order
|
|
45
|
+
*/
|
|
46
|
+
export function parseStashList(stdout) {
|
|
47
|
+
return stdout
|
|
48
|
+
.split("\n")
|
|
49
|
+
.filter(line => line.includes(FIELD))
|
|
50
|
+
.map(line => line.split(FIELD))
|
|
51
|
+
.map(fields => ({
|
|
52
|
+
sha: (fields[0] ?? "").trim(),
|
|
53
|
+
subject: fields[1] ?? "",
|
|
54
|
+
timestamp: Number.parseInt(fields[2] ?? "", 10) || 0,
|
|
55
|
+
}))
|
|
56
|
+
.filter(record => record.sha !== "");
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The ref one stash commit is preserved under.
|
|
60
|
+
* @param sha - Commit id of the stash entry
|
|
61
|
+
* @returns Fully-qualified ref name
|
|
62
|
+
*/
|
|
63
|
+
export function preservedStashRef(sha) {
|
|
64
|
+
return `${PRESERVED_STASH_REF_PREFIX}/${sha}`;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Report whether two revisions have an identical tree.
|
|
68
|
+
* @param repoPath - Any path inside the repository
|
|
69
|
+
* @param left - First revision
|
|
70
|
+
* @param right - Second revision
|
|
71
|
+
* @returns True when the revisions differ in nothing
|
|
72
|
+
*/
|
|
73
|
+
async function isSameTree(repoPath, left, right) {
|
|
74
|
+
const [leftTree, rightTree] = await Promise.all([
|
|
75
|
+
git(["rev-parse", `${left}^{tree}`], repoPath).then(out => out.trim()),
|
|
76
|
+
git(["rev-parse", `${right}^{tree}`], repoPath).then(out => out.trim()),
|
|
77
|
+
]);
|
|
78
|
+
return leftTree !== "" && leftTree === rightTree;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Establish whether one entry holds anything, and whether its base is published.
|
|
82
|
+
*
|
|
83
|
+
* A stash created with `--include-untracked` carries a third parent holding
|
|
84
|
+
* those files. Its presence alone means the entry holds content no comparison
|
|
85
|
+
* against the first two parents would reveal, so it is treated as non-empty
|
|
86
|
+
* without further inspection.
|
|
87
|
+
* @param repoPath - Any path inside the repository
|
|
88
|
+
* @param sha - Commit id of the stash entry
|
|
89
|
+
* @returns Content and publication facts
|
|
90
|
+
*/
|
|
91
|
+
async function probeStashContent(repoPath, sha) {
|
|
92
|
+
const hasUntrackedParent = await git(["rev-parse", "--verify", "--quiet", `${sha}^3`], repoPath).then(() => true, () => false);
|
|
93
|
+
const [sameWorktree, sameIndex, unpublished] = await Promise.all([
|
|
94
|
+
isSameTree(repoPath, `${sha}^`, sha),
|
|
95
|
+
isSameTree(repoPath, `${sha}^`, `${sha}^2`),
|
|
96
|
+
git(["rev-list", "--count", `${sha}^`, "--not", "--remotes"], repoPath),
|
|
97
|
+
]);
|
|
98
|
+
return {
|
|
99
|
+
recordsNoChange: !hasUntrackedParent && sameWorktree && sameIndex,
|
|
100
|
+
baseOnRemote: (Number.parseInt(unpublished.trim(), 10) || 0) === 0,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Gather every fact the entitlement rule may consider for one entry.
|
|
105
|
+
* @param repoPath - Any path inside the repository
|
|
106
|
+
* @param record - Raw listing fields
|
|
107
|
+
* @param index - Position in the listing
|
|
108
|
+
* @param nowSeconds - Clock in epoch seconds
|
|
109
|
+
* @returns Facts for the entitlement rule
|
|
110
|
+
*/
|
|
111
|
+
async function gatherStashFacts(repoPath, record, index, nowSeconds) {
|
|
112
|
+
const shell = {
|
|
113
|
+
index,
|
|
114
|
+
sha: record.sha,
|
|
115
|
+
subject: record.subject,
|
|
116
|
+
ageSeconds: Math.max(0, nowSeconds - record.timestamp),
|
|
117
|
+
};
|
|
118
|
+
const probed = await probeStashContent(repoPath, record.sha).catch(() => undefined);
|
|
119
|
+
return probed === undefined
|
|
120
|
+
? { ...shell, readable: false, recordsNoChange: false, baseOnRemote: false }
|
|
121
|
+
: { ...shell, readable: true, ...probed };
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Compute what a run would do, without dropping anything.
|
|
125
|
+
* @param repoPath - Any path inside the repository
|
|
126
|
+
* @param options - Command-line options
|
|
127
|
+
* @param now - Clock in epoch milliseconds
|
|
128
|
+
* @returns The plan
|
|
129
|
+
*/
|
|
130
|
+
export async function planStashPrune(repoPath, options, now = () => Date.now()) {
|
|
131
|
+
const minDebrisAgeSeconds = resolveMinDebrisAgeSeconds(options.olderThanHours);
|
|
132
|
+
const listed = parseStashList(await git(["stash", "list", STASH_LIST_FORMAT], repoPath).catch(() => ""));
|
|
133
|
+
const nowSeconds = Math.floor(now() / 1000);
|
|
134
|
+
const facts = await Promise.all(listed.map((record, index) => gatherStashFacts(repoPath, record, index, nowSeconds)));
|
|
135
|
+
return {
|
|
136
|
+
repoPath,
|
|
137
|
+
facts,
|
|
138
|
+
verdicts: facts.map(fact => classifyStash(fact, { minDebrisAgeSeconds })),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Locate a stash entry by commit id, as `git stash list` currently numbers it.
|
|
143
|
+
*
|
|
144
|
+
* Re-derived immediately before every drop. Positions shift under a concurrent
|
|
145
|
+
* push or drop from any worktree of the repository, and a stale position is how
|
|
146
|
+
* a cleaner destroys the entry after the one it meant to.
|
|
147
|
+
* @param repoPath - Any path inside the repository
|
|
148
|
+
* @param sha - Commit id of the entry
|
|
149
|
+
* @returns Current `stash@{n}` reference, or undefined when the entry is gone
|
|
150
|
+
*/
|
|
151
|
+
export async function locateStash(repoPath, sha) {
|
|
152
|
+
const listed = parseStashList(await git(["stash", "list", STASH_LIST_FORMAT], repoPath));
|
|
153
|
+
const position = listed.findIndex(record => record.sha === sha);
|
|
154
|
+
return position === -1 ? undefined : `stash@{${position}}`;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Preserve one stash commit under a ref, verify it, then drop the entry.
|
|
158
|
+
* @param repoPath - Any path inside the repository
|
|
159
|
+
* @param sha - Commit id of the entry
|
|
160
|
+
* @returns Outcome for the entry
|
|
161
|
+
*/
|
|
162
|
+
async function dropOne(repoPath, sha) {
|
|
163
|
+
const ref = preservedStashRef(sha);
|
|
164
|
+
const preserved = await git(["update-ref", ref, sha], repoPath).then(() => git(["rev-parse", "--verify", ref], repoPath), () => "");
|
|
165
|
+
if (preserved.trim() !== sha) {
|
|
166
|
+
return {
|
|
167
|
+
sha,
|
|
168
|
+
dropped: false,
|
|
169
|
+
error: "could not preserve the stash commit, so it was not dropped",
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
const located = await locateStash(repoPath, sha);
|
|
173
|
+
if (located === undefined) {
|
|
174
|
+
return {
|
|
175
|
+
sha,
|
|
176
|
+
dropped: false,
|
|
177
|
+
preservedRef: ref,
|
|
178
|
+
error: "entry moved or was dropped elsewhere before this drop",
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
return git(["stash", "drop", located], repoPath).then(() => ({ sha, dropped: true, preservedRef: ref }), (error) => ({
|
|
182
|
+
sha,
|
|
183
|
+
dropped: false,
|
|
184
|
+
preservedRef: ref,
|
|
185
|
+
error: error instanceof Error
|
|
186
|
+
? (error.message.split("\n")[0] ?? "unknown")
|
|
187
|
+
: "unknown",
|
|
188
|
+
}));
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Preserve and drop every eligible stash entry in a plan.
|
|
192
|
+
* @param plan - Plan produced by {@link planStashPrune}
|
|
193
|
+
* @param index - Index of the eligible entry to drop next
|
|
194
|
+
* @returns One outcome per eligible entry
|
|
195
|
+
*/
|
|
196
|
+
export async function applyStashPrune(plan, index = 0) {
|
|
197
|
+
const eligible = plan.verdicts.filter(verdict => verdict.eligible);
|
|
198
|
+
if (index >= eligible.length)
|
|
199
|
+
return [];
|
|
200
|
+
const target = eligible[index];
|
|
201
|
+
return [
|
|
202
|
+
await dropOne(plan.repoPath, target.sha),
|
|
203
|
+
...(await applyStashPrune(plan, index + 1)),
|
|
204
|
+
];
|
|
205
|
+
}
|
|
206
|
+
//# sourceMappingURL=stash-prune.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stash-prune.js","sourceRoot":"","sources":["../../src/cli/stash-prune.ts"],"names":[],"mappings":"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"}
|