diff --git a/packages/core/src/git.ts b/packages/core/src/git.ts index 1778879face..1c387412e15 100644 --- a/packages/core/src/git.ts +++ b/packages/core/src/git.ts @@ -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; 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() - 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 + 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: "" }))) } diff --git a/packages/core/src/plugin/vcs/git.ts b/packages/core/src/plugin/vcs/git.ts index 0b0d8d72144..a2ededa7ecc 100644 --- a/packages/core/src/plugin/vcs/git.ts +++ b/packages/core/src/plugin/vcs/git.ts @@ -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) { const run = Effect.fnUntraced( function* (args: string[], opts: { cwd: string; maxOutputBytes?: number }) { @@ -413,40 +408,40 @@ function makeGit(proc: AppProcess.Interface, env?: Record) { ) }) - 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) { ) { // 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) { 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) { }) 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 {