| author | |
| committer | |
| log | 8130a9f13a1ef506f4393423ded21d71f3c9b5e4 |
| tree | e5f760c3da0f2058893e57eb8343afde33023b93 |
| parent | 4abb07eac3d47543b5804e8039633ac8ad7f67d2 |
| signature | Signed by PGP key B5690EEEBB952194 |
2 files changed, 87 insertions(+), 4 deletions(-)
apps/server/src/vcs/GitVcsDriver.test.ts+64| ... | ... | @@ -623,3 +623,67 @@ it.effect("GitVcsDriver forwards execute env to the VCS process", () => { |
| 623 | 623 | ), |
| 624 | 624 | ); |
| 625 | 625 | }); |
| 626 | ||
| 627 | it.effect("GitVcsDriver flushes checkpoint objects and refs to disk before publishing them", () => { | |
| 628 | const observedArgs: ReadonlyArray<string>[] = []; | |
| 629 | ||
| 630 | return Effect.gen(function* () { | |
| 631 | const driver = yield* GitVcsDriver.makeVcsDriverShape(); | |
| 632 | ||
| 633 | yield* driver.checkpoints.captureCheckpoint({ | |
| 634 | cwd: "/repo", | |
| 635 | checkpointRef: CheckpointRef.make("refs/t3/checkpoints/thread/turn/1"), | |
| 636 | }); | |
| 637 | ||
| 638 | const writeCommands = ["add", "write-tree", "commit-tree", "update-ref"]; | |
| 639 | const writes = observedArgs.filter((args) => | |
| 640 | writeCommands.some((command) => args.includes(command)), | |
| 641 | ); | |
| 642 | assert.strictEqual(writes.length, 4); | |
| 643 | for (const args of writes) { | |
| 644 | const command = args.findIndex((arg) => writeCommands.includes(arg)); | |
| 645 | for (const setting of ["core.fsync=objects,reference", "core.fsyncMethod=fsync"]) { | |
| 646 | const index = args.indexOf(setting); | |
| 647 | assert.strictEqual(args[index - 1], "-c", args.join(" ")); | |
| 648 | assert.isBelow(index, command); | |
| 649 | } | |
| 650 | } | |
| 651 | assert.deepStrictEqual(observedArgs.at(-1), [ | |
| 652 | "-C", | |
| 653 | "/repo", | |
| 654 | "-c", | |
| 655 | "core.fsync=objects,reference", | |
| 656 | "-c", | |
| 657 | "core.fsyncMethod=fsync", | |
| 658 | "update-ref", | |
| 659 | "refs/t3/checkpoints/thread/turn/1", | |
| 660 | "commit0000", | |
| 661 | ]); | |
| 662 | }).pipe( | |
| 663 | Effect.provide( | |
| 664 | Layer.mergeAll( | |
| 665 | NodeServices.layer, | |
| 666 | Layer.mock(VcsProcess.VcsProcess)({ | |
| 667 | run: (input) => | |
| 668 | Effect.sync(() => { | |
| 669 | observedArgs.push(input.args); | |
| 670 | const stdout = input.args.includes("write-tree") | |
| 671 | ? "tree0000\n" | |
| 672 | : input.args.includes("commit-tree") | |
| 673 | ? "commit0000\n" | |
| 674 | : input.args.includes("--git-common-dir") | |
| 675 | ? ".git\n" | |
| 676 | : ""; | |
| 677 | return { | |
| 678 | exitCode: ChildProcessSpawner.ExitCode(0), | |
| 679 | stdout, | |
| 680 | stderr: "", | |
| 681 | stdoutTruncated: false, | |
| 682 | stderrTruncated: false, | |
| 683 | }; | |
| 684 | }), | |
| 685 | }), | |
| 686 | ), | |
| 687 | ), | |
| 688 | ); | |
| 689 | }); |
apps/server/src/vcs/GitVcsDriver.ts+23-4| ... | ... | @@ -750,6 +750,17 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( |
| 750 | 750 | return path.isAbsolute(gitCommonDir) ? gitCommonDir : path.resolve(cwd, gitCommonDir); |
| 751 | 751 | }); |
| 752 | 752 | |
| 753 | // Git renames loose objects and refs into place without fsync by default, so | |
| 754 | // an unclean restart can leave 0-byte files under refs/t3/** that break every | |
| 755 | // later fetch and push. Checkpoint writes flush before they are published; | |
| 756 | // macOS defaults to writeout-only, which does not reach the disk either. | |
| 757 | const durableWrite = [ | |
| 758 | "-c", | |
| 759 | "core.fsync=objects,reference", | |
| 760 | "-c", | |
| 761 | "core.fsyncMethod=fsync", | |
| 762 | ] as const; | |
| 763 | ||
| 753 | 764 | const checkpoints: VcsDriver.VcsCheckpointOps = { |
| 754 | 765 | captureCheckpoint: Effect.fn("GitVcsDriver.checkpoints.captureCheckpoint")(function* (input) { |
| 755 | 766 | const operation = "GitVcsDriver.checkpoints.captureCheckpoint"; |
| ... | ... | @@ -907,14 +918,22 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( |
| 907 | 918 | operation, |
| 908 | 919 | cwd: input.cwd, |
| 909 | 920 | // Preserve absent skipped entries, but capture present nonignored files outside the cone. |
| 910 | args: [...indexConfig, "add", ...(sparseCheckout ? ["--sparse"] : []), "-A", "--", "."], | |
| 921 | args: [ | |
| 922 | ...indexConfig, | |
| 923 | ...durableWrite, | |
| 924 | "add", | |
| 925 | ...(sparseCheckout ? ["--sparse"] : []), | |
| 926 | "-A", | |
| 927 | "--", | |
| 928 | ".", | |
| 929 | ], | |
| 911 | 930 | env: commitEnv, |
| 912 | 931 | }); |
| 913 | 932 | |
| 914 | 933 | const writeTreeResult = yield* execute({ |
| 915 | 934 | operation, |
| 916 | 935 | cwd: input.cwd, |
| 917 | args: [...indexConfig, "write-tree"], | |
| 936 | args: [...indexConfig, ...durableWrite, "write-tree"], | |
| 918 | 937 | env: commitEnv, |
| 919 | 938 | }); |
| 920 | 939 | const treeOid = writeTreeResult.stdout.trim(); |
| ... | ... | @@ -932,7 +951,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( |
| 932 | 951 | const commitTreeResult = yield* execute({ |
| 933 | 952 | operation, |
| 934 | 953 | cwd: input.cwd, |
| 935 | args: ["commit-tree", treeOid, "-m", message], | |
| 954 | args: [...durableWrite, "commit-tree", treeOid, "-m", message], | |
| 936 | 955 | env: commitEnv, |
| 937 | 956 | }); |
| 938 | 957 | const commitOid = commitTreeResult.stdout.trim(); |
| ... | ... | @@ -949,7 +968,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( |
| 949 | 968 | yield* execute({ |
| 950 | 969 | operation, |
| 951 | 970 | cwd: input.cwd, |
| 952 | args: ["update-ref", input.checkpointRef, commitOid], | |
| 971 | args: [...durableWrite, "update-ref", input.checkpointRef, commitOid], | |
| 953 | 972 | }); |
| 954 | 973 | }).pipe(Effect.ensuring(cleanupTempIndex)); |
| 955 | 974 | }), |