@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 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
- if ((await git(repoRoot, ["merge-base", "--is-ancestor", tip, cmp])).exitCode === 0) {
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 contained = await git(repoRoot, ["for-each-ref", "--contains", b.sha, "--format=x", `refs/remotes/${env.remote}`]);
740
- if (contained.stdout.trim()) return "contained-in-remote";
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
- let retried = false;
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 !== 0) {
1037
- results.set(e.ref, { ok: true });
1038
- continue;
1039
- }
1040
- const one = await git(repoRoot, ["push", remote, "--delete", e.name]);
1041
- results.set(e.ref, one.exitCode === 0 ? { ok: true } : fail(one));
1042
- retried = true;
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
  }