Compare commits

...
Sign in to create a new pull request.

2 commits

Author SHA1 Message Date
Dax Raad
9ab0d6a6c9 fix(snapshot): let git drop ignored snapshot paths 2026-04-15 01:47:48 -04:00
opencode-agent[bot]
1f279cd2c8 chore: update nix node_modules hashes 2026-04-15 03:47:58 +00:00
2 changed files with 53 additions and 102 deletions

View file

@ -1,8 +1,8 @@
{ {
"nodeModules": { "nodeModules": {
"x86_64-linux": "sha256-3VYF84QGROFpwBCYDEWHDpRFkSHwmiVWQJR81/bqjYo=", "x86_64-linux": "sha256-gdS7MkWGeVO0qLs0HKD156YE0uCk5vWeYjKu4JR1Apw=",
"aarch64-linux": "sha256-k12n4GqrjBqKqBvLjzXQhDxbc8ZMZ/1TenDp2pKh888=", "aarch64-linux": "sha256-tF4pyVqzbrvdkRG23Fot37FCg8guRZkcU738fHPr/OQ=",
"aarch64-darwin": "sha256-OCRX1VC5SJmrXk7whl6bsdmlRwjARGw+4RSk8c59N10=", "aarch64-darwin": "sha256-FugTWzGMb2ktAbNwQvWRM3GWOb5RTR++8EocDDrQMLc=",
"x86_64-darwin": "sha256-l+g/cMREarOVIK3a01+csC3Mk3ZfMVWAiAosSA5/U6Y=" "x86_64-darwin": "sha256-jpe6EiwKr+CS00cn0eHwcDluO4LvO3t/5l/LcFBBKP0="
} }
} }

View file

@ -91,11 +91,15 @@ export namespace Snapshot {
const args = (cmd: string[]) => ["--git-dir", state.gitdir, "--work-tree", state.worktree, ...cmd] const args = (cmd: string[]) => ["--git-dir", state.gitdir, "--work-tree", state.worktree, ...cmd]
const git = Effect.fnUntraced( const git = Effect.fnUntraced(
function* (cmd: string[], opts?: { cwd?: string; env?: Record<string, string> }) { function* (
cmd: string[],
opts?: { cwd?: string; env?: Record<string, string>; stdin?: Stream.Stream<Uint8Array> },
) {
const proc = ChildProcess.make("git", cmd, { const proc = ChildProcess.make("git", cmd, {
cwd: opts?.cwd, cwd: opts?.cwd,
env: opts?.env, env: opts?.env,
extendEnv: true, extendEnv: true,
stdin: opts?.stdin ?? "ignore",
}) })
const handle = yield* spawner.spawn(proc) const handle = yield* spawner.spawn(proc)
const [text, stderr] = yield* Effect.all( const [text, stderr] = yield* Effect.all(
@ -150,65 +154,26 @@ export namespace Snapshot {
const add = Effect.fnUntraced(function* () { const add = Effect.fnUntraced(function* () {
yield* sync() yield* sync()
const [diff, other] = yield* Effect.all( const list = yield* git(
[ [
git([...quote, ...args(["diff-files", "--name-only", "-z", "--", "."])], { ...quote,
cwd: state.directory, ...args(["ls-files", "--modified", "--deleted", "--others", "--exclude-standard", "-z", "--", "."]),
}),
git([...quote, ...args(["ls-files", "--others", "--exclude-standard", "-z", "--", "."])], {
cwd: state.directory,
}),
], ],
{ concurrency: 2 }, { cwd: state.directory },
) )
if (diff.code !== 0 || other.code !== 0) { if (list.code !== 0) {
log.warn("failed to list snapshot files", { log.warn("failed to list snapshot files", {
diffCode: diff.code, exitCode: list.code,
diffStderr: diff.stderr, stderr: list.stderr,
otherCode: other.code,
otherStderr: other.stderr,
}) })
return return
} }
const tracked = diff.text.split("\0").filter(Boolean) const files = list.text.split("\0").filter(Boolean)
const untracked = other.text.split("\0").filter(Boolean) if (!files.length) return
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<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( const large = (yield* Effect.all(
filtered.map((item) => files.map((item) =>
fs fs
.stat(path.join(state.directory, item)) .stat(path.join(state.directory, item))
.pipe(Effect.catch(() => Effect.void)) .pipe(Effect.catch(() => Effect.void))
@ -223,12 +188,45 @@ export namespace Snapshot {
{ concurrency: 8 }, { concurrency: 8 },
)).filter((item): item is string => Boolean(item)) )).filter((item): item is string => Boolean(item))
yield* sync(large) 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) { if (result.code !== 0) {
log.warn("failed to add snapshot files", { log.warn("failed to add snapshot files", {
exitCode: result.code, exitCode: result.code,
stderr: result.stderr, 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()) .map((x) => x.trim())
.filter(Boolean) .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 { return {
hash, hash,
files: files.map((x) => path.join(state.worktree, x).replaceAll("\\", "/")), 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 step = 100
const patch = (file: string, before: string, after: string) => const patch = (file: string, before: string, after: string) =>
formatPatch(structuredPatch(file, file, before, after, "", "", { context: Number.MAX_SAFE_INTEGER })) formatPatch(structuredPatch(file, file, before, after, "", "", { context: Number.MAX_SAFE_INTEGER }))