Просмотр исходного кода

fix(snapshot): respect info exclude in snapshot staging (#13495)

Shoubhit Dash 6 месяцев назад
Родитель
Сommit
ac0b37a7b7

+ 37 - 3
packages/opencode/src/snapshot/index.ts

@@ -66,7 +66,7 @@ export namespace Snapshot {
       await $`git --git-dir ${git} config core.autocrlf false`.quiet().nothrow()
       log.info("initialized")
     }
-    await $`git --git-dir ${git} --work-tree ${Instance.worktree} add .`.quiet().cwd(Instance.directory).nothrow()
+    await add(git)
     const hash = await $`git --git-dir ${git} --work-tree ${Instance.worktree} write-tree`
       .quiet()
       .cwd(Instance.directory)
@@ -84,7 +84,7 @@ export namespace Snapshot {
 
   export async function patch(hash: string): Promise<Patch> {
     const git = gitdir()
-    await $`git --git-dir ${git} --work-tree ${Instance.worktree} add .`.quiet().cwd(Instance.directory).nothrow()
+    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} -- .`
         .quiet()
@@ -162,7 +162,7 @@ export namespace Snapshot {
 
   export async function diff(hash: string) {
     const git = gitdir()
-    await $`git --git-dir ${git} --work-tree ${Instance.worktree} add .`.quiet().cwd(Instance.directory).nothrow()
+    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} -- .`
         .quiet()
@@ -253,4 +253,38 @@ export namespace Snapshot {
     const project = Instance.project
     return path.join(Global.Path.data, "snapshot", project.id)
   }
+
+  async function add(git: string) {
+    await syncExclude(git)
+    await $`git --git-dir ${git} --work-tree ${Instance.worktree} add .`.quiet().cwd(Instance.directory).nothrow()
+  }
+
+  async function syncExclude(git: string) {
+    const file = await excludes()
+    const target = path.join(git, "info", "exclude")
+    await fs.mkdir(path.join(git, "info"), { recursive: true })
+    if (!file) {
+      await Bun.write(target, "")
+      return
+    }
+    const text = await Bun.file(file)
+      .text()
+      .catch(() => "")
+    await Bun.write(target, text)
+  }
+
+  async function excludes() {
+    const file = await $`git rev-parse --path-format=absolute --git-path info/exclude`
+      .quiet()
+      .cwd(Instance.worktree)
+      .nothrow()
+      .text()
+    if (!file.trim()) return
+    const exists = await fs
+      .stat(file.trim())
+      .then(() => true)
+      .catch(() => false)
+    if (!exists) return
+    return file.trim()
+  }
 }

+ 62 - 0
packages/opencode/test/snapshot/snapshot.test.ts

@@ -508,6 +508,68 @@ test("gitignore changes", async () => {
   })
 })
 
+test("git info exclude changes", async () => {
+  await using tmp = await bootstrap()
+  await Instance.provide({
+    directory: tmp.path,
+    fn: async () => {
+      const before = await Snapshot.track()
+      expect(before).toBeTruthy()
+
+      const file = `${tmp.path}/.git/info/exclude`
+      const text = await Bun.file(file).text()
+      await Bun.write(file, `${text.trimEnd()}\nignored.txt\n`)
+      await Bun.write(`${tmp.path}/ignored.txt`, "ignored content")
+      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`)
+
+      const after = await Snapshot.track()
+      const diffs = await Snapshot.diffFull(before!, after!)
+      expect(diffs.some((x) => x.file === "normal.txt")).toBe(true)
+      expect(diffs.some((x) => x.file === "ignored.txt")).toBe(false)
+    },
+  })
+})
+
+test("git info exclude keeps global excludes", async () => {
+  await using tmp = await bootstrap()
+  await Instance.provide({
+    directory: tmp.path,
+    fn: async () => {
+      const global = `${tmp.path}/global.ignore`
+      const config = `${tmp.path}/global.gitconfig`
+      await Bun.write(global, "global.tmp\n")
+      await Bun.write(config, `[core]\n\texcludesFile = ${global}\n`)
+
+      const prev = process.env.GIT_CONFIG_GLOBAL
+      process.env.GIT_CONFIG_GLOBAL = config
+      try {
+        const before = await Snapshot.track()
+        expect(before).toBeTruthy()
+
+        const file = `${tmp.path}/.git/info/exclude`
+        const text = await Bun.file(file).text()
+        await Bun.write(file, `${text.trimEnd()}\ninfo.tmp\n`)
+
+        await Bun.write(`${tmp.path}/global.tmp`, "global content")
+        await Bun.write(`${tmp.path}/info.tmp`, "info content")
+        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`)
+      } finally {
+        if (prev) process.env.GIT_CONFIG_GLOBAL = prev
+        else delete process.env.GIT_CONFIG_GLOBAL
+      }
+    },
+  })
+})
+
 test("concurrent file operations during patch", async () => {
   await using tmp = await bootstrap()
   await Instance.provide({