1/**
2 * GitHub's client definition. Browser- and React Native-safe.
3 *
4 * @module source-control-github/client/definition
5 */
6import {
7 GitHubHost,
8 makeProviderSettingsSchema,
9 secretSetting,
10 pullRequestHostOf,
11 SourceControlProviderKind,
12 TrimmedNonEmptyString,
13 TrimmedString,
14} from "@t3tools/contracts";
15import * as Effect from "effect/Effect";
16import * as Schema from "effect/Schema";
17import {
18 defineSourceControlClient,
19 isChangeRequestInProjectRepository,
20 isChangeRequestOnProjectHost,
21 isChangeRequestPath,
22} from "@t3tools/source-control-core/client/definition";
23
24const CHECKOUT_COMMAND = /^gh\s+pr\s+checkout\s+(.+)$/i;
25const CHANGE_REQUEST_REFERENCE =
26 /^https:\/\/github\.com\/[^/\s]+\/[^/\s]+\/pull\/(\d+)(?:[/?#].*)?$/i;
27
28const KIND = SourceControlProviderKind.make("github");
29
30export const GitHubHostChoice = Schema.Struct({
31 account: Schema.optionalKey(TrimmedNonEmptyString),
32 enabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),
33});
34export type GitHubHostChoice = typeof GitHubHostChoice.Type;
35
36/**
37 * Per-host choices, keyed by lowercased host such as `github.com`. `hosts` pins one of the logins
38 * `gh` holds for a host instead of its active one, or turns the host off; `tokens` holds a token
39 * per host, used before `GH_TOKEN` and friends, which win over `gh`. Source Control settings draws
40 * both with GitHub's own panels, since the logins come from `gh` at runtime.
41 */
42export const settings = makeProviderSettingsSchema({
43 hosts: Schema.Record(GitHubHost, GitHubHostChoice).pipe(
44 Schema.withDecodingDefault(Effect.succeed({})),
45 Schema.annotateKey({ providerSettingsForm: { hidden: true } }),
46 ),
47 tokens: Schema.Record(GitHubHost, TrimmedString).pipe(
48 Schema.withDecodingDefault(Effect.succeed({})),
49 secretSetting,
50 Schema.annotateKey({ providerSettingsForm: { hidden: true } }),
51 ),
52});
53export type GitHubSettings = typeof settings.Type;
54
55export const definition = defineSourceControlClient({
56 kind: KIND,
57 label: "GitHub",
58 pickerLabel: "GitHub",
59 icon: "github",
60 changeRequest: { shortLabel: "PR", singular: "pull request" },
61 repositoryPathHint: "owner/repo",
62 publicHost: "github.com",
63 publishDescription: "github.com",
64 publishHost: () => "github.com",
65 newRepositoryOwner: (account) => ({ owner: account }),
66 defaultCloneTransport: "https",
67 changeRequestUrl: ({ host, repository, number }) =>
68 `https://${host}/${repository}/pull/${number}`,
69 changeRequestActions: new Set([
70 "merge",
71 "ready",
72 "draft",
73 "close",
74 "reopen",
75 "delete-branch",
76 "update-branch",
77 "enable-auto-merge",
78 "disable-auto-merge",
79 "revert",
80 "approve-workflows",
81 ] as const),
82 checkoutCommand: ({ number }) => `gh pr checkout ${number}`,
83 authorProfileUrl: (login, repositoryUrl) =>
84 login.endsWith("[bot]")
85 ? null
86 : new URL(`/${encodeURIComponent(login)}`, repositoryUrl).toString(),
87 referenceAutolinkRepositoryUrl: (repositoryUrl) => repositoryUrl,
88 reviewSummaryRequired: () => false,
89 checkoutCommandArgument: (input) => CHECKOUT_COMMAND.exec(input)?.[1]?.trim() ?? null,
90 isChangeRequestReference: (url) => CHANGE_REQUEST_REFERENCE.test(url),
91 changeRequestUrlHost: (url) => url.hostname,
92 // A GitHub web host is the checkout's own host, Enterprise installs included.
93 checkoutChangeRequestHost: (identity) => pullRequestHostOf(identity, KIND),
94 isChangeRequestInRepository: (identity, link) =>
95 isChangeRequestInProjectRepository(KIND, identity, link),
96 canReadChangeRequestOnHost: (identity, link) =>
97 isChangeRequestOnProjectHost(KIND, identity, link),
98 isChangeRequestUrl: (url) => isChangeRequestPath(url, "/pull/"),
99 settings,
100});