@agentproto/worktree 0.9.0 → 0.10.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/dist/index.d.ts +13 -0
- package/dist/index.mjs +72 -14
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -1694,6 +1694,8 @@ interface BranchGcPlan {
|
|
|
1694
1694
|
generatedAt: string;
|
|
1695
1695
|
/** Refs of OTHER configured remotes: never classified, never touched. */
|
|
1696
1696
|
otherRemoteRefs: number;
|
|
1697
|
+
/** true = `git fetch --prune` of the base remote succeeded; false = it failed; null = skipped (opt-out or no remote). */
|
|
1698
|
+
fetched: boolean | null;
|
|
1697
1699
|
entries: BranchGcPlanEntry[];
|
|
1698
1700
|
}
|
|
1699
1701
|
/** Base content index, built once per base: path→blob, every blob, every basename. */
|
|
@@ -1713,6 +1715,13 @@ interface LadderContext {
|
|
|
1713
1715
|
* plan's anchor detection asks the same question of every tip) — saves
|
|
1714
1716
|
* the ladder's first `merge-base` per tip. */
|
|
1715
1717
|
related?: ReadonlyMap<string, boolean>;
|
|
1718
|
+
/**
|
|
1719
|
+
* Memoized set of every commit reachable from `cmp` (at most two distinct
|
|
1720
|
+
* values: `baseSha` and the anchor) — ONE `git rev-list <cmp>` replaces one
|
|
1721
|
+
* `merge-base --is-ancestor` per tip. `null` above 500 000 commits (or on a
|
|
1722
|
+
* git failure) = fall back to the per-tip call.
|
|
1723
|
+
*/
|
|
1724
|
+
ancestorsOf?: (cmp: string) => Promise<Set<string> | null>;
|
|
1716
1725
|
}
|
|
1717
1726
|
declare function createLadderContext(repoRoot: string, baseRef: string, anchor: string | null, related?: ReadonlyMap<string, boolean>): Promise<LadderContext>;
|
|
1718
1727
|
/**
|
|
@@ -1760,6 +1769,8 @@ interface PlanBranchGcInput {
|
|
|
1760
1769
|
nowMs?: number;
|
|
1761
1770
|
/** Parallel ladder runs. Default 8. */
|
|
1762
1771
|
concurrency?: number;
|
|
1772
|
+
/** Run `git fetch --prune <baseRemote>` before classifying. Default true. */
|
|
1773
|
+
fetch?: boolean;
|
|
1763
1774
|
}
|
|
1764
1775
|
/** The dry-run plan: classify every ref in `scopes`, mutate nothing. */
|
|
1765
1776
|
declare function planBranchGc(input: PlanBranchGcInput): Promise<BranchGcPlan>;
|
|
@@ -1873,6 +1884,8 @@ interface ApplyBranchGcOptions {
|
|
|
1873
1884
|
nowMs?: number;
|
|
1874
1885
|
/** Remote refs per `git push --delete`. Default 50. */
|
|
1875
1886
|
remoteBatchSize?: number;
|
|
1887
|
+
/** Run `git fetch --prune <baseRemote>` before re-deriving entries. Default true. */
|
|
1888
|
+
fetch?: boolean;
|
|
1876
1889
|
}
|
|
1877
1890
|
interface BranchGcApplyResult {
|
|
1878
1891
|
outcomes: BranchGcApplyOutcome[];
|
package/dist/index.mjs
CHANGED
|
@@ -507,12 +507,28 @@ async function createLadderContext(repoRoot, baseRef, anchor, related) {
|
|
|
507
507
|
const baseSha = (await gitOk(repoRoot, ["rev-parse", "--verify", `${baseRef}^{commit}`])).trim();
|
|
508
508
|
const baseTree = (await gitOk(repoRoot, ["rev-parse", `${baseSha}^{tree}`])).trim();
|
|
509
509
|
let idx = null;
|
|
510
|
+
const ancestors = /* @__PURE__ */ new Map();
|
|
511
|
+
const ancestorsOf = (cmp) => {
|
|
512
|
+
let p = ancestors.get(cmp);
|
|
513
|
+
if (!p) {
|
|
514
|
+
p = (async () => {
|
|
515
|
+
const res = await git(repoRoot, ["rev-list", cmp]);
|
|
516
|
+
if (res.exitCode !== 0) return null;
|
|
517
|
+
const shas = res.stdout.split("\n");
|
|
518
|
+
if (shas.length > 5e5) return null;
|
|
519
|
+
return new Set(shas.filter(Boolean));
|
|
520
|
+
})();
|
|
521
|
+
ancestors.set(cmp, p);
|
|
522
|
+
}
|
|
523
|
+
return p;
|
|
524
|
+
};
|
|
510
525
|
return {
|
|
511
526
|
repoRoot,
|
|
512
527
|
baseSha,
|
|
513
528
|
baseTree,
|
|
514
529
|
anchor,
|
|
515
530
|
index: () => idx ??= indexTree(repoRoot, baseSha),
|
|
531
|
+
ancestorsOf,
|
|
516
532
|
...related ? { related } : {}
|
|
517
533
|
};
|
|
518
534
|
}
|
|
@@ -580,7 +596,9 @@ async function classifyTip(ctx, tip) {
|
|
|
580
596
|
if (!related && !anchor) return { status: "unmerged", history: "unrelated", conflicts: true, ahead: null, behind: null };
|
|
581
597
|
const history = related ? "current" : "pre-rewrite";
|
|
582
598
|
const cmp = related ? baseSha : anchor;
|
|
583
|
-
|
|
599
|
+
const ancestors = ctx.ancestorsOf ? await ctx.ancestorsOf(cmp) : null;
|
|
600
|
+
const isAncestor = ancestors ? ancestors.has(tip) : (await git(repoRoot, ["merge-base", "--is-ancestor", tip, cmp])).exitCode === 0;
|
|
601
|
+
if (isAncestor) {
|
|
584
602
|
return { status: "merged", history, ahead: 0, behind: null };
|
|
585
603
|
}
|
|
586
604
|
const counts = (await gitOk(repoRoot, ["rev-list", "--left-right", "--count", `${cmp}...${tip}`])).trim().split(/\s+/);
|
|
@@ -736,12 +754,19 @@ async function pushStateOf(b, env) {
|
|
|
736
754
|
if (b.kind === "local") {
|
|
737
755
|
if (env.remoteShas.has(b.sha)) return "same-tip-on-remote";
|
|
738
756
|
if (env.remote) {
|
|
739
|
-
const
|
|
740
|
-
if (
|
|
757
|
+
const reachable = env.remoteReachable ? await env.remoteReachable() : null;
|
|
758
|
+
if (reachable) {
|
|
759
|
+
if (reachable.has(b.sha)) return "contained-in-remote";
|
|
760
|
+
} else {
|
|
761
|
+
const contained = await git(repoRoot, ["for-each-ref", "--contains", b.sha, "--format=x", `refs/remotes/${env.remote}`]);
|
|
762
|
+
if (contained.stdout.trim()) return "contained-in-remote";
|
|
763
|
+
}
|
|
741
764
|
}
|
|
742
765
|
return env.remoteNames.has(b.name) ? "diverged-from-remote" : "local-only";
|
|
743
766
|
}
|
|
744
767
|
if (b.kind === "orphan") {
|
|
768
|
+
const reachable = env.localOrRemoteReachable ? await env.localOrRemoteReachable() : null;
|
|
769
|
+
if (reachable) return reachable.has(b.sha) ? "contained-elsewhere" : "only-copy";
|
|
745
770
|
const scopes = ["refs/heads", ...env.remote ? [`refs/remotes/${env.remote}`] : []];
|
|
746
771
|
const contained = await git(repoRoot, ["for-each-ref", "--contains", b.sha, "--format=x", ...scopes]);
|
|
747
772
|
return contained.stdout.trim() ? "contained-elsewhere" : "only-copy";
|
|
@@ -797,6 +822,10 @@ async function classifyRef(b, env) {
|
|
|
797
822
|
async function snapshot(input) {
|
|
798
823
|
const remotes = await configuredRemotes(input.repoRoot);
|
|
799
824
|
const remote = baseRemoteOf(input.base, remotes);
|
|
825
|
+
let fetched = null;
|
|
826
|
+
if (remote && input.fetch !== false) {
|
|
827
|
+
fetched = (await git(input.repoRoot, ["fetch", "--prune", "--quiet", remote])).exitCode === 0;
|
|
828
|
+
}
|
|
800
829
|
const [{ refs, otherRemoteRefs }, worktrees, prs, mergedPrs] = await Promise.all([
|
|
801
830
|
listBranchRefs(input.repoRoot, remote, remotes),
|
|
802
831
|
worktreeBranches(input.repoRoot),
|
|
@@ -811,9 +840,20 @@ async function snapshot(input) {
|
|
|
811
840
|
else anchor = null;
|
|
812
841
|
const ctx = await createLadderContext(input.repoRoot, baseSha, anchor, related);
|
|
813
842
|
const remoteRefs = refs.filter((r) => r.kind === "remote");
|
|
843
|
+
const MAX_REACHABLE = 5e5;
|
|
844
|
+
const revListSet = async (args) => {
|
|
845
|
+
const res = await git(input.repoRoot, ["rev-list", ...args]);
|
|
846
|
+
if (res.exitCode !== 0) return null;
|
|
847
|
+
const shas = res.stdout.split("\n");
|
|
848
|
+
if (shas.length > MAX_REACHABLE) return null;
|
|
849
|
+
return new Set(shas.filter(Boolean));
|
|
850
|
+
};
|
|
851
|
+
let remoteReachableP = null;
|
|
852
|
+
let localOrRemoteP = null;
|
|
814
853
|
return {
|
|
815
854
|
refs,
|
|
816
855
|
otherRemoteRefs,
|
|
856
|
+
fetched,
|
|
817
857
|
env: {
|
|
818
858
|
ctx,
|
|
819
859
|
tips: /* @__PURE__ */ new Map(),
|
|
@@ -824,6 +864,10 @@ async function snapshot(input) {
|
|
|
824
864
|
mergedPrShas: mergedPrs,
|
|
825
865
|
remoteShas: new Set(remoteRefs.map((r) => r.sha)),
|
|
826
866
|
remoteNames: new Set(remoteRefs.map((r) => r.name)),
|
|
867
|
+
...remote ? {
|
|
868
|
+
remoteReachable: () => remoteReachableP ??= revListSet([`--glob=refs/remotes/${remote}/*`]),
|
|
869
|
+
localOrRemoteReachable: () => localOrRemoteP ??= revListSet(["--branches", `--glob=refs/remotes/${remote}/*`])
|
|
870
|
+
} : {},
|
|
827
871
|
minAgeDays: input.minAgeDays,
|
|
828
872
|
includeReviewed: input.includeReviewed,
|
|
829
873
|
...input.verdicts ? { verdicts: input.verdicts } : {},
|
|
@@ -857,7 +901,8 @@ async function planBranchGc(input) {
|
|
|
857
901
|
...input.verdicts ? { verdicts: input.verdicts } : {},
|
|
858
902
|
minAgeDays,
|
|
859
903
|
includeReviewed,
|
|
860
|
-
nowMs
|
|
904
|
+
nowMs,
|
|
905
|
+
...input.fetch === false ? { fetch: false } : {}
|
|
861
906
|
});
|
|
862
907
|
const inScope = snap.refs.filter((r) => scopes.includes(r.kind));
|
|
863
908
|
const entries = await pool(inScope, input.concurrency ?? 8, (r) => classifyRef(r, snap.env));
|
|
@@ -875,6 +920,7 @@ async function planBranchGc(input) {
|
|
|
875
920
|
includeReviewed,
|
|
876
921
|
generatedAt: new Date(nowMs).toISOString(),
|
|
877
922
|
otherRemoteRefs: snap.otherRemoteRefs,
|
|
923
|
+
fetched: snap.fetched,
|
|
878
924
|
entries
|
|
879
925
|
};
|
|
880
926
|
}
|
|
@@ -963,7 +1009,8 @@ async function applyBranchGc(plan, options) {
|
|
|
963
1009
|
...options.verdicts ? { verdicts: options.verdicts } : {},
|
|
964
1010
|
minAgeDays: plan.minAgeDays,
|
|
965
1011
|
includeReviewed: plan.includeReviewed,
|
|
966
|
-
nowMs: options.nowMs ?? Date.now()
|
|
1012
|
+
nowMs: options.nowMs ?? Date.now(),
|
|
1013
|
+
...options.fetch === false ? { fetch: false } : {}
|
|
967
1014
|
});
|
|
968
1015
|
const current = new Map(snap.refs.map((r) => [r.ref, r]));
|
|
969
1016
|
const outcomes = [];
|
|
@@ -1025,22 +1072,33 @@ async function applyBranchGc(plan, options) {
|
|
|
1025
1072
|
for (const [remote, list] of byRemote) {
|
|
1026
1073
|
const failedBatches = [];
|
|
1027
1074
|
for (const batch of chunk(list, options.remoteBatchSize ?? 50)) {
|
|
1028
|
-
const res = await git(repoRoot, ["push", remote, "--delete", ...batch.map((e) => e.name)]);
|
|
1075
|
+
const res = await git(repoRoot, ["push", "--no-verify", remote, "--delete", ...batch.map((e) => e.name)]);
|
|
1029
1076
|
if (res.exitCode === 0) for (const e of batch) results.set(e.ref, { ok: true });
|
|
1030
1077
|
else failedBatches.push({ batch, res });
|
|
1031
1078
|
}
|
|
1032
1079
|
await git(repoRoot, ["fetch", "--prune", "--quiet", remote]);
|
|
1033
|
-
|
|
1080
|
+
const stillThere = [];
|
|
1034
1081
|
for (const { batch } of failedBatches) {
|
|
1035
1082
|
for (const e of batch) {
|
|
1036
|
-
if ((await git(repoRoot, ["rev-parse", "--verify", "--quiet", e.ref])).exitCode
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1083
|
+
if ((await git(repoRoot, ["rev-parse", "--verify", "--quiet", e.ref])).exitCode === 0) stillThere.push(e);
|
|
1084
|
+
else results.set(e.ref, { ok: true });
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
const failedAgain = [];
|
|
1088
|
+
for (const batch of chunk(stillThere, options.remoteBatchSize ?? 50)) {
|
|
1089
|
+
const retry = await git(repoRoot, ["push", "--no-verify", remote, "--delete", ...batch.map((e) => e.name)]);
|
|
1090
|
+
if (retry.exitCode === 0) for (const e of batch) results.set(e.ref, { ok: true });
|
|
1091
|
+
else failedAgain.push(...batch);
|
|
1092
|
+
}
|
|
1093
|
+
const retried = failedAgain.length > 0;
|
|
1094
|
+
if (retried) await git(repoRoot, ["fetch", "--prune", "--quiet", remote]);
|
|
1095
|
+
for (const e of failedAgain) {
|
|
1096
|
+
if ((await git(repoRoot, ["rev-parse", "--verify", "--quiet", e.ref])).exitCode !== 0) {
|
|
1097
|
+
results.set(e.ref, { ok: true });
|
|
1098
|
+
continue;
|
|
1043
1099
|
}
|
|
1100
|
+
const one = await git(repoRoot, ["push", "--no-verify", remote, "--delete", e.name]);
|
|
1101
|
+
results.set(e.ref, one.exitCode === 0 ? { ok: true } : fail(one));
|
|
1044
1102
|
}
|
|
1045
1103
|
if (retried) await git(repoRoot, ["fetch", "--prune", "--quiet", remote]);
|
|
1046
1104
|
}
|