authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-10-10 12:06:59-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-10 12:06:59-07:00
log7bc732804222946c0c9b23af057484e4412698fc
treed0f2837b51b2a0956247c05389a60c4fa39110db
parentfd3549c8ffa5eac8230a8b5e1a87e59d28ba488e
signature Signed by PGP key B5690EEEBB952194

feat(source-control): each host package ships a client definition (#17746)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

18 files changed, 516 insertions(+), 0 deletions(-)

packages/client-runtime/package.json+11
...@@ -390,6 +390,10 @@...@@ -390,6 +390,10 @@
390 "./state/mcp-apps": {390 "./state/mcp-apps": {
391 "types": "./src/state/mcpApps.ts",391 "types": "./src/state/mcpApps.ts",
392 "default": "./src/state/mcpApps.ts"392 "default": "./src/state/mcpApps.ts"
393 },
394 "./source-control-clients": {
395 "types": "./src/sourceControlClients.ts",
396 "default": "./src/sourceControlClients.ts"
393 }397 }
394 },398 },
395 "scripts": {399 "scripts": {
...@@ -399,6 +403,13 @@...@@ -399,6 +403,13 @@
399 "dependencies": {403 "dependencies": {
400 "@t3tools/contracts": "workspace:*",404 "@t3tools/contracts": "workspace:*",
401 "@t3tools/shared": "workspace:*",405 "@t3tools/shared": "workspace:*",
406 "@t3tools/source-control-azure-devops": "workspace:*",
407 "@t3tools/source-control-bitbucket": "workspace:*",
408 "@t3tools/source-control-core": "workspace:*",
409 "@t3tools/source-control-forgejo": "workspace:*",
410 "@t3tools/source-control-gitcafe": "workspace:*",
411 "@t3tools/source-control-github": "workspace:*",
412 "@t3tools/source-control-gitlab": "workspace:*",
402 "effect": "catalog:",413 "effect": "catalog:",
403 "three": "0.180.0"414 "three": "0.180.0"
404 },415 },
packages/client-runtime/src/sourceControlClients.test.ts created+17
...@@ -0,0 +1,17 @@
1import { describe, expect, it } from "vite-plus/test";
2
3import { sourceControlClients } from "./sourceControlClients.ts";
4
5describe("sourceControlClients", () => {
6 it("reads a missing kind as the first host and an unshipped kind as the generic one", () => {
7 expect(sourceControlClients.get(undefined).kind).toBe("github");
8 expect(sourceControlClients.get("forkhost").kind).toBe("unknown");
9 });
10
11 it("gives a change request URL to the first host whose shape it has", () => {
12 // GitCafe and Forgejo share `/pulls/`; GitCafe only claims its own hostnames.
13 const kindOf = (url: string) => sourceControlClients.findByChangeRequestUrl(url)?.kind;
14 expect(kindOf("https://git.cafe/acme/web/pulls/7")).toBe("gitcafe");
15 expect(kindOf("https://codeberg.org/acme/web/pulls/7")).toBe("forgejo");
16 });
17});
packages/client-runtime/src/sourceControlClients.ts created+41
...@@ -0,0 +1,41 @@
1/**
2 * The source control host definitions web and mobile ship, and the one lookup they use.
3 *
4 * Adding a host means writing its `@t3tools/source-control-<host>` package with a
5 * `./client/definition` entry and adding it here; clients then label, list, and check out its
6 * change requests without further branches.
7 *
8 * @module client-runtime/sourceControlClients
9 */
10import * as AzureDevOps from "@t3tools/source-control-azure-devops/client/definition";
11import * as Bitbucket from "@t3tools/source-control-bitbucket/client/definition";
12import { makeSourceControlClientRegistry } from "@t3tools/source-control-core/client/definition";
13
14export {
15 type ChangeRequestTerminology,
16 type SourceControlClientDefinition,
17} from "@t3tools/source-control-core/client/definition";
18import * as Forgejo from "@t3tools/source-control-forgejo/client/definition";
19import * as GitCafe from "@t3tools/source-control-gitcafe/client/definition";
20import * as GitHub from "@t3tools/source-control-github/client/definition";
21import * as GitLab from "@t3tools/source-control-gitlab/client/definition";
22
23/**
24 * GitHub comes first because a repository that has not reported its host yet reads as GitHub,
25 * which is what clients have always shown there. A change request URL goes to the first host
26 * whose shape it has, so GitCafe, which claims `/pulls/` only on its own hostnames, comes before
27 * Forgejo, which claims it everywhere. Pickers sort by readiness and label, not by this order.
28 */
29const BUILT_IN_SOURCE_CONTROL_CLIENTS = [
30 GitHub.definition,
31 GitLab.definition,
32 GitCafe.definition,
33 Forgejo.definition,
34 Bitbucket.definition,
35 AzureDevOps.definition,
36];
37
38/** `definitions` lists the built-in hosts in that order, for pickers that offer every host. */
39export const sourceControlClients = makeSourceControlClientRegistry(
40 BUILT_IN_SOURCE_CONTROL_CLIENTS,
41);
packages/source-control-azure-devops/package.json+4
...@@ -3,6 +3,10 @@...@@ -3,6 +3,10 @@
3 "private": true,3 "private": true,
4 "type": "module",4 "type": "module",
5 "exports": {5 "exports": {
6 "./client/definition": {
7 "types": "./src/client/definition.ts",
8 "import": "./src/client/definition.ts"
9 },
6 "./server/AzureDevOpsCli": {10 "./server/AzureDevOpsCli": {
7 "types": "./src/server/AzureDevOpsCli.ts",11 "types": "./src/server/AzureDevOpsCli.ts",
8 "import": "./src/server/AzureDevOpsCli.ts"12 "import": "./src/server/AzureDevOpsCli.ts"
packages/source-control-azure-devops/src/client/definition.ts created+33
...@@ -0,0 +1,33 @@
1/**
2 * Azure DevOps's client definition. Browser- and React Native-safe.
3 *
4 * @module source-control-azure-devops/client/definition
5 */
6import { SourceControlProviderKind } from "@t3tools/contracts";
7import { canonicalRepositoryKey } from "@t3tools/shared/sourceControl";
8import {
9 defineSourceControlClient,
10 isChangeRequestPath,
11} from "@t3tools/source-control-core/client/definition";
12
13export const definition = defineSourceControlClient({
14 kind: SourceControlProviderKind.make("azure-devops"),
15 label: "Azure DevOps",
16 pickerLabel: "Azure DevOps",
17 icon: "azure-devops",
18 changeRequest: { shortLabel: "PR", singular: "pull request" },
19 repositoryPathHint: "project/repository",
20 // `az repos` reads need the checkout's organization and project, so a bare hostname names no
21 // repository this client can act on.
22 publicHost: null,
23 publishDescription: "dev.azure.com",
24 publishHost: () => "dev.azure.com",
25 defaultCloneTransport: "ssh",
26 changeRequestUrl: ({ host, repository, number }) =>
27 `https://${canonicalRepositoryKey(`${host}/${repository}`.toLowerCase())}/pullrequest/${number}`,
28 checkoutCommand: ({ number }) => `az repos pr checkout --id ${number}`,
29 authorProfileUrl: () => null,
30 referenceAutolinkRepositoryUrl: () => null,
31 reviewSummaryRequired: () => false,
32 isChangeRequestUrl: (url) => isChangeRequestPath(url, "/pullrequest/"),
33});
packages/source-control-bitbucket/package.json+4
...@@ -3,6 +3,10 @@...@@ -3,6 +3,10 @@
3 "private": true,3 "private": true,
4 "type": "module",4 "type": "module",
5 "exports": {5 "exports": {
6 "./client/definition": {
7 "types": "./src/client/definition.ts",
8 "import": "./src/client/definition.ts"
9 },
6 "./server/BitbucketApi": {10 "./server/BitbucketApi": {
7 "types": "./src/server/BitbucketApi.ts",11 "types": "./src/server/BitbucketApi.ts",
8 "import": "./src/server/BitbucketApi.ts"12 "import": "./src/server/BitbucketApi.ts"
packages/source-control-bitbucket/src/client/definition.ts created+39
...@@ -0,0 +1,39 @@
1/**
2 * Bitbucket's client definition. Browser- and React Native-safe.
3 *
4 * @module source-control-bitbucket/client/definition
5 */
6import { SourceControlProviderKind } from "@t3tools/contracts";
7import {
8 defineSourceControlClient,
9 isChangeRequestPath,
10} from "@t3tools/source-control-core/client/definition";
11
12const safeShellArgument = /^[A-Za-z0-9._/@+=,-]+$/;
13const repositoryName = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
14
15export const definition = defineSourceControlClient({
16 kind: SourceControlProviderKind.make("bitbucket"),
17 label: "Bitbucket",
18 pickerLabel: "Bitbucket",
19 icon: "bitbucket",
20 changeRequest: { shortLabel: "PR", singular: "pull request" },
21 repositoryPathHint: "workspace/repository",
22 publicHost: "bitbucket.org",
23 publishDescription: "bitbucket.org",
24 publishHost: () => "bitbucket.org",
25 defaultCloneTransport: "ssh",
26 changeRequestUrl: ({ host, repository, number }) =>
27 `https://${host}/${repository}/pull-requests/${number}`,
28 // Bitbucket has no checkout CLI, so clone the head branch from its own repository.
29 checkoutCommand: ({ number, headBranch, headRepositoryNameWithOwner }) =>
30 headRepositoryNameWithOwner &&
31 repositoryName.test(headRepositoryNameWithOwner) &&
32 safeShellArgument.test(headBranch)
33 ? `git clone --single-branch --branch ${headBranch} https://bitbucket.org/${headRepositoryNameWithOwner}.git t3code-pr-${number}`
34 : null,
35 authorProfileUrl: () => null,
36 referenceAutolinkRepositoryUrl: () => null,
37 reviewSummaryRequired: () => false,
38 isChangeRequestUrl: (url) => isChangeRequestPath(url, "/pull-requests/"),
39});
packages/source-control-core/package.json+4
...@@ -3,6 +3,10 @@...@@ -3,6 +3,10 @@
3 "private": true,3 "private": true,
4 "type": "module",4 "type": "module",
5 "exports": {5 "exports": {
6 "./client/definition": {
7 "types": "./src/client/definition.ts",
8 "import": "./src/client/definition.ts"
9 },
6 "./server/diffFileRevisions": {10 "./server/diffFileRevisions": {
7 "types": "./src/server/diffFileRevisions.ts",11 "types": "./src/server/diffFileRevisions.ts",
8 "import": "./src/server/diffFileRevisions.ts"12 "import": "./src/server/diffFileRevisions.ts"
packages/source-control-core/src/client/definition.ts created+166
...@@ -0,0 +1,166 @@
1/**
2 * The client half of a source control host package.
3 *
4 * Every host package exports a `SourceControlClientDefinition` from its `./client/definition`
5 * entry point, the way `./server/driver` is its server entry. Web, mobile, and client-runtime
6 * read labels, change request nouns, picker copy, and checkout commands from these definitions,
7 * so a new host needs no per-kind branches in client code.
8 *
9 * Definitions are browser- and React Native-safe: plain data and pure functions, no server or
10 * platform UI imports.
11 *
12 * @module source-control-core/client/definition
13 */
14import { type PullRequestReviewVerdict, SourceControlProviderKind } from "@t3tools/contracts";
15
16/** What a host calls a change request, e.g. `MR` and `merge request` on GitLab. */
17export interface ChangeRequestTerminology {
18 readonly shortLabel: string;
19 readonly singular: string;
20}
21
22/** Where a project's repository lives, for building its change request URLs. */
23export interface ChangeRequestUrlInput {
24 /** The host below which the repository is addressed, as `pullRequestHostOf` reports it. */
25 readonly host: string;
26 /** The repository path below the host. */
27 readonly repository: string;
28 readonly number: number;
29 /** The checkout's remote URL, whose origin some hosts serve their web pages from. */
30 readonly remoteUrl?: string | undefined;
31 /** The repository's browser URL, when the server resolved one from a hosting account. */
32 readonly webUrl?: string | undefined;
33}
34
35/** The change request a checkout command is built for. */
36export interface ChangeRequestCheckoutInput {
37 readonly number: number;
38 readonly headBranch: string;
39 readonly headRepositoryNameWithOwner?: string | null | undefined;
40 /** The repository's web URL, for hosts whose checkout fetches from it. */
41 readonly repositoryUrl?: string | null | undefined;
42}
43
44export interface SourceControlClientDefinition {
45 readonly kind: SourceControlProviderKind;
46 /** The host's name, as in "Open on GitHub". */
47 readonly label: string;
48 /** How the clone and publish pickers name the host; Forgejo's covers Gitea too. */
49 readonly pickerLabel: string;
50 /** Glyph key each client maps to its own icon; a key a client lacks draws the generic glyph. */
51 readonly icon: string;
52 readonly changeRequest: ChangeRequestTerminology;
53 /** Placeholder for a repository path on this host, such as `owner/repo`. */
54 readonly repositoryPathHint: string;
55 /**
56 * The hostname a change request reference can be attributed to before any repository
57 * identity is known, or null when a hostname alone does not name this host.
58 */
59 readonly publicHost: string | null;
60 /** Subtitle beside the host in the publish picker. */
61 readonly publishDescription: string;
62 /** Where a publish lands, given the host the server is signed in to, if it reported one. */
63 readonly publishHost: (signedInHost: string | null) => string;
64 /** Which of a repository's clone URLs a new clone uses. */
65 readonly defaultCloneTransport: "https" | "ssh";
66 /** The web URL of a change request, or null when this client cannot build one for the host. */
67 readonly changeRequestUrl: (input: ChangeRequestUrlInput) => string | null;
68 /** A shell command that checks the change request out, or null when it cannot be built. */
69 readonly checkoutCommand: (changeRequest: ChangeRequestCheckoutInput) => string | null;
70 /** A comment author's profile page, or null where the host has none clients can link. */
71 readonly authorProfileUrl: (login: string, repositoryUrl: string) => string | null;
72 /**
73 * The repository URL that `#123` and commit SHAs in change request text link under, using
74 * GitHub's `/issues/` and `/commit/` routes, or null where the host does not route them so.
75 */
76 readonly referenceAutolinkRepositoryUrl: (repositoryUrl: string) => string | null;
77 /** Whether a review with this verdict must carry a summary, beyond what every host asks. */
78 readonly reviewSummaryRequired: (verdict: PullRequestReviewVerdict) => boolean;
79 /** Whether a change request URL has this host's path shape, e.g. GitLab's `/-/merge_requests/`. */
80 readonly isChangeRequestUrl: (url: string) => boolean;
81}
82
83/**
84 * Whether a URL's path is a change request at `route`, such as `/pull/`, followed by its number.
85 * Matching the parsed path keeps a query or fragment that mentions another host's route out.
86 */
87export function isChangeRequestPath(url: string, route: string): boolean {
88 try {
89 const path = new URL(url).pathname;
90 const at = path.indexOf(route);
91 return at > 0 && /^\d+(?:\/|$)/u.test(path.slice(at + route.length));
92 } catch {
93 return false;
94 }
95}
96
97export function defineSourceControlClient<const Definition extends SourceControlClientDefinition>(
98 definition: Definition,
99): Definition {
100 return definition;
101}
102
103/** What clients show for a host they ship no definition for, including `unknown`. */
104const UNKNOWN_SOURCE_CONTROL_CLIENT: SourceControlClientDefinition = {
105 kind: SourceControlProviderKind.make("unknown"),
106 label: "source control",
107 pickerLabel: "source control",
108 icon: "change-request",
109 changeRequest: { shortLabel: "change request", singular: "change request" },
110 repositoryPathHint: "URL",
111 publicHost: null,
112 publishDescription: "Your signed-in server",
113 publishHost: (signedInHost) => signedInHost ?? "your server",
114 defaultCloneTransport: "ssh",
115 changeRequestUrl: () => null,
116 checkoutCommand: () => null,
117 authorProfileUrl: () => null,
118 referenceAutolinkRepositoryUrl: () => null,
119 reviewSummaryRequired: () => false,
120 isChangeRequestUrl: () => false,
121};
122
123/** The host definitions a client loaded. */
124export interface SourceControlClientRegistry {
125 readonly definitions: ReadonlyArray<SourceControlClientDefinition>;
126 /**
127 * The definition for a kind. No kind at all, before a repository reports its host, reads as
128 * the first definition; a kind this client lacks, `unknown` included, reads as
129 * `UNKNOWN_SOURCE_CONTROL_CLIENT`.
130 */
131 readonly get: (kind: string | null | undefined) => SourceControlClientDefinition;
132 /** The definition for a kind, or `undefined` for one this client lacks. */
133 readonly find: (kind: string) => SourceControlClientDefinition | undefined;
134 /** The definition whose public instance is `hostname`, if any. */
135 readonly findByPublicHost: (hostname: string) => SourceControlClientDefinition | undefined;
136 /** The definition whose change request path shape `url` has, if any. */
137 readonly findByChangeRequestUrl: (url: string) => SourceControlClientDefinition | undefined;
138 /** The name of the host a change request URL is on, for copy about that change request. */
139 readonly hostLabelForChangeRequestUrl: (url: string) => string;
140}
141
142export function makeSourceControlClientRegistry(
143 definitions: ReadonlyArray<SourceControlClientDefinition>,
144): SourceControlClientRegistry {
145 const byKind = new Map<string, SourceControlClientDefinition>();
146 for (const definition of definitions) {
147 if (byKind.has(definition.kind)) {
148 throw new Error(`Source control host '${definition.kind}' is defined more than once.`);
149 }
150 byKind.set(definition.kind, definition);
151 }
152 return {
153 definitions,
154 get: (kind) =>
155 (kind == null ? definitions[0] : byKind.get(kind)) ?? UNKNOWN_SOURCE_CONTROL_CLIENT,
156 find: (kind) => byKind.get(kind),
157 findByPublicHost: (hostname) => {
158 const host = hostname.toLowerCase();
159 return definitions.find((definition) => definition.publicHost === host);
160 },
161 findByChangeRequestUrl: (url) =>
162 definitions.find((definition) => definition.isChangeRequestUrl(url)),
163 hostLabelForChangeRequestUrl: (url) =>
164 definitions.find((definition) => definition.isChangeRequestUrl(url))?.label ?? "the host",
165 };
166}
packages/source-control-forgejo/package.json+4
...@@ -3,6 +3,10 @@...@@ -3,6 +3,10 @@
3 "private": true,3 "private": true,
4 "type": "module",4 "type": "module",
5 "exports": {5 "exports": {
6 "./client/definition": {
7 "types": "./src/client/definition.ts",
8 "import": "./src/client/definition.ts"
9 },
6 "./server/driver": {10 "./server/driver": {
7 "types": "./src/server/driver.ts",11 "types": "./src/server/driver.ts",
8 "import": "./src/server/driver.ts"12 "import": "./src/server/driver.ts"
packages/source-control-forgejo/src/client/definition.ts created+52
...@@ -0,0 +1,52 @@
1/**
2 * Forgejo's client definition, which covers Gitea as well. Browser- and React Native-safe.
3 *
4 * @module source-control-forgejo/client/definition
5 */
6import { SourceControlProviderKind } from "@t3tools/contracts";
7import {
8 defineSourceControlClient,
9 isChangeRequestPath,
10} from "@t3tools/source-control-core/client/definition";
11
12export const definition = defineSourceControlClient({
13 kind: SourceControlProviderKind.make("forgejo"),
14 label: "Forgejo",
15 pickerLabel: "Forgejo / Gitea",
16 icon: "forgejo",
17 changeRequest: { shortLabel: "PR", singular: "pull request" },
18 repositoryPathHint: "owner/repo",
19 // Forgejo and Gitea are self-hosted, so no hostname names them.
20 publicHost: null,
21 publishDescription: "Your signed-in server",
22 publishHost: (signedInHost) => signedInHost ?? "your server",
23 defaultCloneTransport: "https",
24 // The server's resolved web URL wins; otherwise an HTTP remote on the same host names the
25 // origin, which may carry a port. SSH remotes say nothing about the web origin.
26 changeRequestUrl: ({ host, repository, number, remoteUrl, webUrl }) => {
27 if (webUrl) return `${webUrl.replace(/\/+$/, "")}/pulls/${number}`;
28 try {
29 const remote = new URL(remoteUrl ?? "");
30 if (
31 (remote.protocol === "http:" || remote.protocol === "https:") &&
32 (remote.hostname.toLowerCase() === host.toLowerCase() ||
33 remote.host.toLowerCase() === host.toLowerCase())
34 ) {
35 return `${remote.origin}/${repository}/pulls/${number}`;
36 }
37 } catch {
38 // Not an HTTP remote.
39 }
40 return `https://${host}/${repository}/pulls/${number}`;
41 },
42 // Neither `fj` nor `tea` checks out by number, so fetch the pull ref from the repository itself.
43 checkoutCommand: ({ number, repositoryUrl }) =>
44 repositoryUrl
45 ? `git fetch '${repositoryUrl.replaceAll("'", "'\\''")}' refs/pull/${number}/head && git checkout -B pulls/${number} FETCH_HEAD`
46 : null,
47 authorProfileUrl: () => null,
48 // Forgejo refuses a change request review without a summary, even with inline comments.
49 referenceAutolinkRepositoryUrl: () => null,
50 reviewSummaryRequired: (verdict) => verdict === "request-changes",
51 isChangeRequestUrl: (url) => isChangeRequestPath(url, "/pulls/"),
52});
packages/source-control-gitcafe/package.json+4
...@@ -3,6 +3,10 @@...@@ -3,6 +3,10 @@
3 "private": true,3 "private": true,
4 "type": "module",4 "type": "module",
5 "exports": {5 "exports": {
6 "./client/definition": {
7 "types": "./src/client/definition.ts",
8 "import": "./src/client/definition.ts"
9 },
6 "./server/driver": {10 "./server/driver": {
7 "types": "./src/server/driver.ts",11 "types": "./src/server/driver.ts",
8 "import": "./src/server/driver.ts"12 "import": "./src/server/driver.ts"
packages/source-control-gitcafe/src/client/definition.ts created+45
...@@ -0,0 +1,45 @@
1/**
2 * GitCafe's client definition. Browser- and React Native-safe.
3 *
4 * @module source-control-gitcafe/client/definition
5 */
6import { SourceControlProviderKind } from "@t3tools/contracts";
7import {
8 defineSourceControlClient,
9 isChangeRequestPath,
10} from "@t3tools/source-control-core/client/definition";
11
12/** GitCafe has no self-hosted installs: production and staging are its only hosts. */
13const GITCAFE_HOSTS = new Set(["git.cafe", "staging.git.cafe"]);
14
15export const definition = defineSourceControlClient({
16 kind: SourceControlProviderKind.make("gitcafe"),
17 label: "GitCafe",
18 pickerLabel: "GitCafe",
19 icon: "gitcafe",
20 changeRequest: { shortLabel: "PR", singular: "pull request" },
21 repositoryPathHint: "owner/repo",
22 // Left unattributed so a reference with no repository identity behind it gets no checkout
23 // command guessed from its hostname alone.
24 publicHost: null,
25 publishDescription: "git.cafe",
26 publishHost: () => "git.cafe",
27 defaultCloneTransport: "ssh",
28 changeRequestUrl: ({ host, repository, number }) =>
29 `https://${host}/${repository}/pulls/${number}`,
30 checkoutCommand: ({ number }) => `cafe pr checkout ${number}`,
31 authorProfileUrl: () => null,
32 referenceAutolinkRepositoryUrl: () => null,
33 reviewSummaryRequired: () => false,
34 // `/pulls/` is Forgejo's shape too, so only GitCafe's own hosts claim it.
35 isChangeRequestUrl: (url) => {
36 try {
37 return (
38 GITCAFE_HOSTS.has(new URL(url).hostname.toLowerCase()) &&
39 isChangeRequestPath(url, "/pulls/")
40 );
41 } catch {
42 return false;
43 }
44 },
45});
packages/source-control-github/package.json+4
...@@ -3,6 +3,10 @@...@@ -3,6 +3,10 @@
3 "private": true,3 "private": true,
4 "type": "module",4 "type": "module",
5 "exports": {5 "exports": {
6 "./client/definition": {
7 "types": "./src/client/definition.ts",
8 "import": "./src/client/definition.ts"
9 },
6 "./server/driver": {10 "./server/driver": {
7 "types": "./src/server/driver.ts",11 "types": "./src/server/driver.ts",
8 "import": "./src/server/driver.ts"12 "import": "./src/server/driver.ts"
packages/source-control-github/src/client/definition.ts created+33
...@@ -0,0 +1,33 @@
1/**
2 * GitHub's client definition. Browser- and React Native-safe.
3 *
4 * @module source-control-github/client/definition
5 */
6import { SourceControlProviderKind } from "@t3tools/contracts";
7import {
8 defineSourceControlClient,
9 isChangeRequestPath,
10} from "@t3tools/source-control-core/client/definition";
11
12export const definition = defineSourceControlClient({
13 kind: SourceControlProviderKind.make("github"),
14 label: "GitHub",
15 pickerLabel: "GitHub",
16 icon: "github",
17 changeRequest: { shortLabel: "PR", singular: "pull request" },
18 repositoryPathHint: "owner/repo",
19 publicHost: "github.com",
20 publishDescription: "github.com",
21 publishHost: () => "github.com",
22 defaultCloneTransport: "https",
23 changeRequestUrl: ({ host, repository, number }) =>
24 `https://${host}/${repository}/pull/${number}`,
25 checkoutCommand: ({ number }) => `gh pr checkout ${number}`,
26 authorProfileUrl: (login, repositoryUrl) =>
27 login.endsWith("[bot]")
28 ? null
29 : new URL(`/${encodeURIComponent(login)}`, repositoryUrl).toString(),
30 referenceAutolinkRepositoryUrl: (repositoryUrl) => repositoryUrl,
31 reviewSummaryRequired: () => false,
32 isChangeRequestUrl: (url) => isChangeRequestPath(url, "/pull/"),
33});
packages/source-control-gitlab/package.json+4
...@@ -3,6 +3,10 @@...@@ -3,6 +3,10 @@
3 "private": true,3 "private": true,
4 "type": "module",4 "type": "module",
5 "exports": {5 "exports": {
6 "./client/definition": {
7 "types": "./src/client/definition.ts",
8 "import": "./src/client/definition.ts"
9 },
6 "./server/driver": {10 "./server/driver": {
7 "types": "./src/server/driver.ts",11 "types": "./src/server/driver.ts",
8 "import": "./src/server/driver.ts"12 "import": "./src/server/driver.ts"
packages/source-control-gitlab/src/client/definition.ts created+30
...@@ -0,0 +1,30 @@
1/**
2 * GitLab's client definition. Browser- and React Native-safe.
3 *
4 * @module source-control-gitlab/client/definition
5 */
6import { SourceControlProviderKind } from "@t3tools/contracts";
7import {
8 defineSourceControlClient,
9 isChangeRequestPath,
10} from "@t3tools/source-control-core/client/definition";
11
12export const definition = defineSourceControlClient({
13 kind: SourceControlProviderKind.make("gitlab"),
14 label: "GitLab",
15 pickerLabel: "GitLab",
16 icon: "gitlab",
17 changeRequest: { shortLabel: "MR", singular: "merge request" },
18 repositoryPathHint: "group/project",
19 publicHost: "gitlab.com",
20 publishDescription: "gitlab.com",
21 publishHost: () => "gitlab.com",
22 defaultCloneTransport: "ssh",
23 changeRequestUrl: ({ host, repository, number }) =>
24 `https://${host}/${repository}/-/merge_requests/${number}`,
25 checkoutCommand: ({ number }) => `glab mr checkout ${number}`,
26 authorProfileUrl: () => null,
27 referenceAutolinkRepositoryUrl: () => null,
28 reviewSummaryRequired: () => false,
29 isChangeRequestUrl: (url) => isChangeRequestPath(url, "/-/merge_requests/"),
30});
pnpm-lock.yaml+21
...@@ -1078,6 +1078,27 @@ importers:...@@ -1078,6 +1078,27 @@ importers:
1078 '@t3tools/shared':1078 '@t3tools/shared':
1079 specifier: workspace:*1079 specifier: workspace:*
1080 version: link:../shared1080 version: link:../shared
1081 '@t3tools/source-control-azure-devops':
1082 specifier: workspace:*
1083 version: link:../source-control-azure-devops
1084 '@t3tools/source-control-bitbucket':
1085 specifier: workspace:*
1086 version: link:../source-control-bitbucket
1087 '@t3tools/source-control-core':
1088 specifier: workspace:*
1089 version: link:../source-control-core
1090 '@t3tools/source-control-forgejo':
1091 specifier: workspace:*
1092 version: link:../source-control-forgejo
1093 '@t3tools/source-control-gitcafe':
1094 specifier: workspace:*
1095 version: link:../source-control-gitcafe
1096 '@t3tools/source-control-github':
1097 specifier: workspace:*
1098 version: link:../source-control-github
1099 '@t3tools/source-control-gitlab':
1100 specifier: workspace:*
1101 version: link:../source-control-gitlab
1081 effect:1102 effect:
1082 specifier: 4.0.21103 specifier: 4.0.2
1083 version: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688)1104 version: 4.0.2(patch_hash=ec61ee307eaec3e17f757e0e6a16296ea4b7a8c3188df0c6ec5e66301fd78688)