Skip to content

Commit 03366aa

Browse files
committed
Fix sandbox sync rollback on stash conflicts
1 parent 3aa81fd commit 03366aa

2 files changed

Lines changed: 78 additions & 0 deletions

File tree

packages/sandbox/git.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ describe("syncToRemotePreservingChanges", () => {
4646
const sandbox = createSandbox([
4747
result(),
4848
result({ stdout: " M file.ts\n" }),
49+
result({ stdout: "original-head\n" }),
4950
result(),
5051
result(),
5152
result(),
@@ -57,6 +58,7 @@ describe("syncToRemotePreservingChanges", () => {
5758
expect(sandbox.commands).toEqual([
5859
"git fetch origin feature:refs/remotes/origin/feature",
5960
"git status --porcelain",
61+
"git rev-parse HEAD",
6062
"git stash push --include-untracked -m open-agents-pre-commit-sync",
6163
"git reset --hard origin/feature",
6264
"git branch --set-upstream-to=origin/feature feature",
@@ -79,4 +81,42 @@ describe("syncToRemotePreservingChanges", () => {
7981
"git fetch origin feature:refs/remotes/origin/feature",
8082
]);
8183
});
84+
85+
test("rolls back and restores local changes when stash restore conflicts after sync", async () => {
86+
const sandbox = createSandbox([
87+
result(),
88+
result({ stdout: " M file.ts\n" }),
89+
result({ stdout: "original-head\n" }),
90+
result(),
91+
result(),
92+
result(),
93+
result({
94+
success: false,
95+
exitCode: 1,
96+
stderr: "CONFLICT (content): Merge conflict in file.ts\n",
97+
}),
98+
result(),
99+
result(),
100+
result(),
101+
]) as Sandbox & { commands: string[] };
102+
103+
await expect(
104+
syncToRemotePreservingChanges(sandbox, "feature"),
105+
).rejects.toThrow(
106+
"Failed to restore local changes after syncing remote branch",
107+
);
108+
109+
expect(sandbox.commands).toEqual([
110+
"git fetch origin feature:refs/remotes/origin/feature",
111+
"git status --porcelain",
112+
"git rev-parse HEAD",
113+
"git stash push --include-untracked -m open-agents-pre-commit-sync",
114+
"git reset --hard origin/feature",
115+
"git branch --set-upstream-to=origin/feature feature",
116+
"git stash pop",
117+
"git reset --hard original-head",
118+
"git clean -fd",
119+
"git stash pop",
120+
]);
121+
});
82122
});

packages/sandbox/git.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,33 @@ async function resetToFetchedRemoteBranch(
9595
}
9696
}
9797

98+
async function getCurrentHead(sandbox: Sandbox): Promise<string> {
99+
const headResult = await exec(sandbox, "git rev-parse HEAD", 10000);
100+
if (!headResult.success) {
101+
throw new Error(
102+
`Failed to inspect current HEAD: ${commandOutput(headResult)}`,
103+
);
104+
}
105+
106+
return headResult.stdout.trim();
107+
}
108+
109+
async function resetToCommit(sandbox: Sandbox, commit: string): Promise<void> {
110+
const resetResult = await exec(sandbox, `git reset --hard ${commit}`, 10000);
111+
if (!resetResult.success) {
112+
throw new Error(
113+
`Failed to restore original HEAD after sync failure: ${commandOutput(resetResult)}`,
114+
);
115+
}
116+
117+
const cleanResult = await exec(sandbox, "git clean -fd", 10000);
118+
if (!cleanResult.success) {
119+
throw new Error(
120+
`Failed to clean worktree after sync failure: ${commandOutput(cleanResult)}`,
121+
);
122+
}
123+
}
124+
98125
// ---- public functions ----
99126

100127
/**
@@ -343,6 +370,8 @@ export async function syncToRemotePreservingChanges(
343370
return;
344371
}
345372

373+
const originalHead = await getCurrentHead(sandbox);
374+
346375
const stashResult = await exec(
347376
sandbox,
348377
"git stash push --include-untracked -m open-agents-pre-commit-sync",
@@ -357,12 +386,21 @@ export async function syncToRemotePreservingChanges(
357386
try {
358387
await resetToFetchedRemoteBranch(sandbox, branch);
359388
} catch (error) {
389+
await resetToCommit(sandbox, originalHead);
360390
await exec(sandbox, "git stash pop", 30000).catch(() => {});
361391
throw error;
362392
}
363393

364394
const popResult = await exec(sandbox, "git stash pop", 30000);
365395
if (!popResult.success) {
396+
await resetToCommit(sandbox, originalHead);
397+
const restoreResult = await exec(sandbox, "git stash pop", 30000);
398+
if (!restoreResult.success) {
399+
throw new Error(
400+
`Failed to restore local changes after rolling back sync failure: ${commandOutput(restoreResult)}`,
401+
);
402+
}
403+
366404
throw new Error(
367405
`Failed to restore local changes after syncing remote branch: ${commandOutput(popResult)}`,
368406
);

0 commit comments

Comments
 (0)