summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDax <[email protected]>2026-04-12 13:41:50 -0400
committerGitHub <[email protected]>2026-04-12 13:41:50 -0400
commit113304a058d569f00f758e0646fa360cf5b052d5 (patch)
tree0420040b33fdd9003cfe1991b4f76241979bf1dc
parent8c4d49c2bc7a08248d78552490b6c0ef8b60042b (diff)
downloadopencode-113304a058d569f00f758e0646fa360cf5b052d5.tar.gz
opencode-113304a058d569f00f758e0646fa360cf5b052d5.zip
fix(snapshot): respect gitignore for previously tracked files (#22171)
-rw-r--r--packages/opencode/src/snapshot/index.ts67
-rw-r--r--packages/opencode/test/snapshot/snapshot.test.ts77
2 files changed, 137 insertions, 7 deletions
diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts
index 834cdde25..3b522a03e 100644
--- a/packages/opencode/src/snapshot/index.ts
+++ b/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("\\", "/")),
}
}),
)
diff --git a/packages/opencode/test/snapshot/snapshot.test.ts b/packages/opencode/test/snapshot/snapshot.test.ts
index 3cedfb941..22253ecab 100644
--- a/packages/opencode/test/snapshot/snapshot.test.ts
+++ b/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({