diff --git a/packages/ui/src/lib/git/integrateWorktreeCommits.ts b/packages/ui/src/lib/git/integrateWorktreeCommits.ts index e72c6204..a1fb8bae 100644 --- a/packages/ui/src/lib/git/integrateWorktreeCommits.ts +++ b/packages/ui/src/lib/git/integrateWorktreeCommits.ts @@ -21,6 +21,8 @@ export type IntegrateInProgress = { tempWorktreePath: string; sourceBranch: string; targetBranch: string; + /** Worktrees on target branch that were clean pre-integration; safe to fast-sync after ref update. */ + cleanTargetWorktrees: string[]; remainingCommits: string[]; currentCommit: string; }; @@ -47,6 +49,60 @@ const isOk = (result: CommandExecResult): boolean => Boolean(result.success); const stdoutText = (result: CommandExecResult): string => (result.stdout || '').trim(); const stderrText = (result: CommandExecResult): string => (result.stderr || '').trim(); +type GitWorktreeEntry = { path: string; branchRef: string | null }; + +async function listGitWorktrees(repoRoot: string): Promise { + const out = await execCommand('git worktree list --porcelain', repoRoot); + const lines = (out.stdout || '').split(/\r?\n/); + + const entries: GitWorktreeEntry[] = []; + let current: GitWorktreeEntry | null = null; + + for (const line of lines) { + if (line.startsWith('worktree ')) { + if (current) entries.push(current); + current = { path: line.slice('worktree '.length).trim(), branchRef: null }; + continue; + } + if (!current) continue; + if (line.startsWith('branch ')) { + current.branchRef = line.slice('branch '.length).trim(); + } + } + if (current) entries.push(current); + + return entries.filter((e) => Boolean(e.path)); +} + +async function computeCleanWorktreesToSync(args: { + repoRoot: string; + targetBranch: string; + excludePaths: string[]; +}): Promise { + const targetRef = `refs/heads/${args.targetBranch}`; + const exclude = new Set(args.excludePaths); + const entries = await listGitWorktrees(args.repoRoot); + const candidates = entries + .filter((e) => e.branchRef === targetRef) + .map((e) => e.path) + .filter((p) => p && !exclude.has(p)); + + const clean: string[] = []; + for (const path of candidates) { + const status = await execCommand('git status --porcelain', path); + if (!stdoutText(status)) { + clean.push(path); + } + } + return clean; +} + +async function syncCleanTargetWorktrees(repoRoot: string, paths: string[]): Promise { + for (const path of paths) { + await execCommand('git reset --hard', path).catch(() => undefined); + } +} + async function ensureLocalBranch(repoRoot: string, candidate: string): Promise { const raw = candidate.trim(); if (!raw || raw === 'HEAD') { @@ -198,6 +254,12 @@ export async function integrateWorktreeCommits(plan: IntegratePlan): Promise []); + remaining = [...plan.commits]; while (remaining.length > 0) { const sha = remaining[0]; @@ -218,6 +280,7 @@ export async function integrateWorktreeCommits(plan: IntegratePlan): Promise undefined); return { kind: 'success', moved: plan.commits.length }; } catch (e) { // Cleanup on any non-conflict error. @@ -279,6 +343,7 @@ export async function continueIntegrate(state: IntegrateInProgress): Promise undefined); return { kind: 'success', moved: remaining.length }; }