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

fix(snapshot): respect gitignore for previously tracked files (#22171)

Dax 4 дней назад
Родитель
Сommit
113304a058

+ 60 - 7
packages/opencode/src/snapshot/index.ts

@@ -177,8 +177,37 @@ export namespace Snapshot {
             const all = Array.from(new Set([...tracked, ...untracked]))
             if (!all.length) return
 
+            // Filter out files that are now gitignored even if previously tracked
+            // Files may have been tracked before being gitignored, so we need to check
+            // against the source project's current gitignore rules
+            const checkArgs = [
+              ...quote,
+              "--git-dir",
+              path.join(state.worktree, ".git"),
+              "--work-tree",
+              state.worktree,
+              "check-ignore",
+              "--",
+              ...all,
+            ]
+            const check = yield* git(checkArgs, { cwd: state.directory })
+            const ignored =
+              check.code === 0 ? new Set(check.text.trim().split("\n").filter(Boolean)) : new Set<string>()
+            const filtered = all.filter((item) => !ignored.has(item))
+
+            // Remove newly-ignored files from snapshot index to prevent re-adding
+            if (ignored.size > 0) {
+              const ignoredFiles = Array.from(ignored)
+              log.info("removing gitignored files from snapshot", { count: ignoredFiles.length })
+              yield* git([...cfg, ...args(["rm", "--cached", "-f", "--", ...ignoredFiles])], {
+                cwd: state.directory,
+              })
+            }
+
+            if (!filtered.length) return
+
             const large = (yield* Effect.all(
-              all.map((item) =>
+              filtered.map((item) =>
                 fs
                   .stat(path.join(state.directory, item))
                   .pipe(Effect.catch(() => Effect.void))
@@ -259,14 +288,38 @@ export namespace Snapshot {
                   log.warn("failed to get diff", { hash, exitCode: result.code })
                   return { hash, files: [] }
                 }
+                const files = result.text
+                  .trim()
+                  .split("\n")
+                  .map((x) => x.trim())
+                  .filter(Boolean)
+
+                // Filter out files that are now gitignored
+                if (files.length > 0) {
+                  const checkArgs = [
+                    ...quote,
+                    "--git-dir",
+                    path.join(state.worktree, ".git"),
+                    "--work-tree",
+                    state.worktree,
+                    "check-ignore",
+                    "--",
+                    ...files,
+                  ]
+                  const check = yield* git(checkArgs, { cwd: state.directory })
+                  if (check.code === 0) {
+                    const ignored = new Set(check.text.trim().split("\n").filter(Boolean))
+                    const filtered = files.filter((item) => !ignored.has(item))
+                    return {
+                      hash,
+                      files: filtered.map((x) => path.join(state.worktree, x).replaceAll("\\", "/")),
+                    }
+                  }
+                }
+
                 return {
                   hash,
-                  files: result.text
-                    .trim()
-                    .split("\n")
-                    .map((x) => x.trim())
-                    .filter(Boolean)
-                    .map((x) => path.join(state.worktree, x).replaceAll("\\", "/")),
+                  files: files.map((x) => path.join(state.worktree, x).replaceAll("\\", "/")),
                 }
               }),
             )

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

@@ -511,6 +511,49 @@ test("circular symlinks", async () => {
   })
 })
 
+test("source project gitignore is respected - ignored files are not snapshotted", async () => {
+  await using tmp = await tmpdir({
+    git: true,
+    init: async (dir) => {
+      // Create gitignore BEFORE any tracking
+      await Filesystem.write(`${dir}/.gitignore`, "*.ignored\nbuild/\nnode_modules/\n")
+      await Filesystem.write(`${dir}/tracked.txt`, "tracked content")
+      await Filesystem.write(`${dir}/ignored.ignored`, "ignored content")
+      await $`mkdir -p ${dir}/build`.quiet()
+      await Filesystem.write(`${dir}/build/output.js`, "build output")
+      await Filesystem.write(`${dir}/normal.js`, "normal js")
+      await $`git add .`.cwd(dir).quiet()
+      await $`git commit -m init`.cwd(dir).quiet()
+    },
+  })
+
+  await Instance.provide({
+    directory: tmp.path,
+    fn: async () => {
+      const before = await Snapshot.track()
+      expect(before).toBeTruthy()
+
+      // Modify tracked files and create new ones - some ignored, some not
+      await Filesystem.write(`${tmp.path}/tracked.txt`, "modified tracked")
+      await Filesystem.write(`${tmp.path}/new.ignored`, "new ignored")
+      await Filesystem.write(`${tmp.path}/new-tracked.txt`, "new tracked")
+      await Filesystem.write(`${tmp.path}/build/new-build.js`, "new build file")
+
+      const patch = await Snapshot.patch(before!)
+
+      // Modified and new tracked files should be in snapshot
+      expect(patch.files).toContain(fwd(tmp.path, "new-tracked.txt"))
+      expect(patch.files).toContain(fwd(tmp.path, "tracked.txt"))
+
+      // Ignored files should NOT be in snapshot
+      expect(patch.files).not.toContain(fwd(tmp.path, "new.ignored"))
+      expect(patch.files).not.toContain(fwd(tmp.path, "ignored.ignored"))
+      expect(patch.files).not.toContain(fwd(tmp.path, "build/output.js"))
+      expect(patch.files).not.toContain(fwd(tmp.path, "build/new-build.js"))
+    },
+  })
+})
+
 test("gitignore changes", async () => {
   await using tmp = await bootstrap()
   await Instance.provide({
@@ -535,6 +578,40 @@ test("gitignore changes", async () => {
   })
 })
 
+test("files tracked in snapshot but now gitignored are filtered out", async () => {
+  await using tmp = await bootstrap()
+  await Instance.provide({
+    directory: tmp.path,
+    fn: async () => {
+      // First, create a file and snapshot it
+      await Filesystem.write(`${tmp.path}/later-ignored.txt`, "initial content")
+      const before = await Snapshot.track()
+      expect(before).toBeTruthy()
+
+      // Modify the file (so it appears in diff-files)
+      await Filesystem.write(`${tmp.path}/later-ignored.txt`, "modified content")
+
+      // Now add gitignore that would exclude this file
+      await Filesystem.write(`${tmp.path}/.gitignore`, "later-ignored.txt\n")
+
+      // Also create another tracked file
+      await Filesystem.write(`${tmp.path}/still-tracked.txt`, "new tracked file")
+
+      const patch = await Snapshot.patch(before!)
+
+      // The file that is now gitignored should NOT appear, even though it was
+      // previously tracked and modified
+      expect(patch.files).not.toContain(fwd(tmp.path, "later-ignored.txt"))
+
+      // The gitignore file itself should appear
+      expect(patch.files).toContain(fwd(tmp.path, ".gitignore"))
+
+      // Other tracked files should appear
+      expect(patch.files).toContain(fwd(tmp.path, "still-tracked.txt"))
+    },
+  })
+})
+
 test("git info exclude changes", async () => {
   await using tmp = await bootstrap()
   await Instance.provide({