feat: sync clean target worktrees during integration
This commit is contained in:
@@ -21,6 +21,8 @@ export type IntegrateInProgress = {
|
|||||||
tempWorktreePath: string;
|
tempWorktreePath: string;
|
||||||
sourceBranch: string;
|
sourceBranch: string;
|
||||||
targetBranch: string;
|
targetBranch: string;
|
||||||
|
/** Worktrees on target branch that were clean pre-integration; safe to fast-sync after ref update. */
|
||||||
|
cleanTargetWorktrees: string[];
|
||||||
remainingCommits: string[];
|
remainingCommits: string[];
|
||||||
currentCommit: string;
|
currentCommit: string;
|
||||||
};
|
};
|
||||||
@@ -47,6 +49,60 @@ const isOk = (result: CommandExecResult): boolean => Boolean(result.success);
|
|||||||
const stdoutText = (result: CommandExecResult): string => (result.stdout || '').trim();
|
const stdoutText = (result: CommandExecResult): string => (result.stdout || '').trim();
|
||||||
const stderrText = (result: CommandExecResult): string => (result.stderr || '').trim();
|
const stderrText = (result: CommandExecResult): string => (result.stderr || '').trim();
|
||||||
|
|
||||||
|
type GitWorktreeEntry = { path: string; branchRef: string | null };
|
||||||
|
|
||||||
|
async function listGitWorktrees(repoRoot: string): Promise<GitWorktreeEntry[]> {
|
||||||
|
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<string[]> {
|
||||||
|
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<void> {
|
||||||
|
for (const path of paths) {
|
||||||
|
await execCommand('git reset --hard', path).catch(() => undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function ensureLocalBranch(repoRoot: string, candidate: string): Promise<string> {
|
async function ensureLocalBranch(repoRoot: string, candidate: string): Promise<string> {
|
||||||
const raw = candidate.trim();
|
const raw = candidate.trim();
|
||||||
if (!raw || raw === 'HEAD') {
|
if (!raw || raw === 'HEAD') {
|
||||||
@@ -198,6 +254,12 @@ export async function integrateWorktreeCommits(plan: IntegratePlan): Promise<Int
|
|||||||
throw new Error('Target branch has local changes; abort integration and retry');
|
throw new Error('Target branch has local changes; abort integration and retry');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cleanTargetWorktrees = await computeCleanWorktreesToSync({
|
||||||
|
repoRoot: plan.repoRoot,
|
||||||
|
targetBranch: plan.targetBranch,
|
||||||
|
excludePaths: [tmpDir],
|
||||||
|
}).catch(() => []);
|
||||||
|
|
||||||
remaining = [...plan.commits];
|
remaining = [...plan.commits];
|
||||||
while (remaining.length > 0) {
|
while (remaining.length > 0) {
|
||||||
const sha = remaining[0];
|
const sha = remaining[0];
|
||||||
@@ -218,6 +280,7 @@ export async function integrateWorktreeCommits(plan: IntegratePlan): Promise<Int
|
|||||||
tempWorktreePath: tmpDir,
|
tempWorktreePath: tmpDir,
|
||||||
sourceBranch: plan.sourceBranch,
|
sourceBranch: plan.sourceBranch,
|
||||||
targetBranch: plan.targetBranch,
|
targetBranch: plan.targetBranch,
|
||||||
|
cleanTargetWorktrees,
|
||||||
remainingCommits: remaining,
|
remainingCommits: remaining,
|
||||||
currentCommit: sha,
|
currentCommit: sha,
|
||||||
},
|
},
|
||||||
@@ -229,6 +292,7 @@ export async function integrateWorktreeCommits(plan: IntegratePlan): Promise<Int
|
|||||||
}
|
}
|
||||||
|
|
||||||
await removeTempWorktree(plan.repoRoot, tmpDir);
|
await removeTempWorktree(plan.repoRoot, tmpDir);
|
||||||
|
await syncCleanTargetWorktrees(plan.repoRoot, cleanTargetWorktrees).catch(() => undefined);
|
||||||
return { kind: 'success', moved: plan.commits.length };
|
return { kind: 'success', moved: plan.commits.length };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Cleanup on any non-conflict error.
|
// Cleanup on any non-conflict error.
|
||||||
@@ -279,6 +343,7 @@ export async function continueIntegrate(state: IntegrateInProgress): Promise<Int
|
|||||||
tempWorktreePath: tmpDir,
|
tempWorktreePath: tmpDir,
|
||||||
sourceBranch: state.sourceBranch,
|
sourceBranch: state.sourceBranch,
|
||||||
targetBranch: state.targetBranch,
|
targetBranch: state.targetBranch,
|
||||||
|
cleanTargetWorktrees: state.cleanTargetWorktrees,
|
||||||
remainingCommits: still,
|
remainingCommits: still,
|
||||||
currentCommit: sha,
|
currentCommit: sha,
|
||||||
},
|
},
|
||||||
@@ -289,5 +354,6 @@ export async function continueIntegrate(state: IntegrateInProgress): Promise<Int
|
|||||||
}
|
}
|
||||||
|
|
||||||
await removeTempWorktree(state.repoRoot, state.tempWorktreePath);
|
await removeTempWorktree(state.repoRoot, state.tempWorktreePath);
|
||||||
|
await syncCleanTargetWorktrees(state.repoRoot, state.cleanTargetWorktrees).catch(() => undefined);
|
||||||
return { kind: 'success', moved: remaining.length };
|
return { kind: 'success', moved: remaining.length };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user