diff --git a/apps/server/src/vcs/GitVcsDriver.test.ts b/apps/server/src/vcs/GitVcsDriver.test.ts index 5ccc2c8c6b5bad8ffa706c1d2b9bf1515df55bd2..deb7c21b69920952ee6a1a898b316e37a202c060 100644 --- a/apps/server/src/vcs/GitVcsDriver.test.ts +++ b/apps/server/src/vcs/GitVcsDriver.test.ts @@ -623,3 +623,67 @@ it.effect("GitVcsDriver forwards execute env to the VCS process", () => { ), ); }); + +it.effect("GitVcsDriver flushes checkpoint objects and refs to disk before publishing them", () => { + const observedArgs: ReadonlyArray[] = []; + + return Effect.gen(function* () { + const driver = yield* GitVcsDriver.makeVcsDriverShape(); + + yield* driver.checkpoints.captureCheckpoint({ + cwd: "/repo", + checkpointRef: CheckpointRef.make("refs/t3/checkpoints/thread/turn/1"), + }); + + const writeCommands = ["add", "write-tree", "commit-tree", "update-ref"]; + const writes = observedArgs.filter((args) => + writeCommands.some((command) => args.includes(command)), + ); + assert.strictEqual(writes.length, 4); + for (const args of writes) { + const command = args.findIndex((arg) => writeCommands.includes(arg)); + for (const setting of ["core.fsync=objects,reference", "core.fsyncMethod=fsync"]) { + const index = args.indexOf(setting); + assert.strictEqual(args[index - 1], "-c", args.join(" ")); + assert.isBelow(index, command); + } + } + assert.deepStrictEqual(observedArgs.at(-1), [ + "-C", + "/repo", + "-c", + "core.fsync=objects,reference", + "-c", + "core.fsyncMethod=fsync", + "update-ref", + "refs/t3/checkpoints/thread/turn/1", + "commit0000", + ]); + }).pipe( + Effect.provide( + Layer.mergeAll( + NodeServices.layer, + Layer.mock(VcsProcess.VcsProcess)({ + run: (input) => + Effect.sync(() => { + observedArgs.push(input.args); + const stdout = input.args.includes("write-tree") + ? "tree0000\n" + : input.args.includes("commit-tree") + ? "commit0000\n" + : input.args.includes("--git-common-dir") + ? ".git\n" + : ""; + return { + exitCode: ChildProcessSpawner.ExitCode(0), + stdout, + stderr: "", + stdoutTruncated: false, + stderrTruncated: false, + }; + }), + }), + ), + ), + ); +}); diff --git a/apps/server/src/vcs/GitVcsDriver.ts b/apps/server/src/vcs/GitVcsDriver.ts index 3c6fbe04f89227bf03e13e1a011ef3ad05b3385e..50e63012f099b2e0d635fd05ce275bb00537032c 100644 --- a/apps/server/src/vcs/GitVcsDriver.ts +++ b/apps/server/src/vcs/GitVcsDriver.ts @@ -750,6 +750,17 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( return path.isAbsolute(gitCommonDir) ? gitCommonDir : path.resolve(cwd, gitCommonDir); }); + // Git renames loose objects and refs into place without fsync by default, so + // an unclean restart can leave 0-byte files under refs/t3/** that break every + // later fetch and push. Checkpoint writes flush before they are published; + // macOS defaults to writeout-only, which does not reach the disk either. + const durableWrite = [ + "-c", + "core.fsync=objects,reference", + "-c", + "core.fsyncMethod=fsync", + ] as const; + const checkpoints: VcsDriver.VcsCheckpointOps = { captureCheckpoint: Effect.fn("GitVcsDriver.checkpoints.captureCheckpoint")(function* (input) { const operation = "GitVcsDriver.checkpoints.captureCheckpoint"; @@ -907,14 +918,22 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( operation, cwd: input.cwd, // Preserve absent skipped entries, but capture present nonignored files outside the cone. - args: [...indexConfig, "add", ...(sparseCheckout ? ["--sparse"] : []), "-A", "--", "."], + args: [ + ...indexConfig, + ...durableWrite, + "add", + ...(sparseCheckout ? ["--sparse"] : []), + "-A", + "--", + ".", + ], env: commitEnv, }); const writeTreeResult = yield* execute({ operation, cwd: input.cwd, - args: [...indexConfig, "write-tree"], + args: [...indexConfig, ...durableWrite, "write-tree"], env: commitEnv, }); const treeOid = writeTreeResult.stdout.trim(); @@ -932,7 +951,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( const commitTreeResult = yield* execute({ operation, cwd: input.cwd, - args: ["commit-tree", treeOid, "-m", message], + args: [...durableWrite, "commit-tree", treeOid, "-m", message], env: commitEnv, }); const commitOid = commitTreeResult.stdout.trim(); @@ -949,7 +968,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( yield* execute({ operation, cwd: input.cwd, - args: ["update-ref", input.checkpointRef, commitOid], + args: [...durableWrite, "update-ref", input.checkpointRef, commitOid], }); }).pipe(Effect.ensuring(cleanupTempIndex)); }),