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
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ const PAGE_TITLES: Record<SettingsPage, string> = {

const PAGE_PROJECT_KEYS: Record<SettingsPage, readonly ProjectScopedServerSettingKey[]> = {
"new-threads": ["defaultThreadEnvMode", "worktreeSubmodules", "defaultRuntimeMode"],
"source-control": ["defaultAutoPull", "newWorktreesStartFromOrigin"],
"source-control": ["enableVersionControl", "defaultAutoPull", "newWorktreesStartFromOrigin"],
"agent-behavior": ["responseStreamingMode", "enableAgentBrowserAccess"],
maintenance: ["continueThreadsAfterServerUpdate"],
};
Expand Down Expand Up @@ -325,6 +325,16 @@ function ServerSettingsDetail(props: { readonly page: SettingsPage }) {

{props.page === "source-control" ? (
<>
<SettingsSection title="Version control">
<FanoutSwitchRow
icon="arrow.triangle.branch"
label="Git"
subtitle="Turn off to skip Git status, fetch, and checkpoints."
value={uniform("enableVersionControl")}
disabled={disabledFor("enableVersionControl")}
onValueChange={(value) => write({ enableVersionControl: value })}
/>
</SettingsSection>
<SettingsSection title="Default branch">
<FanoutSwitchRow
icon="arrow.down.circle"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ import { deriveServerPaths, ServerConfig } from "../src/config.ts";
import * as WorkspaceEntries from "../src/workspace/WorkspaceEntries.ts";
import * as WorkspacePaths from "../src/workspace/WorkspacePaths.ts";
import * as VcsDriverRegistry from "../src/vcs/VcsDriverRegistry.ts";
import * as VersionControlPolicy from "../src/vcs/VersionControlPolicy.ts";
import { VcsStatusBroadcaster } from "../src/vcs/VcsStatusBroadcaster.ts";
import { GitWorkflowService } from "../src/git/GitWorkflowService.ts";
import * as VcsProcess from "../src/vcs/VcsProcess.ts";
Expand Down Expand Up @@ -306,7 +307,10 @@ export const makeOrchestrationIntegrationHarness = (
);
const providerRegistryLayer = makeProviderRegistryLayer();

const checkpointStoreLayer = CheckpointStore.layer.pipe(Layer.provide(VcsDriverRegistry.layer));
const checkpointStoreLayer = CheckpointStore.layer.pipe(
Layer.provide(VcsDriverRegistry.layer),
Layer.provide(VersionControlPolicy.layerTest),
);
const projectionSnapshotQueryLayer = OrchestrationProjectionSnapshotQueryLive;
const runtimeServicesLayer = Layer.mergeAll(
projectionSnapshotQueryLayer,
Expand Down Expand Up @@ -375,7 +379,9 @@ export const makeOrchestrationIntegrationHarness = (
Layer.provideMerge(
WorkspaceEntries.layer.pipe(
Layer.provide(WorkspacePaths.layer),
Layer.provideMerge(VcsDriverRegistry.layer),
Layer.provideMerge(
VcsDriverRegistry.layer.pipe(Layer.provide(VersionControlPolicy.layerTest)),
),
Layer.provide(NodeServices.layer),
),
),
Expand Down
8 changes: 7 additions & 1 deletion apps/server/scripts/evaluate-thread-titles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import * as AzureDevOpsCli from "../src/sourceControl/AzureDevOpsCli.ts";
import * as BitbucketApi from "../src/sourceControl/BitbucketApi.ts";
import * as VcsProcess from "../src/vcs/VcsProcess.ts";
import * as VcsDriverRegistry from "../src/vcs/VcsDriverRegistry.ts";
import * as VersionControlPolicy from "../src/vcs/VersionControlPolicy.ts";
import * as VcsProjectConfig from "../src/vcs/VcsProjectConfig.ts";
import * as GitVcsDriver from "../src/vcs/GitVcsDriver.ts";
import * as ProcessRunner from "../src/processRunner.ts";
Expand Down Expand Up @@ -158,7 +159,12 @@ await Effect.runPromise(
BitbucketApi.layer,
),
),
Layer.provide(VcsDriverRegistry.layer.pipe(Layer.provide(VcsProjectConfig.layer))),
Layer.provide(
VcsDriverRegistry.layer.pipe(
Layer.provide(VcsProjectConfig.layer),
Layer.provide(VersionControlPolicy.layerTest),
),
),
Layer.provide(GitVcsDriver.layer),
Layer.provide(VcsProcess.layer),
Layer.provide(FetchHttpClient.layer),
Expand Down
6 changes: 5 additions & 1 deletion apps/server/src/checkpointing/CheckpointStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,18 @@ import { checkpointRefForThreadTurn } from "./Utils.ts";
import { parseTurnDiffFilesFromNumstat } from "./Diffs.ts";
import * as CheckpointStore from "./CheckpointStore.ts";
import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts";
import * as VersionControlPolicy from "../vcs/VersionControlPolicy.ts";
import * as VcsProcess from "../vcs/VcsProcess.ts";
import * as ServerConfig from "../config.ts";

const ServerConfigLayer = ServerConfig.ServerConfig.layerTest(process.cwd(), {
prefix: "t3-checkpoint-store-test-",
});
const VcsProcessTestLayer = VcsProcess.layer.pipe(Layer.provide(NodeServices.layer));
const VcsDriverTestLayer = VcsDriverRegistry.layer.pipe(Layer.provide(VcsProcessTestLayer));
const VcsDriverTestLayer = VcsDriverRegistry.layer.pipe(
Layer.provide(VcsProcessTestLayer),
Layer.provide(VersionControlPolicy.layerTest),
);
const CheckpointStoreTestLayer = CheckpointStore.layer.pipe(
Layer.provideMerge(VcsDriverTestLayer),
Layer.provideMerge(NodeServices.layer),
Expand Down
13 changes: 11 additions & 2 deletions apps/server/src/orchestration/Layers/CheckpointReactor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { afterEach, describe, expect, it, vi } from "vite-plus/test";

import * as CheckpointStore from "../../checkpointing/CheckpointStore.ts";
import * as VcsDriverRegistry from "../../vcs/VcsDriverRegistry.ts";
import * as VersionControlPolicy from "../../vcs/VersionControlPolicy.ts";
import * as VcsProcess from "../../vcs/VcsProcess.ts";
import { VcsStatusBroadcaster } from "../../vcs/VcsStatusBroadcaster.ts";
import * as RepositoryIdentityResolver from "../../project/RepositoryIdentityResolver.ts";
Expand Down Expand Up @@ -388,13 +389,21 @@ describe("CheckpointReactor", () => {
},
})),
),
).pipe(Layer.provide(VcsDriverRegistry.layer)),
).pipe(
Layer.provide(VcsDriverRegistry.layer),
Layer.provide(VersionControlPolicy.layerTest),
),
),
Layer.provideMerge(
(options?.workspaceRefresh
? Layer.mock(WorkspaceEntries.WorkspaceEntries)({ refresh: options.workspaceRefresh })
: WorkspaceEntries.layer
).pipe(Layer.provide(WorkspacePaths.layer), Layer.provideMerge(VcsDriverRegistry.layer)),
).pipe(
Layer.provide(WorkspacePaths.layer),
Layer.provideMerge(
VcsDriverRegistry.layer.pipe(Layer.provide(VersionControlPolicy.layerTest)),
),
),
),
Layer.provideMerge(WorkspacePaths.layer),
Layer.provideMerge(VcsProcess.layer),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import {
import * as RepositoryIdentityResolver from "../../project/RepositoryIdentityResolver.ts";
import * as CheckpointStore from "../../checkpointing/CheckpointStore.ts";
import * as VcsDriverRegistry from "../../vcs/VcsDriverRegistry.ts";
import * as VersionControlPolicy from "../../vcs/VersionControlPolicy.ts";
import * as VcsProcess from "../../vcs/VcsProcess.ts";
import { OrchestrationEngineLive } from "./OrchestrationEngine.ts";
import { OrchestrationProjectionPipelineLive } from "./ProjectionPipeline.ts";
Expand Down Expand Up @@ -338,7 +339,14 @@ describe("ProviderRuntimeIngestion", () => {
...store,
isGitRepository: options?.isGitRepository ?? store.isGitRepository,
})),
).pipe(Layer.provide(CheckpointStore.layer.pipe(Layer.provide(VcsDriverRegistry.layer)))),
).pipe(
Layer.provide(
CheckpointStore.layer.pipe(
Layer.provide(VcsDriverRegistry.layer),
Layer.provide(VersionControlPolicy.layerTest),
),
),
),
),
Layer.provideMerge(VcsProcess.layer),
Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())),
Expand Down
13 changes: 11 additions & 2 deletions apps/server/src/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ import * as WorkspacePaths from "./workspace/WorkspacePaths.ts";
import * as GitVcsDriver from "./vcs/GitVcsDriver.ts";
import * as VcsDriver from "./vcs/VcsDriver.ts";
import * as VcsStatusBroadcaster from "./vcs/VcsStatusBroadcaster.ts";
import * as VersionControlPolicy from "./vcs/VersionControlPolicy.ts";
import * as VcsDriverRegistry from "./vcs/VcsDriverRegistry.ts";
import * as VcsProvisioningService from "./vcs/VcsProvisioningService.ts";
import * as GitHubCli from "./sourceControl/GitHubCli.ts";
Expand Down Expand Up @@ -749,7 +750,10 @@ const buildAppUnderTest = (options?: {
? Layer.mock(VcsStatusBroadcaster.VcsStatusBroadcaster)({
...options.layers.vcsStatusBroadcaster,
})
: VcsStatusBroadcaster.layer.pipe(Layer.provide(gitWorkflowLayer));
: VcsStatusBroadcaster.layer.pipe(
Layer.provide(gitWorkflowLayer),
Layer.provide(VersionControlPolicy.layerTest),
);
const resourceTelemetryLayer = ResourceTelemetry.layer.pipe(
Layer.provide(
Layer.mergeAll(
Expand Down Expand Up @@ -1241,7 +1245,12 @@ const buildAppUnderTest = (options?: {
? FetchHttpClient.layer
: Layer.succeed(HttpClient.HttpClient, options.layers.httpClient),
),
Layer.provide(GitHubCli.layer.pipe(Layer.provideMerge(VcsProcess.layer))),
Layer.provide(
Layer.mergeAll(
GitHubCli.layer.pipe(Layer.provideMerge(VcsProcess.layer)),
VersionControlPolicy.layerTest,
),
),
Layer.provide(layerConfig),
);

Expand Down
8 changes: 8 additions & 0 deletions apps/server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ import * as VcsProjectConfig from "./vcs/VcsProjectConfig.ts";
import * as VcsProcess from "./vcs/VcsProcess.ts";
import * as VcsProvisioningService from "./vcs/VcsProvisioningService.ts";
import * as VcsStatusBroadcaster from "./vcs/VcsStatusBroadcaster.ts";
import * as VersionControlPolicy from "./vcs/VersionControlPolicy.ts";
import * as ProjectCloneTracker from "./project/ProjectCloneTracker.ts";
import * as GitWorkflowService from "./git/GitWorkflowService.ts";
import * as ReviewService from "./review/ReviewService.ts";
Expand Down Expand Up @@ -198,6 +199,10 @@ const ServerSettingsLayerLive = ServerSettings.layer.pipe(
Layer.provideMerge(SqlitePersistenceLayerLive),
);

const VersionControlPolicyLive = VersionControlPolicy.layer.pipe(
Layer.provide(ServerSettingsLayerLive),
);

const NativeTelemetryLayerLive = NativeTelemetryClient.layer.pipe(
Layer.provide(ResourceMonitorBinary.layer),
);
Expand Down Expand Up @@ -553,6 +558,9 @@ const RuntimeCoreDependenciesLive = ReactorLayerLive.pipe(
Layer.provideMerge(WorkspaceLayerLive),
Layer.provideMerge(Layer.mergeAll(NativeAppIconResolver.layer, ProjectFaviconResolverLayerLive)),
Layer.provideMerge(RepositoryIdentityResolverLayerLive),
// Detection, status, and checkpoints require this. The service has no
// default, so a missing layer cannot silently leave Git on.
Layer.provideMerge(VersionControlPolicyLive),
Layer.provideMerge(ServerEnvironmentLayerLive),
Layer.provideMerge(AuthLayerLive),
Layer.provideMerge(ServerSecretStore.layer),
Expand Down
45 changes: 29 additions & 16 deletions apps/server/src/serverRuntimeStartup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,20 @@ import * as GitVcsDriver from "./vcs/GitVcsDriver.ts";
it.effect("automatic pull only updates enabled, behind, clean default-branch checkouts", () =>
Effect.gen(function* () {
const pulled: string[] = [];
const statusCalls: string[] = [];
const git = {
statusDetails: (cwd: string) =>
Effect.succeed({
isRepo: true,
isDefaultBranch: cwd !== "/feature",
hasUpstream: true,
hasWorkingTreeChanges: cwd === "/dirty",
aheadCount: cwd === "/ahead" ? 1 : 0,
behindCount: cwd === "/current" ? 0 : 1,
} as never),
Effect.sync(() => {
statusCalls.push(cwd);
return {
isRepo: true,
isDefaultBranch: cwd !== "/feature",
hasUpstream: true,
hasWorkingTreeChanges: cwd === "/dirty",
aheadCount: cwd === "/ahead" ? 1 : 0,
behindCount: cwd === "/current" ? 0 : 1,
} as never;
}),
pullCurrentBranch: (cwd: string) =>
Effect.sync(() => {
pulled.push(cwd);
Expand All @@ -58,6 +62,14 @@ it.effect("automatic pull only updates enabled, behind, clean default-branch che
),
});

const pullSettings = overrides({
"/clean": true,
"/current": true,
"/dirty": true,
"/ahead": true,
"/feature": true,
"/disabled": false,
});
yield* ServerRuntimeStartup.autoPullProjects(
[
project("/clean"),
Expand All @@ -66,18 +78,19 @@ it.effect("automatic pull only updates enabled, behind, clean default-branch che
project("/ahead"),
project("/feature"),
project("/disabled"),
project("/vcs-off"),
],
overrides({
"/clean": true,
"/current": true,
"/dirty": true,
"/ahead": true,
"/feature": true,
"/disabled": false,
}),
{
...pullSettings,
projectSettingsOverrides: {
...pullSettings.projectSettingsOverrides,
[ProjectId.make("/vcs-off")]: { defaultAutoPull: true, enableVersionControl: false },
},
},
).pipe(Effect.provideService(GitVcsDriver.GitVcsDriver, git));

assert.deepStrictEqual(pulled, ["/clean"]);
assert.equal(statusCalls.includes("/vcs-off"), false);

pulled.length = 0;
yield* ServerRuntimeStartup.autoPullProjects(
Expand Down
6 changes: 5 additions & 1 deletion apps/server/src/serverRuntimeStartup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -834,6 +834,7 @@ interface StartupOptions {
readonly abort?: (error: ServerRuntimeStartupError) => Effect.Effect<void>;
}

/** Pulls clean default-branch checkouts whose version control and auto-pull settings are on. */
export const autoPullProjects = Effect.fn("autoPullProjects")(function* (
projects: ReadonlyArray<OrchestrationProjectShell>,
settings: ServerSettingsValue = DEFAULT_SERVER_SETTINGS,
Expand All @@ -842,7 +843,10 @@ export const autoPullProjects = Effect.fn("autoPullProjects")(function* (
const workspaceRoots = [
...new Set(
projects
.filter((project) => resolveProjectSettings(settings, project.id).settings.defaultAutoPull)
.filter((project) => {
const resolved = resolveProjectSettings(settings, project.id).settings;
return resolved.enableVersionControl && resolved.defaultAutoPull;
})
.map((project) => project.workspaceRoot),
),
];
Expand Down
38 changes: 38 additions & 0 deletions apps/server/src/vcs/VcsDriverRegistry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ChildProcessSpawner } from "effect/unstable/process";

import * as VcsProcess from "./VcsProcess.ts";
import * as VcsProjectConfig from "./VcsProjectConfig.ts";
import * as VersionControlPolicy from "./VersionControlPolicy.ts";
import * as VcsDriverRegistry from "./VcsDriverRegistry.ts";

const processOutput = (stdout: string): VcsProcess.VcsProcessOutput => ({
Expand Down Expand Up @@ -33,6 +34,7 @@ describe("VcsDriverRegistry", () => {
run: () => Effect.succeed(processOutput("")),
}),
),
Layer.provide(VersionControlPolicy.layerTest),
);

return Effect.gen(function* () {
Expand Down Expand Up @@ -73,6 +75,7 @@ describe("VcsDriverRegistry", () => {
}),
}),
),
Layer.provide(VersionControlPolicy.layerTest),
);

return Effect.gen(function* () {
Expand Down Expand Up @@ -127,6 +130,7 @@ describe("VcsDriverRegistry", () => {
}),
}),
),
Layer.provide(VersionControlPolicy.layerTest),
);

return Effect.gen(function* () {
Expand All @@ -137,4 +141,38 @@ describe("VcsDriverRegistry", () => {
assert.equal(insideWorkTreeChecks, 2);
}).pipe(Effect.provide(layer));
});

it.effect("does not probe Git when version control is disabled", () => {
const calls: string[] = [];
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.sync(() => {
calls.push(input.args.join(" "));
return processOutput("true\n");
}),
}),
),
Layer.provide(
Layer.succeed(VersionControlPolicy.VersionControlPolicy, {
isEnabled: () => Effect.succeed(false),
}),
),
);

return Effect.gen(function* () {
const registry = yield* VcsDriverRegistry.VcsDriverRegistry;
assert.equal(yield* registry.detect({ cwd: "/repo" }), null);
const error = yield* registry.resolve({ cwd: "/repo" }).pipe(Effect.flip);
assert.strictEqual(error._tag, "VcsUnsupportedOperationError");
assert.deepStrictEqual(calls, []);
}).pipe(Effect.provide(layer));
});
});
Loading
Loading