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
46 changes: 45 additions & 1 deletion apps/server/src/git/GitManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
GitActionProgressEvent,
GitPreparePullRequestThreadInput,
ModelSelection,
SourceControlProviderInfo,
ThreadId,
} from "@t3tools/contracts";

Expand Down Expand Up @@ -643,6 +644,8 @@ function preparePullRequestThread(

function makeManager(input?: {
ghScenario?: FakeGhScenario;
sourceControlProviderContext?: SourceControlProviderRegistry.SourceControlProviderHandle["context"];
sourceControlProviderContextSource?: SourceControlProviderRegistry.SourceControlProviderHandle["contextSource"];
textGeneration?: Partial<FakeGitTextGeneration>;
setupScriptRunner?: ProjectSetupScriptRunnerShape;
}) {
Expand All @@ -665,7 +668,12 @@ function makeManager(input?: {
Effect.map((provider) =>
SourceControlProviderRegistry.SourceControlProviderRegistry.of({
get: () => Effect.succeed(provider),
resolveHandle: () => Effect.succeed({ provider, context: null }),
resolveHandle: () =>
Effect.succeed({
provider,
context: input?.sourceControlProviderContext ?? null,
contextSource: input?.sourceControlProviderContextSource ?? null,
}),
resolve: () => Effect.succeed(provider),
discover: Effect.succeed([]),
}),
Expand Down Expand Up @@ -700,6 +708,18 @@ const GitManagerTestLayer = GitVcsDriver.layer.pipe(
Layer.provideMerge(NodeServices.layer),
);

const githubProvider = {
kind: "github",
name: "GitHub",
baseUrl: "https://github.com",
} satisfies SourceControlProviderInfo;

const gitlabProvider = {
kind: "gitlab",
name: "GitLab",
baseUrl: "https://gitlab.com",
} satisfies SourceControlProviderInfo;

it.layer(GitManagerTestLayer)("GitManager", (it) => {
it.effect("status includes PR metadata when branch already has an open PR", () =>
Effect.gen(function* () {
Expand Down Expand Up @@ -742,6 +762,30 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
}),
);

it.effect("status prefers branch remote over detected provider context", () =>
Effect.gen(function* () {
const repoDir = yield* makeTempDir("t3code-git-manager-");
yield* initRepo(repoDir);
yield* runGit(repoDir, ["remote", "add", "origin", "git@gitlab.com:pingdotgg/t3code.git"]);
yield* runGit(repoDir, ["remote", "add", "upstream", "git@github.com:pingdotgg/t3code.git"]);
yield* runGit(repoDir, ["checkout", "-b", "branch-remote"]);
yield* runGit(repoDir, ["config", "branch.branch-remote.remote", "upstream"]);

const { manager } = yield* makeManager({
sourceControlProviderContext: {
provider: gitlabProvider,
remoteName: "origin",
remoteUrl: "git@gitlab.com:pingdotgg/t3code.git",
},
sourceControlProviderContextSource: "detected",
});

const status = yield* manager.localStatus({ cwd: repoDir });

expect(status.sourceControlProvider).toEqual(githubProvider);
}),
);

it.effect("status trims PR metadata returned by gh before publishing it", () =>
Effect.gen(function* () {
const repoDir = yield* makeTempDir("t3code-git-manager-");
Expand Down
21 changes: 20 additions & 1 deletion apps/server/src/git/GitManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -790,6 +790,18 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () {
cwd: string,
branch: string | null,
) {
const providerHandle = yield* sourceControlProviders.resolveHandle({ cwd }).pipe(
Effect.catch(() =>
Effect.succeed({
context: null,
contextSource: null,
}),
),
);
if (providerHandle.contextSource === "override" && providerHandle.context) {
return providerHandle.context.provider;
}
Comment thread
shivamhwp marked this conversation as resolved.

const preferredRemoteName =
branch === null
? "origin"
Expand All @@ -798,7 +810,14 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () {
(yield* readConfigValueNullable(cwd, `remote.${preferredRemoteName}.url`)) ??
(yield* readConfigValueNullable(cwd, "remote.origin.url"));

return remoteUrl ? detectSourceControlProviderFromGitRemoteUrl(remoteUrl) : null;
const providerFromBranchRemote = remoteUrl
? detectSourceControlProviderFromGitRemoteUrl(remoteUrl)
: null;
if (providerFromBranchRemote) {
return providerFromBranchRemote;
}

return providerHandle.context?.provider ?? null;
});

const resolveRemoteRepositoryContext = Effect.fn("resolveRemoteRepositoryContext")(function* (
Expand Down
15 changes: 15 additions & 0 deletions apps/server/src/project/Layers/ProjectSetupScriptRunner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Effect, Layer, Option } from "effect";
import { describe, expect, it, vi } from "vitest";

import { ProjectionSnapshotQuery } from "../../orchestration/Services/ProjectionSnapshotQuery.ts";
import { ServerSettingsService } from "../../serverSettings.ts";
import { TerminalManager } from "../../terminal/Services/Manager.ts";
import { ProjectSetupScriptRunner } from "../Services/ProjectSetupScriptRunner.ts";
import { ProjectSetupScriptRunnerLive } from "./ProjectSetupScriptRunner.ts";
Expand Down Expand Up @@ -47,6 +48,7 @@ describe("ProjectSetupScriptRunner", () => {
Effect.provide(
ProjectSetupScriptRunnerLive.pipe(
Layer.provideMerge(makeProjectionSnapshotQueryLayer(project)),
Layer.provideMerge(ServerSettingsService.layerTest()),
Layer.provideMerge(
Layer.succeed(TerminalManager, {
open,
Expand Down Expand Up @@ -106,6 +108,18 @@ describe("ProjectSetupScriptRunner", () => {
Effect.provide(
ProjectSetupScriptRunnerLive.pipe(
Layer.provideMerge(makeProjectionSnapshotQueryLayer(project)),
Layer.provideMerge(
ServerSettingsService.layerTest({
projectSettings: {
[project.id]: {
remoteOverride: null,
actionEnvironment: {
API_BASE_URL: "https://api.example.test",
},
},
},
}),
),
Layer.provideMerge(
Layer.succeed(TerminalManager, {
open,
Expand Down Expand Up @@ -143,6 +157,7 @@ describe("ProjectSetupScriptRunner", () => {
cwd: "/repo/worktrees/a",
worktreePath: "/repo/worktrees/a",
env: {
API_BASE_URL: "https://api.example.test",
T3CODE_PROJECT_ROOT: "/repo/project",
T3CODE_WORKTREE_PATH: "/repo/worktrees/a",
},
Expand Down
5 changes: 5 additions & 0 deletions apps/server/src/project/Layers/ProjectSetupScriptRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { projectScriptRuntimeEnv, setupProjectScript } from "@t3tools/shared/pro
import { Effect, Layer, Option } from "effect";

import { ProjectionSnapshotQuery } from "../../orchestration/Services/ProjectionSnapshotQuery.ts";
import { ServerSettingsService } from "../../serverSettings.ts";
import { TerminalManager } from "../../terminal/Services/Manager.ts";
import {
type ProjectSetupScriptRunnerShape,
Expand All @@ -11,6 +12,7 @@ import {

const makeProjectSetupScriptRunner = Effect.gen(function* () {
const projectionSnapshotQuery = yield* ProjectionSnapshotQuery;
const serverSettings = yield* ServerSettingsService;
const terminalManager = yield* TerminalManager;

const runForThread: ProjectSetupScriptRunnerShape["runForThread"] = (input) =>
Expand Down Expand Up @@ -41,9 +43,12 @@ const makeProjectSetupScriptRunner = Effect.gen(function* () {

const terminalId = input.preferredTerminalId ?? `setup-${script.id}`;
const cwd = input.worktreePath;
const settings = yield* serverSettings.getSettings;
const actionEnvironment = settings.projectSettings[project.id]?.actionEnvironment ?? {};
const env = projectScriptRuntimeEnv({
project: { cwd: project.workspaceRoot },
worktreePath: input.worktreePath,
extraEnv: actionEnvironment,
});

yield* terminalManager.open({
Expand Down
8 changes: 3 additions & 5 deletions apps/server/src/serverRuntimeStartup.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as NodeServices from "@effect/platform-node/NodeServices";
import { DEFAULT_MODEL, ProjectId, ProviderInstanceId, ThreadId } from "@t3tools/contracts";
import { ProjectId, ThreadId } from "@t3tools/contracts";
import { createDefaultModelSelection } from "@t3tools/shared/model";
import { assert, it } from "@effect/vitest";
import { Deferred, Effect, Fiber, Option, Ref, Stream } from "effect";

Expand All @@ -20,10 +21,7 @@ import {
} from "./serverRuntimeStartup.ts";

it("uses the canonical Codex default for auto-bootstrapped model selection", () => {
assert.deepStrictEqual(getAutoBootstrapDefaultModelSelection(), {
instanceId: ProviderInstanceId.make("codex"),
model: DEFAULT_MODEL,
});
assert.deepStrictEqual(getAutoBootstrapDefaultModelSelection(), createDefaultModelSelection());
});

it.effect("enqueueCommand waits for readiness and then drains queued work", () =>
Expand Down
9 changes: 3 additions & 6 deletions apps/server/src/serverRuntimeStartup.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import {
CommandId,
DEFAULT_MODEL,
DEFAULT_PROVIDER_INTERACTION_MODE,
type ModelSelection,
ProjectId,
ProviderInstanceId,
ThreadId,
} from "@t3tools/contracts";
import { createDefaultModelSelection } from "@t3tools/shared/model";
import {
Data,
Deferred,
Expand Down Expand Up @@ -154,10 +153,8 @@ export const launchStartupHeartbeat = recordStartupHeartbeat.pipe(
Effect.asVoid,
);

export const getAutoBootstrapDefaultModelSelection = (): ModelSelection => ({
instanceId: ProviderInstanceId.make("codex"),
model: DEFAULT_MODEL,
});
export const getAutoBootstrapDefaultModelSelection = (): ModelSelection =>
createDefaultModelSelection();

export const resolveWelcomeBase = Effect.gen(function* () {
const serverConfig = yield* ServerConfig;
Expand Down
77 changes: 77 additions & 0 deletions apps/server/src/sourceControl/RemoteOverride.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import type {
ProjectRemoteOverride,
SourceControlProviderInfo,
SourceControlProviderKind,
} from "@t3tools/contracts";

import * as SourceControlProvider from "./SourceControlProvider.ts";

export function parseRemoteHost(remoteUrl: string): string | null {
const trimmed = remoteUrl.trim();
if (trimmed.startsWith("git@")) {
const hostWithPath = trimmed.slice("git@".length);
const separatorIndex = hostWithPath.search(/[:/]/);
return separatorIndex > 0 ? hostWithPath.slice(0, separatorIndex).toLowerCase() : null;
}

try {
const hostname = new URL(trimmed).hostname.toLowerCase();
return hostname || null;
} catch {
return null;
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
}
}

export function parseBaseUrl(value: string): string | null {
try {
const url = new URL(value);
return `${url.protocol}//${url.host}`;
} catch {
const host = parseRemoteHost(value);
return host ? `https://${host}` : null;
}
}

export function providerName(kind: SourceControlProviderKind, baseUrl: string | null): string {
switch (kind) {
case "github":
return baseUrl === "https://github.com" ? "GitHub" : "GitHub Self-Hosted";
case "gitlab":
return baseUrl === "https://gitlab.com" ? "GitLab" : "GitLab Self-Hosted";
case "azure-devops":
return "Azure DevOps";
case "bitbucket":
return baseUrl === "https://bitbucket.org" ? "Bitbucket" : "Bitbucket Self-Hosted";
case "unknown":
return parseRemoteHost(baseUrl ?? "") ?? "Source control";
}
}

export function providerInfoFromOverride(
override: ProjectRemoteOverride,
): SourceControlProviderInfo | null {
const baseUrl = override.webUrl
? parseBaseUrl(override.webUrl)
: parseBaseUrl(override.remoteUrl);
if (!baseUrl) {
return null;
}
return {
kind: override.provider,
name: providerName(override.provider, baseUrl),
baseUrl,
};
}

export function providerContextFromOverride(
override: ProjectRemoteOverride,
): SourceControlProvider.SourceControlProviderContext | null {
const provider = providerInfoFromOverride(override);
return provider
? {
provider,
remoteName: override.remoteName ?? "origin",
remoteUrl: override.remoteUrl,
}
: null;
}
6 changes: 6 additions & 0 deletions apps/server/src/sourceControl/SourceControlDiscovery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { ChildProcessSpawner } from "effect/unstable/process";
import { VcsProcessSpawnError } from "@t3tools/contracts";

import { ServerConfig } from "../config.ts";
import { ProjectionSnapshotQuery } from "../orchestration/Services/ProjectionSnapshotQuery.ts";
import { ServerSettingsService } from "../serverSettings.ts";
import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts";
import * as VcsProcess from "../vcs/VcsProcess.ts";
import * as AzureDevOpsCli from "./AzureDevOpsCli.ts";
Expand All @@ -28,6 +30,10 @@ const sourceControlProviderRegistryTestLayer = (input: {
Layer.mock(BitbucketApi.BitbucketApi)(input.bitbucket),
Layer.mock(GitHubCli.GitHubCli)({}),
Layer.mock(GitLabCli.GitLabCli)({}),
Layer.mock(ProjectionSnapshotQuery)({
getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()),
}),
ServerSettingsService.layerTest(),
Layer.mock(VcsDriverRegistry.VcsDriverRegistry)({}),
Layer.mock(VcsProcess.VcsProcess)(input.process),
),
Expand Down
Loading
Loading