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
43 changes: 41 additions & 2 deletions apps/mobile/src/components/SourceControlIcon.tsx
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
import Svg, { Defs, LinearGradient, Path, Stop } from "react-native-svg";
import Svg, { Circle, Defs, LinearGradient, Line, Path, Stop } from "react-native-svg";

export type SourceControlIconKind = "github" | "gitlab" | "bitbucket" | "azure-devops";
export type SourceControlIconKind = "github" | "gitlab" | "bitbucket" | "azure-devops" | "gitea";

export function SourceControlIcon(props: {
readonly kind: SourceControlIconKind;
Expand DownExpand Up@@ -95,5 +95,44 @@ export function SourceControlIcon(props: {
/>
</Svg>
);
// Gitea ships no bundled logo here yet, so it uses the neutral pull-request mark rather than
// another host's brand, matching the web client.
case "gitea":
return (
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
<Circle
cx="18"
cy="18"
r="3"
stroke={props.color ?? "currentColor"}
strokeWidth="2"
strokeLinecap="round"
/>
<Circle
cx="6"
cy="6"
r="3"
stroke={props.color ?? "currentColor"}
strokeWidth="2"
strokeLinecap="round"
/>
<Path
d="M13 6h3a2 2 0 0 1 2 2v7"
stroke={props.color ?? "currentColor"}
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<Line
x1="6"
y1="9"
x2="6"
y2="21"
stroke={props.color ?? "currentColor"}
strokeWidth="2"
strokeLinecap="round"
/>
</Svg>
);
}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,7 +18,8 @@ export function AddProjectRepositoryRoute({
source === "github" ||
source === "gitlab" ||
source === "bitbucket" ||
source === "azure-devops"
source === "azure-devops" ||
source === "gitea"
? addProjectRemoteSourceLabel(source)
: "Git URL";

Expand Down
3 changes: 2 additions & 1 deletion apps/mobile/src/features/projects/AddProjectScreen.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -104,7 +104,8 @@ function sourceFromParam(value: string | string[] | undefined): AddProjectRemote
source === "github" ||
source === "gitlab" ||
source === "bitbucket" ||
source === "azure-devops"
source === "azure-devops" ||
source === "gitea"
) {
return source;
}
Expand Down
119 changes: 115 additions & 4 deletions apps/server/src/git/GitManager.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -33,7 +33,10 @@ import * as GitHubCli from "../sourceControl/GitHubCli.ts";
import * as TextGeneration from "../textGeneration/TextGeneration.ts";
import * as GitVcsDriver from "../vcs/GitVcsDriver.ts";
import * as VcsProcess from "../vcs/VcsProcess.ts";
import * as GiteaCli from "../sourceControl/GiteaCli.ts";
import * as GiteaSourceControlProvider from "../sourceControl/GiteaSourceControlProvider.ts";
import * as GitHubSourceControlProvider from "../sourceControl/GitHubSourceControlProvider.ts";
import * as SourceControlProvider from "../sourceControl/SourceControlProvider.ts";
import * as SourceControlProviderRegistry from "../sourceControl/SourceControlProviderRegistry.ts";
import * as ServerConfig from "../config.ts";
import * as ProjectSetupScriptRunner from "../project/ProjectSetupScriptRunner.ts";
Expand DownExpand Up@@ -620,6 +623,8 @@ function makeManager(input?: {
textGeneration?: Partial<FakeGitTextGeneration>;
serverSettings?: Parameters<typeof ServerSettings.layerTest>[0];
setupScriptRunner?: ProjectSetupScriptRunner.ProjectSetupScriptRunner["Service"];
/** Replaces the default GitHub provider, for exercising another host's workflow. */
sourceControlProvider?: SourceControlProvider.SourceControlProvider["Service"];
}) {
const { service: gitHubCli, ghCalls } = createGitHubCliWithFakeGh(input?.ghScenario);
const textGeneration = createTextGeneration(input?.textGeneration);
Expand All@@ -637,14 +642,15 @@ function makeManager(input?: {
const sourceControlRegistryLayer = Layer.effect(
SourceControlProviderRegistry.SourceControlProviderRegistry,
GitHubSourceControlProvider.make.pipe(
Effect.map((provider) =>
SourceControlProviderRegistry.SourceControlProviderRegistry.of({
Effect.map((gitHubProvider) => {
const provider = input?.sourceControlProvider ?? gitHubProvider;
return SourceControlProviderRegistry.SourceControlProviderRegistry.of({
get: () => Effect.succeed(provider),
resolveHandle: () => Effect.succeed({ provider, context: null }),
resolve: () => Effect.succeed(provider),
discover: Effect.succeed([]),
}),
),
});
}),
Effect.provide(Layer.succeed(GitHubCli.GitHubCli, gitHubCli)),
),
);
Expand DownExpand Up@@ -4835,4 +4841,109 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
]);
}),
);
// The original bug this provider fixes: a Gitea remote resolved to `unknown`, whose stub failed
// every call, so "Commit, push & create PR" died with "No unknown source control provider is
// registered." This drives the whole stacked action through the real Gitea provider.
it.effect("commits, pushes and creates a PR against a Gitea remote", () =>
Effect.gen(function* () {
const repoDir = yield* makeTempDir("t3code-git-manager-");
yield* initRepo(repoDir);
yield* runGit(repoDir, ["checkout", "-b", "t3code/gitea-flow"]);
const remoteDir = yield* createBareRemote();
yield* runGit(repoDir, ["remote", "add", "origin", remoteDir]);
NodeFS.writeFileSync(NodePath.join(repoDir, "gitea.txt"), "gitea\n");

const createdPullRequest = {
number: 7,
title: "Add Gitea flow",
url: "https://git.example.com/owner/repo/pulls/7",
baseRefName: "main",
headRefName: "t3code/gitea-flow",
state: "open" as const,
};
// GitManager looks for an existing PR, creates one when there is none, then looks again.
let listCalls = 0;
let createCalls = 0;

const giteaProvider = yield* GiteaSourceControlProvider.make.pipe(
Effect.provide(
Layer.mock(GiteaCli.GiteaCli)({
listPullRequests: () => {
listCalls += 1;
return Effect.succeed(listCalls === 1 ? [] : [createdPullRequest]);
},
createPullRequest: () => {
createCalls += 1;
return Effect.void;
},
getDefaultBranch: () => Effect.succeed("main"),
}),
),
);

const { manager } = yield* makeManager({ sourceControlProvider: giteaProvider });

const result = yield* runStackedAction(manager, {
cwd: repoDir,
action: "commit_push_pr",
});

expect(result.commit.status).toBe("created");
expect(result.push.status).toBe("pushed");
expect(result.pr.status).toBe("created");
expect(result.pr.number).toBe(7);
expect(createCalls).toBe(1);
expect(result.toast?.cta).toEqual({
kind: "open_pr",
label: "View PR",
url: "https://git.example.com/owner/repo/pulls/7",
});
}),
);

it.effect("reports an existing Gitea PR instead of opening a duplicate", () =>
Effect.gen(function* () {
const repoDir = yield* makeTempDir("t3code-git-manager-");
yield* initRepo(repoDir);
yield* runGit(repoDir, ["checkout", "-b", "t3code/gitea-existing"]);
const remoteDir = yield* createBareRemote();
yield* runGit(repoDir, ["remote", "add", "origin", remoteDir]);
yield* runGit(repoDir, ["push", "-u", "origin", "t3code/gitea-existing"]);

let createCalls = 0;
const giteaProvider = yield* GiteaSourceControlProvider.make.pipe(
Effect.provide(
Layer.mock(GiteaCli.GiteaCli)({
listPullRequests: () =>
Effect.succeed([
{
number: 12,
title: "Already open",
url: "https://git.example.com/owner/repo/pulls/12",
baseRefName: "main",
headRefName: "t3code/gitea-existing",
state: "open" as const,
},
]),
createPullRequest: () => {
createCalls += 1;
return Effect.void;
},
getDefaultBranch: () => Effect.succeed("main"),
}),
),
);

const { manager } = yield* makeManager({ sourceControlProvider: giteaProvider });

const result = yield* runStackedAction(manager, {
cwd: repoDir,
action: "commit_push_pr",
});

expect(result.pr.status).toBe("opened_existing");
expect(result.pr.number).toBe(12);
expect(createCalls).toBe(0);
}),
);
});
9 changes: 8 additions & 1 deletion apps/server/src/server.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -39,6 +39,7 @@ import * as CheckpointDiffQuery from "./checkpointing/CheckpointDiffQuery.ts";
import * as CheckpointStore from "./checkpointing/CheckpointStore.ts";
import * as AzureDevOpsCli from "./sourceControl/AzureDevOpsCli.ts";
import * as BitbucketApi from "./sourceControl/BitbucketApi.ts";
import * as GiteaCli from "./sourceControl/GiteaCli.ts";
import * as GitHubCli from "./sourceControl/GitHubCli.ts";
import * as GitLabCli from "./sourceControl/GitLabCli.ts";
import * as TextGeneration from "./textGeneration/TextGeneration.ts";
Expand DownExpand Up@@ -272,7 +273,13 @@ const VcsDriverRegistryLayerLive = VcsDriverRegistry.layer.pipe(

const SourceControlProviderRegistryLayerLive = SourceControlProviderRegistry.layer.pipe(
Layer.provide(
Layer.mergeAll(AzureDevOpsCli.layer, BitbucketApi.layer, GitHubCli.layer, GitLabCli.layer),
Layer.mergeAll(
AzureDevOpsCli.layer,
BitbucketApi.layer,
GiteaCli.layer,
GitHubCli.layer,
GitLabCli.layer,
),
),
Layer.provideMerge(GitVcsDriver.layer),
Layer.provideMerge(VcsDriverRegistryLayerLive),
Expand Down
Loading
Loading