FN-112: capture workspace merge evidence from task worktrees
Capture workspace merge evidence from immutable acquisition baselines to persisted task branches so real linked task worktrees land reviewed changes reliably. - Compare review fingerprints and landing ranges against the persisted task branch rather than the live worktree HEAD. - Add linked-task-worktree regression coverage for per-repository landing proof and exactly-once finalization. - Document the regression and publish the workspace merge fix in a patch changeset. Files changed: .../fn-112-workspace-task-worktree-evidence.md | 7 ++++ docs/architecture.md | 1 + ...e-empty-merge-boundary-finalization-livelock.md | 8 +++- .../engine/src/__tests__/_workspace-fixture.ts | 15 ++++++++ .../engine/src/__tests__/workspace-e2e.test.ts | 31 ++++++++++++--- .../__tests__/workspace-merger-scope-gates.test.ts | 2 +- .../engine/src/__tests__/workspace-merger.test.ts | 45 +++++++++++++++++++++- packages/engine/src/merge/merger-ai.ts | 29 ++++++++++++-- 8 files changed, 125 insertions(+), 13 deletions(-) Fusion-Task-Id: FN-112 Fusion-Task-Lineage: 66e8498e-13f2-461b-ae38-bf750383115b Co-authored-by: Fusion <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-112-workspace-task-worktree-evidence.md
Normal file
7
.changeset/fn-112-workspace-task-worktree-evidence.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Fix workspace auto-merge for work in linked task worktrees.
|
||||
category: fix
|
||||
dev: Capture merge evidence from each acquisition baseline to its persisted task branch.
|
||||
@@ -629,6 +629,7 @@ See [Memory Plugin Contract](./memory-plugin-contract.md) for the full plan.
|
||||
- **Workspace acquisition shape:** per-repo acquisition persists only `workspaceWorktrees`; it never exposes a sub-repo path or branch through singular `task.worktree`/`task.branch`, including if the final workspace-state write fails. This preserves workspace classification for dashboard rendering, self-healing, and executor dispatch.
|
||||
- **No root task worktree (FN-034):** when `.fusion/workspace.json` declares repositories, planning, execution, graph nodes, code/plan review, pause/resume, restart, and worktree-session recovery acquire or reuse only the declared repositories' worktrees. The workspace root and `<workspace>/.worktrees/<task>` are never task checkout or reviewer cwd. Historical singular `worktree`, `branch`, execution-base, and root-session routing metadata is cleared through an advisory-locked, project-scoped mutation that preserves every `workspaceWorktrees` entry and completed-step progress; recovery never deletes or recreates a root checkout.
|
||||
- **Workspace entry mutation (FN-9052):** every per-repository `workspaceWorktrees` update goes through `TaskStore.mergeWorkspaceWorktreeEntry`, which holds the per-task PostgreSQL advisory transaction lock and merges one key under the composite project/task scope. Per-repo callers must never wholesale-replace the map, because a concurrent sibling acquisition, landing, failure, or teardown mutation would otherwise lose its entry.
|
||||
- **Workspace merge-boundary evidence (FN-112):** landing compares the immutable per-repository acquisition baseline (`baseCommitSha`) to the persisted task branch for changed files, binary review fingerprints, and ahead-but-net-zero evidence. A live linked task worktree normally has `HEAD === entry.branch`, so comparing its `HEAD` to `entry.branch` is a self-comparison that falsely reports no work. Legacy entries without a captured SHA resolve their recorded base target and derive its merge base against the task branch. Code Review and landing therefore approve and consume the same range; unreadable bases, missing scope evidence, and unexplained empty ranges still fail closed before leases, intents, ref advancement, or pushes. Main-checkout fixture stand-ins can hide this defect because their `HEAD` is the integration branch, so real-Git lifecycle coverage keeps a registered linked task worktree alive.
|
||||
- **Durable multi-node workspace coordination (FN-9059):** `project.workspace_coordination_leases`, serialized under a project/resource advisory transaction lock, owns sub-repository acquire exclusivity, per-repository land, workspace liveness/recovery guards, and merge-dispatch admission across engine processes. The owner triple `(taskId, nodeId, incarnationId)` makes only same-process re-entry idempotent; the same task from another node or process incarnation contends. Fresh/reclaimed claims receive a monotonically increasing fence token, while re-entry and renewal retain it. Git-writing tenancies publish once per tenancy—on acquire/reclaimed-expired, not re-entry—the per-repository `refs/fusion/workspace-lease/<repo-slug>` or per-merge `refs/fusion/merge-dispatch/<task-id>` fence ref. A workspace merge dispatches its deterministic per-merge pin to every target sub-repository remote before any workspace land begins; the workspace root need not be a git checkout and is never used as a substitute remote. A re-entrant claimant reuses its pin, except to repair a claim-to-publish gap with no pin; acquire-kind leases have no fence ref. A land push atomically CASes the target ref observed by the tenancy plus its repository fence and enclosing merge-dispatch fence refs in one push; a merge-only push CASes its target and dispatch pin. Thus a superseded owner is rejected even when the target tip has not moved. Lease-protected durable writes run through `withValidWorkspaceLeaseAsync`, and lease mutation is owner-and-fence conditional; no validate-then-act path is valid.
|
||||
- **Workspace merge and land crash boundaries (FN-9059):** merge dispatch claims at body dispatch rather than enqueue; a losing claimant benignly drops. Each live per-repository land lease renews before its five-minute TTL throughout dependency sync, AI merge/review, retry, push, and intent resolution. A refused or failed renewal aborts the land body before its next durable or remote commit point; renewal only maintains liveness, while the current owner/fence handle remains the write authority. The body re-proves its fence at admission, the target-plus-fence atomic push, subsequent PR/branch/status effects, and terminal outcome persistence. Non-CASable effects are idempotent and follow the fenced push, so mid-merge expiry stops a superseded body at its next commit point; a pushed result whose outcome cannot persist is `merge-completed-unrecorded`, never a re-push. Before workspace land pushes, `project.workspace_land_intents` records the expected tip, intended SHA, remote/integration identity, and fence pin. The ordered protocol is intent → atomic push → lease-validated `landedSha` persist plus intent resolution. The node-independent reconciler lists pending intents project-wide, fetches the recorded remote, and resolves reachability on its integration ref. Only a live holder (its own/equal fence or a lower-fence predecessor) or recovery with no held unexpired lease can resolve an intent; stale fence matches, local tip equality, and local object availability are not authority.
|
||||
- **Workspace File Scope interpretation (FN-078):** squash gates, completion scope-leak checks, and main-checkout guards use one repo-local resolver. A matching repo-prefixed declaration has priority; only a declaration with no configured repository prefix can fall back as repo-local scope. If any declaration belongs to another configured repository, a repo with no matching prefix receives no fallback authority. This preserves whole-repo `repo` → `**`, normalized segment-aware nested keys, empty-scope behavior, and strict foreign-repository isolation.
|
||||
|
||||
@@ -35,6 +35,12 @@ Workspace review remediation stores only the scope revision, failing repository,
|
||||
| H12 optional deployment callback | Confirmed risk | Direct `landWorkspaceTask` performs its own canonical blocker check. |
|
||||
| H13 planning dispatch noise | Non-causal signal | Existing PostgreSQL planning-episode deduplication remains unchanged. |
|
||||
|
||||
## FN-112 linked-task-worktree regression
|
||||
|
||||
FN-106 correctly rejected unexplained empty obligations, but its landing capture compared `HEAD` with `entry.branch` from `entry.worktreePath`. In production that path is the live linked task worktree, where both names normally resolve to the same task tip. The self-comparison made reviewed changes look empty and produced `Workspace merge has no evidenced landing obligations`.
|
||||
|
||||
Landing now uses one range for all evidence: the immutable acquisition `baseCommitSha` to the persisted task branch. Legacy entries without that SHA resolve their recorded repository target and derive a merge base against that branch. This matches Code Review's acquisition-base-to-linked-`HEAD` fingerprint while retaining the fail-closed readiness rule for unreadable bases, stale review fingerprints, out-of-scope work, duplicates, net-zero branches, and unexplained emptiness. Real-Git lifecycle coverage keeps a registered linked task worktree alive rather than substituting the main checkout, and proves exactly-once integration advancement, durable per-repository and aggregate landing proof, and terminal finalization.
|
||||
|
||||
## Regression coverage
|
||||
|
||||
Run the targeted engine workspace merger, self-healing, checkout-guard, review-routing, lease, real-Git slow, PostgreSQL workspace, CLI, and dashboard consumer tests listed in FN-106. The symptom fixture covers a pre-baseline same-ID commit, a previously landed scoped repository, an all-landed second pass, failed Code Review admission, and recreated recovery owners.
|
||||
Run the targeted engine workspace merger, self-healing, checkout-guard, review-routing, lease, real-Git slow, PostgreSQL workspace, CLI, and dashboard consumer tests listed in FN-106. The symptom fixture covers a pre-baseline same-ID commit, a previously landed scoped repository, an all-landed second pass, failed Code Review admission, recreated recovery owners, and the live linked-task-worktree shape repaired by FN-112.
|
||||
|
||||
@@ -37,6 +37,8 @@ export interface WorkspaceFixture {
|
||||
repoPath(rel: string): string;
|
||||
/** Run a git command inside a sub-repo. */
|
||||
git(rel: string, command: string): string;
|
||||
/** Create a live linked task worktree and capture its immutable acquisition baseline. */
|
||||
createLinkedTaskWorktree(rel: string, branch: string): { worktreePath: string; baseCommitSha: string };
|
||||
/** Remove all on-disk fixture state. */
|
||||
cleanup(): void;
|
||||
}
|
||||
@@ -62,6 +64,19 @@ export async function createWorkspaceFixture(
|
||||
repos,
|
||||
repoPath: (rel: string) => path.join(rootDir, rel),
|
||||
git: (rel: string, command: string) => git(path.join(rootDir, rel), command),
|
||||
/*
|
||||
FNXC:WorkspaceMergeEvidence 2026-08-21-17:33:
|
||||
FN-112 needs real-Git coverage where the persisted path is the live task worktree and HEAD
|
||||
equals its task branch. Main-checkout stand-ins hid the invalid self-comparison that erased
|
||||
merge evidence, so this fixture captures the acquisition baseline before the task commit.
|
||||
*/
|
||||
createLinkedTaskWorktree: (rel: string, branch: string) => {
|
||||
const repoDir = path.join(rootDir, rel);
|
||||
const worktreePath = path.join(rootDir, `.task-worktree-${rel.replace(/[^a-z0-9]+/gi, "-")}`);
|
||||
const baseCommitSha = git(repoDir, "git rev-parse HEAD");
|
||||
git(repoDir, `git worktree add -b ${branch} ${worktreePath} HEAD`);
|
||||
return { worktreePath, baseCommitSha };
|
||||
},
|
||||
cleanup: () => rmSync(rootDir, { recursive: true, force: true }),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ function makeTask(workspaceWorktrees: Task["workspaceWorktrees"], extra: Partial
|
||||
.filter(([repo]) => scopedRepositories.includes(repo))
|
||||
.map(([repo, entry]) => {
|
||||
const mergeBase = execSync(`git merge-base HEAD ${entry.branch}`, { cwd: entry.worktreePath, encoding: "utf8" }).trim();
|
||||
const diff = execSync(`git diff --binary ${entry.baseCommitSha ?? mergeBase}..HEAD`, { cwd: entry.worktreePath, encoding: "utf8" });
|
||||
const diff = execSync(`git diff --binary ${entry.baseCommitSha ?? mergeBase}..${entry.branch}`, { cwd: entry.worktreePath, encoding: "utf8" });
|
||||
return [repo, { fingerprint: createHash("sha256").update(diff).digest("hex"), approvedAt: new Date().toISOString() }];
|
||||
}));
|
||||
const task = {
|
||||
@@ -201,6 +201,19 @@ function addRepoBranchWithEdit(fx: WorkspaceFixture, repoRel: string, content: s
|
||||
fx.git(repoRel, `git worktree remove --force ${wt}`);
|
||||
}
|
||||
|
||||
function addLinkedTaskWorktreeWithEdit(
|
||||
fx: WorkspaceFixture,
|
||||
repoRel: string,
|
||||
content: string,
|
||||
): { worktreePath: string; branch: string; baseCommitSha: string } {
|
||||
const linked = fx.createLinkedTaskWorktree(repoRel, BRANCH);
|
||||
configureIdentity(linked.worktreePath);
|
||||
writeFileSync(path.join(linked.worktreePath, "feature.txt"), content, "utf-8");
|
||||
execSync("git add feature.txt", { cwd: linked.worktreePath, stdio: "pipe" });
|
||||
execSync(`git commit -m "feat(${TASK_ID}): linked task worktree feature in ${repoRel}"`, { cwd: linked.worktreePath, stdio: "pipe" });
|
||||
return { ...linked, branch: BRANCH };
|
||||
}
|
||||
|
||||
/** Make a sub-repo's integration tip and the task branch BOTH edit README so the squash conflicts. */
|
||||
function makeConflictingRepo(fx: WorkspaceFixture, repoRel: string): void {
|
||||
const repoDir = fx.repoPath(repoRel);
|
||||
@@ -249,8 +262,8 @@ describeIfGit("workspace e2e — merge (no-push) + partial-land recovery (Phase
|
||||
fx = await createWorkspaceFixture(["repo-a", "repo-b"]);
|
||||
const originA = addOriginRemote(fx, "repo-a");
|
||||
const originB = addOriginRemote(fx, "repo-b");
|
||||
addRepoBranchWithEdit(fx, "repo-a", "a feature\n");
|
||||
addRepoBranchWithEdit(fx, "repo-b", "b feature\n");
|
||||
const repoA = addLinkedTaskWorktreeWithEdit(fx, "repo-a", "a feature\n");
|
||||
const repoB = addLinkedTaskWorktreeWithEdit(fx, "repo-b", "b feature\n");
|
||||
|
||||
const tipABefore = fx.git("repo-a", "git rev-parse refs/heads/main");
|
||||
const tipBBefore = fx.git("repo-b", "git rev-parse refs/heads/main");
|
||||
@@ -263,8 +276,8 @@ describeIfGit("workspace e2e — merge (no-push) + partial-land recovery (Phase
|
||||
|
||||
const store = createStore([
|
||||
makeTask({
|
||||
"repo-a": { worktreePath: fx.repoPath("repo-a"), branch: BRANCH },
|
||||
"repo-b": { worktreePath: fx.repoPath("repo-b"), branch: BRANCH },
|
||||
"repo-a": repoA,
|
||||
"repo-b": repoB,
|
||||
}),
|
||||
]);
|
||||
const task = store.tasks.get(TASK_ID)!;
|
||||
@@ -289,6 +302,12 @@ describeIfGit("workspace e2e — merge (no-push) + partial-land recovery (Phase
|
||||
expect(persisted["repo-b"].landedSha).toBeTruthy();
|
||||
expect(persisted["repo-a"].landedSha).toBe(fx.git("repo-a", "git rev-parse refs/heads/main"));
|
||||
expect(persisted["repo-b"].landedSha).toBe(fx.git("repo-b", "git rev-parse refs/heads/main"));
|
||||
const finalizedTask = store.tasks.get(TASK_ID)!;
|
||||
expect(finalizedTask.mergeDetails?.workspaceLandedShas).toEqual({
|
||||
"repo-a": persisted["repo-a"].landedSha,
|
||||
"repo-b": persisted["repo-b"].landedSha,
|
||||
});
|
||||
expect(finalizedTask.mergeDetails?.mergeConfirmed).toBe(true);
|
||||
|
||||
// Finalize EXACTLY once.
|
||||
expect(store.moveTaskCalls).toEqual([{ id: TASK_ID, column: "done" }]);
|
||||
@@ -387,7 +406,7 @@ describeIfGit("workspace e2e — merge (no-push) + partial-land recovery (Phase
|
||||
const repoB = recoveringTask.workspaceWorktrees!["repo-b"];
|
||||
const repoBMergeBase = execSync(`git merge-base HEAD ${repoB.branch}`, { cwd: repoB.worktreePath, encoding: "utf8" }).trim();
|
||||
recoveringTask.repositoryScope!.reviewEvidence!["repo-b"] = {
|
||||
fingerprint: createHash("sha256").update(execSync(`git diff --binary ${repoB.baseCommitSha ?? repoBMergeBase}..HEAD`, { cwd: repoB.worktreePath, encoding: "utf8" })).digest("hex"),
|
||||
fingerprint: createHash("sha256").update(execSync(`git diff --binary ${repoB.baseCommitSha ?? repoBMergeBase}..${repoB.branch}`, { cwd: repoB.worktreePath, encoding: "utf8" })).digest("hex"),
|
||||
approvedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ function storeFor(task: Task, scope: string[]): TaskStore & { updates: Array<Rec
|
||||
function reviewEvidence(workspaceWorktrees: NonNullable<Task["workspaceWorktrees"]>): NonNullable<Task["repositoryScope"]>["reviewEvidence"] {
|
||||
return Object.fromEntries(Object.entries(workspaceWorktrees).map(([repo, entry]) => {
|
||||
const mergeBase = execSync(`git merge-base HEAD ${entry.branch}`, { cwd: entry.worktreePath, encoding: "utf8" }).trim();
|
||||
const diff = execSync(`git diff --binary ${entry.baseCommitSha ?? mergeBase}..HEAD`, { cwd: entry.worktreePath, encoding: "utf8" });
|
||||
const diff = execSync(`git diff --binary ${entry.baseCommitSha ?? mergeBase}..${entry.branch}`, { cwd: entry.worktreePath, encoding: "utf8" });
|
||||
return [repo, { fingerprint: createHash("sha256").update(diff).digest("hex"), approvedAt: new Date().toISOString() }];
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -104,6 +104,24 @@ function addRepoBranchWithEdit(fx: WorkspaceFixture, repoRel: string, content: s
|
||||
fx.git(repoRel, `git worktree remove --force ${worktreePath}`);
|
||||
}
|
||||
|
||||
/** Create production-shaped evidence: the persisted path remains a live task worktree at its branch tip. */
|
||||
function addRegisteredTaskWorktreeWithEdit(
|
||||
fx: WorkspaceFixture,
|
||||
repoRel: string,
|
||||
content: string,
|
||||
): { worktreePath: string; baseCommitSha: string } {
|
||||
const repoDir = fx.repoPath(repoRel);
|
||||
// Keep the linked checkout outside its main checkout so the production dirty-root guard stays meaningful.
|
||||
const worktreePath = path.join(fx.rootDir, `.task-worktree-${repoRel}`);
|
||||
const baseCommitSha = fx.git(repoRel, "git rev-parse HEAD");
|
||||
fx.git(repoRel, `git worktree add -b ${BRANCH} ${worktreePath} HEAD`);
|
||||
configureIdentity(worktreePath);
|
||||
writeFileSync(path.join(worktreePath, "feature.txt"), content, "utf-8");
|
||||
execSync("git add feature.txt", { cwd: worktreePath, stdio: "pipe" });
|
||||
execSync(`git commit -m "feat(${TASK_ID}): linked task worktree feature"`, { cwd: worktreePath, stdio: "pipe" });
|
||||
return { worktreePath, baseCommitSha };
|
||||
}
|
||||
|
||||
/**
|
||||
* FN-8141 shape: a `fusion/<id>` branch that committed work then REVERTED it — AHEAD of the
|
||||
* integration tip (two real commits) but net-zero, so its tip is NOT an ancestor of main and the
|
||||
@@ -184,7 +202,7 @@ function makeTask(workspaceWorktrees: Task["workspaceWorktrees"]): Task {
|
||||
// merge-boundary fingerprint that production requires from Code Review.
|
||||
reviewEvidence: Object.fromEntries(Object.entries(workspaceWorktrees ?? {}).map(([repo, entry]) => {
|
||||
const mergeBase = execSync(`git merge-base HEAD ${entry.branch}`, { cwd: entry.worktreePath, encoding: "utf8" }).trim();
|
||||
const diff = execSync(`git diff --binary ${entry.baseCommitSha ?? mergeBase}..HEAD`, { cwd: entry.worktreePath, encoding: "utf8" });
|
||||
const diff = execSync(`git diff --binary ${entry.baseCommitSha ?? mergeBase}..${entry.branch}`, { cwd: entry.worktreePath, encoding: "utf8" });
|
||||
return [repo, { fingerprint: createHash("sha256").update(diff).digest("hex"), approvedAt: new Date().toISOString() }];
|
||||
})),
|
||||
},
|
||||
@@ -240,6 +258,31 @@ describeIfGit("landWorkspaceTask — per-repo merge loop (Phase C U1)", () => {
|
||||
expect(store.emitted.filter((e) => e.event === "task:merged")).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("lands reviewed work from a live linked task worktree baseline", async () => {
|
||||
fx = await createWorkspaceFixture(["repo-a"]);
|
||||
const linked = addRegisteredTaskWorktreeWithEdit(fx, "repo-a", "linked feature\n");
|
||||
const integrationTipBefore = fx.git("repo-a", "git rev-parse refs/heads/main");
|
||||
const store = createStore();
|
||||
const task = makeTask({
|
||||
"repo-a": { worktreePath: linked.worktreePath, branch: BRANCH, baseCommitSha: linked.baseCommitSha },
|
||||
});
|
||||
|
||||
// The production shape that regressed: HEAD and the branch name resolve to the same task tip.
|
||||
expect(execSync("git rev-parse HEAD", { cwd: linked.worktreePath, encoding: "utf8" }).trim())
|
||||
.toBe(execSync(`git rev-parse ${BRANCH}`, { cwd: linked.worktreePath, encoding: "utf8" }).trim());
|
||||
|
||||
const result = await landWorkspaceTask(store, task, fx.rootDir, {}, {
|
||||
mergeAgent: squashMergeAgent(BRANCH),
|
||||
reviewAgent: approveReviewAgent,
|
||||
});
|
||||
|
||||
expect(result.repos[0]?.error).toBeUndefined();
|
||||
expect(result.repos).toMatchObject([{ repo: "repo-a", status: "landed" }]);
|
||||
expect(result.allLanded).toBe(true);
|
||||
expect(fx.git("repo-a", "git rev-parse refs/heads/main")).not.toBe(integrationTipBefore);
|
||||
expect(store.moveTaskCalls).toEqual([{ id: TASK_ID, column: "done" }]);
|
||||
});
|
||||
|
||||
it("reuses reconciled review findings for a workspace sub-repository", async () => {
|
||||
fx = await createWorkspaceFixture(["repo-a"]);
|
||||
addRepoBranchWithEdit(fx, "repo-a", "a feature\n");
|
||||
|
||||
@@ -2152,11 +2152,32 @@ export async function landWorkspaceTask(
|
||||
.sort(([left], [right]) => left.localeCompare(right))) {
|
||||
if (!entry.branch) throw new Error(`Workspace repository ${repoRel} has no task branch for fresh merge evidence`);
|
||||
try {
|
||||
const { stdout: mergeBase } = await execFileAsync("git", ["merge-base", "HEAD", entry.branch], { cwd: entry.worktreePath, encoding: "utf8" });
|
||||
const { stdout } = await execFileAsync("git", ["diff", "--name-only", `${mergeBase.trim()}..${entry.branch}`], { cwd: entry.worktreePath, encoding: "utf8" });
|
||||
/*
|
||||
FNXC:WorkspaceMergeEvidence 2026-08-21-17:33:
|
||||
FN-112 requires landing evidence to compare the immutable acquisition baseline with the
|
||||
persisted task branch. A linked task worktree has HEAD checked out at entry.branch, so the
|
||||
former HEAD-to-entry.branch comparison self-compared the same commit and incorrectly erased
|
||||
reviewed work. The range below intentionally matches Code Review's baseCommitSha..HEAD range.
|
||||
*/
|
||||
const comparisonBase = entry.baseCommitSha
|
||||
? await git(["rev-parse", "--verify", `${entry.baseCommitSha}^{commit}`], entry.worktreePath)
|
||||
: await (async () => {
|
||||
const baseResolution = await resolveWorkspaceRepoBaseBranch({
|
||||
mode: "recorded",
|
||||
repoRootDir: join(workspaceRootDir, repoRel),
|
||||
repoRelPath: repoRel,
|
||||
task,
|
||||
settings,
|
||||
recordedBaseBranch: entry.baseBranch,
|
||||
});
|
||||
return git(["merge-base", baseResolution.branch, entry.branch!], entry.worktreePath);
|
||||
})();
|
||||
if (!comparisonBase) throw new Error("comparison base is empty");
|
||||
const comparisonRange = `${comparisonBase}..${entry.branch}`;
|
||||
const { stdout } = await execFileAsync("git", ["diff", "--name-only", comparisonRange], { cwd: entry.worktreePath, encoding: "utf8" });
|
||||
const files = stdout.split("\n").map((file) => file.trim()).filter(Boolean);
|
||||
if (files.length > 0) {
|
||||
const { stdout: diffContent } = await execFileAsync("git", ["diff", "--binary", `${entry.baseCommitSha ?? mergeBase.trim()}..HEAD`], { cwd: entry.worktreePath, encoding: "utf8" });
|
||||
const { stdout: diffContent } = await execFileAsync("git", ["diff", "--binary", comparisonRange], { cwd: entry.worktreePath, encoding: "utf8" });
|
||||
mergeBoundaryFingerprints[repoRel] = createHash("sha256").update(diffContent).digest("hex");
|
||||
}
|
||||
if (files.length > 0 && !confirmedScope?.has(repoRel)) {
|
||||
@@ -2165,7 +2186,7 @@ export async function landWorkspaceTask(
|
||||
if (files.length > 0) mergeBoundaryModifiedRepositories.add(repoRel);
|
||||
mergeBoundaryModifiedFiles.push(...files.map((file) => `${repoRel}/${file}`));
|
||||
if (files.length === 0) {
|
||||
const { stdout: aheadCount } = await execFileAsync("git", ["rev-list", "--count", `HEAD..${entry.branch}`], { cwd: entry.worktreePath, encoding: "utf8" });
|
||||
const { stdout: aheadCount } = await execFileAsync("git", ["rev-list", "--count", comparisonRange], { cwd: entry.worktreePath, encoding: "utf8" });
|
||||
if (Number(aheadCount.trim()) > 0) netZeroBranchRepositories.add(repoRel);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user