Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/opencode/src/session/summary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Identifier } from "@/id/id"
import { Snapshot } from "@/snapshot"

import { Log } from "@/util/log"
import { decodeGitQuotepath } from "@opencode-ai/util/encode"
import path from "path"
import { Instance } from "@/project/instance"
import { Storage } from "@/storage/storage"
Expand Down Expand Up @@ -40,7 +41,7 @@ export namespace SessionSummary {
.flatMap((x) => x.parts)
.filter((x) => x.type === "patch")
.flatMap((x) => x.files)
.map((x) => path.relative(Instance.worktree, x)),
.map((x) => decodeGitQuotepath(path.relative(Instance.worktree, x))),
)
const diffs = await computeDiff({ messages: input.messages }).then((x) =>
x.filter((x) => {
Expand Down
54 changes: 39 additions & 15 deletions packages/opencode/src/snapshot/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Global } from "../global"
import z from "zod"
import { Config } from "../config/config"
import { Instance } from "../project/instance"
import { decodeGitQuotepath } from "@opencode-ai/util/encode"

export namespace Snapshot {
const log = Log.create({ service: "snapshot" })
Expand Down Expand Up @@ -159,26 +160,49 @@ export namespace Snapshot {
export async function diffFull(from: string, to: string): Promise<FileDiff[]> {
const git = gitdir()
const result: FileDiff[] = []
for await (const line of $`git -c core.autocrlf=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff --no-renames --numstat ${from} ${to} -- .`

// Use env for diff cmd just in case
const diffCmd = $`git -c core.autocrlf=false diff --no-ext-diff --no-renames --numstat ${from} ${to} -- .`
.env({
...process.env,
GIT_DIR: git,
GIT_WORK_TREE: Instance.worktree,
})
.quiet()
.cwd(Instance.directory)
.nothrow()
.lines()) {

const readFile = async (ref: string, file: string) => {
const proc = Bun.spawn(["git", "-c", "core.autocrlf=false", "show", `${ref}:${file}`], {
env: {
...process.env,
GIT_DIR: git,
GIT_WORK_TREE: Instance.worktree,
},
cwd: Instance.directory,
stderr: "pipe",
stdout: "pipe",
})
const output = await new Response(proc.stdout).text().catch(() => "")
const exitCode = await proc.exited.catch(() => 1)
if (exitCode !== 0) return ""
return output
}

for await (const line of diffCmd.lines()) {
if (!line) continue
const [additions, deletions, file] = line.split("\t")
const parts = line.split("\t")
const additions = parts[0]
const deletions = parts[1]
const rawFile = parts.slice(2).join("\t")

if (!rawFile) continue

const file = decodeGitQuotepath(rawFile)
const isBinaryFile = additions === "-" && deletions === "-"
const before = isBinaryFile
? ""
: await $`git -c core.autocrlf=false --git-dir ${git} --work-tree ${Instance.worktree} show ${from}:${file}`
.quiet()
.nothrow()
.text()
const after = isBinaryFile
? ""
: await $`git -c core.autocrlf=false --git-dir ${git} --work-tree ${Instance.worktree} show ${to}:${file}`
.quiet()
.nothrow()
.text()
const before = isBinaryFile ? "" : await readFile(from, file)
const after = isBinaryFile ? "" : await readFile(to, file)

const added = isBinaryFile ? 0 : parseInt(additions)
const deleted = isBinaryFile ? 0 : parseInt(deletions)
result.push({
Expand Down
37 changes: 37 additions & 0 deletions packages/util/src/encode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,40 @@ export function checksum(content: string): string | undefined {
}
return (hash >>> 0).toString(36)
}

export function decodeGitQuotepath(value: string): string {
if (!value.startsWith('"') || !value.endsWith('"')) return value
const input = value.slice(1, -1)

const append = (bytes: number[], value: number) => bytes.concat(value)

const parse = (index: number, bytes: number[]): number[] => {
if (index >= input.length) return bytes
const char = input[index]
if (char !== "\\") {
return parse(index + 1, append(bytes, input.charCodeAt(index)))
}

const rest = input.slice(index + 1)
const octalMatch = rest.match(/^([0-7]{1,3})/)
if (octalMatch) {
return parse(index + 1 + octalMatch[1].length, append(bytes, parseInt(octalMatch[1], 8)))
}

const next = input[index + 1]
if (!next) return parse(index + 1, bytes)

const decoded = (() => {
if (next === "\\") return 92
if (next === '"') return 34
if (next === "n") return 10
if (next === "t") return 9
if (next === "r") return 13
return next.charCodeAt(0)
})()
return parse(index + 2, append(bytes, decoded))
}

const bytes = parse(0, [])
return new TextDecoder().decode(new Uint8Array(bytes))
}