Skip to content
Draft
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
23 changes: 21 additions & 2 deletions apps/server/src/vcs/VcsProjectConfig.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,7 @@ describe("VcsProjectConfig", () => {
yield* fileSystem.makeDirectory(nested, { recursive: true });
yield* fileSystem.writeFileString(
path.join(configDir, "vcs.json"),
// @effect-diagnostics-next-line preferSchemaOverJson:off
JSON.stringify({ vcs: { kind: "jj" } }),
'{"vcs":{"kind":"jj"}}',
);

const config = yield* VcsProjectConfig.VcsProjectConfig;
Expand All @@ -53,6 +52,26 @@ describe("VcsProjectConfig", () => {
);
});

it.layer(TestLayer)("ignores malformed .t3code/vcs.json files", (it) => {
it.effect("falls back to auto", () =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const root = yield* fileSystem.makeTempDirectoryScoped({
prefix: "t3-vcs-config-test-",
});
const configDir = path.join(root, ".t3code");
yield* fileSystem.makeDirectory(configDir, { recursive: true });
yield* fileSystem.writeFileString(path.join(configDir, "vcs.json"), "{ not-json");

const config = yield* VcsProjectConfig.VcsProjectConfig;
const kind = yield* config.resolveKind({ cwd: root });

assert.equal(kind, "auto");
}),
);
});

it.layer(TestLayer)("falls back to auto when no config exists", (it) => {
it.effect("returns auto", () =>
Effect.gen(function* () {
Expand Down
41 changes: 16 additions & 25 deletions apps/server/src/vcs/VcsProjectConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
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 Schema from "effect/Schema";

Expand All @@ -15,16 +16,10 @@ const ProjectVcsConfig = Schema.Struct({
),
vcsKind: Schema.optional(VcsDriverKind),
});
const isProjectVcsConfig = Schema.is(ProjectVcsConfig);
const ProjectVcsConfigJson = Schema.fromJsonString(ProjectVcsConfig);
const decodeProjectVcsConfig = Schema.decodeUnknownOption(ProjectVcsConfigJson);

interface ProjectVcsConfigFile {
readonly vcs?:
| {
readonly kind?: VcsDriverKindType | undefined;
}
| undefined;
readonly vcsKind?: VcsDriverKindType | undefined;
}
type ProjectVcsConfigFile = Schema.Schema.Type<typeof ProjectVcsConfig>;

export interface VcsProjectConfigResolveInput {
readonly cwd: string;
Expand All @@ -45,13 +40,8 @@ function configuredKind(config: ProjectVcsConfigFile): VcsDriverKindType | "auto
return config.vcs?.kind ?? config.vcsKind ?? "auto";
}

function parseConfig(raw: string): ProjectVcsConfigFile | null {
try {
const parsed = JSON.parse(raw) as unknown;
return isProjectVcsConfig(parsed) ? parsed : null;
} catch {
return null;
}
function parseConfig(raw: string): Option.Option<ProjectVcsConfigFile> {
return decodeProjectVcsConfig(raw);
}

export const make = Effect.fn("makeVcsProjectConfig")(function* () {
Expand All @@ -63,12 +53,12 @@ export const make = Effect.fn("makeVcsProjectConfig")(function* () {
while (true) {
const candidate = path.join(current, ".t3code", "vcs.json");
if (yield* fileSystem.exists(candidate).pipe(Effect.orElseSucceed(() => false))) {
return candidate;
return Option.some(candidate);
}

const parent = path.dirname(current);
if (parent === current) {
return null;
return Option.none<string>();
}
current = parent;
}
Expand All @@ -78,26 +68,27 @@ export const make = Effect.fn("makeVcsProjectConfig")(function* () {
configPath: string,
) {
const raw = yield* fileSystem.readFileString(configPath).pipe(
Effect.map(Option.some),
Effect.catch((error) =>
Effect.logWarning("failed to read VCS project config", {
configPath,
error,
}).pipe(Effect.as(null)),
}).pipe(Effect.as(Option.none<string>())),
),
);
if (raw === null) {
if (Option.isNone(raw)) {
return "auto" as const;
}

const parsed = parseConfig(raw);
if (parsed === null) {
const parsed = parseConfig(raw.value);
if (Option.isNone(parsed)) {
yield* Effect.logWarning("invalid VCS project config", {
configPath,
});
return "auto" as const;
}

return configuredKind(parsed);
return configuredKind(parsed.value);
});

const resolveKind: VcsProjectConfigShape["resolveKind"] = Effect.fn(
Expand All @@ -108,11 +99,11 @@ export const make = Effect.fn("makeVcsProjectConfig")(function* () {
}

const configPath = yield* findConfigPath(input.cwd);
if (configPath === null) {
if (Option.isNone(configPath)) {
return "auto";
}

return yield* readConfiguredKind(configPath);
return yield* readConfiguredKind(configPath.value);
});

return VcsProjectConfig.of({
Expand Down
5 changes: 2 additions & 3 deletions apps/server/src/workspace/Layers/WorkspaceEntries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import {
} from "../Services/WorkspaceEntries.ts";
import { WorkspacePaths } from "../Services/WorkspacePaths.ts";

const WORKSPACE_CACHE_TTL_MS = 15_000;
const WORKSPACE_CACHE_TTL = Duration.seconds(15);
const WORKSPACE_CACHE_MAX_KEYS = 4;
const WORKSPACE_INDEX_MAX_ENTRIES = 25_000;
const WORKSPACE_SCAN_READDIR_CONCURRENCY = 32;
Expand Down Expand Up @@ -402,8 +402,7 @@ export const makeWorkspaceEntries = Effect.gen(function* () {
buildWorkspaceIndex,
{
capacity: WORKSPACE_CACHE_MAX_KEYS,
timeToLive: (exit) =>
Exit.isSuccess(exit) ? Duration.millis(WORKSPACE_CACHE_TTL_MS) : Duration.zero,
timeToLive: (exit) => (Exit.isSuccess(exit) ? WORKSPACE_CACHE_TTL : Duration.zero),
},
);

Expand Down
Loading