authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 11:53:53-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 11:53:53-07:00
log4238a0e769c9f149d4083968823298ace1e2bd0b
tree6f8c5e292a314581aa03be18f177595a7e464f0e
parentff0f702c7cb5006acf72b63edf5c0df7686f60c4
signature Signed by PGP key B5690EEEBB952194

[codex] Enrich Git VCS driver errors (#3253)

Co-authored-by: codex <codex@users.noreply.github.com>

5 files changed, 407 insertions(+), 222 deletions(-)

apps/server/src/git/GitManager.test.ts+6-1
...@@ -3377,13 +3377,18 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -3377,13 +3377,18 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
3377 Effect.map((error) => error.message),3377 Effect.map((error) => error.message),
3378 );3378 );
33793379
3380 expect(errorMessage).toContain("hook: fail");3380 expect(errorMessage).toContain("Git command failed in GitVcsDriver.commit.commit");
3381 expect(errorMessage).not.toContain("hook: fail");
3381 expect(events).toEqual(3382 expect(events).toEqual(
3382 expect.arrayContaining([3383 expect.arrayContaining([
3383 expect.objectContaining({3384 expect.objectContaining({
3384 kind: "hook_started",3385 kind: "hook_started",
3385 hookName: "pre-commit",3386 hookName: "pre-commit",
3386 }),3387 }),
3388 expect.objectContaining({
3389 kind: "hook_output",
3390 text: "hook: fail",
3391 }),
3387 expect.objectContaining({3392 expect.objectContaining({
3388 kind: "action_failed",3393 kind: "action_failed",
3389 phase: "commit",3394 phase: "commit",
apps/server/src/vcs/GitVcsDriverCore.test.ts+108
...@@ -78,6 +78,114 @@ const initRepoWithCommit = (...@@ -78,6 +78,114 @@ const initRepoWithCommit = (
78 });78 });
7979
80it.layer(TestLayer)("GitVcsDriver core integration", (it) => {80it.layer(TestLayer)("GitVcsDriver core integration", (it) => {
81 describe("structured errors", () => {
82 it.effect("preserves structured spawn context and the platform cause", () =>
83 Effect.gen(function* () {
84 const parent = yield* makeTmpDir();
85 const pathService = yield* Path.Path;
86 const cwd = pathService.join(parent, "missing");
87 const driver = yield* GitVcsDriver.GitVcsDriver;
88
89 const error = yield* driver
90 .execute({
91 operation: "GitVcsDriver.test.missingCwd",
92 cwd,
93 args: ["status", "--short"],
94 })
95 .pipe(Effect.flip);
96
97 assert.deepInclude(error, {
98 _tag: "GitCommandError",
99 operation: "GitVcsDriver.test.missingCwd",
100 command: "git",
101 argumentCount: 2,
102 cwd,
103 detail: "Failed to spawn Git process.",
104 });
105 if (!(error.cause instanceof PlatformError.PlatformError)) {
106 return assert.fail("expected the original platform error cause");
107 }
108 assert.equal(error.cause.reason._tag, "NotFound");
109 assert.notInclude(error.detail, error.cause.message);
110 }),
111 );
112
113 it.effect("does not retain git arguments or stderr in command failures", () =>
114 Effect.gen(function* () {
115 const cwd = yield* makeTmpDir();
116 const driver = yield* GitVcsDriver.GitVcsDriver;
117 yield* driver.initRepo({ cwd });
118
119 const secret = "secret-token-value";
120 const error = yield* driver
121 .execute({
122 operation: "GitVcsDriver.test.redactedFailure",
123 cwd,
124 args: ["status", `--unknown-option=${secret}`],
125 })
126 .pipe(Effect.flip);
127
128 assert.deepInclude(error, {
129 _tag: "GitCommandError",
130 operation: "GitVcsDriver.test.redactedFailure",
131 command: "git",
132 argumentCount: 2,
133 cwd,
134 });
135 assert.isNumber(error.exitCode);
136 assert.isAbove(error.stderrLength ?? 0, 0);
137 assert.notInclude(error.detail, secret);
138 assert.notInclude(error.message, secret);
139 assert.notProperty(error, "args");
140 assert.notProperty(error, "stderr");
141 }),
142 );
143
144 it.effect("recovers a structurally identified missing cwd as a non-repository", () =>
145 Effect.gen(function* () {
146 const parent = yield* makeTmpDir();
147 const pathService = yield* Path.Path;
148 const cwd = pathService.join(parent, "missing");
149 const driver = yield* GitVcsDriver.GitVcsDriver;
150
151 const [localStatus, remoteStatus, refs] = yield* Effect.all([
152 driver.statusDetails(cwd),
153 driver.statusDetailsRemote(cwd, { refreshUpstream: false }),
154 driver.listRefs({ cwd }),
155 ]);
156
157 assert.equal(localStatus.isRepo, false);
158 assert.equal(remoteStatus.isRepo, false);
159 assert.equal(refs.isRepo, false);
160 assert.deepStrictEqual(refs.refs, []);
161 }),
162 );
163
164 it.effect("does not wrap a remove-worktree command failure in a synthetic error", () =>
165 Effect.gen(function* () {
166 const cwd = yield* makeTmpDir();
167 const pathService = yield* Path.Path;
168 const missingWorktree = pathService.join(cwd, "missing-worktree");
169 const driver = yield* GitVcsDriver.GitVcsDriver;
170 yield* driver.initRepo({ cwd });
171
172 const error = yield* driver
173 .removeWorktree({ cwd, path: missingWorktree })
174 .pipe(Effect.flip);
175
176 assert.deepInclude(error, {
177 _tag: "GitCommandError",
178 operation: "GitVcsDriver.removeWorktree",
179 command: "git",
180 argumentCount: 3,
181 cwd,
182 });
183 assert.notProperty(error, "cause");
184 assert.notInclude(error.detail, "Git command failed in");
185 }),
186 );
187 });
188
81 describe("review diff previews", () => {189 describe("review diff previews", () => {
82 it.effect("drops an unterminated path from truncated NUL-separated git output", () =>190 it.effect("drops an unterminated path from truncated NUL-separated git output", () =>
83 Effect.sync(() => {191 Effect.sync(() => {
apps/server/src/vcs/GitVcsDriverCore.ts+274-205
...@@ -36,7 +36,6 @@ import {...@@ -36,7 +36,6 @@ import {
36 parseRemoteRefWithRemoteNames,36 parseRemoteRefWithRemoteNames,
37} from "../git/remoteRefs.ts";37} from "../git/remoteRefs.ts";
38import { ServerConfig } from "../config.ts";38import { ServerConfig } from "../config.ts";
39const isGitCommandError = Schema.is(GitCommandError);
4039
41const DEFAULT_TIMEOUT_MS = 30_000;40const DEFAULT_TIMEOUT_MS = 30_000;
42const DEFAULT_MAX_OUTPUT_BYTES = 1_000_000;41const DEFAULT_MAX_OUTPUT_BYTES = 1_000_000;
...@@ -100,7 +99,7 @@ interface ExecuteGitOptions {...@@ -100,7 +99,7 @@ interface ExecuteGitOptions {
100 stdin?: string | undefined;99 stdin?: string | undefined;
101 timeoutMs?: number | undefined;100 timeoutMs?: number | undefined;
102 allowNonZeroExit?: boolean | undefined;101 allowNonZeroExit?: boolean | undefined;
103 fallbackErrorMessage?: string | undefined;102 fallbackErrorDetail?: string | undefined;
104 env?: NodeJS.ProcessEnv | undefined;103 env?: NodeJS.ProcessEnv | undefined;
105 maxOutputBytes?: number | undefined;104 maxOutputBytes?: number | undefined;
106 appendTruncationMarker?: boolean | undefined;105 appendTruncationMarker?: boolean | undefined;
...@@ -326,8 +325,15 @@ function deriveLocalBranchNameFromRemoteRef(branchName: string): string | null {...@@ -326,8 +325,15 @@ function deriveLocalBranchNameFromRemoteRef(branchName: string): string | null {
326 return localBranch.length > 0 ? localBranch : null;325 return localBranch.length > 0 ? localBranch : null;
327}326}
328327
329function commandLabel(args: readonly string[]): string {328function gitCommandContext(
330 return `git ${args.join(" ")}`;329 input: Pick<GitVcsDriver.ExecuteGitInput, "operation" | "cwd" | "args">,
330) {
331 return {
332 operation: input.operation,
333 command: "git",
334 cwd: input.cwd,
335 argumentCount: input.args.length,
336 } as const;
331}337}
332338
333function parseDefaultBranchFromRemoteHeadRef(value: string, remoteName: string): string | null {339function parseDefaultBranchFromRemoteHeadRef(value: string, remoteName: string): string | null {
...@@ -340,50 +346,28 @@ function parseDefaultBranchFromRemoteHeadRef(value: string, remoteName: string):...@@ -340,50 +346,28 @@ function parseDefaultBranchFromRemoteHeadRef(value: string, remoteName: string):
340 return refName.length > 0 ? refName : null;346 return refName.length > 0 ? refName : null;
341}347}
342348
343function createGitCommandError(349function isMissingGitCwdError(error: GitCommandError): boolean {
344 operation: string,350 if (!(error.cause instanceof PlatformError.PlatformError)) {
345 cwd: string,351 return false;
346 args: readonly string[],352 }
347 detail: string,
348 cause?: unknown,
349): GitCommandError {
350 return new GitCommandError({
351 operation,
352 command: commandLabel(args),
353 cwd,
354 detail,
355 ...(cause !== undefined ? { cause } : {}),
356 });
357}
358353
359function quoteGitCommand(args: ReadonlyArray<string>): string {354 const reason = error.cause.reason;
360 return `git ${args.join(" ")}`;355 if (reason._tag === "NotFound") {
361}356 return reason.pathOrDescriptor === error.cwd;
357 }
362358
363function isMissingGitCwdError(error: GitCommandError): boolean {
364 const normalized = `${error.detail}\n${error.message}`.toLowerCase();
365 return (359 return (
366 normalized.includes("no such file or directory") ||360 reason._tag === "BadResource" &&
367 normalized.includes("notfound: filesystem.access") ||361 reason.pathOrDescriptor === error.cwd &&
368 normalized.includes("enoent") ||362 typeof reason.cause === "object" &&
369 normalized.includes("not a directory")363 reason.cause !== null &&
364 "code" in reason.cause &&
365 reason.cause.code === "ENOTDIR"
370 );366 );
371}367}
372368
373function toGitCommandError(369function isNonRepositoryGitStderr(stderr: string): boolean {
374 input: Pick<GitVcsDriver.ExecuteGitInput, "operation" | "cwd" | "args">,370 return stderr.toLowerCase().includes("not a git repository");
375 detail: string,
376) {
377 return (cause: unknown) =>
378 isGitCommandError(cause)
379 ? cause
380 : new GitCommandError({
381 operation: input.operation,
382 command: quoteGitCommand(input.args),
383 cwd: input.cwd,
384 detail: `${cause instanceof Error && cause.message.length > 0 ? cause.message : "Unknown error"} - ${detail}`,
385 ...(cause !== undefined ? { cause } : {}),
386 });
387}371}
388372
389interface Trace2Monitor {373interface Trace2Monitor {
...@@ -402,7 +386,11 @@ const addCurrentSpanEvent = (name: string, attributes: Record<string, unknown>)...@@ -402,7 +386,11 @@ const addCurrentSpanEvent = (name: string, attributes: Record<string, unknown>)
402 yield* Effect.sync(() => {386 yield* Effect.sync(() => {
403 span.event(name, timestamp, compactTraceAttributes(attributes));387 span.event(name, timestamp, compactTraceAttributes(attributes));
404 });388 });
405 }).pipe(Effect.catch(() => Effect.void));389 }).pipe(
390 Effect.catchTags({
391 NoSuchElementError: () => Effect.void,
392 }),
393 );
406394
407function trace2ChildKey(record: Record<string, unknown>): string | null {395function trace2ChildKey(record: Record<string, unknown>): string | null {
408 const childId = record.child_id;396 const childId = record.child_id;
...@@ -451,7 +439,7 @@ const createTrace2Monitor = Effect.fn("createTrace2Monitor")(function* (...@@ -451,7 +439,7 @@ const createTrace2Monitor = Effect.fn("createTrace2Monitor")(function* (
451 const traceRecord = decodeJsonResult(Trace2Record)(trimmedLine);439 const traceRecord = decodeJsonResult(Trace2Record)(trimmedLine);
452 if (Result.isFailure(traceRecord)) {440 if (Result.isFailure(traceRecord)) {
453 yield* Effect.logDebug(441 yield* Effect.logDebug(
454 `GitVcsDriver.trace2: failed to parse trace line for ${quoteGitCommand(input.args)} in ${input.cwd}`,442 `GitVcsDriver.trace2: failed to parse trace line for ${input.operation} in ${input.cwd} (${input.args.length} arguments)`,
455 traceRecord.failure,443 traceRecord.failure,
456 );444 );
457 return;445 return;
...@@ -574,9 +562,9 @@ const createTrace2Monitor = Effect.fn("createTrace2Monitor")(function* (...@@ -574,9 +562,9 @@ const createTrace2Monitor = Effect.fn("createTrace2Monitor")(function* (
574 };562 };
575});563});
576564
577const collectOutput = Effect.fnUntraced(function* <E>(565const collectOutput = Effect.fnUntraced(function* (
578 input: Pick<GitVcsDriver.ExecuteGitInput, "operation" | "cwd" | "args">,566 input: Pick<GitVcsDriver.ExecuteGitInput, "operation" | "cwd" | "args">,
579 stream: Stream.Stream<Uint8Array, E>,567 stream: Stream.Stream<Uint8Array, PlatformError.PlatformError>,
580 maxOutputBytes: number,568 maxOutputBytes: number,
581 appendTruncationMarker: boolean,569 appendTruncationMarker: boolean,
582 onLine: ((line: string) => Effect.Effect<void, never>) | undefined,570 onLine: ((line: string) => Effect.Effect<void, never>) | undefined,
...@@ -614,10 +602,9 @@ const collectOutput = Effect.fnUntraced(function* <E>(...@@ -614,10 +602,9 @@ const collectOutput = Effect.fnUntraced(function* <E>(
614 const nextBytes = bytes + chunk.byteLength;602 const nextBytes = bytes + chunk.byteLength;
615 if (!appendTruncationMarker && nextBytes > maxOutputBytes) {603 if (!appendTruncationMarker && nextBytes > maxOutputBytes) {
616 return yield* new GitCommandError({604 return yield* new GitCommandError({
617 operation: input.operation,605 ...gitCommandContext(input),
618 command: quoteGitCommand(input.args),606 detail: `Git output exceeded ${maxOutputBytes} bytes and was truncated.`,
619 cwd: input.cwd,607 outputLength: nextBytes,
620 detail: `${quoteGitCommand(input.args)} output exceeded ${maxOutputBytes} bytes and was truncated.`,
621 });608 });
622 }609 }
623610
...@@ -635,7 +622,14 @@ const collectOutput = Effect.fnUntraced(function* <E>(...@@ -635,7 +622,14 @@ const collectOutput = Effect.fnUntraced(function* <E>(
635 });622 });
636623
637 yield* Stream.runForEach(stream, processChunk).pipe(624 yield* Stream.runForEach(stream, processChunk).pipe(
638 Effect.mapError(toGitCommandError(input, "output stream failed.")),625 Effect.catchTags({
626 PlatformError: (cause) =>
627 new GitCommandError({
628 ...gitCommandContext(input),
629 detail: "Failed to read Git process output.",
630 cause,
631 }),
632 }),
639 );633 );
640634
641 const remainder = truncated ? "" : decoder.decode();635 const remainder = truncated ? "" : decoder.decode();
...@@ -669,7 +663,14 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*...@@ -669,7 +663,14 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
669 const trace2Monitor = yield* createTrace2Monitor(commandInput, input.progress).pipe(663 const trace2Monitor = yield* createTrace2Monitor(commandInput, input.progress).pipe(
670 Effect.provideService(Path.Path, path),664 Effect.provideService(Path.Path, path),
671 Effect.provideService(FileSystem.FileSystem, fileSystem),665 Effect.provideService(FileSystem.FileSystem, fileSystem),
672 Effect.mapError(toGitCommandError(commandInput, "failed to create trace2 monitor.")),666 Effect.mapError(
667 (cause) =>
668 new GitCommandError({
669 ...gitCommandContext(commandInput),
670 detail: "Failed to create Git trace monitor.",
671 cause,
672 }),
673 ),
673 );674 );
674 const child = yield* commandSpawner675 const child = yield* commandSpawner
675 .spawn(676 .spawn(
...@@ -682,7 +683,16 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*...@@ -682,7 +683,16 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
682 },683 },
683 }),684 }),
684 )685 )
685 .pipe(Effect.mapError(toGitCommandError(commandInput, "failed to spawn.")));686 .pipe(
687 Effect.mapError(
688 (cause) =>
689 new GitCommandError({
690 ...gitCommandContext(commandInput),
691 detail: "Failed to spawn Git process.",
692 cause,
693 }),
694 ),
695 );
686696
687 const [stdout, stderr, exitCode] = yield* Effect.all(697 const [stdout, stderr, exitCode] = yield* Effect.all(
688 [698 [
...@@ -701,12 +711,26 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*...@@ -701,12 +711,26 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
701 input.progress?.onStderrLine,711 input.progress?.onStderrLine,
702 ),712 ),
703 child.exitCode.pipe(713 child.exitCode.pipe(
704 Effect.mapError(toGitCommandError(commandInput, "failed to report exit code.")),714 Effect.mapError(
715 (cause) =>
716 new GitCommandError({
717 ...gitCommandContext(commandInput),
718 detail: "Failed to read Git process exit code.",
719 cause,
720 }),
721 ),
705 ),722 ),
706 input.stdin === undefined723 input.stdin === undefined
707 ? Effect.void724 ? Effect.void
708 : Stream.run(Stream.encodeText(Stream.make(input.stdin)), child.stdin).pipe(725 : Stream.run(Stream.encodeText(Stream.make(input.stdin)), child.stdin).pipe(
709 Effect.mapError(toGitCommandError(commandInput, "failed to write stdin.")),726 Effect.mapError(
727 (cause) =>
728 new GitCommandError({
729 ...gitCommandContext(commandInput),
730 detail: "Failed to write Git process input.",
731 cause,
732 }),
733 ),
710 ),734 ),
711 ],735 ],
712 { concurrency: "unbounded" },736 { concurrency: "unbounded" },
...@@ -714,15 +738,12 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*...@@ -714,15 +738,12 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
714 yield* trace2Monitor.flush;738 yield* trace2Monitor.flush;
715739
716 if (!input.allowNonZeroExit && exitCode !== 0) {740 if (!input.allowNonZeroExit && exitCode !== 0) {
717 const trimmedStderr = stderr.text.trim();
718 return yield* new GitCommandError({741 return yield* new GitCommandError({
719 operation: commandInput.operation,742 ...gitCommandContext(commandInput),
720 command: quoteGitCommand(commandInput.args),743 detail: "Git command exited with a non-zero status.",
721 cwd: commandInput.cwd,744 exitCode,
722 detail:745 stdoutLength: stdout.text.length,
723 trimmedStderr.length > 0746 stderrLength: stderr.text.length,
724 ? `${quoteGitCommand(commandInput.args)} failed: ${trimmedStderr}`
725 : `${quoteGitCommand(commandInput.args)} failed with code ${exitCode}.`,
726 });747 });
727 }748 }
728749
...@@ -743,10 +764,8 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*...@@ -743,10 +764,8 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
743 onNone: () =>764 onNone: () =>
744 Effect.fail(765 Effect.fail(
745 new GitCommandError({766 new GitCommandError({
746 operation: commandInput.operation,767 ...gitCommandContext(commandInput),
747 command: quoteGitCommand(commandInput.args),768 detail: "Git command timed out.",
748 cwd: commandInput.cwd,
749 detail: `${quoteGitCommand(commandInput.args)} timed out.`,
750 }),769 }),
751 ),770 ),
752 onSome: Effect.succeed,771 onSome: Effect.succeed,
...@@ -799,22 +818,14 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*...@@ -799,22 +818,14 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
799 if (options.allowNonZeroExit || result.exitCode === 0) {818 if (options.allowNonZeroExit || result.exitCode === 0) {
800 return Effect.succeed(result);819 return Effect.succeed(result);
801 }820 }
802 const stderr = result.stderr.trim();
803 if (stderr.length > 0) {
804 return Effect.fail(createGitCommandError(operation, cwd, args, stderr));
805 }
806 if (options.fallbackErrorMessage) {
807 return Effect.fail(
808 createGitCommandError(operation, cwd, args, options.fallbackErrorMessage),
809 );
810 }
811 return Effect.fail(821 return Effect.fail(
812 createGitCommandError(822 new GitCommandError({
813 operation,823 ...gitCommandContext({ operation, cwd, args }),
814 cwd,824 detail: options.fallbackErrorDetail ?? "Git command exited with a non-zero status.",
815 args,825 ...(result.exitCode === null ? {} : { exitCode: result.exitCode }),
816 `${commandLabel(args)} failed: code=${result.exitCode ?? "null"}`,826 stdoutLength: result.stdout.length,
817 ),827 stderrLength: result.stderr.length,
828 }),
818 );829 );
819 }),830 }),
820 );831 );
...@@ -877,12 +888,14 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*...@@ -877,12 +888,14 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
877 }888 }
878 }889 }
879890
880 return yield* createGitCommandError(891 return yield* new GitCommandError({
881 "GitVcsDriver.renameBranch",892 ...gitCommandContext({
882 cwd,893 operation: "GitVcsDriver.renameBranch",
883 ["branch", "-m", "--", desiredBranch],894 cwd,
884 `Could not find an available branch name for '${desiredBranch}'.`,895 args: ["branch", "-m", "--", desiredBranch],
885 );896 }),
897 detail: `Could not find an available branch name for '${desiredBranch}'.`,
898 });
886 });899 });
887900
888 const resolveCurrentUpstream = Effect.fn("resolveCurrentUpstream")(function* (cwd: string) {901 const resolveCurrentUpstream = Effect.fn("resolveCurrentUpstream")(function* (cwd: string) {
...@@ -1024,12 +1037,14 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*...@@ -1024,12 +1037,14 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
1024 if (firstRemote) {1037 if (firstRemote) {
1025 return firstRemote;1038 return firstRemote;
1026 }1039 }
1027 return yield* createGitCommandError(1040 return yield* new GitCommandError({
1028 "GitVcsDriver.resolvePrimaryRemoteName",1041 ...gitCommandContext({
1029 cwd,1042 operation: "GitVcsDriver.resolvePrimaryRemoteName",
1030 ["remote"],1043 cwd,
1031 "No git remote is configured for this repository.",1044 args: ["remote"],
1032 );1045 }),
1046 detail: "No git remote is configured for this repository.",
1047 });
1033 });1048 });
10341049
1035 const resolvePushRemoteName = Effect.fn("resolvePushRemoteName")(function* (1050 const resolvePushRemoteName = Effect.fn("resolvePushRemoteName")(function* (
...@@ -1174,19 +1189,31 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*...@@ -1174,19 +1189,31 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
1174 cwd,1189 cwd,
1175 ["rev-parse", "--abbrev-ref", "HEAD"],1190 ["rev-parse", "--abbrev-ref", "HEAD"],
1176 { allowNonZeroExit: true },1191 { allowNonZeroExit: true },
1177 ).pipe(Effect.catchIf(isMissingGitCwdError, () => Effect.succeed(null)));1192 ).pipe(
1193 Effect.catchTags({
1194 GitCommandError: (error) =>
1195 isMissingGitCwdError(error) ? Effect.succeed(null) : Effect.fail(error),
1196 }),
1197 );
11781198
1179 if (branchResult === null) {1199 if (branchResult === null) {
1180 return NON_REPOSITORY_REMOTE_STATUS_DETAILS;1200 return NON_REPOSITORY_REMOTE_STATUS_DETAILS;
1181 }1201 }
1182 if (branchResult.exitCode !== 0) {1202 if (branchResult.exitCode !== 0) {
1183 const stderr = branchResult.stderr.trim();1203 if (isNonRepositoryGitStderr(branchResult.stderr)) {
1184 return yield* createGitCommandError(1204 return NON_REPOSITORY_REMOTE_STATUS_DETAILS;
1185 "GitVcsDriver.statusDetailsRemote.branch",1205 }
1186 cwd,1206 return yield* new GitCommandError({
1187 ["rev-parse", "--abbrev-ref", "HEAD"],1207 ...gitCommandContext({
1188 stderr || "git branch lookup failed",1208 operation: "GitVcsDriver.statusDetailsRemote.branch",
1189 );1209 cwd,
1210 args: ["rev-parse", "--abbrev-ref", "HEAD"],
1211 }),
1212 detail: "Git branch lookup failed.",
1213 exitCode: branchResult.exitCode,
1214 stdoutLength: branchResult.stdout.length,
1215 stderrLength: branchResult.stderr.length,
1216 });
1190 }1217 }
11911218
1192 const branchValue = branchResult.stdout.trim();1219 const branchValue = branchResult.stdout.trim();
...@@ -1284,20 +1311,32 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*...@@ -1284,20 +1311,32 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
1284 {1311 {
1285 allowNonZeroExit: true,1312 allowNonZeroExit: true,
1286 },1313 },
1287 ).pipe(Effect.catchIf(isMissingGitCwdError, () => Effect.succeed(null)));1314 ).pipe(
1315 Effect.catchTags({
1316 GitCommandError: (error) =>
1317 isMissingGitCwdError(error) ? Effect.succeed(null) : Effect.fail(error),
1318 }),
1319 );
12881320
1289 if (statusResult === null) {1321 if (statusResult === null) {
1290 return NON_REPOSITORY_STATUS_DETAILS;1322 return NON_REPOSITORY_STATUS_DETAILS;
1291 }1323 }
12921324
1293 if (statusResult.exitCode !== 0) {1325 if (statusResult.exitCode !== 0) {
1294 const stderr = statusResult.stderr.trim();1326 if (isNonRepositoryGitStderr(statusResult.stderr)) {
1295 return yield* createGitCommandError(1327 return NON_REPOSITORY_STATUS_DETAILS;
1296 "GitVcsDriver.statusDetails.status",1328 }
1297 cwd,1329 return yield* new GitCommandError({
1298 ["status", "--porcelain=2", "--branch"],1330 ...gitCommandContext({
1299 stderr || "git status failed",1331 operation: "GitVcsDriver.statusDetails.status",
1300 );1332 cwd,
1333 args: ["status", "--porcelain=2", "--branch"],
1334 }),
1335 detail: "Git status failed.",
1336 exitCode: statusResult.exitCode,
1337 stdoutLength: statusResult.stdout.length,
1338 stderrLength: statusResult.stderr.length,
1339 });
1301 }1340 }
13021341
1303 const [unstagedNumstatStdout, stagedNumstatStdout, defaultRefResult, hasPrimaryRemote] =1342 const [unstagedNumstatStdout, stagedNumstatStdout, defaultRefResult, hasPrimaryRemote] =
...@@ -1436,7 +1475,10 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*...@@ -1436,7 +1475,10 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
1436 "statusDetails",1475 "statusDetails",
1437 )(function* (cwd) {1476 )(function* (cwd) {
1438 yield* refreshStatusUpstreamIfStale(cwd).pipe(1477 yield* refreshStatusUpstreamIfStale(cwd).pipe(
1439 Effect.catchIf(isMissingGitCwdError, () => Effect.void),1478 Effect.catchTags({
1479 GitCommandError: (error) =>
1480 isMissingGitCwdError(error) ? Effect.void : Effect.fail(error),
1481 }),
1440 Effect.ignoreCause({ log: true }),1482 Effect.ignoreCause({ log: true }),
1441 );1483 );
1442 return yield* readStatusDetailsLocal(cwd);1484 return yield* readStatusDetailsLocal(cwd);
...@@ -1446,7 +1488,10 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*...@@ -1446,7 +1488,10 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
1446 Effect.fn("statusDetailsRemote")(function* (cwd, options) {1488 Effect.fn("statusDetailsRemote")(function* (cwd, options) {
1447 if (options?.refreshUpstream !== false) {1489 if (options?.refreshUpstream !== false) {
1448 yield* refreshStatusUpstreamIfStale(cwd).pipe(1490 yield* refreshStatusUpstreamIfStale(cwd).pipe(
1449 Effect.catchIf(isMissingGitCwdError, () => Effect.void),1491 Effect.catchTags({
1492 GitCommandError: (error) =>
1493 isMissingGitCwdError(error) ? Effect.void : Effect.fail(error),
1494 }),
1450 Effect.ignoreCause({ log: true }),1495 Effect.ignoreCause({ log: true }),
1451 );1496 );
1452 }1497 }
...@@ -1474,7 +1519,9 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*...@@ -1474,7 +1519,9 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
1474 Effect.fn("prepareCommitContext")(function* (cwd, filePaths) {1519 Effect.fn("prepareCommitContext")(function* (cwd, filePaths) {
1475 if (filePaths && filePaths.length > 0) {1520 if (filePaths && filePaths.length > 0) {
1476 yield* runGit("GitVcsDriver.prepareCommitContext.reset", cwd, ["reset"]).pipe(1521 yield* runGit("GitVcsDriver.prepareCommitContext.reset", cwd, ["reset"]).pipe(
1477 Effect.catch(() => Effect.void),1522 Effect.catchTags({
1523 GitCommandError: () => Effect.void,
1524 }),
1478 );1525 );
1479 yield* runGit("GitVcsDriver.prepareCommitContext.addSelected", cwd, [1526 yield* runGit("GitVcsDriver.prepareCommitContext.addSelected", cwd, [
1480 "add",1527 "add",
...@@ -1550,12 +1597,14 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*...@@ -1550,12 +1597,14 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
1550 const details = yield* statusDetails(cwd);1597 const details = yield* statusDetails(cwd);
1551 const branch = details.branch ?? fallbackBranch;1598 const branch = details.branch ?? fallbackBranch;
1552 if (!branch) {1599 if (!branch) {
1553 return yield* createGitCommandError(1600 return yield* new GitCommandError({
1554 "GitVcsDriver.pushCurrentBranch",1601 ...gitCommandContext({
1555 cwd,1602 operation: "GitVcsDriver.pushCurrentBranch",
1556 ["push"],1603 cwd,
1557 "Cannot push from detached HEAD.",1604 args: ["push"],
1558 );1605 }),
1606 detail: "Cannot push from detached HEAD.",
1607 });
1559 }1608 }
15601609
1561 const requestedRemoteName = options?.remoteName?.trim() || null;1610 const requestedRemoteName = options?.remoteName?.trim() || null;
...@@ -1614,12 +1663,14 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*...@@ -1614,12 +1663,14 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
1614 if (!details.hasUpstream) {1663 if (!details.hasUpstream) {
1615 const publishRemoteName = yield* resolvePushRemoteName(cwd, branch);1664 const publishRemoteName = yield* resolvePushRemoteName(cwd, branch);
1616 if (!publishRemoteName) {1665 if (!publishRemoteName) {
1617 return yield* createGitCommandError(1666 return yield* new GitCommandError({
1618 "GitVcsDriver.pushCurrentBranch",1667 ...gitCommandContext({
1619 cwd,1668 operation: "GitVcsDriver.pushCurrentBranch",
1620 ["push"],1669 cwd,
1621 "Cannot push because no git remote is configured for this repository.",1670 args: ["push"],
1622 );1671 }),
1672 detail: "Cannot push because no git remote is configured for this repository.",
1673 });
1623 }1674 }
1624 const publishBranch = yield* resolvePublishBranchName(cwd, branch);1675 const publishBranch = yield* resolvePublishBranchName(cwd, branch);
1625 yield* runGit("GitVcsDriver.pushCurrentBranch.pushWithUpstream", cwd, [1676 yield* runGit("GitVcsDriver.pushCurrentBranch.pushWithUpstream", cwd, [
...@@ -1668,20 +1719,24 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*...@@ -1668,20 +1719,24 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
1668 const details = yield* statusDetails(cwd);1719 const details = yield* statusDetails(cwd);
1669 const refName = details.branch;1720 const refName = details.branch;
1670 if (!refName) {1721 if (!refName) {
1671 return yield* createGitCommandError(1722 return yield* new GitCommandError({
1672 "GitVcsDriver.pullCurrentBranch",1723 ...gitCommandContext({
1673 cwd,1724 operation: "GitVcsDriver.pullCurrentBranch",
1674 ["pull", "--ff-only"],1725 cwd,
1675 "Cannot pull from detached HEAD.",1726 args: ["pull", "--ff-only"],
1676 );1727 }),
1728 detail: "Cannot pull from detached HEAD.",
1729 });
1677 }1730 }
1678 if (!details.hasUpstream) {1731 if (!details.hasUpstream) {
1679 return yield* createGitCommandError(1732 return yield* new GitCommandError({
1680 "GitVcsDriver.pullCurrentBranch",1733 ...gitCommandContext({
1681 cwd,1734 operation: "GitVcsDriver.pullCurrentBranch",
1682 ["pull", "--ff-only"],1735 cwd,
1683 "Current branch has no upstream configured. Push with upstream first.",1736 args: ["pull", "--ff-only"],
1684 );1737 }),
1738 detail: "Current branch has no upstream configured. Push with upstream first.",
1739 });
1685 }1740 }
1686 const beforeSha = yield* runGitStdout(1741 const beforeSha = yield* runGitStdout(
1687 "GitVcsDriver.pullCurrentBranch.beforeSha",1742 "GitVcsDriver.pullCurrentBranch.beforeSha",
...@@ -1691,7 +1746,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*...@@ -1691,7 +1746,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
1691 ).pipe(Effect.map((stdout) => stdout.trim()));1746 ).pipe(Effect.map((stdout) => stdout.trim()));
1692 yield* executeGit("GitVcsDriver.pullCurrentBranch.pull", cwd, ["pull", "--ff-only"], {1747 yield* executeGit("GitVcsDriver.pullCurrentBranch.pull", cwd, ["pull", "--ff-only"], {
1693 timeoutMs: 30_000,1748 timeoutMs: 30_000,
1694 fallbackErrorMessage: "git pull failed",1749 fallbackErrorDetail: "git pull failed",
1695 });1750 });
1696 const afterSha = yield* runGitStdout(1751 const afterSha = yield* runGitStdout(
1697 "GitVcsDriver.pullCurrentBranch.afterSha",1752 "GitVcsDriver.pullCurrentBranch.afterSha",
...@@ -1874,14 +1929,14 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*...@@ -1874,14 +1929,14 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
1874 crypto.digest("SHA-256", new TextEncoder().encode(diff)).pipe(1929 crypto.digest("SHA-256", new TextEncoder().encode(diff)).pipe(
1875 Effect.map(Encoding.encodeHex),1930 Effect.map(Encoding.encodeHex),
1876 Effect.mapError(1931 Effect.mapError(
1877 toGitCommandError(1932 (cause) =>
1878 {1933 new GitCommandError({
1879 operation: "GitVcsDriver.getReviewDiffPreview.hash",1934 operation: "GitVcsDriver.getReviewDiffPreview.hash",
1935 command: "crypto.digest SHA-256",
1880 cwd: input.cwd,1936 cwd: input.cwd,
1881 args: [],1937 detail: "Failed to hash review diff.",
1882 },1938 cause,
1883 "failed to hash review diff.",1939 }),
1884 ),
1885 ),1940 ),
1886 );1941 );
1887 const [dirtyDiffHash, baseDiffHash] = yield* Effect.all([1942 const [dirtyDiffHash, baseDiffHash] = yield* Effect.all([
...@@ -1939,20 +1994,23 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*...@@ -1939,20 +1994,23 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
1939 allowNonZeroExit: true,1994 allowNonZeroExit: true,
1940 },1995 },
1941 ).pipe(1996 ).pipe(
1942 Effect.catchIf(isMissingGitCwdError, () =>1997 Effect.catchTags({
1943 Effect.succeed({1998 GitCommandError: (error) =>
1944 exitCode: ChildProcessSpawner.ExitCode(128),1999 isMissingGitCwdError(error)
1945 stdout: "",2000 ? Effect.succeed({
1946 stderr: "fatal: not a git repository",2001 exitCode: ChildProcessSpawner.ExitCode(128),
1947 stdoutTruncated: false,2002 stdout: "",
1948 stderrTruncated: false,2003 stderr: "fatal: not a git repository",
1949 }),2004 stdoutTruncated: false,
1950 ),2005 stderrTruncated: false,
2006 })
2007 : Effect.fail(error),
2008 }),
1951 );2009 );
19522010
1953 if (localBranchResult.exitCode !== 0) {2011 if (localBranchResult.exitCode !== 0) {
1954 const stderr = localBranchResult.stderr.trim();2012 const stderr = localBranchResult.stderr.trim();
1955 if (stderr.toLowerCase().includes("not a git repository")) {2013 if (isNonRepositoryGitStderr(stderr)) {
1956 return {2014 return {
1957 refs: [],2015 refs: [],
1958 isRepo: false,2016 isRepo: false,
...@@ -1961,12 +2019,17 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*...@@ -1961,12 +2019,17 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
1961 totalCount: 0,2019 totalCount: 0,
1962 };2020 };
1963 }2021 }
1964 return yield* createGitCommandError(2022 return yield* new GitCommandError({
1965 "GitVcsDriver.listRefs",2023 ...gitCommandContext({
1966 input.cwd,2024 operation: "GitVcsDriver.listRefs",
1967 ["branch", "--no-color", "--no-column"],2025 cwd: input.cwd,
1968 stderr || "git branch failed",2026 args: ["branch", "--no-color", "--no-column"],
1969 );2027 }),
2028 detail: "Git branch listing failed.",
2029 exitCode: localBranchResult.exitCode,
2030 stdoutLength: localBranchResult.stdout.length,
2031 stderrLength: localBranchResult.stderr.length,
2032 });
1970 }2033 }
19712034
1972 const remoteBranchResultEffect = executeGit(2035 const remoteBranchResultEffect = executeGit(
...@@ -1978,19 +2041,27 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*...@@ -1978,19 +2041,27 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
1978 allowNonZeroExit: true,2041 allowNonZeroExit: true,
1979 },2042 },
1980 ).pipe(2043 ).pipe(
1981 Effect.catch((error) =>2044 Effect.catchTags({
1982 Effect.logWarning(2045 GitCommandError: (error) =>
1983 `GitVcsDriver.listRefs: remote refName lookup failed for ${input.cwd}: ${error.message}. Falling back to an empty remote refName list.`,2046 Effect.logWarning(
1984 ).pipe(2047 "Git remote ref lookup failed; falling back to an empty remote ref list.",
1985 Effect.as({2048 {
1986 exitCode: ChildProcessSpawner.ExitCode(1),2049 operation: error.operation,
1987 stdout: "",2050 command: error.command,
1988 stderr: "",2051 cwd: error.cwd,
1989 stdoutTruncated: false,2052 detail: error.detail,
1990 stderrTruncated: false,2053 cause: error,
1991 } satisfies GitVcsDriver.ExecuteGitResult),2054 },
1992 ),2055 ).pipe(
1993 ),2056 Effect.as({
2057 exitCode: ChildProcessSpawner.ExitCode(1),
2058 stdout: "",
2059 stderr: "",
2060 stdoutTruncated: false,
2061 stderrTruncated: false,
2062 } satisfies GitVcsDriver.ExecuteGitResult),
2063 ),
2064 }),
1994 );2065 );
19952066
1996 const remoteNamesResultEffect = executeGit(2067 const remoteNamesResultEffect = executeGit(
...@@ -2002,19 +2073,27 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*...@@ -2002,19 +2073,27 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
2002 allowNonZeroExit: true,2073 allowNonZeroExit: true,
2003 },2074 },
2004 ).pipe(2075 ).pipe(
2005 Effect.catch((error) =>2076 Effect.catchTags({
2006 Effect.logWarning(2077 GitCommandError: (error) =>
2007 `GitVcsDriver.listRefs: remote name lookup failed for ${input.cwd}: ${error.message}. Falling back to an empty remote name list.`,2078 Effect.logWarning(
2008 ).pipe(2079 "Git remote name lookup failed; falling back to an empty remote name list.",
2009 Effect.as({2080 {
2010 exitCode: ChildProcessSpawner.ExitCode(1),2081 operation: error.operation,
2011 stdout: "",2082 command: error.command,
2012 stderr: "",2083 cwd: error.cwd,
2013 stdoutTruncated: false,2084 detail: error.detail,
2014 stderrTruncated: false,2085 cause: error,
2015 } satisfies GitVcsDriver.ExecuteGitResult),2086 },
2016 ),2087 ).pipe(
2017 ),2088 Effect.as({
2089 exitCode: ChildProcessSpawner.ExitCode(1),
2090 stdout: "",
2091 stderr: "",
2092 stdoutTruncated: false,
2093 stderrTruncated: false,
2094 } satisfies GitVcsDriver.ExecuteGitResult),
2095 ),
2096 }),
2018 );2097 );
20192098
2020 const [defaultRef, worktreeList, remoteBranchResult, remoteNamesResult, branchLastCommit] =2099 const [defaultRef, worktreeList, remoteBranchResult, remoteNamesResult, branchLastCommit] =
...@@ -2175,7 +2254,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*...@@ -2175,7 +2254,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
2175 : ["worktree", "add", worktreePath, input.refName];2254 : ["worktree", "add", worktreePath, input.refName];
21762255
2177 yield* executeGit("GitVcsDriver.createWorktree", input.cwd, args, {2256 yield* executeGit("GitVcsDriver.createWorktree", input.cwd, args, {
2178 fallbackErrorMessage: "git worktree add failed",2257 fallbackErrorDetail: "git worktree add failed",
2179 });2258 });
21802259
2181 if (input.newRefName && input.baseRefName) {2260 if (input.newRefName && input.baseRefName) {
...@@ -2214,7 +2293,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*...@@ -2214,7 +2293,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
2214 `+refs/pull/${input.prNumber}/head:refs/heads/${input.branch}`,2293 `+refs/pull/${input.prNumber}/head:refs/heads/${input.branch}`,
2215 ],2294 ],
2216 {2295 {
2217 fallbackErrorMessage: "git fetch pull request branch failed",2296 fallbackErrorDetail: "git fetch pull request branch failed",
2218 },2297 },
2219 );2298 );
2220 });2299 });
...@@ -2227,7 +2306,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*...@@ -2227,7 +2306,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
2227 ["fetch", "--quiet", input.remoteName],2306 ["fetch", "--quiet", input.remoteName],
2228 {2307 {
2229 env: STATUS_UPSTREAM_REFRESH_ENV,2308 env: STATUS_UPSTREAM_REFRESH_ENV,
2230 fallbackErrorMessage: `git fetch ${input.remoteName} failed`,2309 fallbackErrorDetail: `git fetch ${input.remoteName} failed`,
2231 },2310 },
2232 );2311 );
2233 },2312 },
...@@ -2302,18 +2381,8 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*...@@ -2302,18 +2381,8 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
2302 args.push(input.path);2381 args.push(input.path);
2303 yield* executeGit("GitVcsDriver.removeWorktree", input.cwd, args, {2382 yield* executeGit("GitVcsDriver.removeWorktree", input.cwd, args, {
2304 timeoutMs: 15_000,2383 timeoutMs: 15_000,
2305 fallbackErrorMessage: "git worktree remove failed",2384 fallbackErrorDetail: "git worktree remove failed",
2306 }).pipe(2385 });
2307 Effect.mapError((error) =>
2308 createGitCommandError(
2309 "GitVcsDriver.removeWorktree",
2310 input.cwd,
2311 args,
2312 `${commandLabel(args)} failed (cwd: ${input.cwd}): ${error.message}`,
2313 error,
2314 ),
2315 ),
2316 );
2317 });2386 });
23182387
2319 const renameBranch: GitVcsDriver.GitVcsDriver["Service"]["renameBranch"] = Effect.fn(2388 const renameBranch: GitVcsDriver.GitVcsDriver["Service"]["renameBranch"] = Effect.fn(
...@@ -2330,7 +2399,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*...@@ -2330,7 +2399,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
2330 ["branch", "-m", "--", input.oldBranch, targetBranch],2399 ["branch", "-m", "--", input.oldBranch, targetBranch],
2331 {2400 {
2332 timeoutMs: 10_000,2401 timeoutMs: 10_000,
2333 fallbackErrorMessage: "git branch rename failed",2402 fallbackErrorDetail: "git branch rename failed",
2334 },2403 },
2335 );2404 );
23362405
...@@ -2407,7 +2476,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*...@@ -2407,7 +2476,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
24072476
2408 yield* executeGit("GitVcsDriver.switchRef.checkout", input.cwd, checkoutArgs, {2477 yield* executeGit("GitVcsDriver.switchRef.checkout", input.cwd, checkoutArgs, {
2409 timeoutMs: 10_000,2478 timeoutMs: 10_000,
2410 fallbackErrorMessage: "git checkout failed",2479 fallbackErrorDetail: "git checkout failed",
2411 });2480 });
24122481
2413 const refName = yield* runGitStdout("GitVcsDriver.switchRef.currentBranch", input.cwd, [2482 const refName = yield* runGitStdout("GitVcsDriver.switchRef.currentBranch", input.cwd, [
...@@ -2423,7 +2492,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*...@@ -2423,7 +2492,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
2423 function* (input) {2492 function* (input) {
2424 yield* executeGit("GitVcsDriver.createRef", input.cwd, ["branch", input.refName], {2493 yield* executeGit("GitVcsDriver.createRef", input.cwd, ["branch", input.refName], {
2425 timeoutMs: 10_000,2494 timeoutMs: 10_000,
2426 fallbackErrorMessage: "git branch create failed",2495 fallbackErrorDetail: "git branch create failed",
2427 });2496 });
2428 if (input.switchRef) {2497 if (input.switchRef) {
2429 yield* switchRef({ cwd: input.cwd, refName: input.refName });2498 yield* switchRef({ cwd: input.cwd, refName: input.refName });
...@@ -2436,7 +2505,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*...@@ -2436,7 +2505,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
2436 const initRepo: GitVcsDriver.GitVcsDriver["Service"]["initRepo"] = (input) =>2505 const initRepo: GitVcsDriver.GitVcsDriver["Service"]["initRepo"] = (input) =>
2437 executeGit("GitVcsDriver.initRepo", input.cwd, ["init"], {2506 executeGit("GitVcsDriver.initRepo", input.cwd, ["init"], {
2438 timeoutMs: 10_000,2507 timeoutMs: 10_000,
2439 fallbackErrorMessage: "git init failed",2508 fallbackErrorDetail: "git init failed",
2440 }).pipe(Effect.asVoid);2509 }).pipe(Effect.asVoid);
24412510
2442 const listLocalBranchNames: GitVcsDriver.GitVcsDriver["Service"]["listLocalBranchNames"] = (2511 const listLocalBranchNames: GitVcsDriver.GitVcsDriver["Service"]["listLocalBranchNames"] = (
apps/server/src/vcs/VcsDriverRegistry.ts+13-15
...@@ -36,13 +36,6 @@ export class VcsDriverRegistry extends Context.Service<...@@ -36,13 +36,6 @@ export class VcsDriverRegistry extends Context.Service<
36 }36 }
37>()("t3/vcs/VcsDriverRegistry") {}37>()("t3/vcs/VcsDriverRegistry") {}
3838
39const unsupported = (operation: string, kind: VcsDriverKind, detail: string) =>
40 new VcsUnsupportedOperationError({
41 operation,
42 kind,
43 detail,
44 });
45
46function detectionCacheKey(input: {39function detectionCacheKey(input: {
47 readonly cwd: string;40 readonly cwd: string;
48 readonly requestedKind: VcsDriverKind | "auto";41 readonly requestedKind: VcsDriverKind | "auto";
...@@ -78,7 +71,11 @@ export const make = Effect.gen(function* () {...@@ -78,7 +71,11 @@ export const make = Effect.gen(function* () {
78 const driver = drivers[kind];71 const driver = drivers[kind];
79 if (!driver) {72 if (!driver) {
80 return Effect.fail(73 return Effect.fail(
81 unsupported("VcsDriverRegistry.get", kind, `No ${kind} VCS driver is registered.`),74 new VcsUnsupportedOperationError({
75 operation: "VcsDriverRegistry.get",
76 kind,
77 detail: `No ${kind} VCS driver is registered.`,
78 }),
82 );79 );
83 }80 }
84 return Effect.succeed(driver);81 return Effect.succeed(driver);
...@@ -137,13 +134,14 @@ export const make = Effect.gen(function* () {...@@ -137,13 +134,14 @@ export const make = Effect.gen(function* () {
137 }134 }
138135
139 const requestedKind = input.requestedKind ?? "auto";136 const requestedKind = input.requestedKind ?? "auto";
140 return yield* unsupported(137 return yield* new VcsUnsupportedOperationError({
141 "VcsDriverRegistry.resolve",138 operation: "VcsDriverRegistry.resolve",
142 requestedKind === "auto" ? "unknown" : requestedKind,139 kind: requestedKind === "auto" ? "unknown" : requestedKind,
143 requestedKind === "auto"140 detail:
144 ? `No supported VCS repository was detected at ${input.cwd}.`141 requestedKind === "auto"
145 : `No ${requestedKind} repository was detected at ${input.cwd}.`,142 ? `No supported VCS repository was detected at ${input.cwd}.`
146 );143 : `No ${requestedKind} repository was detected at ${input.cwd}.`,
144 });
147 },145 },
148 );146 );
149147
packages/contracts/src/git.ts+6-1
...@@ -324,11 +324,16 @@ export class GitCommandError extends Schema.TaggedErrorClass<GitCommandError>()(...@@ -324,11 +324,16 @@ export class GitCommandError extends Schema.TaggedErrorClass<GitCommandError>()(
324 operation: Schema.String,324 operation: Schema.String,
325 command: Schema.String,325 command: Schema.String,
326 cwd: Schema.String,326 cwd: Schema.String,
327 argumentCount: Schema.optional(Schema.Number),
328 exitCode: Schema.optional(Schema.Number),
329 stdoutLength: Schema.optional(Schema.Number),
330 stderrLength: Schema.optional(Schema.Number),
331 outputLength: Schema.optional(Schema.Number),
327 detail: Schema.String,332 detail: Schema.String,
328 cause: Schema.optional(Schema.Defect()),333 cause: Schema.optional(Schema.Defect()),
329}) {334}) {
330 override get message(): string {335 override get message(): string {
331 return `Git command failed in ${this.operation}: ${this.command} (${this.cwd}) - ${this.detail}`;336 return `Git command failed in ${this.operation} (${this.cwd}): ${this.detail}`;
332 }337 }
333}338}
334339