| ... | ... | @@ -65,6 +65,243 @@ runVcsDriverContractSuite<GitVcsDriver.GitVcsDriver, GitContractError>({ |
| 65 | 65 | }, |
| 66 | 66 | }); |
| 67 | 67 | |
| 68 | const makeCheckpointFixture = Effect.fn("makeCheckpointFixture")(function* ( |
| 69 | driver: Effect.Success<ReturnType<typeof GitVcsDriver.makeVcsDriverShape>>, |
| 70 | cwd: string, |
| 71 | ) { |
| 72 | const fileSystem = yield* FileSystem.FileSystem; |
| 73 | const path = yield* Path.Path; |
| 74 | const git = (args: ReadonlyArray<string>) => |
| 75 | driver.execute({ operation: "checkpoint-test", cwd, args }); |
| 76 | yield* git(["init"]); |
| 77 | yield* git(["config", "user.name", "Test"]); |
| 78 | yield* git(["config", "user.email", "test@test.com"]); |
| 79 | yield* fileSystem.writeFileString(path.join(cwd, "file.txt"), "initial\n"); |
| 80 | yield* git(["add", "."]); |
| 81 | yield* git(["commit", "-m", "initial"]); |
| 82 | const checkpointRef = CheckpointRef.make("refs/t3/checkpoints/test"); |
| 83 | yield* fileSystem.writeFileString(path.join(cwd, "file.txt"), "staged\n"); |
| 84 | yield* git(["add", "."]); |
| 85 | yield* fileSystem.writeFileString(path.join(cwd, "file.txt"), "unstaged\n"); |
| 86 | return { git, checkpointRef }; |
| 87 | }); |
| 88 | |
| 89 | it.effect("checkpoint capture does not rerun clean filters for unchanged indexed files", () => |
| 90 | Effect.gen(function* () { |
| 91 | const fileSystem = yield* FileSystem.FileSystem; |
| 92 | const path = yield* Path.Path; |
| 93 | const driver = yield* GitVcsDriver.makeVcsDriverShape(); |
| 94 | const cwd = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-checkpoint-cache-" }); |
| 95 | const { git, checkpointRef } = yield* makeCheckpointFixture(driver, cwd); |
| 96 | yield* fileSystem.writeFileString( |
| 97 | path.join(cwd, ".gitattributes"), |
| 98 | "stable.txt filter=probe\n", |
| 99 | ); |
| 100 | yield* fileSystem.writeFileString(path.join(cwd, "stable.txt"), "unchanged\n"); |
| 101 | yield* fileSystem.writeFileString( |
| 102 | path.join(cwd, ".git", "filter.cjs"), |
| 103 | 'require("node:fs").appendFileSync(".git/filter-runs", "read\\n"); process.stdin.pipe(process.stdout);', |
| 104 | ); |
| 105 | yield* git(["config", "filter.probe.clean", "node .git/filter.cjs"]); |
| 106 | yield* fileSystem.utimes(path.join(cwd, "stable.txt"), 1_700_000_000, 1_700_000_000); |
| 107 | yield* git(["add", "."]); |
| 108 | yield* git(["commit", "-m", "record stable file"]); |
| 109 | yield* fileSystem.writeFileString(path.join(cwd, ".git", "filter-runs"), ""); |
| 110 | yield* fileSystem.writeFileString(path.join(cwd, "file.txt"), "changed\n"); |
| 111 | const originalIndex = yield* fileSystem.readFile(path.join(cwd, ".git", "index")); |
| 112 | |
| 113 | yield* driver.checkpoints.captureCheckpoint({ cwd, checkpointRef }); |
| 114 | |
| 115 | assert.strictEqual(yield* fileSystem.readFileString(path.join(cwd, ".git", "filter-runs")), ""); |
| 116 | assert.strictEqual((yield* git(["show", `${checkpointRef}:file.txt`])).stdout, "changed\n"); |
| 117 | assert.strictEqual((yield* git(["show", `${checkpointRef}:stable.txt`])).stdout, "unchanged\n"); |
| 118 | assert.deepEqual(yield* fileSystem.readFile(path.join(cwd, ".git", "index")), originalIndex); |
| 119 | }).pipe(Effect.scoped, Effect.provide(GitContractLayer)), |
| 120 | ); |
| 121 | |
| 122 | for (const timestamp of [1_700_000_000, 1_700_000_000.9999]) { |
| 123 | it.effect( |
| 124 | `checkpoint capture preserves same-size edits with racy index timestamps (${timestamp})`, |
| 125 | () => |
| 126 | Effect.gen(function* () { |
| 127 | const fileSystem = yield* FileSystem.FileSystem; |
| 128 | const path = yield* Path.Path; |
| 129 | const driver = yield* GitVcsDriver.makeVcsDriverShape(); |
| 130 | const cwd = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-checkpoint-racy-" }); |
| 131 | const { git, checkpointRef } = yield* makeCheckpointFixture(driver, cwd); |
| 132 | const filePath = path.join(cwd, "file.txt"); |
| 133 | const indexPath = path.join(cwd, ".git", "index"); |
| 134 | yield* git(["config", "core.trustctime", "false"]); |
| 135 | yield* fileSystem.writeFileString(filePath, "before\n"); |
| 136 | yield* fileSystem.utimes(filePath, timestamp, timestamp); |
| 137 | yield* git(["add", "file.txt"]); |
| 138 | yield* git(["commit", "-m", "record racy file"]); |
| 139 | yield* fileSystem.utimes(indexPath, timestamp, timestamp); |
| 140 | const originalIndex = yield* fileSystem.readFile(indexPath); |
| 141 | const originalIndexMtime = (yield* fileSystem.stat(indexPath)).mtime; |
| 142 | yield* fileSystem.writeFileString(filePath, "after!\n"); |
| 143 | yield* fileSystem.utimes(filePath, timestamp, timestamp); |
| 144 | |
| 145 | yield* driver.checkpoints.captureCheckpoint({ cwd, checkpointRef }); |
| 146 | |
| 147 | assert.strictEqual((yield* git(["show", `${checkpointRef}:file.txt`])).stdout, "after!\n"); |
| 148 | assert.deepEqual(yield* fileSystem.readFile(indexPath), originalIndex); |
| 149 | assert.deepEqual((yield* fileSystem.stat(indexPath)).mtime, originalIndexMtime); |
| 150 | }).pipe(Effect.scoped, Effect.provide(GitContractLayer)), |
| 151 | ); |
| 152 | } |
| 153 | |
| 154 | it.effect("checkpoint capture preserves racy edits made after resetting the index", () => |
| 155 | Effect.gen(function* () { |
| 156 | const fileSystem = yield* FileSystem.FileSystem; |
| 157 | const path = yield* Path.Path; |
| 158 | const liveProcess = yield* VcsProcess.VcsProcess; |
| 159 | const driver = yield* GitVcsDriver.makeVcsDriverShape(); |
| 160 | const cwd = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-checkpoint-racy-reset-" }); |
| 161 | const { git, checkpointRef } = yield* makeCheckpointFixture(driver, cwd); |
| 162 | const racyPath = path.join(cwd, "racy.txt"); |
| 163 | const indexPath = path.join(cwd, ".git", "index"); |
| 164 | const timestamp = 1_700_000_000; |
| 165 | yield* git(["config", "core.trustctime", "false"]); |
| 166 | yield* fileSystem.writeFileString(racyPath, "before\n"); |
| 167 | yield* fileSystem.utimes(racyPath, timestamp, timestamp); |
| 168 | yield* git(["add", "."]); |
| 169 | yield* git(["commit", "-m", "record racy file"]); |
| 170 | yield* fileSystem.writeFileString(path.join(cwd, "file.txt"), "staged\n"); |
| 171 | yield* git(["add", "file.txt"]); |
| 172 | yield* fileSystem.utimes(indexPath, timestamp, timestamp); |
| 173 | const originalIndex = yield* fileSystem.readFile(indexPath); |
| 174 | const originalIndexMtime = (yield* fileSystem.stat(indexPath)).mtime; |
| 175 | const captureDriver = yield* GitVcsDriver.makeVcsDriverShape().pipe( |
| 176 | Effect.provideService(VcsProcess.VcsProcess, { |
| 177 | run: Effect.fn(function* (input: VcsProcess.VcsProcessInput) { |
| 178 | const result = yield* liveProcess.run(input); |
| 179 | if (input.args.includes("read-tree") && input.args.includes("--reset")) { |
| 180 | yield* fileSystem.writeFileString(racyPath, "after!\n").pipe(Effect.orDie); |
| 181 | yield* fileSystem.utimes(racyPath, timestamp, timestamp).pipe(Effect.orDie); |
| 182 | } |
| 183 | return result; |
| 184 | }), |
| 185 | }), |
| 186 | ); |
| 187 | |
| 188 | yield* captureDriver.checkpoints.captureCheckpoint({ cwd, checkpointRef }); |
| 189 | |
| 190 | assert.strictEqual((yield* git(["show", `${checkpointRef}:racy.txt`])).stdout, "after!\n"); |
| 191 | assert.strictEqual((yield* git(["show", `${checkpointRef}:file.txt`])).stdout, "staged\n"); |
| 192 | assert.deepEqual(yield* fileSystem.readFile(indexPath), originalIndex); |
| 193 | assert.deepEqual((yield* fileSystem.stat(indexPath)).mtime, originalIndexMtime); |
| 194 | }).pipe(Effect.scoped, Effect.provide(GitContractLayer)), |
| 195 | ); |
| 196 | |
| 197 | for (const nested of [false, true]) { |
| 198 | for (const indexMode of ["normal", "flags", "split"] as const) { |
| 199 | it.effect( |
| 200 | `checkpoint index reuse preserves two turns (nested=${nested}, index=${indexMode})`, |
| 201 | () => |
| 202 | Effect.gen(function* () { |
| 203 | const fileSystem = yield* FileSystem.FileSystem; |
| 204 | const path = yield* Path.Path; |
| 205 | const driver = yield* GitVcsDriver.makeVcsDriverShape(); |
| 206 | const cwd = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-checkpoint-turns-" }); |
| 207 | const { git } = yield* makeCheckpointFixture(driver, cwd); |
| 208 | const write = (name: string, contents: string) => |
| 209 | fileSystem.writeFileString(path.join(cwd, name), contents); |
| 210 | yield* fileSystem.makeDirectory(path.join(cwd, "scope")); |
| 211 | for (const name of [ |
| 212 | "scope/staged", |
| 213 | "scope/deleted", |
| 214 | "scope/assumed", |
| 215 | "scope/skipped", |
| 216 | "outside", |
| 217 | ]) { |
| 218 | yield* write(name, "original\n"); |
| 219 | } |
| 220 | yield* git(["add", "."]); |
| 221 | yield* git(["commit", "-m", "initial scoped files"]); |
| 222 | yield* write("scope/staged", "staged\n"); |
| 223 | yield* write("scope/new-deleted", "staged then deleted\n"); |
| 224 | yield* write("outside", "staged outside\n"); |
| 225 | yield* git(["add", "."]); |
| 226 | if (indexMode === "flags") { |
| 227 | yield* git(["update-index", "--assume-unchanged", "scope/assumed"]); |
| 228 | yield* git(["update-index", "--skip-worktree", "scope/skipped"]); |
| 229 | } |
| 230 | if (indexMode === "split") { |
| 231 | yield* git(["update-index", "--split-index"]); |
| 232 | } |
| 233 | const originalIndex = yield* fileSystem.readFile(path.join(cwd, ".git", "index")); |
| 234 | for (const name of ["scope/staged", "scope/assumed", "scope/skipped", "outside"]) { |
| 235 | yield* write(name, "working\n"); |
| 236 | } |
| 237 | yield* write("scope/new", "first\n"); |
| 238 | yield* fileSystem.remove(path.join(cwd, "scope/deleted")); |
| 239 | yield* fileSystem.remove(path.join(cwd, "scope/new-deleted")); |
| 240 | const captureCwd = nested ? path.join(cwd, "scope") : cwd; |
| 241 | const first = CheckpointRef.make("refs/t3/checkpoints/turns/1"); |
| 242 | const second = CheckpointRef.make("refs/t3/checkpoints/turns/2"); |
| 243 | yield* driver.checkpoints.captureCheckpoint({ cwd: captureCwd, checkpointRef: first }); |
| 244 | for (const name of ["scope/staged", "scope/assumed", "scope/skipped"]) { |
| 245 | assert.strictEqual((yield* git(["show", `${first}:${name}`])).stdout, "working\n"); |
| 246 | } |
| 247 | assert.strictEqual( |
| 248 | (yield* git(["show", `${first}:outside`])).stdout, |
| 249 | nested ? "original\n" : "working\n", |
| 250 | ); |
| 251 | const files = (yield* git(["ls-tree", "-r", "--name-only", first])).stdout.split("\n"); |
| 252 | assert.notInclude(files, "scope/deleted"); |
| 253 | assert.notInclude(files, "scope/new-deleted"); |
| 254 | assert.include(files, "scope/new"); |
| 255 | |
| 256 | yield* write("scope/staged", "second\n"); |
| 257 | yield* fileSystem.remove(path.join(cwd, "scope/new")); |
| 258 | yield* write("scope/second", "added in second turn\n"); |
| 259 | yield* driver.checkpoints.captureCheckpoint({ cwd: captureCwd, checkpointRef: second }); |
| 260 | assert.strictEqual( |
| 261 | (yield* git(["diff", "--name-only", first, second])).stdout, |
| 262 | "scope/new\nscope/second\nscope/staged\n", |
| 263 | ); |
| 264 | assert.strictEqual((yield* git(["show", `${second}:scope/staged`])).stdout, "second\n"); |
| 265 | assert.strictEqual( |
| 266 | (yield* git(["show", `${second}:scope/second`])).stdout, |
| 267 | "added in second turn\n", |
| 268 | ); |
| 269 | assert.deepEqual( |
| 270 | yield* fileSystem.readFile(path.join(cwd, ".git", "index")), |
| 271 | originalIndex, |
| 272 | ); |
| 273 | }).pipe(Effect.scoped, Effect.provide(GitContractLayer)), |
| 274 | ); |
| 275 | } |
| 276 | } |
| 277 | |
| 278 | for (const indexState of ["missing", "invalid"] as const) { |
| 279 | it.effect(`checkpoint capture falls back when the user index is ${indexState}`, () => |
| 280 | Effect.gen(function* () { |
| 281 | const fileSystem = yield* FileSystem.FileSystem; |
| 282 | const path = yield* Path.Path; |
| 283 | const driver = yield* GitVcsDriver.makeVcsDriverShape(); |
| 284 | const cwd = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-checkpoint-index-" }); |
| 285 | const { git, checkpointRef } = yield* makeCheckpointFixture(driver, cwd); |
| 286 | const indexPath = path.join(cwd, ".git", "index"); |
| 287 | if (indexState === "missing") { |
| 288 | yield* fileSystem.remove(indexPath); |
| 289 | } else { |
| 290 | yield* fileSystem.writeFileString(indexPath, "invalid index"); |
| 291 | } |
| 292 | |
| 293 | yield* driver.checkpoints.captureCheckpoint({ cwd, checkpointRef }); |
| 294 | |
| 295 | assert.strictEqual((yield* git(["show", `${checkpointRef}:file.txt`])).stdout, "unstaged\n"); |
| 296 | if (indexState === "missing") { |
| 297 | assert.isFalse(yield* fileSystem.exists(indexPath)); |
| 298 | } else { |
| 299 | assert.strictEqual(yield* fileSystem.readFileString(indexPath), "invalid index"); |
| 300 | } |
| 301 | }).pipe(Effect.scoped, Effect.provide(GitContractLayer)), |
| 302 | ); |
| 303 | } |
| 304 | |
| 68 | 305 | it.effect("restores empty checkpoints without changing paths outside the workspace", () => |
| 69 | 306 | Effect.gen(function* () { |
| 70 | 307 | const fileSystem = yield* FileSystem.FileSystem; |