feat(FN-4068): add branch conflict detection and recovery for stale worktre
Implements branch conflict detection and recovery across the Fusion engine, CLI, and dashboard — surfacing git worktree conflicts when tasks conflict with unrelated branch state, and providing a recovery workflow to resolve them. The executor and worktree pool now integrate typed branch conflict che Fusion-Task-Id: FN-4068
This commit is contained in:
@@ -3,6 +3,7 @@ import { promisify } from "node:util";
|
||||
import { existsSync, lstatSync, readdirSync, rmSync } from "node:fs";
|
||||
import { join, relative, resolve, isAbsolute } from "node:path";
|
||||
import type { Column, TaskStore } from "@fusion/core";
|
||||
import { inspectBranchConflict } from "./branch-conflicts.js";
|
||||
import { worktreePoolLog } from "./logger.js";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
@@ -181,14 +182,20 @@ export class WorktreePool {
|
||||
* 4. `git checkout -B <branchName> <startPoint>` — create/reset branch from start point
|
||||
*
|
||||
* Returns the actual branch name used. This may differ from `branchName`
|
||||
* when conflict recovery generates a suffixed name (e.g., `fusion/fn-042-2`).
|
||||
* when legacy conflict recovery is explicitly enabled and generates a suffixed
|
||||
* name (e.g., `fusion/fn-042-2`).
|
||||
*
|
||||
* @param worktreePath — Absolute path to the recycled worktree
|
||||
* @param branchName — Branch name for the new task (e.g., `fusion/fn-042`)
|
||||
* @param startPoint — Git ref to branch from (e.g., `fusion/fn-041`). Defaults to `main`.
|
||||
* @returns The actual branch name checked out in the worktree
|
||||
*/
|
||||
async prepareForTask(worktreePath: string, branchName: string, startPoint?: string): Promise<string> {
|
||||
async prepareForTask(
|
||||
worktreePath: string,
|
||||
branchName: string,
|
||||
startPoint?: string,
|
||||
options?: { allowSiblingBranchRename?: boolean; repoDir?: string },
|
||||
): Promise<string> {
|
||||
// Clean tracked modifications
|
||||
try {
|
||||
await execAsync("git checkout -- .", { cwd: worktreePath });
|
||||
@@ -223,20 +230,27 @@ export class WorktreePool {
|
||||
throw err;
|
||||
}
|
||||
|
||||
// The branch is checked out in a different worktree.
|
||||
// First check if the conflicting worktree still exists on disk.
|
||||
// The branch is checked out in a different worktree. Keep stale-conflict
|
||||
// cleanup behavior for missing paths; otherwise either surface a typed
|
||||
// conflict or, when explicitly enabled, fall back to the legacy sibling
|
||||
// suffix flow.
|
||||
const conflictingPath = match[1];
|
||||
if (!existsSync(conflictingPath)) {
|
||||
// Conflicting worktree no longer exists — prune and retry with original name
|
||||
const inspection = await inspectBranchConflict({
|
||||
repoDir: options?.repoDir ?? worktreePath,
|
||||
branchName,
|
||||
conflictingWorktreePath: conflictingPath,
|
||||
startPoint: base,
|
||||
});
|
||||
if (inspection.kind === "stale") {
|
||||
await execAsync("git worktree prune", { cwd: worktreePath });
|
||||
await execAsync(checkoutCmd, { cwd: worktreePath });
|
||||
return branchName;
|
||||
}
|
||||
|
||||
// Conflicting worktree exists and is active — use a suffixed branch name
|
||||
// to avoid disrupting the other worktree. Seed the suffix from the
|
||||
// original task branch tip rather than the generic base ref so retries
|
||||
// preserve the task's commits instead of resetting to main/baseBranch.
|
||||
if (!options?.allowSiblingBranchRename) {
|
||||
throw inspection.error;
|
||||
}
|
||||
|
||||
const conflictBase = branchName;
|
||||
for (let suffix = 2; suffix <= 6; suffix++) {
|
||||
const suffixedName = `${branchName}-${suffix}`;
|
||||
@@ -252,11 +266,9 @@ export class WorktreePool {
|
||||
if (!suffixStderr.includes("already used by worktree")) {
|
||||
throw suffixErr;
|
||||
}
|
||||
// This suffixed name is also in use — try the next one
|
||||
}
|
||||
}
|
||||
|
||||
// All suffixed names exhausted — should not happen in practice
|
||||
throw new Error(
|
||||
`Cannot create branch for task: "${branchName}" and suffixes -2 through -6 are all in use by other worktrees`,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user