Skip to content

Commit a8220d0

Browse files
Merge branch 'dev' into fix-env-caching-12698
2 parents 301e91c + d1f597b commit a8220d0

4 files changed

Lines changed: 332 additions & 65 deletions

File tree

packages/opencode/src/git/index.ts

Lines changed: 99 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const fail = (err: unknown) =>
2424
text: () => "",
2525
stdout: Buffer.alloc(0),
2626
stderr: Buffer.from(err instanceof Error ? err.message : String(err)),
27+
truncated: false,
2728
}) satisfies Result
2829

2930
export type Kind = "added" | "deleted" | "modified"
@@ -45,16 +46,28 @@ export type Stat = {
4546
readonly deletions: number
4647
}
4748

49+
export type Patch = {
50+
readonly text: string
51+
readonly truncated: boolean
52+
}
53+
54+
export interface PatchOptions {
55+
readonly context?: number
56+
readonly maxOutputBytes?: number
57+
}
58+
4859
export interface Result {
4960
readonly exitCode: number
5061
readonly text: () => string
5162
readonly stdout: Buffer
5263
readonly stderr: Buffer
64+
readonly truncated: boolean
5365
}
5466

5567
export interface Options {
5668
readonly cwd: string
5769
readonly env?: Record<string, string>
70+
readonly maxOutputBytes?: number
5871
}
5972

6073
export interface Interface {
@@ -68,6 +81,10 @@ export interface Interface {
6881
readonly status: (cwd: string) => Effect.Effect<Item[]>
6982
readonly diff: (cwd: string, ref: string) => Effect.Effect<Item[]>
7083
readonly stats: (cwd: string, ref: string) => Effect.Effect<Stat[]>
84+
readonly patch: (cwd: string, ref: string, file: string, options?: PatchOptions) => Effect.Effect<Patch>
85+
readonly patchAll: (cwd: string, ref: string, options?: PatchOptions) => Effect.Effect<Patch>
86+
readonly patchUntracked: (cwd: string, file: string, options?: PatchOptions) => Effect.Effect<Patch>
87+
readonly statUntracked: (cwd: string, file: string) => Effect.Effect<Stat | undefined>
7188
}
7289

7390
const kind = (code: string): Kind => {
@@ -96,15 +113,31 @@ export const layer = Layer.effect(
96113
stderr: "pipe",
97114
})
98115
const handle = yield* spawner.spawn(proc)
99-
const [stdout, stderr] = yield* Effect.all(
100-
[Stream.mkString(Stream.decodeText(handle.stdout)), Stream.mkString(Stream.decodeText(handle.stderr))],
101-
{ concurrency: 2 },
102-
)
116+
const collect = (stream: typeof handle.stdout) =>
117+
Stream.runFold(
118+
stream,
119+
() => ({ chunks: [] as Uint8Array[], bytes: 0, truncated: false }),
120+
(acc, chunk) => {
121+
if (opts.maxOutputBytes === undefined) {
122+
acc.chunks.push(chunk)
123+
acc.bytes += chunk.length
124+
return acc
125+
}
126+
127+
const remaining = opts.maxOutputBytes - acc.bytes
128+
if (remaining > 0) acc.chunks.push(remaining >= chunk.length ? chunk : chunk.slice(0, remaining))
129+
acc.bytes += chunk.length
130+
acc.truncated = acc.truncated || acc.bytes > opts.maxOutputBytes
131+
return acc
132+
},
133+
).pipe(Effect.map((x) => ({ buffer: Buffer.concat(x.chunks), truncated: x.truncated })))
134+
const [stdout, stderr] = yield* Effect.all([collect(handle.stdout), collect(handle.stderr)], { concurrency: 2 })
103135
return {
104136
exitCode: yield* handle.exitCode,
105-
text: () => stdout,
106-
stdout: Buffer.from(stdout),
107-
stderr: Buffer.from(stderr),
137+
text: () => stdout.buffer.toString("utf8"),
138+
stdout: stdout.buffer,
139+
stderr: stderr.buffer,
140+
truncated: stdout.truncated || stderr.truncated,
108141
} satisfies Result
109142
},
110143
Effect.scoped,
@@ -240,6 +273,61 @@ export const layer = Layer.effect(
240273
})
241274
})
242275

276+
const patch = Effect.fn("Git.patch")(function* (cwd: string, ref: string, file: string, options?: PatchOptions) {
277+
const result = yield* run(
278+
["diff", "--patch", "--no-ext-diff", "--no-renames", `--unified=${options?.context ?? 3}`, ref, "--", file],
279+
{ cwd, maxOutputBytes: options?.maxOutputBytes },
280+
)
281+
return { text: result.truncated ? "" : result.text(), truncated: result.truncated } satisfies Patch
282+
})
283+
284+
const patchAll = Effect.fn("Git.patchAll")(function* (cwd: string, ref: string, options?: PatchOptions) {
285+
const result = yield* run(
286+
["diff", "--patch", "--no-ext-diff", "--no-renames", `--unified=${options?.context ?? 3}`, ref, "--", "."],
287+
{ cwd, maxOutputBytes: options?.maxOutputBytes },
288+
)
289+
return { text: result.text(), truncated: result.truncated } satisfies Patch
290+
})
291+
292+
const patchUntracked = Effect.fn("Git.patchUntracked")(function* (
293+
cwd: string,
294+
file: string,
295+
options?: PatchOptions,
296+
) {
297+
const result = yield* run(
298+
[
299+
"diff",
300+
"--no-index",
301+
"--patch",
302+
"--no-ext-diff",
303+
"--no-renames",
304+
`--unified=${options?.context ?? 3}`,
305+
"--",
306+
"/dev/null",
307+
file,
308+
],
309+
{ cwd, maxOutputBytes: options?.maxOutputBytes },
310+
)
311+
return { text: result.truncated ? "" : result.text(), truncated: result.truncated } satisfies Patch
312+
})
313+
314+
const statUntracked = Effect.fn("Git.statUntracked")(function* (cwd: string, file: string) {
315+
const result = yield* run(["diff", "--no-index", "--numstat", "--", "/dev/null", file], {
316+
cwd,
317+
maxOutputBytes: 4096,
318+
})
319+
if (result.truncated) return
320+
const parts = result.text().split("\t")
321+
if (parts.length < 2) return
322+
const additions = parts[0] === "-" ? 0 : Number.parseInt(parts[0] || "0", 10)
323+
const deletions = parts[1] === "-" ? 0 : Number.parseInt(parts[1] || "0", 10)
324+
return {
325+
file,
326+
additions: Number.isFinite(additions) ? additions : 0,
327+
deletions: Number.isFinite(deletions) ? deletions : 0,
328+
} satisfies Stat
329+
})
330+
243331
return Service.of({
244332
run,
245333
branch,
@@ -251,6 +339,10 @@ export const layer = Layer.effect(
251339
status,
252340
diff,
253341
stats,
342+
patch,
343+
patchAll,
344+
patchUntracked,
345+
statUntracked,
254346
})
255347
}),
256348
)

0 commit comments

Comments
 (0)