Skip to content
Closed
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
67 changes: 66 additions & 1 deletion apps/server/src/vcs/GitVcsDriver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import * as PlatformError from "effect/PlatformError";
import { ChildProcessSpawner } from "effect/unstable/process";
import { assert, it } from "@effect/vitest";

import { GitCommandError } from "@t3tools/contracts";
import { GitCommandError, VcsProcessSpawnError } from "@t3tools/contracts";
import * as ServerConfig from "../config.ts";
import * as GitVcsDriver from "./GitVcsDriver.ts";
import * as VcsProcess from "./VcsProcess.ts";
Expand Down Expand Up @@ -112,3 +112,68 @@ it.effect("GitVcsDriver forwards execute env to the VCS process", () => {
),
);
});

const missingGitSpawnError = (cwd: string, operation: string) =>
new VcsProcessSpawnError({
operation,
command: "git",
cwd,
argumentCount: 4,
cause: PlatformError.systemError({
_tag: "NotFound",
module: "ChildProcess",
method: "spawn",
pathOrDescriptor: "git",
}),
});

it.effect("treats a missing git executable as outside a work tree", () => {
const cwd = "/repo";
return Effect.gen(function* () {
const driver = yield* GitVcsDriver.makeVcsDriverShape();

assert.equal(yield* driver.isInsideWorkTree(cwd), false);
assert.equal(yield* driver.detectRepository(cwd), null);
}).pipe(
Effect.provide(
Layer.mergeAll(
NodeServices.layer,
Layer.mock(VcsProcess.VcsProcess)({
run: (input) => Effect.fail(missingGitSpawnError(input.cwd, input.operation)),
}),
),
),
);
});

it.effect("does not treat a missing working directory as a missing git executable", () => {
const cwd = "/missing/repo";
const cause = new VcsProcessSpawnError({
operation: "GitVcsDriver.isInsideWorkTree",
command: "git",
cwd,
argumentCount: 4,
cause: PlatformError.systemError({
_tag: "NotFound",
module: "FileSystem",
method: "access",
pathOrDescriptor: cwd,
}),
});

return Effect.gen(function* () {
const driver = yield* GitVcsDriver.makeVcsDriverShape();
const error = yield* driver.isInsideWorkTree(cwd).pipe(Effect.flip);

assert.strictEqual(error, cause);
}).pipe(
Effect.provide(
Layer.mergeAll(
NodeServices.layer,
Layer.mock(VcsProcess.VcsProcess)({
run: () => Effect.fail(cause),
}),
),
),
);
});
20 changes: 19 additions & 1 deletion apps/server/src/vcs/GitVcsDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ import * as FileSystem from "effect/FileSystem";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Path from "effect/Path";
import * as PlatformError from "effect/PlatformError";
import { ChildProcessSpawner } from "effect/unstable/process";

import {
GitCommandError,
type VcsError,
VcsProcessExitError,
VcsProcessSpawnError,
type VcsSwitchRefInput,
type VcsSwitchRefResult,
type VcsCreateRefInput,
Expand Down Expand Up @@ -420,6 +423,18 @@ function parseGitRemoteVerboseOutput(
return remotes;
}

// Missing `git` is "not a repository," same as exit 128. Keep missing cwd as a spawn error.
function isMissingGitExecutableError(error: VcsError): error is VcsProcessSpawnError {
return (
error._tag === "VcsProcessSpawnError" &&
error.cause instanceof PlatformError.PlatformError &&
error.cause.reason._tag === "NotFound" &&
error.cause.reason.module === "ChildProcess" &&
error.cause.reason.method === "spawn" &&
error.cause.reason.syscall !== "chdir"
);
}

const gitCommand = (
process: VcsProcess.VcsProcess["Service"],
operation: string,
Expand Down Expand Up @@ -478,7 +493,10 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* (
timeoutMs: 5_000,
maxOutputBytes: 4_096,
},
).pipe(Effect.map((result) => result.exitCode === 0 && result.stdout.trim() === "true"));
).pipe(
Effect.map((result) => result.exitCode === 0 && result.stdout.trim() === "true"),
Effect.catchIf(isMissingGitExecutableError, () => Effect.succeed(false)),
);

const execute: VcsDriver.VcsDriver["Service"]["execute"] = (input) =>
gitCommand(vcsProcess, input.operation, input.cwd, input.args, {
Expand Down
39 changes: 39 additions & 0 deletions apps/server/src/vcs/VcsDriverRegistry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { assert, it, describe } from "@effect/vitest";
import * as NodeServices from "@effect/platform-node/NodeServices";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as PlatformError from "effect/PlatformError";
import { ChildProcessSpawner } from "effect/unstable/process";
import { VcsProcessSpawnError } from "@t3tools/contracts";

import * as VcsProcess from "./VcsProcess.ts";
import * as VcsProjectConfig from "./VcsProjectConfig.ts";
Expand Down Expand Up @@ -137,4 +139,41 @@ describe("VcsDriverRegistry", () => {
assert.equal(insideWorkTreeChecks, 2);
}).pipe(Effect.provide(layer));
});

it.effect("treats a missing git executable as no repository", () => {
const cwd = "/repo";
const layer = Layer.effect(VcsDriverRegistry.VcsDriverRegistry, VcsDriverRegistry.make).pipe(
Layer.provide(NodeServices.layer),
Layer.provide(
Layer.mock(VcsProjectConfig.VcsProjectConfig)({
resolveKind: (input) => Effect.succeed(input.requestedKind ?? "auto"),
}),
),
Layer.provide(
Layer.mock(VcsProcess.VcsProcess)({
run: (input) =>
Effect.fail(
new VcsProcessSpawnError({
operation: input.operation,
command: "git",
cwd,
argumentCount: input.args.length,
cause: PlatformError.systemError({
_tag: "NotFound",
module: "ChildProcess",
method: "spawn",
pathOrDescriptor: "git",
}),
}),
),
}),
),
);

return Effect.gen(function* () {
const registry = yield* VcsDriverRegistry.VcsDriverRegistry;

assert.equal(yield* registry.detect({ cwd }), null);
}).pipe(Effect.provide(layer));
});
});
Loading