diff --git a/apps/server/src/server.ts b/apps/server/src/server.ts index d5e42154898cd..8b1cce579162f 100644 --- a/apps/server/src/server.ts +++ b/apps/server/src/server.ts @@ -130,6 +130,7 @@ import * as KnownWorkspaces from "./workspace/KnownWorkspaces.ts"; import * as WorkspaceFileSystem from "./workspace/WorkspaceFileSystem.ts"; import * as WorkspacePaths from "./workspace/WorkspacePaths.ts"; import * as GitVcsDriver from "./vcs/GitVcsDriver.ts"; +import * as VcsAutomaticFetchInterval from "./vcs/VcsAutomaticFetchInterval.ts"; import * as VcsDriverRegistry from "./vcs/VcsDriverRegistry.ts"; import * as VcsProjectConfig from "./vcs/VcsProjectConfig.ts"; import * as VcsProcess from "./vcs/VcsProcess.ts"; @@ -489,7 +490,6 @@ const layerVcs = Layer.empty.pipe( Layer.provideMerge( VcsStatusBroadcaster.layer.pipe( Layer.provide(layerGitWorkflow), - Layer.provide(VcsStatusBroadcaster.layerAutomaticFetchInterval), // The orchestration runtime also consumes the broadcaster (run // finalization), so the policy cannot read the store from the runtime's // output. @@ -802,6 +802,9 @@ const layerRuntimeDependencies = layerRuntimeCoreDependencies.pipe( Layer.provideMerge(RemoteOpenTargets.layer), Layer.provideMerge(DirectEndpoints.layer.pipe(Layer.provide(layerServerEnvironment))), Layer.provideMerge(ServerLifecycleEvents.layer), + // GitVcsDriver.layer is memoized and some of its build sites live outside this file, so the + // fetch interval goes in context here, before the first build captures it. + Layer.provideMerge(VcsAutomaticFetchInterval.layer.pipe(Layer.provide(layerServerSettings))), Layer.provide(NetService.layer), ); diff --git a/apps/server/src/vcs/GitVcsDriverCore.test.ts b/apps/server/src/vcs/GitVcsDriverCore.test.ts index 76b1fb4f898b5..50b4b447f684b 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.test.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.test.ts @@ -12,6 +12,7 @@ import { afterAll, vi } from "vite-plus/test"; import * as Cause from "effect/Cause"; import * as Clock from "effect/Clock"; import * as Deferred from "effect/Deferred"; +import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; import * as Exit from "effect/Exit"; import * as FileSystem from "effect/FileSystem"; @@ -50,6 +51,7 @@ import { windowsLongPathConfigEnv, } from "./GitVcsDriverCore.ts"; import * as GitVcsDriver from "./GitVcsDriver.ts"; +import { VcsAutomaticFetchInterval } from "./VcsAutomaticFetchInterval.ts"; import { gitConfigEnv } from "./VcsProcess.ts"; const encodeGitCommandError = Schema.encodeEffect(Schema.fromJsonString(GitCommandError)); @@ -1831,6 +1833,37 @@ it.effect("does not fetch for status when the branch tracks a local branch", () ).pipe(Effect.provide(layerTest)), ); +it.effect("does not fetch for status when the automatic fetch interval is zero", () => + Effect.scoped( + Effect.gen(function* () { + const delegate = yield* ChildProcessSpawner.ChildProcessSpawner; + const fetches: Array> = []; + const spawner = ChildProcessSpawner.make((command) => { + if (ChildProcess.isStandardCommand(command) && command.args.includes("fetch")) { + fetches.push(command.args.map(String)); + } + return delegate.spawn(command); + }); + const driver = yield* makeGitVcsDriverCore().pipe( + Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), + Effect.provideService(VcsAutomaticFetchInterval, Effect.succeed(Duration.zero)), + ); + const cwd = yield* makeTmpDir(); + const remote = yield* makeTmpDir("git-vcs-driver-remote-"); + const { initialBranch } = yield* initRepoWithCommit(cwd); + yield* git(remote, ["init", "--bare"]); + yield* git(cwd, ["remote", "add", "origin", remote]); + yield* git(cwd, ["push", "--set-upstream", "origin", initialBranch]); + + yield* driver.statusDetails(cwd); + const status = yield* driver.statusDetailsRemote(cwd); + + assert.equal(status.upstreamRef, `origin/${initialBranch}`); + assert.deepStrictEqual(fetches, []); + }), + ).pipe(Effect.provide(layerTest)), +); + it.effect("lets a worktree checkout that is still working run past five minutes", () => Effect.gen(function* () { const checkoutStarted = yield* Deferred.make(); diff --git a/apps/server/src/vcs/GitVcsDriverCore.ts b/apps/server/src/vcs/GitVcsDriverCore.ts index 280664cf8a136..ebcc16f61e5ab 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.ts @@ -44,6 +44,7 @@ import { resolveProjectFileBackedSetting } from "@t3tools/shared/projectSettings import { gitCommandDuration, gitCommandsTotal, withMetrics } from "../observability/Metrics.ts"; import * as GitVcsDriver from "./GitVcsDriver.ts"; import { removeEmptyDirectory, type RemoveEmptyDirectoryError } from "./removeEmptyDirectory.ts"; +import { VcsAutomaticFetchInterval } from "./VcsAutomaticFetchInterval.ts"; import { explicitBareRepositoryEnv, gitConfigEnv, @@ -992,6 +993,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* const random = yield* SecureRandom.SecureRandom; const hostPlatform = yield* HostProcess.Platform; const serviceScope = yield* Effect.scope; + const automaticFetchInterval = yield* VcsAutomaticFetchInterval; const statusFetches = new Map>(); const statusFetchProcesses = yield* Semaphore.make(4); @@ -1710,6 +1712,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* const refreshStatusUpstreamIfStale = Effect.fn("refreshStatusUpstreamIfStale")(function* ( cwd: string, ) { + if (Duration.isZero(yield* automaticFetchInterval)) return; const headRef = yield* runGitStdout( "GitVcsDriver.refreshStatusUpstreamIfStale.headRef", cwd, diff --git a/apps/server/src/vcs/VcsAutomaticFetchInterval.ts b/apps/server/src/vcs/VcsAutomaticFetchInterval.ts new file mode 100644 index 0000000000000..0f2c900b3ba11 --- /dev/null +++ b/apps/server/src/vcs/VcsAutomaticFetchInterval.ts @@ -0,0 +1,26 @@ +import * as Context from "effect/Context"; +import * as Duration from "effect/Duration"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import { DEFAULT_AUTOMATIC_GIT_FETCH_INTERVAL } from "@t3tools/contracts"; +import { resolveServerBackgroundActivitySettings } from "@t3tools/shared/backgroundActivitySettings"; + +import * as ServerSettings from "../serverSettings.ts"; + +export class VcsAutomaticFetchInterval extends Context.Reference>( + "t3/vcs/VcsAutomaticFetchInterval", + { defaultValue: () => Effect.succeed(DEFAULT_AUTOMATIC_GIT_FETCH_INTERVAL) }, +) {} + +export const layer = Layer.effect( + VcsAutomaticFetchInterval, + Effect.gen(function* () { + const serverSettings = yield* ServerSettings.ServerSettingsService; + return serverSettings.getSettings.pipe( + Effect.map( + (settings) => resolveServerBackgroundActivitySettings(settings).automaticGitFetchInterval, + ), + Effect.orElseSucceed(() => DEFAULT_AUTOMATIC_GIT_FETCH_INTERVAL), + ); + }), +); diff --git a/apps/server/src/vcs/VcsStatusBroadcaster.test.ts b/apps/server/src/vcs/VcsStatusBroadcaster.test.ts index 0a336a66ca2ff..cbe3dafd8fc5b 100644 --- a/apps/server/src/vcs/VcsStatusBroadcaster.test.ts +++ b/apps/server/src/vcs/VcsStatusBroadcaster.test.ts @@ -28,6 +28,7 @@ import type { import { GitCommandError, GitManagerError } from "@t3tools/contracts"; import { applyGitStatusStreamEvent } from "@t3tools/shared/git"; +import * as VcsAutomaticFetchInterval from "./VcsAutomaticFetchInterval.ts"; import * as VcsStatusBroadcaster from "./VcsStatusBroadcaster.ts"; import * as BackgroundPolicy from "../background/BackgroundPolicy.ts"; import * as GitWorkflowService from "../git/GitWorkflowService.ts"; @@ -101,7 +102,7 @@ function layerTestFor(state: { Layer.provide(layerBackgroundPolicy(() => state.backgroundWorkEnabled !== false)), Layer.provide( Layer.succeed( - VcsStatusBroadcaster.VcsAutomaticFetchInterval, + VcsAutomaticFetchInterval.VcsAutomaticFetchInterval, state.automaticFetchInterval ?? Effect.succeed(Duration.seconds(30)), ), ), diff --git a/apps/server/src/vcs/VcsStatusBroadcaster.ts b/apps/server/src/vcs/VcsStatusBroadcaster.ts index 00edfc3634a67..eeb2b6cc982b9 100644 --- a/apps/server/src/vcs/VcsStatusBroadcaster.ts +++ b/apps/server/src/vcs/VcsStatusBroadcaster.ts @@ -25,7 +25,6 @@ import type { VcsStatusSubscriptionInput, } from "@t3tools/contracts"; import { DEFAULT_AUTOMATIC_GIT_FETCH_INTERVAL } from "@t3tools/contracts"; -import { resolveServerBackgroundActivitySettings } from "@t3tools/shared/backgroundActivitySettings"; import { mergeGitStatusParts } from "@t3tools/shared/git"; import * as KeyedLock from "@t3tools/shared/KeyedLock"; import { resolveProjectSettings } from "@t3tools/shared/projectSettings"; @@ -34,6 +33,7 @@ import * as BackgroundPolicy from "../background/BackgroundPolicy.ts"; import * as GitWorkflowService from "../git/GitWorkflowService.ts"; import * as ProjectStore from "../orchestration-v2/ProjectStore.ts"; import * as ServerSettings from "../serverSettings.ts"; +import { VcsAutomaticFetchInterval } from "./VcsAutomaticFetchInterval.ts"; const VCS_STATUS_REFRESH_FAILURE_BASE_DELAY = Duration.seconds(30); const VCS_STATUS_REFRESH_FAILURE_MAX_DELAY = Duration.minutes(15); @@ -160,24 +160,6 @@ interface ActiveRemotePoller { readonly demandCwds: Ref.Ref>; } -export class VcsAutomaticFetchInterval extends Context.Reference>( - "t3/vcs/VcsAutomaticFetchInterval", - { defaultValue: () => Effect.succeed(DEFAULT_AUTOMATIC_GIT_FETCH_INTERVAL) }, -) {} - -export const layerAutomaticFetchInterval = Layer.effect( - VcsAutomaticFetchInterval, - Effect.gen(function* () { - const serverSettings = yield* ServerSettings.ServerSettingsService; - return serverSettings.getSettings.pipe( - Effect.map( - (settings) => resolveServerBackgroundActivitySettings(settings).automaticGitFetchInterval, - ), - Effect.orElseSucceed(() => DEFAULT_AUTOMATIC_GIT_FETCH_INTERVAL), - ); - }), -); - export class VcsAutoPullPolicy extends Context.Reference<{ readonly isEnabled: (cwd: string) => Effect.Effect; }>("t3/vcs/VcsAutoPullPolicy", {