diff --git a/nix/hashes.json b/nix/hashes.json index ba60491c6ffc..0558d6b3d849 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=" } } diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index ef92ddcbf3ed..5e1dc7a1d75c 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 }))