Skip to content
Open
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
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