Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
9 changes: 1 addition & 8 deletions apps/server/src/checkpointing/Layers/CheckpointStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,9 @@ import { CheckpointStore } from "../Services/CheckpointStore.ts";
import { GitCoreLive } from "../../git/Layers/GitCore.ts";
import { GitCore } from "../../git/Services/GitCore.ts";
import { GitCommandError } from "@t3tools/contracts";
import { ServerConfig } from "../../config.ts";
import { ThreadId } from "@t3tools/contracts";

const ServerConfigLayer = ServerConfig.layerTest(process.cwd(), {
prefix: "t3-checkpoint-store-test-",
});
const GitCoreTestLayer = GitCoreLive.pipe(
Layer.provide(ServerConfigLayer),
Layer.provide(NodeServices.layer),
);
const GitCoreTestLayer = GitCoreLive.pipe(Layer.provide(NodeServices.layer));
const CheckpointStoreTestLayer = CheckpointStoreLive.pipe(
Layer.provide(GitCoreTestLayer),
Layer.provide(NodeServices.layer),
Expand Down
11 changes: 2 additions & 9 deletions apps/server/src/git/Layers/GitCore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,10 @@ import { GitCoreLive, makeGitCore } from "./GitCore.ts";
import { GitCore, type GitCoreShape } from "../Services/GitCore.ts";
import { GitCommandError } from "@t3tools/contracts";
import { type ProcessRunResult, runProcess } from "../../processRunner.ts";
import { ServerConfig } from "../../config.ts";

// ── Helpers ──

const ServerConfigLayer = ServerConfig.layerTest(process.cwd(), { prefix: "t3-git-core-test-" });
const GitCoreTestLayer = GitCoreLive.pipe(
Layer.provide(ServerConfigLayer),
Layer.provide(NodeServices.layer),
);
const GitCoreTestLayer = GitCoreLive.pipe(Layer.provide(NodeServices.layer));
const TestLayer = Layer.mergeAll(NodeServices.layer, GitCoreTestLayer);

function makeTmpDir(
Expand Down Expand Up @@ -120,9 +115,7 @@ function runShellCommand(input: {
}

const makeIsolatedGitCore = (executeOverride: GitCoreShape["execute"]) =>
makeGitCore({ executeOverride }).pipe(
Effect.provide(Layer.provideMerge(ServerConfigLayer, NodeServices.layer)),
);
makeGitCore({ executeOverride }).pipe(Effect.provide(NodeServices.layer));

/** Create a repo with an initial commit so branches work. */
function initRepoWithCommit(
Expand Down
6 changes: 1 addition & 5 deletions apps/server/src/git/Layers/GitCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ import {
parseRemoteNamesInGitOrder,
parseRemoteRefWithRemoteNames,
} from "../remoteRefs.ts";
import { ServerConfig } from "../../config.ts";
import { decodeJsonResult } from "@t3tools/shared/schemaJson";

const DEFAULT_TIMEOUT_MS = 30_000;
Expand Down Expand Up @@ -660,7 +659,6 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: {
}) {
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const { worktreesDir } = yield* ServerConfig;

let executeRaw: GitCoreShape["execute"];

Expand Down Expand Up @@ -1955,9 +1953,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: {
const createWorktree: GitCoreShape["createWorktree"] = Effect.fn("createWorktree")(
function* (input) {
const targetBranch = input.newBranch ?? input.branch;
const sanitizedBranch = targetBranch.replace(/\//g, "-");
const repoName = path.basename(input.cwd);
const worktreePath = input.path ?? path.join(worktreesDir, repoName, sanitizedBranch);
const worktreePath = input.path;
const args = input.newBranch
? ["worktree", "add", "-b", input.newBranch, worktreePath, input.branch]
: ["worktree", "add", worktreePath, input.branch];
Expand Down
20 changes: 7 additions & 13 deletions apps/server/src/git/Layers/GitManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ import {
type GitHubPullRequestSummary,
GitHubCli,
} from "../Services/GitHubCli.ts";
import { WorktreeLocationResolver } from "../../project/Services/WorktreeLocationResolver.ts";
import { type TextGenerationShape, TextGeneration } from "../Services/TextGeneration.ts";
import { GitCoreLive } from "./GitCore.ts";
import { GitCore } from "../Services/GitCore.ts";
import { makeGitManager } from "./GitManager.ts";
import { ServerConfig } from "../../config.ts";
import { ServerSettingsService } from "../../serverSettings.ts";
import {
ProjectSetupScriptRunner,
Expand Down Expand Up @@ -630,19 +630,15 @@ function makeManager(input?: {
ghScenario?: FakeGhScenario;
textGeneration?: Partial<FakeGitTextGeneration>;
setupScriptRunner?: ProjectSetupScriptRunnerShape;
serverSettings?: Parameters<typeof ServerSettingsService.layerTest>[0];
}) {
const { service: gitHubCli, ghCalls } = createGitHubCliWithFakeGh(input?.ghScenario);
const textGeneration = createTextGeneration(input?.textGeneration);
const ServerConfigLayer = ServerConfig.layerTest(process.cwd(), {
prefix: "t3-git-manager-test-",
});

const serverSettingsLayer = ServerSettingsService.layerTest();
const serverSettingsLayer = ServerSettingsService.layerTest(input?.serverSettings);
const worktreeLocationResolverLayer = WorktreeLocationResolver.layerTest();

const gitCoreLayer = GitCoreLive.pipe(
Layer.provideMerge(NodeServices.layer),
Layer.provideMerge(ServerConfigLayer),
);
const gitCoreLayer = GitCoreLive.pipe(Layer.provideMerge(NodeServices.layer));

const managerLayer = Layer.mergeAll(
Layer.succeed(GitHubCli, gitHubCli),
Expand All @@ -655,6 +651,7 @@ function makeManager(input?: {
),
gitCoreLayer,
serverSettingsLayer,
worktreeLocationResolverLayer,
).pipe(Layer.provideMerge(NodeServices.layer));

return makeGitManager().pipe(
Expand All @@ -665,10 +662,7 @@ function makeManager(input?: {

const asThreadId = (threadId: string) => threadId as ThreadId;

const GitManagerTestLayer = GitCoreLive.pipe(
Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-git-manager-test-" })),
Layer.provideMerge(NodeServices.layer),
);
const GitManagerTestLayer = GitCoreLive.pipe(Layer.provideMerge(NodeServices.layer));

it.layer(GitManagerTestLayer)("GitManager", (it) => {
it.effect("status includes PR metadata when branch already has an open PR", () =>
Expand Down
17 changes: 16 additions & 1 deletion apps/server/src/git/Layers/GitManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import { ProjectSetupScriptRunner } from "../../project/Services/ProjectSetupScr
import { extractBranchNameFromRemoteRef } from "../remoteRefs.ts";
import { ServerSettingsService } from "../../serverSettings.ts";
import type { GitManagerServiceError } from "@t3tools/contracts";
import { WorktreeLocationResolver } from "../../project/Services/WorktreeLocationResolver.ts";
import {
decodeGitHubPullRequestListJson,
formatGitHubJsonDecodeError,
Expand Down Expand Up @@ -495,6 +496,7 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () {
const textGeneration = yield* TextGeneration;
const projectSetupScriptRunner = yield* ProjectSetupScriptRunner;
const serverSettingsService = yield* ServerSettingsService;
const worktreeLocationResolver = yield* WorktreeLocationResolver;

const createProgressEmitter = (
input: { cwd: string; action: GitStackedAction },
Expand Down Expand Up @@ -616,6 +618,15 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () {
},
);

const resolveCreateWorktreePath = Effect.fn("GitManager.resolveCreateWorktreePath")(
function* (input: { projectRoot: string; branch: string; newBranch?: string }) {
return yield* worktreeLocationResolver.resolveCreateWorktreePath({
projectRoot: input.projectRoot,
name: input.newBranch ?? input.branch,
});
},
);

const materializePullRequestHeadBranch = (
cwd: string,
pullRequest: ResolvedPullRequest & PullRequestHeadRemoteInfo,
Expand Down Expand Up @@ -1486,10 +1497,14 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () {
);
}

const worktreePath = yield* resolveCreateWorktreePath({
projectRoot: input.cwd,
branch: localPullRequestBranch,
});
const worktree = yield* gitCore.createWorktree({
cwd: input.cwd,
branch: localPullRequestBranch,
path: null,
path: worktreePath,
});
yield* ensureExistingWorktreeUpstream(worktree.worktree.path);
yield* maybeRunSetupScript(worktree.worktree.path);
Expand Down
10 changes: 8 additions & 2 deletions apps/server/src/git/Services/GitCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import type {
GitCheckoutResult,
GitCreateBranchInput,
GitCreateBranchResult,
GitCreateWorktreeInput,
GitCreateWorktreeResult,
GitInitInput,
GitListBranchesInput,
Expand All @@ -26,6 +25,13 @@ import type {

import type { GitCommandError } from "@t3tools/contracts";

export interface GitCoreCreateWorktreeInput {
readonly cwd: string;
readonly branch: string;
readonly newBranch?: string;
readonly path: string;
}

export interface ExecuteGitInput {
readonly operation: string;
readonly cwd: string;
Expand Down Expand Up @@ -241,7 +247,7 @@ export interface GitCoreShape {
* Create a worktree and branch from a base branch.
*/
readonly createWorktree: (
input: GitCreateWorktreeInput,
input: GitCoreCreateWorktreeInput,
) => Effect.Effect<GitCreateWorktreeResult, GitCommandError>;

/**
Expand Down
67 changes: 67 additions & 0 deletions apps/server/src/project/Layers/WorktreeLocationResolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Effect, Layer } from "effect";
import { WorktreeLocationResolverError } from "@t3tools/contracts";

import {
createWorktreeLocationTemplateContext,
resolveWorktreeLocation,
sanitizeWorktreeName,
} from "@t3tools/shared/worktreeLocation";
import { ServerConfig } from "../../config.ts";
import { ServerSettingsService } from "../../serverSettings.ts";
import {
WorktreeLocationResolver,
type WorktreeLocationResolverShape,
} from "../Services/WorktreeLocationResolver.ts";

function createWorktreeLocationResolverError(
projectRoot: string,
detail: string,
cause?: unknown,
): WorktreeLocationResolverError {
return new WorktreeLocationResolverError({
projectRoot,
detail,
...(cause !== undefined ? { cause } : {}),
});
}

export const makeWorktreeLocationResolver = Effect.gen(function* () {
const { baseDir, worktreesDir } = yield* ServerConfig;
const serverSettings = yield* ServerSettingsService;

const resolveCreateWorktreePath: WorktreeLocationResolverShape["resolveCreateWorktreePath"] =
Effect.fn("WorktreeLocationResolver.resolveCreateWorktreePath")(function* (input) {
const settings = yield* serverSettings.getSettings.pipe(
Effect.mapError((error) =>
createWorktreeLocationResolverError(input.projectRoot, error.message, error),
),
);
const resolvedLocation = resolveWorktreeLocation({
mode: settings.worktreeLocation.mode,
template: settings.worktreeLocation.template,
context: createWorktreeLocationTemplateContext({
t3Home: baseDir,
projectRoot: input.projectRoot,
worktreeName: sanitizeWorktreeName(input.name),
}),
defaultWorktreesDir: worktreesDir,
});
if (!resolvedLocation.ok) {
return yield* createWorktreeLocationResolverError(
input.projectRoot,
`invalid custom worktree template: ${resolvedLocation.error}`,
);
}

return resolvedLocation.path;
});

return {
resolveCreateWorktreePath,
} satisfies WorktreeLocationResolverShape;
});

export const WorktreeLocationResolverLive = Layer.effect(
WorktreeLocationResolver,
makeWorktreeLocationResolver,
);
31 changes: 31 additions & 0 deletions apps/server/src/project/Services/WorktreeLocationResolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { WorktreeLocationResolverError } from "@t3tools/contracts";
import { Effect, Context, Layer } from "effect";
import path from "node:path";

export interface ResolveCreateWorktreePathInput {
readonly projectRoot: string;
readonly name: string;
}

export interface WorktreeLocationResolverShape {
readonly resolveCreateWorktreePath: (
input: ResolveCreateWorktreePathInput,
) => Effect.Effect<string, WorktreeLocationResolverError>;
}

export class WorktreeLocationResolver extends Context.Service<
WorktreeLocationResolver,
WorktreeLocationResolverShape
>()("t3/project/Services/WorktreeLocationResolver") {
static readonly layerTest = ({
resolveCreateWorktreePath = (input) =>
Effect.succeed(
path.join(input.projectRoot, ".mock-worktrees", input.name.replace(/\//g, "-")),
),
}: {
resolveCreateWorktreePath?: WorktreeLocationResolverShape["resolveCreateWorktreePath"];
} = {}) =>
Layer.succeed(WorktreeLocationResolver, {
resolveCreateWorktreePath,
});
}
14 changes: 13 additions & 1 deletion apps/server/src/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ import {
import { GitCore, type GitCoreShape } from "./git/Services/GitCore.ts";
import { GitManager, type GitManagerShape } from "./git/Services/GitManager.ts";
import { GitStatusBroadcasterLive } from "./git/Layers/GitStatusBroadcaster.ts";
import {
WorktreeLocationResolver,
type WorktreeLocationResolverShape,
} from "./project/Services/WorktreeLocationResolver.ts";
import { Keybindings, type KeybindingsShape } from "./keybindings.ts";
import { Open, type OpenShape } from "./open.ts";
import {
Expand Down Expand Up @@ -293,6 +297,7 @@ const buildAppUnderTest = (options?: {
open?: Partial<OpenShape>;
gitCore?: Partial<GitCoreShape>;
gitManager?: Partial<GitManagerShape>;
worktreeLocationResolver?: Partial<WorktreeLocationResolverShape>;
projectSetupScriptRunner?: Partial<ProjectSetupScriptRunnerShape>;
terminalManager?: Partial<TerminalManagerShape>;
orchestrationEngine?: Partial<OrchestrationEngineShape>;
Expand Down Expand Up @@ -385,6 +390,13 @@ const buildAppUnderTest = (options?: {
...options?.layers?.gitCore,
}),
),
Layer.provide(
Layer.mock(WorktreeLocationResolver)({
resolveCreateWorktreePath: (input) =>
Effect.succeed(`/tmp/worktrees/${input.name.replace(/\//g, "-")}`),
...options?.layers?.worktreeLocationResolver,
}),
),
Layer.provide(gitManagerLayer),
Layer.provideMerge(gitStatusBroadcasterLayer),
Layer.provide(
Expand Down Expand Up @@ -2997,7 +3009,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
cwd: "/tmp/project",
branch: "main",
newBranch: "t3code/bootstrap-branch",
path: null,
path: "/tmp/worktrees/t3code-bootstrap-branch",
});
assert.deepEqual(runForThread.mock.calls[0]?.[0], {
threadId: ThreadId.make("thread-bootstrap"),
Expand Down
11 changes: 9 additions & 2 deletions apps/server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { GitCoreLive } from "./git/Layers/GitCore";
import { GitHubCliLive } from "./git/Layers/GitHubCli";
import { GitStatusBroadcasterLive } from "./git/Layers/GitStatusBroadcaster";
import { RoutingTextGenerationLive } from "./git/Layers/RoutingTextGeneration";
import { WorktreeLocationResolverLive } from "./project/Layers/WorktreeLocationResolver";
import { TerminalManagerLive } from "./terminal/Layers/Manager";
import { GitManagerLive } from "./git/Layers/GitManager";
import { KeybindingsLive } from "./keybindings";
Expand Down Expand Up @@ -165,17 +166,22 @@ const ProviderLayerLive = Layer.unwrap(

const PersistenceLayerLive = Layer.empty.pipe(Layer.provideMerge(SqlitePersistenceLayerLive));

const GitCoreLayerLive = GitCoreLive;
const WorktreeLocationLayerLive = WorktreeLocationResolverLive.pipe(
Layer.provideMerge(ServerSettingsLive),
);

const GitManagerLayerLive = GitManagerLive.pipe(
Layer.provideMerge(ProjectSetupScriptRunnerLive),
Layer.provideMerge(GitCoreLive),
Layer.provideMerge(GitCoreLayerLive),
Layer.provideMerge(GitHubCliLive),
Layer.provideMerge(RoutingTextGenerationLive),
);

const GitLayerLive = Layer.empty.pipe(
Layer.provideMerge(GitManagerLayerLive),
Layer.provideMerge(GitStatusBroadcasterLive.pipe(Layer.provide(GitManagerLayerLive))),
Layer.provideMerge(GitCoreLive),
Layer.provideMerge(GitCoreLayerLive),
);

const TerminalLayerLive = TerminalManagerLive.pipe(Layer.provide(PtyAdapterLive));
Expand Down Expand Up @@ -205,6 +211,7 @@ const RuntimeDependenciesLive = ReactorLayerLive.pipe(
Layer.provideMerge(KeybindingsLive),
Layer.provideMerge(ProviderRegistryLive),
Layer.provideMerge(ServerSettingsLive),
Layer.provideMerge(WorktreeLocationLayerLive),
Layer.provideMerge(WorkspaceLayerLive),
Layer.provideMerge(ProjectFaviconResolverLive),
Layer.provideMerge(RepositoryIdentityResolverLive),
Expand Down
Loading
Loading