Skip to content

Commit ce50f5c

Browse files
committed
core: preserve upstream Myers diff code to keep upstream merges clean
The previous commit deleted ~150 lines of upstream OpenCode diff code because the git-based path always runs first. Restoring those lines as dead code (behind an early-return kilocode_change block) keeps our diff from upstream minimal, so future OpenCode syncs to the hot diffFull code path don't conflict. No runtime behavior change — the git-based DiffFull path still short-circuits before the restored Myers loop executes.
1 parent a5946ab commit ce50f5c

2 files changed

Lines changed: 176 additions & 4 deletions

File tree

packages/opencode/src/file/index.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { AppFileSystem } from "@/filesystem"
55
import { Git } from "@/git"
66
import { Effect, Layer, Context } from "effect"
77
import * as Stream from "effect/Stream"
8+
import { formatPatch, structuredPatch } from "diff"
89
import { DiffFull } from "@/kilocode/snapshot/diff-full" // kilocode_change
910
import fuzzysort from "fuzzysort"
1011
import ignore from "ignore"
@@ -559,10 +560,19 @@ export namespace File {
559560
diff = yield* gitText(["-c", "core.fsmonitor=false", "diff", "--staged", "--", file])
560561
}
561562
if (diff.trim()) {
562-
// kilocode_change start — patch via git, never the JS Myers implementation
563+
// kilocode_change start — patch via git (DiffFull.file) instead of the JS Myers
564+
// implementation. Upstream structuredPatch branch below is kept as dead code so
565+
// our diff from upstream stays minimal and future merges don't conflict.
563566
const got = yield* DiffFull.file(gitText, file)
564567
if (got) return { type: "text" as const, content, patch: got.patch, diff: got.text }
568+
return { type: "text" as const, content }
565569
// kilocode_change end
570+
const original = yield* git.show(Instance.directory, "HEAD", file)
571+
const patch = structuredPatch(file, file, original, content, "old", "new", {
572+
context: Infinity,
573+
ignoreWhitespace: true,
574+
})
575+
return { type: "text" as const, content, patch, diff: formatPatch(patch) }
566576
}
567577
return { type: "text" as const, content }
568578
}

packages/opencode/src/snapshot/index.ts

Lines changed: 165 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Cause, Duration, Effect, Layer, Schedule, Semaphore, Context, Stream } from "effect"
22
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
3+
import { formatPatch, structuredPatch } from "diff"
34
import path from "path"
45
import z from "zod"
56
import { makeRuntime } from "@/effect/run-service" // kilocode_change
@@ -525,6 +526,144 @@ export namespace Snapshot {
525526
const diffFull = Effect.fnUntraced(function* (from: string, to: string) {
526527
return yield* locked(
527528
Effect.gen(function* () {
529+
type Row = {
530+
file: string
531+
status: "added" | "deleted" | "modified"
532+
binary: boolean
533+
additions: number
534+
deletions: number
535+
}
536+
537+
type Ref = {
538+
file: string
539+
side: "before" | "after"
540+
ref: string
541+
}
542+
543+
const show = Effect.fnUntraced(function* (row: Row) {
544+
if (row.binary) return ["", ""]
545+
if (row.status === "added") {
546+
return [
547+
"",
548+
yield* git([...cfg, ...args(["show", `${to}:${row.file}`])]).pipe(
549+
Effect.map((item) => item.text),
550+
),
551+
]
552+
}
553+
if (row.status === "deleted") {
554+
return [
555+
yield* git([...cfg, ...args(["show", `${from}:${row.file}`])]).pipe(
556+
Effect.map((item) => item.text),
557+
),
558+
"",
559+
]
560+
}
561+
return yield* Effect.all(
562+
[
563+
git([...cfg, ...args(["show", `${from}:${row.file}`])]).pipe(Effect.map((item) => item.text)),
564+
git([...cfg, ...args(["show", `${to}:${row.file}`])]).pipe(Effect.map((item) => item.text)),
565+
],
566+
{ concurrency: 2 },
567+
)
568+
})
569+
570+
const load = Effect.fnUntraced(
571+
function* (rows: Row[]) {
572+
const refs = rows.flatMap((row) => {
573+
if (row.binary) return []
574+
if (row.status === "added")
575+
return [{ file: row.file, side: "after", ref: `${to}:${row.file}` } satisfies Ref]
576+
if (row.status === "deleted") {
577+
return [{ file: row.file, side: "before", ref: `${from}:${row.file}` } satisfies Ref]
578+
}
579+
return [
580+
{ file: row.file, side: "before", ref: `${from}:${row.file}` } satisfies Ref,
581+
{ file: row.file, side: "after", ref: `${to}:${row.file}` } satisfies Ref,
582+
]
583+
})
584+
if (!refs.length) return new Map<string, { before: string; after: string }>()
585+
586+
const proc = ChildProcess.make("git", [...cfg, ...args(["cat-file", "--batch"])], {
587+
cwd: state.directory,
588+
extendEnv: true,
589+
stdin: Stream.make(new TextEncoder().encode(refs.map((item) => item.ref).join("\n") + "\n")),
590+
})
591+
const handle = yield* spawner.spawn(proc)
592+
const [out, err] = yield* Effect.all(
593+
[Stream.mkUint8Array(handle.stdout), Stream.mkString(Stream.decodeText(handle.stderr))],
594+
{ concurrency: 2 },
595+
)
596+
const code = yield* handle.exitCode
597+
if (code !== 0) {
598+
log.info("git cat-file --batch failed during snapshot diff, falling back to per-file git show", {
599+
stderr: err,
600+
refs: refs.length,
601+
})
602+
return
603+
}
604+
605+
const fail = (msg: string, extra?: Record<string, string>) => {
606+
log.info(msg, { ...extra, refs: refs.length })
607+
return undefined
608+
}
609+
610+
const map = new Map<string, { before: string; after: string }>()
611+
const dec = new TextDecoder()
612+
let i = 0
613+
for (const ref of refs) {
614+
let end = i
615+
while (end < out.length && out[end] !== 10) end += 1
616+
if (end >= out.length) {
617+
return fail(
618+
"git cat-file --batch returned a truncated header during snapshot diff, falling back to per-file git show",
619+
)
620+
}
621+
622+
const head = dec.decode(out.slice(i, end))
623+
i = end + 1
624+
const hit = map.get(ref.file) ?? { before: "", after: "" }
625+
if (head.endsWith(" missing")) {
626+
map.set(ref.file, hit)
627+
continue
628+
}
629+
630+
const match = head.match(/^[0-9a-f]+ blob (\d+)$/)
631+
if (!match) {
632+
return fail(
633+
"git cat-file --batch returned an unexpected header during snapshot diff, falling back to per-file git show",
634+
{ head },
635+
)
636+
}
637+
638+
const size = Number(match[1])
639+
if (!Number.isInteger(size) || size < 0 || i + size >= out.length || out[i + size] !== 10) {
640+
return fail(
641+
"git cat-file --batch returned truncated content during snapshot diff, falling back to per-file git show",
642+
{ head },
643+
)
644+
}
645+
646+
const text = dec.decode(out.slice(i, i + size))
647+
if (ref.side === "before") hit.before = text
648+
if (ref.side === "after") hit.after = text
649+
map.set(ref.file, hit)
650+
i += size + 1
651+
}
652+
653+
if (i !== out.length) {
654+
return fail(
655+
"git cat-file --batch returned trailing data during snapshot diff, falling back to per-file git show",
656+
)
657+
}
658+
659+
return map
660+
},
661+
Effect.scoped,
662+
Effect.catch(() =>
663+
Effect.succeed<Map<string, { before: string; after: string }> | undefined>(undefined),
664+
),
665+
)
666+
528667
const result: Snapshot.FileDiff[] = []
529668
const status = new Map<string, "added" | "deleted" | "modified">()
530669

@@ -564,7 +703,7 @@ export namespace Snapshot {
564703
binary,
565704
additions: Number.isFinite(additions) ? additions : 0,
566705
deletions: Number.isFinite(deletions) ? deletions : 0,
567-
},
706+
} satisfies Row,
568707
]
569708
})
570709

@@ -577,9 +716,14 @@ export namespace Snapshot {
577716
}
578717

579718
const step = 100
719+
const patch = (file: string, before: string, after: string) =>
720+
formatPatch(structuredPatch(file, file, before, after, "", "", { context: Number.MAX_SAFE_INTEGER }))
721+
722+
// kilocode_change start — route patches through git (DiffFull.batch) instead of the
723+
// JS Myers implementation. Upstream Myers loop below is kept as dead code so our
724+
// diff from upstream stays minimal and future merges don't conflict.
580725
for (let i = 0; i < rows.length; i += step) {
581726
const run = rows.slice(i, i + step)
582-
// kilocode_change start — bulk-load patches from `git diff`; no JS Myers fallback
583727
const patches = yield* DiffFull.batch(
584728
(cmd) => git([...quote, ...args(cmd)], { cwd: state.directory }),
585729
from,
@@ -595,7 +739,25 @@ export namespace Snapshot {
595739
status: row.status,
596740
})
597741
}
598-
// kilocode_change end
742+
}
743+
return result
744+
// kilocode_change end
745+
746+
for (let i = 0; i < rows.length; i += step) {
747+
const run = rows.slice(i, i + step)
748+
const text = yield* load(run)
749+
750+
for (const row of run) {
751+
const hit = text?.get(row.file) ?? { before: "", after: "" }
752+
const [before, after] = row.binary ? ["", ""] : text ? [hit.before, hit.after] : yield* show(row)
753+
result.push({
754+
file: row.file,
755+
patch: row.binary ? "" : patch(row.file, before, after),
756+
additions: row.additions,
757+
deletions: row.deletions,
758+
status: row.status,
759+
})
760+
}
599761
}
600762

601763
return result

0 commit comments

Comments
 (0)