mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-15 12:41:37 +03:00
refactor(core): share git command and patch helpers (#61168)
This commit is contained in:
committed by
GitHub
Unverified
parent
a2ccb66ce4
commit
bec6c3e925
+49
-53
@@ -329,15 +329,9 @@ const layer = Layer.effect(
|
||||
directory: AbsolutePath,
|
||||
args: string[],
|
||||
) {
|
||||
const result = yield* execute(directory, proc, args).pipe(
|
||||
Effect.mapError((cause) => new OperationError({ operation, directory, message: cause.message, cause })),
|
||||
)
|
||||
const result = yield* execute(directory, proc, args).pipe(Effect.mapError(spawnFailure(operation, directory)))
|
||||
if (result.exitCode === 0) return
|
||||
return yield* new OperationError({
|
||||
operation,
|
||||
directory,
|
||||
message: result.stderr.trim() || result.text.trim() || `Git ${operation} failed`,
|
||||
})
|
||||
return yield* commandFailure(operation, directory, result)
|
||||
})
|
||||
|
||||
const clone = Effect.fn("Git.repo.clone")(function* (input: {
|
||||
@@ -403,13 +397,13 @@ const layer = Layer.effect(
|
||||
...args,
|
||||
]
|
||||
|
||||
const repositoryOperation = Effect.fnUntraced(function* (
|
||||
const repositoryRun = (
|
||||
operationName: OperationError["operation"],
|
||||
repository: Repository,
|
||||
args: string[],
|
||||
options?: { stdin?: string | Uint8Array; env?: Record<string, string>; maxOutputBytes?: number; index?: string },
|
||||
) {
|
||||
const result = yield* proc
|
||||
options?: RepositoryOptions,
|
||||
) =>
|
||||
proc
|
||||
.run(
|
||||
ChildProcess.make(gitExecutable(), repositoryArgs(repository, args), {
|
||||
cwd: repository.worktree,
|
||||
@@ -418,25 +412,19 @@ const layer = Layer.effect(
|
||||
}),
|
||||
{ stdin: options?.stdin, maxOutputBytes: options?.maxOutputBytes },
|
||||
)
|
||||
.pipe(
|
||||
Effect.mapError(
|
||||
(cause) =>
|
||||
new OperationError({
|
||||
operation: operationName,
|
||||
directory: repository.worktree,
|
||||
message: cause.message,
|
||||
cause,
|
||||
}),
|
||||
),
|
||||
)
|
||||
.pipe(Effect.mapError(spawnFailure(operationName, repository.worktree)))
|
||||
|
||||
const repositoryOperation = Effect.fnUntraced(function* (
|
||||
operationName: OperationError["operation"],
|
||||
repository: Repository,
|
||||
args: string[],
|
||||
options?: RepositoryOptions,
|
||||
) {
|
||||
const result = yield* repositoryRun(operationName, repository, args, options)
|
||||
const text = result.stdout.toString("utf8")
|
||||
if (result.exitCode === 0)
|
||||
return { text, stderr: result.stderr.toString("utf8"), truncated: result.stdoutTruncated }
|
||||
return yield* new OperationError({
|
||||
operation: operationName,
|
||||
directory: repository.worktree,
|
||||
message: result.stderr.toString("utf8").trim() || text.trim() || `Git ${operationName} failed`,
|
||||
})
|
||||
const stderr = result.stderr.toString("utf8")
|
||||
if (result.exitCode === 0) return { text, stderr, truncated: result.stdoutTruncated }
|
||||
return yield* commandFailure(operationName, repository.worktree, { text, stderr })
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -637,29 +625,13 @@ const layer = Layer.effect(
|
||||
paths: readonly RelativePath[]
|
||||
}) {
|
||||
if (!input.paths.length) return new Set<RelativePath>()
|
||||
const result = yield* proc
|
||||
.run(
|
||||
ChildProcess.make(
|
||||
gitExecutable(),
|
||||
repositoryArgs(input.repository, ["check-ignore", "--no-index", "--stdin", "-z"]),
|
||||
{
|
||||
cwd: input.repository.worktree,
|
||||
extendEnv: true,
|
||||
},
|
||||
),
|
||||
{ stdin: input.paths.join("\0") + "\0" },
|
||||
)
|
||||
.pipe(
|
||||
Effect.mapError(
|
||||
(cause) =>
|
||||
new OperationError({
|
||||
operation: "list_files",
|
||||
directory: input.repository.worktree,
|
||||
message: cause.message,
|
||||
cause,
|
||||
}),
|
||||
),
|
||||
)
|
||||
const result = yield* repositoryRun(
|
||||
"list_files",
|
||||
input.repository,
|
||||
["check-ignore", "--no-index", "--stdin", "-z"],
|
||||
{ stdin: input.paths.join("\0") + "\0" },
|
||||
)
|
||||
// Exit 1: none of the paths are ignored.
|
||||
if (result.exitCode !== 0 && result.exitCode !== 1)
|
||||
return yield* new OperationError({
|
||||
operation: "list_files",
|
||||
@@ -1507,6 +1479,30 @@ interface Result {
|
||||
readonly stderr: string
|
||||
}
|
||||
|
||||
interface RepositoryOptions {
|
||||
readonly stdin?: string | Uint8Array
|
||||
readonly env?: Record<string, string>
|
||||
readonly maxOutputBytes?: number
|
||||
readonly index?: string
|
||||
}
|
||||
|
||||
function spawnFailure(operation: OperationError["operation"], directory: AbsolutePath) {
|
||||
return (cause: { readonly message: string }) =>
|
||||
new OperationError({ operation, directory, message: cause.message, cause })
|
||||
}
|
||||
|
||||
function commandFailure(
|
||||
operation: OperationError["operation"],
|
||||
directory: AbsolutePath,
|
||||
result: { readonly text: string; readonly stderr: string },
|
||||
) {
|
||||
return new OperationError({
|
||||
operation,
|
||||
directory,
|
||||
message: result.stderr.trim() || result.text.trim() || `Git ${operation} failed`,
|
||||
})
|
||||
}
|
||||
|
||||
function run(cwd: string, proc: AppProcess.Interface, args: string[]) {
|
||||
return execute(cwd, proc, args).pipe(Effect.orElseSucceed(() => ({ exitCode: 1, text: "", stderr: "" })))
|
||||
}
|
||||
|
||||
@@ -196,19 +196,14 @@ const numstat = (text: string) =>
|
||||
if (a === -1 || b === -1) return []
|
||||
const file = item.slice(b + 1)
|
||||
if (!file) return []
|
||||
const adds = item.slice(0, a)
|
||||
const dels = item.slice(a + 1, b)
|
||||
const additions = adds === "-" ? 0 : Number.parseInt(adds || "0", 10)
|
||||
const deletions = dels === "-" ? 0 : Number.parseInt(dels || "0", 10)
|
||||
return [
|
||||
{
|
||||
file,
|
||||
additions: Number.isFinite(additions) ? additions : 0,
|
||||
deletions: Number.isFinite(deletions) ? deletions : 0,
|
||||
} satisfies Stat,
|
||||
]
|
||||
return [{ file, additions: count(item.slice(0, a)), deletions: count(item.slice(a + 1, b)) } satisfies Stat]
|
||||
})
|
||||
|
||||
const count = (value: string) => {
|
||||
const parsed = value === "-" ? 0 : Number.parseInt(value || "0", 10)
|
||||
return Number.isFinite(parsed) ? parsed : 0
|
||||
}
|
||||
|
||||
function makeGit(proc: AppProcess.Interface, env?: Record<string, string>) {
|
||||
const run = Effect.fnUntraced(
|
||||
function* (args: string[], opts: { cwd: string; maxOutputBytes?: number }) {
|
||||
@@ -413,40 +408,40 @@ function makeGit(proc: AppProcess.Interface, env?: Record<string, string>) {
|
||||
)
|
||||
})
|
||||
|
||||
const patch = Effect.fn("VcsGit.patch")(function* (cwd: string, ref: string, file: string, options?: PatchOptions) {
|
||||
const diffPatch = Effect.fnUntraced(function* (
|
||||
cwd: string,
|
||||
args: string[],
|
||||
options: PatchOptions | undefined,
|
||||
mode: { dropTruncated: boolean; noIndex?: boolean },
|
||||
) {
|
||||
const result = yield* run(
|
||||
[
|
||||
"diff",
|
||||
...(mode.noIndex ? ["--no-index"] : []),
|
||||
"--patch",
|
||||
"--no-ext-diff",
|
||||
"--no-renames",
|
||||
`--unified=${options?.context ?? 3}`,
|
||||
ref,
|
||||
...(options?.target ? [options.target] : []),
|
||||
"--",
|
||||
file,
|
||||
...args,
|
||||
],
|
||||
{ cwd, maxOutputBytes: options?.maxOutputBytes },
|
||||
)
|
||||
return { text: result.truncated ? "" : result.text(), truncated: result.truncated } satisfies Patch
|
||||
return {
|
||||
text: mode.dropTruncated && result.truncated ? "" : result.text(),
|
||||
truncated: result.truncated,
|
||||
} satisfies Patch
|
||||
})
|
||||
|
||||
const patch = Effect.fn("VcsGit.patch")(function* (cwd: string, ref: string, file: string, options?: PatchOptions) {
|
||||
return yield* diffPatch(cwd, [ref, ...(options?.target ? [options.target] : []), "--", file], options, {
|
||||
dropTruncated: true,
|
||||
})
|
||||
})
|
||||
|
||||
const patchAll = Effect.fn("VcsGit.patchAll")(function* (cwd: string, ref: string, options?: PatchOptions) {
|
||||
const result = yield* run(
|
||||
[
|
||||
"diff",
|
||||
"--patch",
|
||||
"--no-ext-diff",
|
||||
"--no-renames",
|
||||
`--unified=${options?.context ?? 3}`,
|
||||
ref,
|
||||
...(options?.target ? [options.target] : []),
|
||||
"--",
|
||||
".",
|
||||
],
|
||||
{ cwd, maxOutputBytes: options?.maxOutputBytes },
|
||||
)
|
||||
return { text: result.text(), truncated: result.truncated } satisfies Patch
|
||||
return yield* diffPatch(cwd, [ref, ...(options?.target ? [options.target] : []), "--", "."], options, {
|
||||
dropTruncated: false,
|
||||
})
|
||||
})
|
||||
|
||||
const patchUntracked = Effect.fn("VcsGit.patchUntracked")(function* (
|
||||
@@ -456,21 +451,7 @@ function makeGit(proc: AppProcess.Interface, env?: Record<string, string>) {
|
||||
) {
|
||||
// A folder entry has no patch of its own.
|
||||
if (file.endsWith("/")) return { text: "", truncated: false } satisfies Patch
|
||||
const result = yield* run(
|
||||
[
|
||||
"diff",
|
||||
"--no-index",
|
||||
"--patch",
|
||||
"--no-ext-diff",
|
||||
"--no-renames",
|
||||
`--unified=${options?.context ?? 3}`,
|
||||
"--",
|
||||
"/dev/null",
|
||||
file,
|
||||
],
|
||||
{ cwd, maxOutputBytes: options?.maxOutputBytes },
|
||||
)
|
||||
return { text: result.truncated ? "" : result.text(), truncated: result.truncated } satisfies Patch
|
||||
return yield* diffPatch(cwd, ["--", "/dev/null", file], options, { dropTruncated: true, noIndex: true })
|
||||
})
|
||||
|
||||
const statUntracked = Effect.fn("VcsGit.statUntracked")(function* (cwd: string, file: string) {
|
||||
@@ -484,13 +465,7 @@ function makeGit(proc: AppProcess.Interface, env?: Record<string, string>) {
|
||||
const parts = result.text().split("\t")
|
||||
if (parts.length < 2) return
|
||||
|
||||
const additions = parts[0] === "-" ? 0 : Number.parseInt(parts[0] || "0", 10)
|
||||
const deletions = parts[1] === "-" ? 0 : Number.parseInt(parts[1] || "0", 10)
|
||||
return {
|
||||
file,
|
||||
additions: Number.isFinite(additions) ? additions : 0,
|
||||
deletions: Number.isFinite(deletions) ? deletions : 0,
|
||||
} satisfies Stat
|
||||
return { file, additions: count(parts[0]), deletions: count(parts[1]) } satisfies Stat
|
||||
})
|
||||
|
||||
// Paths are literal so names containing pathspec magic such as `*` or `[` match only themselves.
|
||||
@@ -517,11 +492,7 @@ function makeGit(proc: AppProcess.Interface, env?: Record<string, string>) {
|
||||
})
|
||||
|
||||
const worktreePatch = Effect.fn("VcsGit.worktreePatch")(function* (cwd: string, options?: PatchOptions) {
|
||||
const result = yield* run(
|
||||
["diff", "--patch", "--no-ext-diff", "--no-renames", `--unified=${options?.context ?? 3}`],
|
||||
{ cwd, maxOutputBytes: options?.maxOutputBytes },
|
||||
)
|
||||
return { text: result.text(), truncated: result.truncated } satisfies Patch
|
||||
return yield* diffPatch(cwd, [], options, { dropTruncated: false })
|
||||
})
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user