From 1f279cd2c8719601c72eff071dd69c58cda93219 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Wed, 15 Apr 2026 03:47:58 +0000 Subject: [PATCH 1/2] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index ba60491c6f..0558d6b3d8 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-3VYF84QGROFpwBCYDEWHDpRFkSHwmiVWQJR81/bqjYo=", - "aarch64-linux": "sha256-k12n4GqrjBqKqBvLjzXQhDxbc8ZMZ/1TenDp2pKh888=", - "aarch64-darwin": "sha256-OCRX1VC5SJmrXk7whl6bsdmlRwjARGw+4RSk8c59N10=", - "x86_64-darwin": "sha256-l+g/cMREarOVIK3a01+csC3Mk3ZfMVWAiAosSA5/U6Y=" + "x86_64-linux": "sha256-gdS7MkWGeVO0qLs0HKD156YE0uCk5vWeYjKu4JR1Apw=", + "aarch64-linux": "sha256-tF4pyVqzbrvdkRG23Fot37FCg8guRZkcU738fHPr/OQ=", + "aarch64-darwin": "sha256-FugTWzGMb2ktAbNwQvWRM3GWOb5RTR++8EocDDrQMLc=", + "x86_64-darwin": "sha256-jpe6EiwKr+CS00cn0eHwcDluO4LvO3t/5l/LcFBBKP0=" } } From 9ab0d6a6c9b1a082a03a0d025307a21fdba13ba2 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Wed, 15 Apr 2026 01:30:49 -0400 Subject: [PATCH 2/2] fix(snapshot): let git drop ignored snapshot paths --- packages/opencode/src/snapshot/index.ts | 147 ++++++++---------------- 1 file changed, 49 insertions(+), 98 deletions(-) diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index ef92ddcbf3..5e1dc7a1d7 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -91,11 +91,15 @@ export namespace Snapshot { const args = (cmd: string[]) => ["--git-dir", state.gitdir, "--work-tree", state.worktree, ...cmd] const git = Effect.fnUntraced( - function* (cmd: string[], opts?: { cwd?: string; env?: Record }) { + function* ( + cmd: string[], + opts?: { cwd?: string; env?: Record; stdin?: Stream.Stream }, + ) { const proc = ChildProcess.make("git", cmd, { cwd: opts?.cwd, env: opts?.env, extendEnv: true, + stdin: opts?.stdin ?? "ignore", }) const handle = yield* spawner.spawn(proc) const [text, stderr] = yield* Effect.all( @@ -150,65 +154,26 @@ export namespace Snapshot { const add = Effect.fnUntraced(function* () { yield* sync() - const [diff, other] = yield* Effect.all( + const list = yield* git( [ - git([...quote, ...args(["diff-files", "--name-only", "-z", "--", "."])], { - cwd: state.directory, - }), - git([...quote, ...args(["ls-files", "--others", "--exclude-standard", "-z", "--", "."])], { - cwd: state.directory, - }), + ...quote, + ...args(["ls-files", "--modified", "--deleted", "--others", "--exclude-standard", "-z", "--", "."]), ], - { concurrency: 2 }, + { cwd: state.directory }, ) - if (diff.code !== 0 || other.code !== 0) { + if (list.code !== 0) { log.warn("failed to list snapshot files", { - diffCode: diff.code, - diffStderr: diff.stderr, - otherCode: other.code, - otherStderr: other.stderr, + exitCode: list.code, + stderr: list.stderr, }) return } - const tracked = diff.text.split("\0").filter(Boolean) - const untracked = other.text.split("\0").filter(Boolean) - 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 - // Use --no-index to check purely against patterns (ignoring whether file is tracked) - const checkArgs = [ - ...quote, - "--git-dir", - path.join(state.worktree, ".git"), - "--work-tree", - state.worktree, - "check-ignore", - "--no-index", - "--", - ...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() - 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 files = list.text.split("\0").filter(Boolean) + if (!files.length) return const large = (yield* Effect.all( - filtered.map((item) => + files.map((item) => fs .stat(path.join(state.directory, item)) .pipe(Effect.catch(() => Effect.void)) @@ -223,12 +188,45 @@ export namespace Snapshot { { concurrency: 8 }, )).filter((item): item is string => Boolean(item)) yield* sync(large) - const result = yield* git([...cfg, ...args(["add", "--sparse", "."])], { cwd: state.directory }) + + // Snapshot the current index before git add mutates it. + // Tracked files that are now ignored still get staged by git add; + // we reset just those entries back to this tree afterward. + const [prev, keep] = yield* Effect.all( + [ + git(args(["write-tree"]), { cwd: state.directory }), + git([...quote, ...args(["ls-files", "-ci", "--exclude-standard", "-z", "--", "."])], { + cwd: state.directory, + }), + ], + { concurrency: 2 }, + ) + + const result = yield* git([...cfg, ...args(["add", "--all", "--sparse", "."])], { cwd: state.directory }) if (result.code !== 0) { log.warn("failed to add snapshot files", { exitCode: result.code, stderr: result.stderr, }) + return + } + + const base = prev.code === 0 ? prev.text.trim() : "" + const cached = keep.code === 0 ? keep.text : "" + if (!base || !cached) return + + const reset = yield* git( + [...cfg, ...args(["reset", "-q", "--pathspec-from-file=-", "--pathspec-file-nul", base])], + { + cwd: state.directory, + stdin: Stream.make(new TextEncoder().encode(cached)), + }, + ) + if (reset.code !== 0) { + log.warn("failed to reset ignored snapshot files", { + exitCode: reset.code, + stderr: reset.stderr, + }) } }) @@ -295,30 +293,6 @@ export namespace Snapshot { .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", - "--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 = files.filter((item) => !ignored.has(item)) - return { - hash, - files: filtered.map((x) => path.join(state.worktree, x).replaceAll("\\", "/")), - } - } - } - return { hash, files: files.map((x) => path.join(state.worktree, x).replaceAll("\\", "/")), @@ -672,29 +646,6 @@ export namespace Snapshot { ] }) - // 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 }))