Skip to content

Commit a992d8b

Browse files
authored
fix(snapshot): avoid ENAMETOOLONG and improve staging perf via stdin pathspecs (anomalyco#22560)
1 parent ccaa12e commit a992d8b

1 file changed

Lines changed: 100 additions & 93 deletions

File tree

packages/opencode/src/snapshot/index.ts

Lines changed: 100 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -90,12 +90,19 @@ export namespace Snapshot {
9090

9191
const args = (cmd: string[]) => ["--git-dir", state.gitdir, "--work-tree", state.worktree, ...cmd]
9292

93+
const enc = new TextEncoder()
94+
const feed = (list: string[]) => Stream.make(enc.encode(list.join("\0") + "\0"))
95+
9396
const git = Effect.fnUntraced(
94-
function* (cmd: string[], opts?: { cwd?: string; env?: Record<string, string> }) {
97+
function* (
98+
cmd: string[],
99+
opts?: { cwd?: string; env?: Record<string, string>; stdin?: ChildProcess.CommandInput },
100+
) {
95101
const proc = ChildProcess.make("git", cmd, {
96102
cwd: opts?.cwd,
97103
env: opts?.env,
98104
extendEnv: true,
105+
stdin: opts?.stdin,
99106
})
100107
const handle = yield* spawner.spawn(proc)
101108
const [text, stderr] = yield* Effect.all(
@@ -115,6 +122,59 @@ export namespace Snapshot {
115122
),
116123
)
117124

125+
const ignore = Effect.fnUntraced(function* (files: string[]) {
126+
if (!files.length) return new Set<string>()
127+
const check = yield* git(
128+
[
129+
...quote,
130+
"--git-dir",
131+
path.join(state.worktree, ".git"),
132+
"--work-tree",
133+
state.worktree,
134+
"check-ignore",
135+
"--no-index",
136+
"--stdin",
137+
"-z",
138+
],
139+
{
140+
cwd: state.directory,
141+
stdin: feed(files),
142+
},
143+
)
144+
if (check.code !== 0 && check.code !== 1) return new Set<string>()
145+
return new Set(check.text.split("\0").filter(Boolean))
146+
})
147+
148+
const drop = Effect.fnUntraced(function* (files: string[]) {
149+
if (!files.length) return
150+
yield* git(
151+
[
152+
...cfg,
153+
...args(["rm", "--cached", "-f", "--ignore-unmatch", "--pathspec-from-file=-", "--pathspec-file-nul"]),
154+
],
155+
{
156+
cwd: state.directory,
157+
stdin: feed(files),
158+
},
159+
)
160+
})
161+
162+
const stage = Effect.fnUntraced(function* (files: string[]) {
163+
if (!files.length) return
164+
const result = yield* git(
165+
[...cfg, ...args(["add", "--all", "--sparse", "--pathspec-from-file=-", "--pathspec-file-nul"])],
166+
{
167+
cwd: state.directory,
168+
stdin: feed(files),
169+
},
170+
)
171+
if (result.code === 0) return
172+
log.warn("failed to add snapshot files", {
173+
exitCode: result.code,
174+
stderr: result.stderr,
175+
})
176+
})
177+
118178
const exists = (file: string) => fs.exists(file).pipe(Effect.orDie)
119179
const read = (file: string) => fs.readFileString(file).pipe(Effect.catch(() => Effect.succeed("")))
120180
const remove = (file: string) => fs.remove(file).pipe(Effect.catch(() => Effect.void))
@@ -176,60 +236,41 @@ export namespace Snapshot {
176236
const all = Array.from(new Set([...tracked, ...untracked]))
177237
if (!all.length) return
178238

179-
// Filter out files that are now gitignored even if previously tracked
180-
// Files may have been tracked before being gitignored, so we need to check
181-
// against the source project's current gitignore rules
182-
// Use --no-index to check purely against patterns (ignoring whether file is tracked)
183-
const checkArgs = [
184-
...quote,
185-
"--git-dir",
186-
path.join(state.worktree, ".git"),
187-
"--work-tree",
188-
state.worktree,
189-
"check-ignore",
190-
"--no-index",
191-
"--",
192-
...all,
193-
]
194-
const check = yield* git(checkArgs, { cwd: state.directory })
195-
const ignored =
196-
check.code === 0 ? new Set(check.text.trim().split("\n").filter(Boolean)) : new Set<string>()
197-
const filtered = all.filter((item) => !ignored.has(item))
239+
// Resolve source-repo ignore rules against the exact candidate set.
240+
// --no-index keeps this pattern-based even when a path is already tracked.
241+
const ignored = yield* ignore(all)
198242

199243
// Remove newly-ignored files from snapshot index to prevent re-adding
200244
if (ignored.size > 0) {
201245
const ignoredFiles = Array.from(ignored)
202246
log.info("removing gitignored files from snapshot", { count: ignoredFiles.length })
203-
yield* git([...cfg, ...args(["rm", "--cached", "-f", "--", ...ignoredFiles])], {
204-
cwd: state.directory,
205-
})
247+
yield* drop(ignoredFiles)
206248
}
207249

208-
if (!filtered.length) return
209-
210-
const large = (yield* Effect.all(
211-
filtered.map((item) =>
212-
fs
213-
.stat(path.join(state.directory, item))
214-
.pipe(Effect.catch(() => Effect.void))
215-
.pipe(
216-
Effect.map((stat) => {
217-
if (!stat || stat.type !== "File") return
218-
const size = typeof stat.size === "bigint" ? Number(stat.size) : stat.size
219-
return size > limit ? item : undefined
220-
}),
221-
),
222-
),
223-
{ concurrency: 8 },
224-
)).filter((item): item is string => Boolean(item))
225-
yield* sync(large)
226-
const result = yield* git([...cfg, ...args(["add", "--sparse", "."])], { cwd: state.directory })
227-
if (result.code !== 0) {
228-
log.warn("failed to add snapshot files", {
229-
exitCode: result.code,
230-
stderr: result.stderr,
231-
})
232-
}
250+
const allow = all.filter((item) => !ignored.has(item))
251+
if (!allow.length) return
252+
253+
const large = new Set(
254+
(yield* Effect.all(
255+
allow.map((item) =>
256+
fs
257+
.stat(path.join(state.directory, item))
258+
.pipe(Effect.catch(() => Effect.void))
259+
.pipe(
260+
Effect.map((stat) => {
261+
if (!stat || stat.type !== "File") return
262+
const size = typeof stat.size === "bigint" ? Number(stat.size) : stat.size
263+
return size > limit ? item : undefined
264+
}),
265+
),
266+
),
267+
{ concurrency: 8 },
268+
)).filter((item): item is string => Boolean(item)),
269+
)
270+
const block = new Set(untracked.filter((item) => large.has(item)))
271+
yield* sync(Array.from(block))
272+
// Stage only the allowed candidate paths so snapshot updates stay scoped.
273+
yield* stage(allow.filter((item) => !block.has(item)))
233274
})
234275

235276
const cleanup = Effect.fnUntraced(function* () {
@@ -295,33 +336,14 @@ export namespace Snapshot {
295336
.map((x) => x.trim())
296337
.filter(Boolean)
297338

298-
// Filter out files that are now gitignored
299-
if (files.length > 0) {
300-
const checkArgs = [
301-
...quote,
302-
"--git-dir",
303-
path.join(state.worktree, ".git"),
304-
"--work-tree",
305-
state.worktree,
306-
"check-ignore",
307-
"--no-index",
308-
"--",
309-
...files,
310-
]
311-
const check = yield* git(checkArgs, { cwd: state.directory })
312-
if (check.code === 0) {
313-
const ignored = new Set(check.text.trim().split("\n").filter(Boolean))
314-
const filtered = files.filter((item) => !ignored.has(item))
315-
return {
316-
hash,
317-
files: filtered.map((x) => path.join(state.worktree, x).replaceAll("\\", "/")),
318-
}
319-
}
320-
}
339+
// Hide ignored-file removals from the user-facing patch output.
340+
const ignored = yield* ignore(files)
321341

322342
return {
323343
hash,
324-
files: files.map((x) => path.join(state.worktree, x).replaceAll("\\", "/")),
344+
files: files
345+
.filter((item) => !ignored.has(item))
346+
.map((x) => path.join(state.worktree, x).replaceAll("\\", "/")),
325347
}
326348
}),
327349
)
@@ -672,27 +694,12 @@ export namespace Snapshot {
672694
]
673695
})
674696

675-
// Filter out files that are now gitignored
676-
if (rows.length > 0) {
677-
const files = rows.map((r) => r.file)
678-
const checkArgs = [
679-
...quote,
680-
"--git-dir",
681-
path.join(state.worktree, ".git"),
682-
"--work-tree",
683-
state.worktree,
684-
"check-ignore",
685-
"--no-index",
686-
"--",
687-
...files,
688-
]
689-
const check = yield* git(checkArgs, { cwd: state.directory })
690-
if (check.code === 0) {
691-
const ignored = new Set(check.text.trim().split("\n").filter(Boolean))
692-
const filtered = rows.filter((r) => !ignored.has(r.file))
693-
rows.length = 0
694-
rows.push(...filtered)
695-
}
697+
// Hide ignored-file removals from the user-facing diff output.
698+
const ignored = yield* ignore(rows.map((r) => r.file))
699+
if (ignored.size > 0) {
700+
const filtered = rows.filter((r) => !ignored.has(r.file))
701+
rows.length = 0
702+
rows.push(...filtered)
696703
}
697704

698705
const step = 100

0 commit comments

Comments
 (0)