fix(FN-4623): align worktrunk sync target and prune fallback
Fusion-Task-Id: FN-4623 Fusion-Task-Lineage: 58122853-cab7-4102-9649-4ceda4c5959e
This commit is contained in:
committed by
gsxdsm
parent
67b8051b91
commit
c98a4e2f82
@@ -242,6 +242,23 @@ describe("WorktrunkWorktreeBackend", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("sync supports explicit trunk target", async () => {
|
||||
execMock.mockResolvedValue({ stdout: "", stderr: "" });
|
||||
const backend = new WorktrunkWorktreeBackend({ binaryPath: "worktrunk" });
|
||||
|
||||
await backend.sync({ rootDir: "/repo", worktreePath: "/repo/.worktrees/fn-1", branch: "fusion/fn-1", trunk: "release" });
|
||||
expect(execMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'git fetch origin "release"',
|
||||
expect.objectContaining({ cwd: "/repo/.worktrees/fn-1" }),
|
||||
);
|
||||
expect(execMock).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'git rebase "release"',
|
||||
expect.objectContaining({ cwd: "/repo/.worktrees/fn-1" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("maps rebase conflicts to worktrunk_sync_conflict", async () => {
|
||||
execMock.mockResolvedValueOnce({ stdout: "", stderr: "" }).mockRejectedValueOnce({ stderr: "CONFLICT" });
|
||||
const backend = new WorktrunkWorktreeBackend({ binaryPath: "worktrunk" });
|
||||
@@ -251,13 +268,23 @@ describe("WorktrunkWorktreeBackend", () => {
|
||||
).rejects.toMatchObject({ code: "worktrunk_sync_conflict", operation: "sync" });
|
||||
});
|
||||
|
||||
it("prunes via git worktree prune fallback", async () => {
|
||||
execMock.mockResolvedValue({ stdout: "", stderr: "" });
|
||||
it("prunes by listing worktrees and removing worktrunk managed entries", async () => {
|
||||
execMock.mockResolvedValue({
|
||||
stdout:
|
||||
"worktree /repo\nbranch refs/heads/main\n\nworktree /repo/.worktrees/fusion-fn-1\nbranch refs/heads/fusion/fn-1\n\n",
|
||||
stderr: "",
|
||||
});
|
||||
execFileMock.mockResolvedValue({ stdout: "", stderr: "" });
|
||||
const backend = new WorktrunkWorktreeBackend({ binaryPath: "worktrunk" });
|
||||
|
||||
await expect(backend.prune({ rootDir: "/repo" })).resolves.toBeUndefined();
|
||||
expect(execMock).toHaveBeenCalledWith(
|
||||
"git worktree prune",
|
||||
"git worktree list --porcelain",
|
||||
expect.objectContaining({ cwd: "/repo", timeout: 60000, maxBuffer: 10485760 }),
|
||||
);
|
||||
expect(execFileMock).toHaveBeenCalledWith(
|
||||
"worktrunk",
|
||||
["remove", "--foreground", "fusion/fn-1"],
|
||||
expect.objectContaining({ cwd: "/repo", timeout: 60000, maxBuffer: 10485760 }),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -55,6 +55,7 @@ export interface WorktreeSyncInput {
|
||||
rootDir: string;
|
||||
worktreePath: string;
|
||||
branch: string;
|
||||
trunk?: string;
|
||||
taskId?: string;
|
||||
}
|
||||
|
||||
@@ -116,12 +117,21 @@ function getErrorExitCode(error: unknown): number | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseWorktreePathsFromPorcelain(porcelain: string): string[] {
|
||||
return porcelain
|
||||
.split("\n")
|
||||
.filter((line) => line.startsWith("worktree "))
|
||||
.map((line) => line.slice("worktree ".length).trim())
|
||||
.filter(Boolean);
|
||||
function parseWorktreesFromPorcelain(porcelain: string): Array<{ path: string; branch?: string }> {
|
||||
const lines = porcelain.split("\n");
|
||||
const rows: Array<{ path: string; branch?: string }> = [];
|
||||
let current: { path?: string; branch?: string } = {};
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) {
|
||||
if (current.path) rows.push({ path: current.path, branch: current.branch });
|
||||
current = {};
|
||||
continue;
|
||||
}
|
||||
if (line.startsWith("worktree ")) current.path = line.slice("worktree ".length).trim();
|
||||
if (line.startsWith("branch refs/heads/")) current.branch = line.slice("branch refs/heads/".length).trim();
|
||||
}
|
||||
if (current.path) rows.push({ path: current.path, branch: current.branch });
|
||||
return rows;
|
||||
}
|
||||
|
||||
export class NativeWorktreeBackend implements WorktreeBackend {
|
||||
@@ -201,7 +211,7 @@ export class NativeWorktreeBackend implements WorktreeBackend {
|
||||
maxBuffer: MAX_BUFFER,
|
||||
});
|
||||
|
||||
await execAsync(`git rebase ${quoteShellArg(`origin/${input.branch}`)}`, {
|
||||
await execAsync(`git rebase ${quoteShellArg(input.trunk ? input.trunk : `origin/${input.branch}`)}`, {
|
||||
cwd: input.worktreePath,
|
||||
encoding: "utf-8",
|
||||
timeout: NATIVE_TIMEOUT_MS,
|
||||
@@ -307,8 +317,11 @@ export class WorktrunkWorktreeBackend implements WorktreeBackend {
|
||||
timeout: WORKTRUNK_TIMEOUTS_MS.layout,
|
||||
maxBuffer: MAX_BUFFER,
|
||||
});
|
||||
const paths = parseWorktreePathsFromPorcelain(stdout);
|
||||
const resolved = paths.find((path) => path.endsWith(input.branch) || path === input.worktreePath) ?? input.worktreePath;
|
||||
const rows = parseWorktreesFromPorcelain(stdout);
|
||||
const resolved =
|
||||
rows.find((row) => row.branch === input.branch)?.path ??
|
||||
rows.find((row) => row.path.endsWith(input.branch) || row.path === input.worktreePath)?.path ??
|
||||
input.worktreePath;
|
||||
return { path: resolved, branch: input.branch };
|
||||
}
|
||||
|
||||
@@ -333,13 +346,14 @@ export class WorktrunkWorktreeBackend implements WorktreeBackend {
|
||||
|
||||
async sync(input: WorktreeSyncInput): Promise<{ skipped: boolean }> {
|
||||
try {
|
||||
await execAsync(`git fetch origin ${quoteShellArg(input.branch)}`, {
|
||||
const trunk = input.trunk ?? "main";
|
||||
await execAsync(`git fetch origin ${quoteShellArg(trunk)}`, {
|
||||
cwd: input.worktreePath,
|
||||
encoding: "utf-8",
|
||||
timeout: WORKTRUNK_TIMEOUTS_MS.sync,
|
||||
maxBuffer: MAX_BUFFER,
|
||||
});
|
||||
await execAsync(`git rebase ${quoteShellArg(input.branch)}`, {
|
||||
await execAsync(`git rebase ${quoteShellArg(trunk)}`, {
|
||||
cwd: input.worktreePath,
|
||||
encoding: "utf-8",
|
||||
timeout: WORKTRUNK_TIMEOUTS_MS.sync,
|
||||
@@ -366,12 +380,18 @@ export class WorktrunkWorktreeBackend implements WorktreeBackend {
|
||||
}
|
||||
|
||||
async prune(input: WorktreePruneInput): Promise<void> {
|
||||
await execAsync("git worktree prune", {
|
||||
const { stdout } = await execAsync("git worktree list --porcelain", {
|
||||
cwd: input.rootDir,
|
||||
encoding: "utf-8",
|
||||
timeout: WORKTRUNK_TIMEOUTS_MS.prune,
|
||||
maxBuffer: MAX_BUFFER,
|
||||
});
|
||||
const rows = parseWorktreesFromPorcelain(stdout).filter(
|
||||
(row) => row.path !== input.rootDir && row.path.includes(".worktrees") && row.branch,
|
||||
);
|
||||
for (const row of rows) {
|
||||
await this.remove({ rootDir: input.rootDir, worktreePath: row.path, branch: row.branch });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user