Skip to content

Commit 13cabae

Browse files
authored
fix(win32): add git flags for snapshot operations and fix tests for cross-platform (anomalyco#14890)
1 parent 6590689 commit 13cabae

3 files changed

Lines changed: 67 additions & 54 deletions

File tree

packages/opencode/src/snapshot/index.ts

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ export namespace Snapshot {
6464
.nothrow()
6565
// Configure git to not convert line endings on Windows
6666
await $`git --git-dir ${git} config core.autocrlf false`.quiet().nothrow()
67+
await $`git --git-dir ${git} config core.longpaths true`.quiet().nothrow()
68+
await $`git --git-dir ${git} config core.symlinks true`.quiet().nothrow()
69+
await $`git --git-dir ${git} config core.fsmonitor false`.quiet().nothrow()
6770
log.info("initialized")
6871
}
6972
await add(git)
@@ -86,7 +89,7 @@ export namespace Snapshot {
8689
const git = gitdir()
8790
await add(git)
8891
const result =
89-
await $`git -c core.autocrlf=false -c core.quotepath=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff --name-only ${hash} -- .`
92+
await $`git -c core.autocrlf=false -c core.longpaths=true -c core.symlinks=true -c core.quotepath=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff --name-only ${hash} -- .`
9093
.quiet()
9194
.cwd(Instance.directory)
9295
.nothrow()
@@ -113,7 +116,7 @@ export namespace Snapshot {
113116
log.info("restore", { commit: snapshot })
114117
const git = gitdir()
115118
const result =
116-
await $`git --git-dir ${git} --work-tree ${Instance.worktree} read-tree ${snapshot} && git --git-dir ${git} --work-tree ${Instance.worktree} checkout-index -a -f`
119+
await $`git -c core.longpaths=true -c core.symlinks=true --git-dir ${git} --work-tree ${Instance.worktree} read-tree ${snapshot} && git -c core.longpaths=true -c core.symlinks=true --git-dir ${git} --work-tree ${Instance.worktree} checkout-index -a -f`
117120
.quiet()
118121
.cwd(Instance.worktree)
119122
.nothrow()
@@ -135,14 +138,15 @@ export namespace Snapshot {
135138
for (const file of item.files) {
136139
if (files.has(file)) continue
137140
log.info("reverting", { file, hash: item.hash })
138-
const result = await $`git --git-dir ${git} --work-tree ${Instance.worktree} checkout ${item.hash} -- ${file}`
139-
.quiet()
140-
.cwd(Instance.worktree)
141-
.nothrow()
141+
const result =
142+
await $`git -c core.longpaths=true -c core.symlinks=true --git-dir ${git} --work-tree ${Instance.worktree} checkout ${item.hash} -- ${file}`
143+
.quiet()
144+
.cwd(Instance.worktree)
145+
.nothrow()
142146
if (result.exitCode !== 0) {
143147
const relativePath = path.relative(Instance.worktree, file)
144148
const checkTree =
145-
await $`git --git-dir ${git} --work-tree ${Instance.worktree} ls-tree ${item.hash} -- ${relativePath}`
149+
await $`git -c core.longpaths=true -c core.symlinks=true --git-dir ${git} --work-tree ${Instance.worktree} ls-tree ${item.hash} -- ${relativePath}`
146150
.quiet()
147151
.cwd(Instance.worktree)
148152
.nothrow()
@@ -164,7 +168,7 @@ export namespace Snapshot {
164168
const git = gitdir()
165169
await add(git)
166170
const result =
167-
await $`git -c core.autocrlf=false -c core.quotepath=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff ${hash} -- .`
171+
await $`git -c core.autocrlf=false -c core.longpaths=true -c core.symlinks=true -c core.quotepath=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff ${hash} -- .`
168172
.quiet()
169173
.cwd(Instance.worktree)
170174
.nothrow()
@@ -201,7 +205,7 @@ export namespace Snapshot {
201205
const status = new Map<string, "added" | "deleted" | "modified">()
202206

203207
const statuses =
204-
await $`git -c core.autocrlf=false -c core.quotepath=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff --name-status --no-renames ${from} ${to} -- .`
208+
await $`git -c core.autocrlf=false -c core.longpaths=true -c core.symlinks=true -c core.quotepath=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff --name-status --no-renames ${from} ${to} -- .`
205209
.quiet()
206210
.cwd(Instance.directory)
207211
.nothrow()
@@ -215,7 +219,7 @@ export namespace Snapshot {
215219
status.set(file, kind)
216220
}
217221

218-
for await (const line of $`git -c core.autocrlf=false -c core.quotepath=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff --no-renames --numstat ${from} ${to} -- .`
222+
for await (const line of $`git -c core.autocrlf=false -c core.longpaths=true -c core.symlinks=true -c core.quotepath=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff --no-renames --numstat ${from} ${to} -- .`
219223
.quiet()
220224
.cwd(Instance.directory)
221225
.nothrow()
@@ -225,13 +229,13 @@ export namespace Snapshot {
225229
const isBinaryFile = additions === "-" && deletions === "-"
226230
const before = isBinaryFile
227231
? ""
228-
: await $`git -c core.autocrlf=false --git-dir ${git} --work-tree ${Instance.worktree} show ${from}:${file}`
232+
: await $`git -c core.autocrlf=false -c core.longpaths=true -c core.symlinks=true --git-dir ${git} --work-tree ${Instance.worktree} show ${from}:${file}`
229233
.quiet()
230234
.nothrow()
231235
.text()
232236
const after = isBinaryFile
233237
? ""
234-
: await $`git -c core.autocrlf=false --git-dir ${git} --work-tree ${Instance.worktree} show ${to}:${file}`
238+
: await $`git -c core.autocrlf=false -c core.longpaths=true -c core.symlinks=true --git-dir ${git} --work-tree ${Instance.worktree} show ${to}:${file}`
235239
.quiet()
236240
.nothrow()
237241
.text()
@@ -256,7 +260,10 @@ export namespace Snapshot {
256260

257261
async function add(git: string) {
258262
await syncExclude(git)
259-
await $`git --git-dir ${git} --work-tree ${Instance.worktree} add .`.quiet().cwd(Instance.directory).nothrow()
263+
await $`git -c core.autocrlf=false -c core.longpaths=true -c core.symlinks=true --git-dir ${git} --work-tree ${Instance.worktree} add .`
264+
.quiet()
265+
.cwd(Instance.directory)
266+
.nothrow()
260267
}
261268

262269
async function syncExclude(git: string) {

packages/opencode/test/preload.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { afterAll } from "bun:test"
1010
const dir = path.join(os.tmpdir(), "opencode-test-data-" + process.pid)
1111
await fs.mkdir(dir, { recursive: true })
1212
afterAll(() => {
13-
fsSync.rmSync(dir, { recursive: true, force: true })
13+
fsSync.rmSync(dir, { recursive: true, force: true, maxRetries: 3, retryDelay: 500 })
1414
})
1515

1616
process.env["XDG_DATA_HOME"] = path.join(dir, "share")

packages/opencode/test/snapshot/snapshot.test.ts

Lines changed: 46 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
import { test, expect } from "bun:test"
22
import { $ } from "bun"
33
import fs from "fs/promises"
4+
import path from "path"
45
import { Snapshot } from "../../src/snapshot"
56
import { Instance } from "../../src/project/instance"
67
import { Filesystem } from "../../src/util/filesystem"
78
import { tmpdir } from "../fixture/fixture"
89

10+
// Git always outputs /-separated paths internally. Snapshot.patch() joins them
11+
// with path.join (which produces \ on Windows) then normalizes back to /.
12+
// This helper does the same for expected values so assertions match cross-platform.
13+
const fwd = (...parts: string[]) => path.join(...parts).replaceAll("\\", "/")
14+
915
async function bootstrap() {
1016
return tmpdir({
1117
git: true,
@@ -35,7 +41,7 @@ test("tracks deleted files correctly", async () => {
3541

3642
await $`rm ${tmp.path}/a.txt`.quiet()
3743

38-
expect((await Snapshot.patch(before!)).files).toContain(`${tmp.path}/a.txt`)
44+
expect((await Snapshot.patch(before!)).files).toContain(fwd(tmp.path, "a.txt"))
3945
},
4046
})
4147
})
@@ -143,7 +149,7 @@ test("binary file handling", async () => {
143149
await Filesystem.write(`${tmp.path}/image.png`, new Uint8Array([0x89, 0x50, 0x4e, 0x47]))
144150

145151
const patch = await Snapshot.patch(before!)
146-
expect(patch.files).toContain(`${tmp.path}/image.png`)
152+
expect(patch.files).toContain(fwd(tmp.path, "image.png"))
147153

148154
await Snapshot.revert([patch])
149155
expect(
@@ -164,9 +170,9 @@ test("symlink handling", async () => {
164170
const before = await Snapshot.track()
165171
expect(before).toBeTruthy()
166172

167-
await $`ln -s ${tmp.path}/a.txt ${tmp.path}/link.txt`.quiet()
173+
await fs.symlink(`${tmp.path}/a.txt`, `${tmp.path}/link.txt`, "file")
168174

169-
expect((await Snapshot.patch(before!)).files).toContain(`${tmp.path}/link.txt`)
175+
expect((await Snapshot.patch(before!)).files).toContain(fwd(tmp.path, "link.txt"))
170176
},
171177
})
172178
})
@@ -181,7 +187,7 @@ test("large file handling", async () => {
181187

182188
await Filesystem.write(`${tmp.path}/large.txt`, "x".repeat(1024 * 1024))
183189

184-
expect((await Snapshot.patch(before!)).files).toContain(`${tmp.path}/large.txt`)
190+
expect((await Snapshot.patch(before!)).files).toContain(fwd(tmp.path, "large.txt"))
185191
},
186192
})
187193
})
@@ -222,9 +228,9 @@ test("special characters in filenames", async () => {
222228
await Filesystem.write(`${tmp.path}/file_with_underscores.txt`, "UNDERSCORES")
223229

224230
const files = (await Snapshot.patch(before!)).files
225-
expect(files).toContain(`${tmp.path}/file with spaces.txt`)
226-
expect(files).toContain(`${tmp.path}/file-with-dashes.txt`)
227-
expect(files).toContain(`${tmp.path}/file_with_underscores.txt`)
231+
expect(files).toContain(fwd(tmp.path, "file with spaces.txt"))
232+
expect(files).toContain(fwd(tmp.path, "file-with-dashes.txt"))
233+
expect(files).toContain(fwd(tmp.path, "file_with_underscores.txt"))
228234
},
229235
})
230236
})
@@ -293,10 +299,10 @@ test("unicode filenames", async () => {
293299
expect(before).toBeTruthy()
294300

295301
const unicodeFiles = [
296-
{ path: `${tmp.path}/文件.txt`, content: "chinese content" },
297-
{ path: `${tmp.path}/🚀rocket.txt`, content: "emoji content" },
298-
{ path: `${tmp.path}/café.txt`, content: "accented content" },
299-
{ path: `${tmp.path}/файл.txt`, content: "cyrillic content" },
302+
{ path: fwd(tmp.path, "文件.txt"), content: "chinese content" },
303+
{ path: fwd(tmp.path, "🚀rocket.txt"), content: "emoji content" },
304+
{ path: fwd(tmp.path, "café.txt"), content: "accented content" },
305+
{ path: fwd(tmp.path, "файл.txt"), content: "cyrillic content" },
300306
]
301307

302308
for (const file of unicodeFiles) {
@@ -329,8 +335,8 @@ test.skip("unicode filenames modification and restore", async () => {
329335
await Instance.provide({
330336
directory: tmp.path,
331337
fn: async () => {
332-
const chineseFile = `${tmp.path}/文件.txt`
333-
const cyrillicFile = `${tmp.path}/файл.txt`
338+
const chineseFile = fwd(tmp.path, "文件.txt")
339+
const cyrillicFile = fwd(tmp.path, "файл.txt")
334340

335341
await Filesystem.write(chineseFile, "original chinese")
336342
await Filesystem.write(cyrillicFile, "original cyrillic")
@@ -362,7 +368,7 @@ test("unicode filenames in subdirectories", async () => {
362368
expect(before).toBeTruthy()
363369

364370
await $`mkdir -p "${tmp.path}/目录/подкаталог"`.quiet()
365-
const deepFile = `${tmp.path}/目录/подкаталог/文件.txt`
371+
const deepFile = fwd(tmp.path, "目录", "подкаталог", "文件.txt")
366372
await Filesystem.write(deepFile, "deep unicode content")
367373

368374
const patch = await Snapshot.patch(before!)
@@ -388,7 +394,7 @@ test("very long filenames", async () => {
388394
expect(before).toBeTruthy()
389395

390396
const longName = "a".repeat(200) + ".txt"
391-
const longFile = `${tmp.path}/${longName}`
397+
const longFile = fwd(tmp.path, longName)
392398

393399
await Filesystem.write(longFile, "long filename content")
394400

@@ -419,9 +425,9 @@ test("hidden files", async () => {
419425
await Filesystem.write(`${tmp.path}/.config`, "config content")
420426

421427
const patch = await Snapshot.patch(before!)
422-
expect(patch.files).toContain(`${tmp.path}/.hidden`)
423-
expect(patch.files).toContain(`${tmp.path}/.gitignore`)
424-
expect(patch.files).toContain(`${tmp.path}/.config`)
428+
expect(patch.files).toContain(fwd(tmp.path, ".hidden"))
429+
expect(patch.files).toContain(fwd(tmp.path, ".gitignore"))
430+
expect(patch.files).toContain(fwd(tmp.path, ".config"))
425431
},
426432
})
427433
})
@@ -436,12 +442,12 @@ test("nested symlinks", async () => {
436442

437443
await $`mkdir -p ${tmp.path}/sub/dir`.quiet()
438444
await Filesystem.write(`${tmp.path}/sub/dir/target.txt`, "target content")
439-
await $`ln -s ${tmp.path}/sub/dir/target.txt ${tmp.path}/sub/dir/link.txt`.quiet()
440-
await $`ln -s ${tmp.path}/sub ${tmp.path}/sub-link`.quiet()
445+
await fs.symlink(`${tmp.path}/sub/dir/target.txt`, `${tmp.path}/sub/dir/link.txt`, "file")
446+
await fs.symlink(`${tmp.path}/sub`, `${tmp.path}/sub-link`, "dir")
441447

442448
const patch = await Snapshot.patch(before!)
443-
expect(patch.files).toContain(`${tmp.path}/sub/dir/link.txt`)
444-
expect(patch.files).toContain(`${tmp.path}/sub-link`)
449+
expect(patch.files).toContain(fwd(tmp.path, "sub", "dir", "link.txt"))
450+
expect(patch.files).toContain(fwd(tmp.path, "sub-link"))
445451
},
446452
})
447453
})
@@ -476,7 +482,7 @@ test("circular symlinks", async () => {
476482
expect(before).toBeTruthy()
477483

478484
// Create circular symlink
479-
await $`ln -s ${tmp.path}/circular ${tmp.path}/circular`.quiet().nothrow()
485+
await fs.symlink(`${tmp.path}/circular`, `${tmp.path}/circular`, "dir").catch(() => {})
480486

481487
const patch = await Snapshot.patch(before!)
482488
expect(patch.files.length).toBeGreaterThanOrEqual(0) // Should not crash
@@ -499,11 +505,11 @@ test("gitignore changes", async () => {
499505
const patch = await Snapshot.patch(before!)
500506

501507
// Should track gitignore itself
502-
expect(patch.files).toContain(`${tmp.path}/.gitignore`)
508+
expect(patch.files).toContain(fwd(tmp.path, ".gitignore"))
503509
// Should track normal files
504-
expect(patch.files).toContain(`${tmp.path}/normal.txt`)
510+
expect(patch.files).toContain(fwd(tmp.path, "normal.txt"))
505511
// Should not track ignored files (git won't see them)
506-
expect(patch.files).not.toContain(`${tmp.path}/test.ignored`)
512+
expect(patch.files).not.toContain(fwd(tmp.path, "test.ignored"))
507513
},
508514
})
509515
})
@@ -523,8 +529,8 @@ test("git info exclude changes", async () => {
523529
await Bun.write(`${tmp.path}/normal.txt`, "normal content")
524530

525531
const patch = await Snapshot.patch(before!)
526-
expect(patch.files).toContain(`${tmp.path}/normal.txt`)
527-
expect(patch.files).not.toContain(`${tmp.path}/ignored.txt`)
532+
expect(patch.files).toContain(fwd(tmp.path, "normal.txt"))
533+
expect(patch.files).not.toContain(fwd(tmp.path, "ignored.txt"))
528534

529535
const after = await Snapshot.track()
530536
const diffs = await Snapshot.diffFull(before!, after!)
@@ -559,9 +565,9 @@ test("git info exclude keeps global excludes", async () => {
559565
await Bun.write(`${tmp.path}/normal.txt`, "normal content")
560566

561567
const patch = await Snapshot.patch(before!)
562-
expect(patch.files).toContain(`${tmp.path}/normal.txt`)
563-
expect(patch.files).not.toContain(`${tmp.path}/global.tmp`)
564-
expect(patch.files).not.toContain(`${tmp.path}/info.tmp`)
568+
expect(patch.files).toContain(fwd(tmp.path, "normal.txt"))
569+
expect(patch.files).not.toContain(fwd(tmp.path, "global.tmp"))
570+
expect(patch.files).not.toContain(fwd(tmp.path, "info.tmp"))
565571
} finally {
566572
if (prev) process.env.GIT_CONFIG_GLOBAL = prev
567573
else delete process.env.GIT_CONFIG_GLOBAL
@@ -610,7 +616,7 @@ test("snapshot state isolation between projects", async () => {
610616
const before1 = await Snapshot.track()
611617
await Filesystem.write(`${tmp1.path}/project1.txt`, "project1 content")
612618
const patch1 = await Snapshot.patch(before1!)
613-
expect(patch1.files).toContain(`${tmp1.path}/project1.txt`)
619+
expect(patch1.files).toContain(fwd(tmp1.path, "project1.txt"))
614620
},
615621
})
616622

@@ -620,10 +626,10 @@ test("snapshot state isolation between projects", async () => {
620626
const before2 = await Snapshot.track()
621627
await Filesystem.write(`${tmp2.path}/project2.txt`, "project2 content")
622628
const patch2 = await Snapshot.patch(before2!)
623-
expect(patch2.files).toContain(`${tmp2.path}/project2.txt`)
629+
expect(patch2.files).toContain(fwd(tmp2.path, "project2.txt"))
624630

625631
// Ensure project1 files don't appear in project2
626-
expect(patch2.files).not.toContain(`${tmp1?.path}/project1.txt`)
632+
expect(patch2.files).not.toContain(fwd(tmp1?.path ?? "", "project1.txt"))
627633
},
628634
})
629635
})
@@ -647,7 +653,7 @@ test("patch detects changes in secondary worktree", async () => {
647653
const before = await Snapshot.track()
648654
expect(before).toBeTruthy()
649655

650-
const worktreeFile = `${worktreePath}/worktree.txt`
656+
const worktreeFile = fwd(worktreePath, "worktree.txt")
651657
await Filesystem.write(worktreeFile, "worktree content")
652658

653659
const patch = await Snapshot.patch(before!)
@@ -681,7 +687,7 @@ test("revert only removes files in invoking worktree", async () => {
681687
const before = await Snapshot.track()
682688
expect(before).toBeTruthy()
683689

684-
const worktreeFile = `${worktreePath}/worktree.txt`
690+
const worktreeFile = fwd(worktreePath, "worktree.txt")
685691
await Filesystem.write(worktreeFile, "worktree content")
686692

687693
const patch = await Snapshot.patch(before!)
@@ -832,7 +838,7 @@ test("revert should not delete files that existed but were deleted in snapshot",
832838
await Filesystem.write(`${tmp.path}/a.txt`, "recreated content")
833839

834840
const patch = await Snapshot.patch(snapshot2!)
835-
expect(patch.files).toContain(`${tmp.path}/a.txt`)
841+
expect(patch.files).toContain(fwd(tmp.path, "a.txt"))
836842

837843
await Snapshot.revert([patch])
838844

@@ -861,8 +867,8 @@ test("revert preserves file that existed in snapshot when deleted then recreated
861867
await Filesystem.write(`${tmp.path}/newfile.txt`, "new")
862868

863869
const patch = await Snapshot.patch(snapshot!)
864-
expect(patch.files).toContain(`${tmp.path}/existing.txt`)
865-
expect(patch.files).toContain(`${tmp.path}/newfile.txt`)
870+
expect(patch.files).toContain(fwd(tmp.path, "existing.txt"))
871+
expect(patch.files).toContain(fwd(tmp.path, "newfile.txt"))
866872

867873
await Snapshot.revert([patch])
868874

0 commit comments

Comments
 (0)