From 264418c0cdda37e214c01688dca66c0dcfd3e0b0 Mon Sep 17 00:00:00 2001 From: Dax Date: Sun, 12 Apr 2026 14:05:46 -0400 Subject: [PATCH] fix(snapshot): complete gitignore respect for previously tracked files (#22172) --- packages/opencode/src/snapshot/index.ts | 27 ++++++++++++++ .../opencode/test/snapshot/snapshot.test.ts | 35 +++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index 3b522a03ea..995e8d3fda 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -180,6 +180,7 @@ export namespace Snapshot { // 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 + // Use --no-index to check purely against patterns (ignoring whether file is tracked) const checkArgs = [ ...quote, "--git-dir", @@ -187,6 +188,7 @@ export namespace Snapshot { "--work-tree", state.worktree, "check-ignore", + "--no-index", "--", ...all, ] @@ -303,6 +305,7 @@ export namespace Snapshot { "--work-tree", state.worktree, "check-ignore", + "--no-index", "--", ...files, ] @@ -669,6 +672,30 @@ export namespace Snapshot { } satisfies Row, ] }) + + // Filter out files that are now gitignored + if (rows.length > 0) { + const files = rows.map((r) => r.file) + const checkArgs = [ + ...quote, + "--git-dir", + path.join(state.worktree, ".git"), + "--work-tree", + state.worktree, + "check-ignore", + "--no-index", + "--", + ...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 = rows.filter((r) => !ignored.has(r.file)) + rows.length = 0 + rows.push(...filtered) + } + } + const step = 100 const patch = (file: string, before: string, after: string) => formatPatch(structuredPatch(file, file, before, after, "", "", { context: Number.MAX_SAFE_INTEGER })) diff --git a/packages/opencode/test/snapshot/snapshot.test.ts b/packages/opencode/test/snapshot/snapshot.test.ts index 22253ecaba..971d053bd6 100644 --- a/packages/opencode/test/snapshot/snapshot.test.ts +++ b/packages/opencode/test/snapshot/snapshot.test.ts @@ -612,6 +612,41 @@ test("files tracked in snapshot but now gitignored are filtered out", async () = }) }) +test("gitignore updated between track calls filters from diff", async () => { + await using tmp = await bootstrap() + await Instance.provide({ + directory: tmp.path, + fn: async () => { + // a.txt is already committed from bootstrap - track it in snapshot + const before = await Snapshot.track() + expect(before).toBeTruthy() + + // Modify a.txt (so it appears in diff-files) + await Filesystem.write(`${tmp.path}/a.txt`, "modified content") + + // Now add gitignore that would exclude a.txt + await Filesystem.write(`${tmp.path}/.gitignore`, "a.txt\n") + + // Also modify b.txt which is not gitignored + await Filesystem.write(`${tmp.path}/b.txt`, "also modified") + + // Second track - should not include a.txt even though it changed + const after = await Snapshot.track() + expect(after).toBeTruthy() + + // Verify a.txt is NOT in the diff between snapshots + const diffs = await Snapshot.diffFull(before!, after!) + expect(diffs.some((x) => x.file === "a.txt")).toBe(false) + + // But .gitignore should be in the diff + expect(diffs.some((x) => x.file === ".gitignore")).toBe(true) + + // b.txt should be in the diff (not gitignored) + expect(diffs.some((x) => x.file === "b.txt")).toBe(true) + }, + }) +}) + test("git info exclude changes", async () => { await using tmp = await bootstrap() await Instance.provide({