@agentproto/worktree 0.14.2 → 0.14.4
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/README.md
CHANGED
|
@@ -234,7 +234,10 @@ in place rather than silently cleaned up.
|
|
|
234
234
|
merged worktrees on a schedule, so a long-running workspace doesn't accumulate
|
|
235
235
|
stale `_worktrees/*` trees and dead `wt/*` branches. It fires the `worktree_gc`
|
|
236
236
|
tool (the daemon's MCP/HTTP surface over `planGc` / `applyGc` in `src/gc.ts`)
|
|
237
|
-
with `apply: true`, `salvageDirty: false` on a daily cron (`0 4 * * *`, UTC).
|
|
237
|
+
with `apply: true`, `salvageDirty: false`, `wait: true` on a daily cron (`0 4 * * *`, UTC).
|
|
238
|
+
`wait: true` makes the cron run block for the real outcomes; without it a big
|
|
239
|
+
repo outlasts the tool's 25 s default `waitMs`, the run records a bare jobId
|
|
240
|
+
as success, and a failed apply never reaches `on_failure`.
|
|
238
241
|
|
|
239
242
|
**It ships disabled (`enabled: false`)** — it registers but never fires until
|
|
240
243
|
you turn it on. To activate it in a workspace:
|
package/dist/index.d.ts
CHANGED
|
@@ -2141,6 +2141,17 @@ interface LadderContext {
|
|
|
2141
2141
|
* plan's anchor detection asks the same question of every tip) — saves
|
|
2142
2142
|
* the ladder's first `merge-base` per tip. */
|
|
2143
2143
|
related?: ReadonlyMap<string, boolean>;
|
|
2144
|
+
/** Tip sha → `git merge-base baseSha tip`, filled by the same pass as
|
|
2145
|
+
* `related` — saves the ladder's second `merge-base` per related tip. */
|
|
2146
|
+
mergeBases?: ReadonlyMap<string, string>;
|
|
2147
|
+
/**
|
|
2148
|
+
* Memoized `git patch-id --stable` of non-merge commits (the key `git
|
|
2149
|
+
* cherry` matches on), batched and shared across tips: `git cherry` re-did
|
|
2150
|
+
* the patch-id of every base commit since the merge-base for EVERY tip.
|
|
2151
|
+
* `null` for a commit with no patch (empty diff) and `null` for the whole
|
|
2152
|
+
* call when git failed — both mean "ask `git cherry` itself".
|
|
2153
|
+
*/
|
|
2154
|
+
patchIds?: (shas: readonly string[]) => Promise<Map<string, string | null> | null>;
|
|
2144
2155
|
/**
|
|
2145
2156
|
* Memoized set of every commit reachable from `cmp` (at most two distinct
|
|
2146
2157
|
* values: `baseSha` and the anchor) — ONE `git rev-list <cmp>` replaces one
|
|
@@ -2149,7 +2160,7 @@ interface LadderContext {
|
|
|
2149
2160
|
*/
|
|
2150
2161
|
ancestorsOf?: (cmp: string) => Promise<Set<string> | null>;
|
|
2151
2162
|
}
|
|
2152
|
-
declare function createLadderContext(repoRoot: string, baseRef: string, anchor: string | null, related?: ReadonlyMap<string, boolean>): Promise<LadderContext>;
|
|
2163
|
+
declare function createLadderContext(repoRoot: string, baseRef: string, anchor: string | null, related?: ReadonlyMap<string, boolean>, mergeBases?: ReadonlyMap<string, string>): Promise<LadderContext>;
|
|
2153
2164
|
/**
|
|
2154
2165
|
* Run the ladder for one tip against `ctx`'s base. Pure read. A tip with no
|
|
2155
2166
|
* history in common with base is checked against the anchor when there is one
|
|
@@ -2173,7 +2184,9 @@ declare function classifyTipAgainstBase(repoRoot: string, baseRef: string, tip:
|
|
|
2173
2184
|
declare function detectAnchor(repoRoot: string, baseSha: string, tips: readonly string[],
|
|
2174
2185
|
/** Filled with every tip's answer (`true` = shares history with base) for
|
|
2175
2186
|
* the ladder to reuse — see {@link LadderContext.related}. */
|
|
2176
|
-
related?: Map<string, boolean
|
|
2187
|
+
related?: Map<string, boolean>,
|
|
2188
|
+
/** Filled with `merge-base baseSha tip` for every related tip — see {@link LadderContext.mergeBases}. */
|
|
2189
|
+
mergeBases?: Map<string, string>): Promise<string | null>;
|
|
2177
2190
|
declare const DEFAULT_BRANCH_GC_MIN_AGE_DAYS = 3;
|
|
2178
2191
|
interface PlanBranchGcInput {
|
|
2179
2192
|
repoRoot: string;
|
package/dist/index.mjs
CHANGED
|
@@ -503,7 +503,7 @@ async function indexTree(repoRoot, sha) {
|
|
|
503
503
|
const names = new Set([...byPath.keys()].map(basenameOf));
|
|
504
504
|
return { byPath, blobs: new Set(byPath.values()), names };
|
|
505
505
|
}
|
|
506
|
-
async function createLadderContext(repoRoot, baseRef, anchor, related) {
|
|
506
|
+
async function createLadderContext(repoRoot, baseRef, anchor, related, mergeBases) {
|
|
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;
|
|
@@ -522,6 +522,33 @@ async function createLadderContext(repoRoot, baseRef, anchor, related) {
|
|
|
522
522
|
}
|
|
523
523
|
return p;
|
|
524
524
|
};
|
|
525
|
+
const patchIdCache = /* @__PURE__ */ new Map();
|
|
526
|
+
const patchIds = async (shas) => {
|
|
527
|
+
const fresh = [...new Set(shas)].filter((sha) => !patchIdCache.has(sha));
|
|
528
|
+
if (fresh.length) {
|
|
529
|
+
const settle = /* @__PURE__ */ new Map();
|
|
530
|
+
for (const sha of fresh) {
|
|
531
|
+
const p = new Promise((resolve4, reject) => settle.set(sha, { resolve: resolve4, reject }));
|
|
532
|
+
p.catch(() => patchIdCache.delete(sha));
|
|
533
|
+
patchIdCache.set(sha, p);
|
|
534
|
+
}
|
|
535
|
+
await pool(chunk(fresh, PATCH_ID_BATCH), 2, async (batch) => {
|
|
536
|
+
try {
|
|
537
|
+
const got = await patchIdsOf(repoRoot, batch);
|
|
538
|
+
for (const sha of batch) settle.get(sha)?.resolve(got.get(sha) ?? null);
|
|
539
|
+
} catch (err) {
|
|
540
|
+
for (const sha of batch) settle.get(sha)?.reject(err);
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
try {
|
|
545
|
+
const out = /* @__PURE__ */ new Map();
|
|
546
|
+
for (const sha of shas) out.set(sha, await patchIdCache.get(sha) ?? null);
|
|
547
|
+
return out;
|
|
548
|
+
} catch {
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
};
|
|
525
552
|
return {
|
|
526
553
|
repoRoot,
|
|
527
554
|
baseSha,
|
|
@@ -529,11 +556,96 @@ async function createLadderContext(repoRoot, baseRef, anchor, related) {
|
|
|
529
556
|
anchor,
|
|
530
557
|
index: () => idx ??= indexTree(repoRoot, baseSha),
|
|
531
558
|
ancestorsOf,
|
|
532
|
-
|
|
559
|
+
patchIds,
|
|
560
|
+
...related ? { related } : {},
|
|
561
|
+
...mergeBases ? { mergeBases } : {}
|
|
533
562
|
};
|
|
534
563
|
}
|
|
535
|
-
|
|
536
|
-
|
|
564
|
+
var PATCH_ID_BATCH = 500;
|
|
565
|
+
function patchIdsOf(repoRoot, shas) {
|
|
566
|
+
return new Promise((resolvePromise, reject) => {
|
|
567
|
+
const log = spawn(
|
|
568
|
+
"git",
|
|
569
|
+
[
|
|
570
|
+
"-C",
|
|
571
|
+
repoRoot,
|
|
572
|
+
"-c",
|
|
573
|
+
"diff.noprefix=false",
|
|
574
|
+
"-c",
|
|
575
|
+
"diff.mnemonicPrefix=false",
|
|
576
|
+
"log",
|
|
577
|
+
"--no-walk=unsorted",
|
|
578
|
+
"--stdin",
|
|
579
|
+
"-p",
|
|
580
|
+
"--no-color",
|
|
581
|
+
"--no-renames",
|
|
582
|
+
"--no-textconv",
|
|
583
|
+
"--no-ext-diff"
|
|
584
|
+
],
|
|
585
|
+
{ cwd: repoRoot, stdio: ["pipe", "pipe", "pipe"] }
|
|
586
|
+
);
|
|
587
|
+
const pid = spawn("git", ["-C", repoRoot, "patch-id", "--stable"], { cwd: repoRoot, stdio: ["pipe", "pipe", "pipe"] });
|
|
588
|
+
let out = "";
|
|
589
|
+
let logCode = null;
|
|
590
|
+
let pidCode = null;
|
|
591
|
+
let failed = false;
|
|
592
|
+
const fail = (err) => {
|
|
593
|
+
if (failed) return;
|
|
594
|
+
failed = true;
|
|
595
|
+
log.kill();
|
|
596
|
+
pid.kill();
|
|
597
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
598
|
+
};
|
|
599
|
+
const done = () => {
|
|
600
|
+
if (failed || logCode === null || pidCode === null) return;
|
|
601
|
+
if (logCode !== 0 || pidCode !== 0) return fail(new Error(`patch-id batch failed (log exit ${logCode}, patch-id exit ${pidCode})`));
|
|
602
|
+
const ids = /* @__PURE__ */ new Map();
|
|
603
|
+
for (const line of out.split("\n")) {
|
|
604
|
+
const [id, commit] = line.split(" ");
|
|
605
|
+
if (id && commit) ids.set(commit, id);
|
|
606
|
+
}
|
|
607
|
+
resolvePromise(ids);
|
|
608
|
+
};
|
|
609
|
+
log.stdout.pipe(pid.stdin);
|
|
610
|
+
log.stdout.on("error", fail);
|
|
611
|
+
pid.stdin.on("error", () => {
|
|
612
|
+
});
|
|
613
|
+
pid.stdout.on("data", (d) => out += d.toString("utf8"));
|
|
614
|
+
log.stderr.resume();
|
|
615
|
+
pid.stderr.resume();
|
|
616
|
+
log.on("error", fail);
|
|
617
|
+
pid.on("error", fail);
|
|
618
|
+
log.on("close", (code) => {
|
|
619
|
+
logCode = code ?? -1;
|
|
620
|
+
done();
|
|
621
|
+
});
|
|
622
|
+
pid.on("close", (code) => {
|
|
623
|
+
pidCode = code ?? -1;
|
|
624
|
+
done();
|
|
625
|
+
});
|
|
626
|
+
log.stdin.on("error", () => {
|
|
627
|
+
});
|
|
628
|
+
log.stdin.end(shas.join("\n") + "\n");
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
async function allPatchEquivalent(ctx, cmp, tip) {
|
|
632
|
+
if (!ctx.patchIds) return null;
|
|
633
|
+
const lines = async (range) => (await gitOk(ctx.repoRoot, ["rev-list", "--no-merges", range])).split("\n").filter(Boolean);
|
|
634
|
+
const [tipOnly, upstreamOnly] = await Promise.all([lines(`${cmp}..${tip}`), lines(`${tip}..${cmp}`)]);
|
|
635
|
+
const ids = await ctx.patchIds([...tipOnly, ...upstreamOnly]);
|
|
636
|
+
if (!ids) return null;
|
|
637
|
+
const upstream = /* @__PURE__ */ new Set();
|
|
638
|
+
for (const sha of upstreamOnly) {
|
|
639
|
+
const id = ids.get(sha);
|
|
640
|
+
if (id) upstream.add(id);
|
|
641
|
+
}
|
|
642
|
+
let unknown = false;
|
|
643
|
+
for (const sha of tipOnly) {
|
|
644
|
+
const id = ids.get(sha);
|
|
645
|
+
if (!id) unknown = true;
|
|
646
|
+
else if (!upstream.has(id)) return false;
|
|
647
|
+
}
|
|
648
|
+
return unknown ? null : true;
|
|
537
649
|
}
|
|
538
650
|
var NULL_SHA_RE = /^0+$/;
|
|
539
651
|
async function changedBlobs(repoRoot, mergeBase, tip) {
|
|
@@ -592,7 +704,13 @@ async function coverage(ctx, tip, mergeBase) {
|
|
|
592
704
|
}
|
|
593
705
|
async function classifyTip(ctx, tip) {
|
|
594
706
|
const { repoRoot, baseSha, baseTree, anchor } = ctx;
|
|
595
|
-
|
|
707
|
+
let relatedMergeBase = ctx.mergeBases?.get(tip);
|
|
708
|
+
let related = ctx.related?.get(tip);
|
|
709
|
+
if (related === void 0) {
|
|
710
|
+
const res = await git(repoRoot, ["merge-base", baseSha, tip]);
|
|
711
|
+
related = res.exitCode === 0;
|
|
712
|
+
if (related) relatedMergeBase = res.stdout.trim();
|
|
713
|
+
}
|
|
596
714
|
if (!related && !anchor) return { status: "unmerged", history: "unrelated", conflicts: true, ahead: null, behind: null };
|
|
597
715
|
const history = related ? "current" : "pre-rewrite";
|
|
598
716
|
const cmp = related ? baseSha : anchor;
|
|
@@ -604,13 +722,13 @@ async function classifyTip(ctx, tip) {
|
|
|
604
722
|
const counts = (await gitOk(repoRoot, ["rev-list", "--left-right", "--count", `${cmp}...${tip}`])).trim().split(/\s+/);
|
|
605
723
|
const behind = Number(counts[0]);
|
|
606
724
|
const ahead = Number(counts[1]);
|
|
607
|
-
const mergeBase = (await git(repoRoot, ["merge-base", cmp, tip])).stdout.trim();
|
|
725
|
+
const mergeBase = (related ? relatedMergeBase : void 0) ?? (await git(repoRoot, ["merge-base", cmp, tip])).stdout.trim();
|
|
608
726
|
const mtArgs = related ? [baseSha, tip] : [`--merge-base=${mergeBase}`, baseSha, tip];
|
|
609
727
|
const mt = await git(repoRoot, ["merge-tree", "--write-tree", "--no-messages", ...mtArgs]);
|
|
610
728
|
const mergedTree = (mt.stdout.split("\n")[0] ?? "").trim();
|
|
611
729
|
const conflicts = mt.exitCode !== 0;
|
|
612
730
|
if (!conflicts && mergedTree === baseTree) return { status: "squash-merged", history, ahead, behind, conflicts };
|
|
613
|
-
if (conflicts) {
|
|
731
|
+
if (conflicts && await allPatchEquivalent(ctx, cmp, tip) !== false) {
|
|
614
732
|
const cherry = await gitOk(repoRoot, ["cherry", cmp, tip]);
|
|
615
733
|
if (!cherry.split("\n").some((l) => l.startsWith("+"))) return { status: "patch-merged", history, ahead, behind, conflicts };
|
|
616
734
|
}
|
|
@@ -653,14 +771,18 @@ async function classifyTipAgainstBase(repoRoot, baseRef, tip) {
|
|
|
653
771
|
}
|
|
654
772
|
return classifyTip(await ctx, tip);
|
|
655
773
|
}
|
|
656
|
-
async function detectAnchor(repoRoot, baseSha, tips, related) {
|
|
774
|
+
async function detectAnchor(repoRoot, baseSha, tips, related, mergeBases) {
|
|
657
775
|
const roots = (await gitOk(repoRoot, ["rev-list", "--max-parents=0", baseSha])).trim().split("\n").filter(Boolean);
|
|
658
776
|
if (roots.length !== 1) return null;
|
|
659
777
|
const root = roots[0];
|
|
660
778
|
const unique = [...new Set(tips)];
|
|
661
|
-
const
|
|
662
|
-
|
|
663
|
-
|
|
779
|
+
const bases = await pool(unique, 8, async (sha) => {
|
|
780
|
+
const res = await git(repoRoot, ["merge-base", baseSha, sha]);
|
|
781
|
+
return res.exitCode === 0 ? res.stdout.trim() : null;
|
|
782
|
+
});
|
|
783
|
+
const unrelated = unique.filter((_, i) => bases[i] === null);
|
|
784
|
+
if (related) unique.forEach((sha, i) => related.set(sha, bases[i] !== null));
|
|
785
|
+
if (mergeBases) unique.forEach((sha, i) => bases[i] !== null && mergeBases.set(sha, bases[i]));
|
|
664
786
|
if (!unrelated.length) return null;
|
|
665
787
|
const rootDate = (await gitOk(repoRoot, ["log", "-1", "--format=%cI", root])).trim();
|
|
666
788
|
const since = new Date(new Date(rootDate).getTime() - 3 * 864e5).toISOString();
|
|
@@ -835,10 +957,11 @@ async function snapshot(input) {
|
|
|
835
957
|
const baseSha = (await gitOk(input.repoRoot, ["rev-parse", "--verify", `${input.base}^{commit}`])).trim();
|
|
836
958
|
let anchor;
|
|
837
959
|
const related = /* @__PURE__ */ new Map();
|
|
838
|
-
|
|
960
|
+
const mergeBases = /* @__PURE__ */ new Map();
|
|
961
|
+
if (input.anchor === "auto") anchor = await detectAnchor(input.repoRoot, baseSha, refs.map((r) => r.sha), related, mergeBases);
|
|
839
962
|
else if (input.anchor) anchor = (await gitOk(input.repoRoot, ["rev-parse", "--verify", `${input.anchor}^{commit}`])).trim();
|
|
840
963
|
else anchor = null;
|
|
841
|
-
const ctx = await createLadderContext(input.repoRoot, baseSha, anchor, related);
|
|
964
|
+
const ctx = await createLadderContext(input.repoRoot, baseSha, anchor, related, mergeBases);
|
|
842
965
|
const remoteRefs = refs.filter((r) => r.kind === "remote");
|
|
843
966
|
const MAX_REACHABLE = 5e5;
|
|
844
967
|
const revListSet = async (args) => {
|