authorgravatar for 52527831+EricTsai83@users.noreply.github.comEric Tsai <52527831+EricTsai83@users.noreply.github.com> 2026-07-17 19:15:31+08:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-17 11:15:31+00:00
log8b5469863ae1dd696e696de30240ec3da607962d
tree1f66f335cfb0a2ed4f7ad522270736fc537346e9
parent58302b2010913d6e236570bbe8c9abe38de7fa18
signature Signed by PGP key B5690EEEBB952194

fix(server): stabilize non-repository Git diagnostics (#4077)


2 files changed, 90 insertions(+), 3 deletions(-)

apps/server/src/vcs/GitVcsDriverCore.test.ts+73
...@@ -6,6 +6,9 @@ import * as Layer from "effect/Layer";...@@ -6,6 +6,9 @@ import * as Layer from "effect/Layer";
6import * as Path from "effect/Path";6import * as Path from "effect/Path";
7import * as PlatformError from "effect/PlatformError";7import * as PlatformError from "effect/PlatformError";
8import * as Scope from "effect/Scope";8import * as Scope from "effect/Scope";
9import * as Sink from "effect/Sink";
10import * as Stream from "effect/Stream";
11import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
912
10import { GitCommandError } from "@t3tools/contracts";13import { GitCommandError } from "@t3tools/contracts";
11import { ServerConfig } from "../config.ts";14import { ServerConfig } from "../config.ts";
...@@ -20,6 +23,21 @@ const TestLayer = GitVcsDriver.layer.pipe(...@@ -20,6 +23,21 @@ const TestLayer = GitVcsDriver.layer.pipe(
20 Layer.provideMerge(NodeServices.layer),23 Layer.provideMerge(NodeServices.layer),
21);24);
2225
26const makeNonRepositoryHandle = () =>
27 ChildProcessSpawner.makeHandle({
28 pid: ChildProcessSpawner.ProcessId(1),
29 exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(128)),
30 isRunning: Effect.succeed(false),
31 kill: () => Effect.void,
32 unref: Effect.succeed(Effect.void),
33 stdin: Sink.drain,
34 stdout: Stream.empty,
35 stderr: Stream.encodeText(Stream.make("fatal: not a git repository")),
36 all: Stream.empty,
37 getInputFd: () => Sink.drain,
38 getOutputFd: () => Stream.empty,
39 });
40
23const makeTmpDir = (41const makeTmpDir = (
24 prefix = "git-vcs-driver-test-",42 prefix = "git-vcs-driver-test-",
25): Effect.Effect<string, PlatformError.PlatformError, FileSystem.FileSystem | Scope.Scope> =>43): Effect.Effect<string, PlatformError.PlatformError, FileSystem.FileSystem | Scope.Scope> =>
...@@ -77,7 +95,62 @@ const initRepoWithCommit = (...@@ -77,7 +95,62 @@ const initRepoWithCommit = (
77 return { initialBranch };95 return { initialBranch };
78 });96 });
7997
98it.effect("uses stable diagnostics for every parsed non-repository command", () => {
99 const commands: Array<{ readonly args: ReadonlyArray<string>; readonly lcAll?: string }> = [];
100 const spawner = ChildProcessSpawner.make((command) =>
101 Effect.sync(() => {
102 if (!ChildProcess.isStandardCommand(command)) {
103 return assert.fail("expected a standard Git command");
104 }
105 commands.push({
106 args: command.args,
107 ...(command.options.env?.LC_ALL ? { lcAll: command.options.env.LC_ALL } : {}),
108 });
109 return makeNonRepositoryHandle();
110 }),
111 );
112 const nodeServicesLayer = Layer.merge(
113 NodeServices.layer,
114 Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner),
115 );
116 const layer = GitVcsDriver.layer.pipe(
117 Layer.provide(ServerConfigLayer),
118 Layer.provideMerge(nodeServicesLayer),
119 );
120
121 return Effect.gen(function* () {
122 const driver = yield* GitVcsDriver.GitVcsDriver;
123 const cwd = "/repo";
124
125 yield* driver.statusDetailsLocal(cwd);
126 yield* driver.statusDetailsRemote(cwd, { refreshUpstream: false });
127 yield* driver.listRefs({ cwd });
128
129 assert.deepStrictEqual(commands, [
130 { args: ["status", "--porcelain=2", "--branch"], lcAll: "C" },
131 { args: ["rev-parse", "--abbrev-ref", "HEAD"], lcAll: "C" },
132 { args: ["branch", "--no-color", "--no-column"], lcAll: "C" },
133 ]);
134 }).pipe(Effect.provide(layer));
135});
136
80it.layer(TestLayer)("GitVcsDriver core integration", (it) => {137it.layer(TestLayer)("GitVcsDriver core integration", (it) => {
138 describe("process environment", () => {
139 it.effect("preserves the caller locale for general Git subprocesses", () =>
140 Effect.gen(function* () {
141 const cwd = yield* makeTmpDir();
142
143 const locale = yield* git(
144 cwd,
145 ["-c", 'alias.print-locale=!printf "%s" "$LC_ALL"', "print-locale"],
146 { LC_ALL: "zh_CN.UTF-8" },
147 );
148
149 assert.equal(locale, "zh_CN.UTF-8");
150 }),
151 );
152 });
153
81 describe("structured errors", () => {154 describe("structured errors", () => {
82 it.effect("preserves structured spawn context and the platform cause", () =>155 it.effect("preserves structured spawn context and the platform cause", () =>
83 Effect.gen(function* () {156 Effect.gen(function* () {
apps/server/src/vcs/GitVcsDriverCore.ts+17-3
...@@ -830,6 +830,20 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*...@@ -830,6 +830,20 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
830 }),830 }),
831 );831 );
832832
833 const executeGitWithStableDiagnostics = (
834 operation: string,
835 cwd: string,
836 args: readonly string[],
837 options: ExecuteGitOptions = {},
838 ): Effect.Effect<GitVcsDriver.ExecuteGitResult, GitCommandError> =>
839 executeGit(operation, cwd, args, {
840 ...options,
841 env: {
842 ...options.env,
843 LC_ALL: "C",
844 },
845 });
846
833 const runGit = (847 const runGit = (
834 operation: string,848 operation: string,
835 cwd: string,849 cwd: string,
...@@ -1184,7 +1198,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*...@@ -1184,7 +1198,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
1184 });1198 });
11851199
1186 const readStatusDetailsRemote = Effect.fn("readStatusDetailsRemote")(function* (cwd: string) {1200 const readStatusDetailsRemote = Effect.fn("readStatusDetailsRemote")(function* (cwd: string) {
1187 const branchResult = yield* executeGit(1201 const branchResult = yield* executeGitWithStableDiagnostics(
1188 "GitVcsDriver.statusDetailsRemote.branch",1202 "GitVcsDriver.statusDetailsRemote.branch",
1189 cwd,1203 cwd,
1190 ["rev-parse", "--abbrev-ref", "HEAD"],1204 ["rev-parse", "--abbrev-ref", "HEAD"],
...@@ -1304,7 +1318,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*...@@ -1304,7 +1318,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
1304 });1318 });
13051319
1306 const readStatusDetailsLocal = Effect.fn("readStatusDetailsLocal")(function* (cwd: string) {1320 const readStatusDetailsLocal = Effect.fn("readStatusDetailsLocal")(function* (cwd: string) {
1307 const statusResult = yield* executeGit(1321 const statusResult = yield* executeGitWithStableDiagnostics(
1308 "GitVcsDriver.statusDetails.status",1322 "GitVcsDriver.statusDetails.status",
1309 cwd,1323 cwd,
1310 ["status", "--porcelain=2", "--branch"],1324 ["status", "--porcelain=2", "--branch"],
...@@ -1986,7 +2000,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*...@@ -1986,7 +2000,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
1986 const branchRecencyPromise = readBranchRecency(input.cwd).pipe(2000 const branchRecencyPromise = readBranchRecency(input.cwd).pipe(
1987 Effect.orElseSucceed(() => new Map<string, number>()),2001 Effect.orElseSucceed(() => new Map<string, number>()),
1988 );2002 );
1989 const localBranchResult = yield* executeGit(2003 const localBranchResult = yield* executeGitWithStableDiagnostics(
1990 "GitVcsDriver.listRefs.branchNoColor",2004 "GitVcsDriver.listRefs.branchNoColor",
1991 input.cwd,2005 input.cwd,
1992 ["branch", "--no-color", "--no-column"],2006 ["branch", "--no-color", "--no-column"],