fix(win32): add git flags for snapshot operations and fix tests for cross-platform (#14890)
This commit is contained in:
@@ -64,6 +64,9 @@ export namespace Snapshot {
|
||||
.nothrow()
|
||||
// Configure git to not convert line endings on Windows
|
||||
await $`git --git-dir ${git} config core.autocrlf false`.quiet().nothrow()
|
||||
await $`git --git-dir ${git} config core.longpaths true`.quiet().nothrow()
|
||||
await $`git --git-dir ${git} config core.symlinks true`.quiet().nothrow()
|
||||
await $`git --git-dir ${git} config core.fsmonitor false`.quiet().nothrow()
|
||||
log.info("initialized")
|
||||
}
|
||||
await add(git)
|
||||
@@ -86,7 +89,7 @@ export namespace Snapshot {
|
||||
const git = gitdir()
|
||||
await add(git)
|
||||
const result =
|
||||
await $`git -c core.autocrlf=false -c core.quotepath=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff --name-only ${hash} -- .`
|
||||
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} -- .`
|
||||
.quiet()
|
||||
.cwd(Instance.directory)
|
||||
.nothrow()
|
||||
@@ -113,7 +116,7 @@ export namespace Snapshot {
|
||||
log.info("restore", { commit: snapshot })
|
||||
const git = gitdir()
|
||||
const result =
|
||||
await $`git --git-dir ${git} --work-tree ${Instance.worktree} read-tree ${snapshot} && git --git-dir ${git} --work-tree ${Instance.worktree} checkout-index -a -f`
|
||||
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`
|
||||
.quiet()
|
||||
.cwd(Instance.worktree)
|
||||
.nothrow()
|
||||
@@ -135,14 +138,15 @@ export namespace Snapshot {
|
||||
for (const file of item.files) {
|
||||
if (files.has(file)) continue
|
||||
log.info("reverting", { file, hash: item.hash })
|
||||
const result = await $`git --git-dir ${git} --work-tree ${Instance.worktree} checkout ${item.hash} -- ${file}`
|
||||
.quiet()
|
||||
.cwd(Instance.worktree)
|
||||
.nothrow()
|
||||
const result =
|
||||
await $`git -c core.longpaths=true -c core.symlinks=true --git-dir ${git} --work-tree ${Instance.worktree} checkout ${item.hash} -- ${file}`
|
||||
.quiet()
|
||||
.cwd(Instance.worktree)
|
||||
.nothrow()
|
||||
if (result.exitCode !== 0) {
|
||||
const relativePath = path.relative(Instance.worktree, file)
|
||||
const checkTree =
|
||||
await $`git --git-dir ${git} --work-tree ${Instance.worktree} ls-tree ${item.hash} -- ${relativePath}`
|
||||
await $`git -c core.longpaths=true -c core.symlinks=true --git-dir ${git} --work-tree ${Instance.worktree} ls-tree ${item.hash} -- ${relativePath}`
|
||||
.quiet()
|
||||
.cwd(Instance.worktree)
|
||||
.nothrow()
|
||||
@@ -164,7 +168,7 @@ export namespace Snapshot {
|
||||
const git = gitdir()
|
||||
await add(git)
|
||||
const result =
|
||||
await $`git -c core.autocrlf=false -c core.quotepath=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff ${hash} -- .`
|
||||
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} -- .`
|
||||
.quiet()
|
||||
.cwd(Instance.worktree)
|
||||
.nothrow()
|
||||
@@ -201,7 +205,7 @@ export namespace Snapshot {
|
||||
const status = new Map<string, "added" | "deleted" | "modified">()
|
||||
|
||||
const statuses =
|
||||
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} -- .`
|
||||
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} -- .`
|
||||
.quiet()
|
||||
.cwd(Instance.directory)
|
||||
.nothrow()
|
||||
@@ -215,7 +219,7 @@ export namespace Snapshot {
|
||||
status.set(file, kind)
|
||||
}
|
||||
|
||||
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} -- .`
|
||||
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} -- .`
|
||||
.quiet()
|
||||
.cwd(Instance.directory)
|
||||
.nothrow()
|
||||
@@ -225,13 +229,13 @@ export namespace Snapshot {
|
||||
const isBinaryFile = additions === "-" && deletions === "-"
|
||||
const before = isBinaryFile
|
||||
? ""
|
||||
: await $`git -c core.autocrlf=false --git-dir ${git} --work-tree ${Instance.worktree} show ${from}:${file}`
|
||||
: await $`git -c core.autocrlf=false -c core.longpaths=true -c core.symlinks=true --git-dir ${git} --work-tree ${Instance.worktree} show ${from}:${file}`
|
||||
.quiet()
|
||||
.nothrow()
|
||||
.text()
|
||||
const after = isBinaryFile
|
||||
? ""
|
||||
: await $`git -c core.autocrlf=false --git-dir ${git} --work-tree ${Instance.worktree} show ${to}:${file}`
|
||||
: await $`git -c core.autocrlf=false -c core.longpaths=true -c core.symlinks=true --git-dir ${git} --work-tree ${Instance.worktree} show ${to}:${file}`
|
||||
.quiet()
|
||||
.nothrow()
|
||||
.text()
|
||||
@@ -256,7 +260,10 @@ export namespace Snapshot {
|
||||
|
||||
async function add(git: string) {
|
||||
await syncExclude(git)
|
||||
await $`git --git-dir ${git} --work-tree ${Instance.worktree} add .`.quiet().cwd(Instance.directory).nothrow()
|
||||
await $`git -c core.autocrlf=false -c core.longpaths=true -c core.symlinks=true --git-dir ${git} --work-tree ${Instance.worktree} add .`
|
||||
.quiet()
|
||||
.cwd(Instance.directory)
|
||||
.nothrow()
|
||||
}
|
||||
|
||||
async function syncExclude(git: string) {
|
||||
|
||||
@@ -10,7 +10,7 @@ import { afterAll } from "bun:test"
|
||||
const dir = path.join(os.tmpdir(), "opencode-test-data-" + process.pid)
|
||||
await fs.mkdir(dir, { recursive: true })
|
||||
afterAll(() => {
|
||||
fsSync.rmSync(dir, { recursive: true, force: true })
|
||||
fsSync.rmSync(dir, { recursive: true, force: true, maxRetries: 3, retryDelay: 500 })
|
||||
})
|
||||
|
||||
process.env["XDG_DATA_HOME"] = path.join(dir, "share")
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { test, expect } from "bun:test"
|
||||
import { $ } from "bun"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { Snapshot } from "../../src/snapshot"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { Filesystem } from "../../src/util/filesystem"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
|
||||
// Git always outputs /-separated paths internally. Snapshot.patch() joins them
|
||||
// with path.join (which produces \ on Windows) then normalizes back to /.
|
||||
// This helper does the same for expected values so assertions match cross-platform.
|
||||
const fwd = (...parts: string[]) => path.join(...parts).replaceAll("\\", "/")
|
||||
|
||||
async function bootstrap() {
|
||||
return tmpdir({
|
||||
git: true,
|
||||
@@ -35,7 +41,7 @@ test("tracks deleted files correctly", async () => {
|
||||
|
||||
await $`rm ${tmp.path}/a.txt`.quiet()
|
||||
|
||||
expect((await Snapshot.patch(before!)).files).toContain(`${tmp.path}/a.txt`)
|
||||
expect((await Snapshot.patch(before!)).files).toContain(fwd(tmp.path, "a.txt"))
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -143,7 +149,7 @@ test("binary file handling", async () => {
|
||||
await Filesystem.write(`${tmp.path}/image.png`, new Uint8Array([0x89, 0x50, 0x4e, 0x47]))
|
||||
|
||||
const patch = await Snapshot.patch(before!)
|
||||
expect(patch.files).toContain(`${tmp.path}/image.png`)
|
||||
expect(patch.files).toContain(fwd(tmp.path, "image.png"))
|
||||
|
||||
await Snapshot.revert([patch])
|
||||
expect(
|
||||
@@ -164,9 +170,9 @@ test("symlink handling", async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
await $`ln -s ${tmp.path}/a.txt ${tmp.path}/link.txt`.quiet()
|
||||
await fs.symlink(`${tmp.path}/a.txt`, `${tmp.path}/link.txt`, "file")
|
||||
|
||||
expect((await Snapshot.patch(before!)).files).toContain(`${tmp.path}/link.txt`)
|
||||
expect((await Snapshot.patch(before!)).files).toContain(fwd(tmp.path, "link.txt"))
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -181,7 +187,7 @@ test("large file handling", async () => {
|
||||
|
||||
await Filesystem.write(`${tmp.path}/large.txt`, "x".repeat(1024 * 1024))
|
||||
|
||||
expect((await Snapshot.patch(before!)).files).toContain(`${tmp.path}/large.txt`)
|
||||
expect((await Snapshot.patch(before!)).files).toContain(fwd(tmp.path, "large.txt"))
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -222,9 +228,9 @@ test("special characters in filenames", async () => {
|
||||
await Filesystem.write(`${tmp.path}/file_with_underscores.txt`, "UNDERSCORES")
|
||||
|
||||
const files = (await Snapshot.patch(before!)).files
|
||||
expect(files).toContain(`${tmp.path}/file with spaces.txt`)
|
||||
expect(files).toContain(`${tmp.path}/file-with-dashes.txt`)
|
||||
expect(files).toContain(`${tmp.path}/file_with_underscores.txt`)
|
||||
expect(files).toContain(fwd(tmp.path, "file with spaces.txt"))
|
||||
expect(files).toContain(fwd(tmp.path, "file-with-dashes.txt"))
|
||||
expect(files).toContain(fwd(tmp.path, "file_with_underscores.txt"))
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -293,10 +299,10 @@ test("unicode filenames", async () => {
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
const unicodeFiles = [
|
||||
{ path: `${tmp.path}/文件.txt`, content: "chinese content" },
|
||||
{ path: `${tmp.path}/🚀rocket.txt`, content: "emoji content" },
|
||||
{ path: `${tmp.path}/café.txt`, content: "accented content" },
|
||||
{ path: `${tmp.path}/файл.txt`, content: "cyrillic content" },
|
||||
{ path: fwd(tmp.path, "文件.txt"), content: "chinese content" },
|
||||
{ path: fwd(tmp.path, "🚀rocket.txt"), content: "emoji content" },
|
||||
{ path: fwd(tmp.path, "café.txt"), content: "accented content" },
|
||||
{ path: fwd(tmp.path, "файл.txt"), content: "cyrillic content" },
|
||||
]
|
||||
|
||||
for (const file of unicodeFiles) {
|
||||
@@ -329,8 +335,8 @@ test.skip("unicode filenames modification and restore", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const chineseFile = `${tmp.path}/文件.txt`
|
||||
const cyrillicFile = `${tmp.path}/файл.txt`
|
||||
const chineseFile = fwd(tmp.path, "文件.txt")
|
||||
const cyrillicFile = fwd(tmp.path, "файл.txt")
|
||||
|
||||
await Filesystem.write(chineseFile, "original chinese")
|
||||
await Filesystem.write(cyrillicFile, "original cyrillic")
|
||||
@@ -362,7 +368,7 @@ test("unicode filenames in subdirectories", async () => {
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
await $`mkdir -p "${tmp.path}/目录/подкаталог"`.quiet()
|
||||
const deepFile = `${tmp.path}/目录/подкаталог/文件.txt`
|
||||
const deepFile = fwd(tmp.path, "目录", "подкаталог", "文件.txt")
|
||||
await Filesystem.write(deepFile, "deep unicode content")
|
||||
|
||||
const patch = await Snapshot.patch(before!)
|
||||
@@ -388,7 +394,7 @@ test("very long filenames", async () => {
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
const longName = "a".repeat(200) + ".txt"
|
||||
const longFile = `${tmp.path}/${longName}`
|
||||
const longFile = fwd(tmp.path, longName)
|
||||
|
||||
await Filesystem.write(longFile, "long filename content")
|
||||
|
||||
@@ -419,9 +425,9 @@ test("hidden files", async () => {
|
||||
await Filesystem.write(`${tmp.path}/.config`, "config content")
|
||||
|
||||
const patch = await Snapshot.patch(before!)
|
||||
expect(patch.files).toContain(`${tmp.path}/.hidden`)
|
||||
expect(patch.files).toContain(`${tmp.path}/.gitignore`)
|
||||
expect(patch.files).toContain(`${tmp.path}/.config`)
|
||||
expect(patch.files).toContain(fwd(tmp.path, ".hidden"))
|
||||
expect(patch.files).toContain(fwd(tmp.path, ".gitignore"))
|
||||
expect(patch.files).toContain(fwd(tmp.path, ".config"))
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -436,12 +442,12 @@ test("nested symlinks", async () => {
|
||||
|
||||
await $`mkdir -p ${tmp.path}/sub/dir`.quiet()
|
||||
await Filesystem.write(`${tmp.path}/sub/dir/target.txt`, "target content")
|
||||
await $`ln -s ${tmp.path}/sub/dir/target.txt ${tmp.path}/sub/dir/link.txt`.quiet()
|
||||
await $`ln -s ${tmp.path}/sub ${tmp.path}/sub-link`.quiet()
|
||||
await fs.symlink(`${tmp.path}/sub/dir/target.txt`, `${tmp.path}/sub/dir/link.txt`, "file")
|
||||
await fs.symlink(`${tmp.path}/sub`, `${tmp.path}/sub-link`, "dir")
|
||||
|
||||
const patch = await Snapshot.patch(before!)
|
||||
expect(patch.files).toContain(`${tmp.path}/sub/dir/link.txt`)
|
||||
expect(patch.files).toContain(`${tmp.path}/sub-link`)
|
||||
expect(patch.files).toContain(fwd(tmp.path, "sub", "dir", "link.txt"))
|
||||
expect(patch.files).toContain(fwd(tmp.path, "sub-link"))
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -476,7 +482,7 @@ test("circular symlinks", async () => {
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
// Create circular symlink
|
||||
await $`ln -s ${tmp.path}/circular ${tmp.path}/circular`.quiet().nothrow()
|
||||
await fs.symlink(`${tmp.path}/circular`, `${tmp.path}/circular`, "dir").catch(() => {})
|
||||
|
||||
const patch = await Snapshot.patch(before!)
|
||||
expect(patch.files.length).toBeGreaterThanOrEqual(0) // Should not crash
|
||||
@@ -499,11 +505,11 @@ test("gitignore changes", async () => {
|
||||
const patch = await Snapshot.patch(before!)
|
||||
|
||||
// Should track gitignore itself
|
||||
expect(patch.files).toContain(`${tmp.path}/.gitignore`)
|
||||
expect(patch.files).toContain(fwd(tmp.path, ".gitignore"))
|
||||
// Should track normal files
|
||||
expect(patch.files).toContain(`${tmp.path}/normal.txt`)
|
||||
expect(patch.files).toContain(fwd(tmp.path, "normal.txt"))
|
||||
// Should not track ignored files (git won't see them)
|
||||
expect(patch.files).not.toContain(`${tmp.path}/test.ignored`)
|
||||
expect(patch.files).not.toContain(fwd(tmp.path, "test.ignored"))
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -523,8 +529,8 @@ test("git info exclude changes", async () => {
|
||||
await Bun.write(`${tmp.path}/normal.txt`, "normal content")
|
||||
|
||||
const patch = await Snapshot.patch(before!)
|
||||
expect(patch.files).toContain(`${tmp.path}/normal.txt`)
|
||||
expect(patch.files).not.toContain(`${tmp.path}/ignored.txt`)
|
||||
expect(patch.files).toContain(fwd(tmp.path, "normal.txt"))
|
||||
expect(patch.files).not.toContain(fwd(tmp.path, "ignored.txt"))
|
||||
|
||||
const after = await Snapshot.track()
|
||||
const diffs = await Snapshot.diffFull(before!, after!)
|
||||
@@ -559,9 +565,9 @@ test("git info exclude keeps global excludes", async () => {
|
||||
await Bun.write(`${tmp.path}/normal.txt`, "normal content")
|
||||
|
||||
const patch = await Snapshot.patch(before!)
|
||||
expect(patch.files).toContain(`${tmp.path}/normal.txt`)
|
||||
expect(patch.files).not.toContain(`${tmp.path}/global.tmp`)
|
||||
expect(patch.files).not.toContain(`${tmp.path}/info.tmp`)
|
||||
expect(patch.files).toContain(fwd(tmp.path, "normal.txt"))
|
||||
expect(patch.files).not.toContain(fwd(tmp.path, "global.tmp"))
|
||||
expect(patch.files).not.toContain(fwd(tmp.path, "info.tmp"))
|
||||
} finally {
|
||||
if (prev) process.env.GIT_CONFIG_GLOBAL = prev
|
||||
else delete process.env.GIT_CONFIG_GLOBAL
|
||||
@@ -610,7 +616,7 @@ test("snapshot state isolation between projects", async () => {
|
||||
const before1 = await Snapshot.track()
|
||||
await Filesystem.write(`${tmp1.path}/project1.txt`, "project1 content")
|
||||
const patch1 = await Snapshot.patch(before1!)
|
||||
expect(patch1.files).toContain(`${tmp1.path}/project1.txt`)
|
||||
expect(patch1.files).toContain(fwd(tmp1.path, "project1.txt"))
|
||||
},
|
||||
})
|
||||
|
||||
@@ -620,10 +626,10 @@ test("snapshot state isolation between projects", async () => {
|
||||
const before2 = await Snapshot.track()
|
||||
await Filesystem.write(`${tmp2.path}/project2.txt`, "project2 content")
|
||||
const patch2 = await Snapshot.patch(before2!)
|
||||
expect(patch2.files).toContain(`${tmp2.path}/project2.txt`)
|
||||
expect(patch2.files).toContain(fwd(tmp2.path, "project2.txt"))
|
||||
|
||||
// Ensure project1 files don't appear in project2
|
||||
expect(patch2.files).not.toContain(`${tmp1?.path}/project1.txt`)
|
||||
expect(patch2.files).not.toContain(fwd(tmp1?.path ?? "", "project1.txt"))
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -647,7 +653,7 @@ test("patch detects changes in secondary worktree", async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
const worktreeFile = `${worktreePath}/worktree.txt`
|
||||
const worktreeFile = fwd(worktreePath, "worktree.txt")
|
||||
await Filesystem.write(worktreeFile, "worktree content")
|
||||
|
||||
const patch = await Snapshot.patch(before!)
|
||||
@@ -681,7 +687,7 @@ test("revert only removes files in invoking worktree", async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
const worktreeFile = `${worktreePath}/worktree.txt`
|
||||
const worktreeFile = fwd(worktreePath, "worktree.txt")
|
||||
await Filesystem.write(worktreeFile, "worktree content")
|
||||
|
||||
const patch = await Snapshot.patch(before!)
|
||||
@@ -832,7 +838,7 @@ test("revert should not delete files that existed but were deleted in snapshot",
|
||||
await Filesystem.write(`${tmp.path}/a.txt`, "recreated content")
|
||||
|
||||
const patch = await Snapshot.patch(snapshot2!)
|
||||
expect(patch.files).toContain(`${tmp.path}/a.txt`)
|
||||
expect(patch.files).toContain(fwd(tmp.path, "a.txt"))
|
||||
|
||||
await Snapshot.revert([patch])
|
||||
|
||||
@@ -861,8 +867,8 @@ test("revert preserves file that existed in snapshot when deleted then recreated
|
||||
await Filesystem.write(`${tmp.path}/newfile.txt`, "new")
|
||||
|
||||
const patch = await Snapshot.patch(snapshot!)
|
||||
expect(patch.files).toContain(`${tmp.path}/existing.txt`)
|
||||
expect(patch.files).toContain(`${tmp.path}/newfile.txt`)
|
||||
expect(patch.files).toContain(fwd(tmp.path, "existing.txt"))
|
||||
expect(patch.files).toContain(fwd(tmp.path, "newfile.txt"))
|
||||
|
||||
await Snapshot.revert([patch])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user