diff --git a/packages/server/src/server/paseo-worktree-service.ts b/packages/server/src/server/paseo-worktree-service.ts index 10931411b..07abb501e 100644 --- a/packages/server/src/server/paseo-worktree-service.ts +++ b/packages/server/src/server/paseo-worktree-service.ts @@ -39,7 +39,6 @@ export interface CreatePaseoWorktreeInput extends CreateWorktreeCoreInput { export interface CreatePaseoWorktreeResult { worktree: WorktreeConfig; worktreeIncludeSummary?: WorktreeIncludeSummary; - worktreeIncludeSummary?: WorktreeIncludeSummary; intent: WorktreeCreationIntent; workspace: PersistedWorkspaceRecord; repoRoot: string; @@ -121,7 +120,6 @@ async function createPaseoWorktreeWithPriority( return { worktree: createdWorktree.worktree, worktreeIncludeSummary: createdWorktree.worktreeIncludeSummary, - worktreeIncludeSummary: createdWorktree.worktreeIncludeSummary, intent: createdWorktree.intent, workspace, repoRoot: createdWorktree.repoRoot, diff --git a/packages/server/src/server/worktree-core.ts b/packages/server/src/server/worktree-core.ts index 5973e6d18..e3f1eca48 100644 --- a/packages/server/src/server/worktree-core.ts +++ b/packages/server/src/server/worktree-core.ts @@ -1,7 +1,12 @@ import { createNameId } from "mnemonic-id"; import type { ForgeService } from "../services/forge-service.js"; -import { createWorktree, slugify, validateBranchSlug, type WorktreeConfig } from "../utils/worktree.js"; +import { + createWorktree, + slugify, + validateBranchSlug, + type WorktreeConfig, +} from "../utils/worktree.js"; import type { WorktreeIncludeSummary } from "../utils/worktree-include.js"; import { resolveWorktreeCreationIntent, @@ -39,7 +44,6 @@ export interface CreateWorktreeCoreDeps { export interface CreateWorktreeCoreResult { worktree: WorktreeConfig; worktreeIncludeSummary?: WorktreeIncludeSummary; - worktreeIncludeSummary?: WorktreeIncludeSummary; intent: WorktreeCreationIntent; repoRoot: string; created: boolean; diff --git a/packages/server/src/utils/worktree.ts b/packages/server/src/utils/worktree.ts index 6b0aba8cb..463028884 100644 --- a/packages/server/src/utils/worktree.ts +++ b/packages/server/src/utils/worktree.ts @@ -1154,12 +1154,18 @@ export async function deletePaseoWorktree({ } export async function rollbackCreatedPaseoWorktree( - options: DeletePaseoWorktreeOptions, + options: DeletePaseoWorktreeOptions & { createdBranchName?: string }, cause: unknown, ): Promise { let cleanupError: unknown; try { await deletePaseoWorktree(options); + if (options.createdBranchName && options.cwd) { + await runGitCommand(["branch", "--delete", "--force", options.createdBranchName], { + cwd: options.cwd, + acceptExitCodes: [0, 1], + }); + } } catch (error) { cleanupError = error; } @@ -1228,10 +1234,22 @@ export const createWorktree = async ({ }: CreateWorktreeOptions): Promise => { const sourcePlan = await resolveWorktreeSourcePlan({ cwd, source, desiredSlug: worktreeSlug }); const paseoWorktreesBaseRoot = resolvePaseoWorktreesBaseRoot({ paseoHome, worktreesRoot }); - const worktreeIncludePlan = await readWorktreeIncludePlan({ - sourceRoot: cwd, - excludedSourceRoots: [paseoWorktreesBaseRoot], - }); + const worktreeIncludePlan = await (async () => { + try { + return await readWorktreeIncludePlan({ + sourceRoot: cwd, + excludedSourceRoots: [paseoWorktreesBaseRoot], + }); + } catch (error) { + if (sourcePlan.createdBranchName) { + await runGitCommand(["branch", "--delete", "--force", sourcePlan.createdBranchName], { + cwd, + acceptExitCodes: [0, 1], + }); + } + throw error; + } + })(); let worktreePath = join(await getPaseoWorktreesRoot(cwd, paseoHome, worktreesRoot), worktreeSlug); mkdirSync(dirname(worktreePath), { recursive: true }); @@ -1289,7 +1307,14 @@ export const createWorktree = async ({ }; } catch (error) { return rollbackCreatedPaseoWorktree( - { cwd, worktreePath, teardownCwds: [], paseoHome, worktreesBaseRoot: worktreesRoot }, + { + cwd, + worktreePath, + teardownCwds: [], + paseoHome, + worktreesBaseRoot: worktreesRoot, + createdBranchName: sourcePlan.createdBranchName, + }, error, ); } @@ -1317,6 +1342,7 @@ interface ResolveWorktreeSourcePlanOptions { interface WorktreeSourcePlan { branchName: string; + createdBranchName?: string; // Display name and exact ref are two different facts. The name cannot round-trip to a // commit — "main" resolves local-first even when the worktree was cut from a fork's // upstream — so comparisons and actions read the ref and the UI reads the name. @@ -1354,6 +1380,7 @@ async function resolveWorktreeSourcePlan({ return { branchName: newBranchName, + createdBranchName: newBranchName, metadataBaseRefName: normalizedBaseBranch, metadataBaseRef: resolvedBaseBranch, changeRequestLookupTarget: createPaseoWorktreeChangeRequestHint({ @@ -1374,11 +1401,22 @@ async function resolveWorktreeSourcePlan({ } catch { throw new UnknownBranchError({ branchName: source.branchName, cwd }); } + return { + branchName: source.branchName, + createdBranchName: source.branchName, + metadataBaseRefName: source.branchName, + changeRequestLookupTarget: createPaseoWorktreeChangeRequestHint({ + headRef: source.branchName, + localBranchName: source.branchName, + }), + addArguments: [source.branchName], + }; } if (await isBranchCheckedOut(cwd, source.branchName)) { const branchName = await resolveUniqueLocalBranchName(cwd, source.branchName); return { branchName, + createdBranchName: branchName, metadataBaseRefName: source.branchName, changeRequestLookupTarget: createPaseoWorktreeChangeRequestHint({ headRef: branchName, @@ -1447,6 +1485,7 @@ async function resolveWorktreeSourcePlan({ return { branchName: localBranchName, + createdBranchName: localBranchName, metadataBaseRefName: normalizedBaseRefName, changeRequestLookupTarget: createPaseoWorktreeChangeRequestHint({ headRef: source.headRef,