Open History to see the last commit
1import type { OrchestrationProjectShell, SourceControlProviderKind } from "@t3tools/contracts";
2import { canonicalizeGitCafePullRequestUrl } from "@t3tools/source-control-definitions/registry";
3import * as Effect from "effect/Effect";
4import * as Schema from "effect/Schema";
5
6import { sourceControlDefinitions } from "../sourceControl/sourceControlDefinitions.ts";
7import { normalizeThreadPullRequestKey } from "../sourceControl/threadPullRequests.ts";
8
9export interface PullRequestTarget {
10 readonly url?: string | undefined;
11 readonly repository?: string | undefined;
12 readonly number?: number | undefined;
13 readonly host?: string | undefined;
14}
15
16export class PullRequestUrlInvalidError extends Schema.TaggedError<PullRequestUrlInvalidError>()(
17 "PullRequestUrlInvalidError",
18 {},
19) {
20 override get message(): string {
21 return "This is not a recognised pull request URL. Pass repository and number instead.";
22 }
23}
24
25export class PullRequestTargetIncompleteError extends Schema.TaggedError<PullRequestTargetIncompleteError>()(
26 "PullRequestTargetIncompleteError",
27 {},
28) {
29 override get message(): string {
30 return "Pass either url, or both repository and number.";
31 }
32}
33
34export class PullRequestHostRequiredError extends Schema.TaggedError<PullRequestHostRequiredError>()(
35 "PullRequestHostRequiredError",
36 {},
37) {
38 override get message(): string {
39 return "This thread's project has no recognised remote. Pass host or url.";
40 }
41}
42
43export class PullRequestUrlUnavailableError extends Schema.TaggedError<PullRequestUrlUnavailableError>()(
44 "PullRequestUrlUnavailableError",
45 {},
46) {
47 override get message(): string {
48 return "This thread's project is on a host with no known pull request URL shape. Pass url.";
49 }
50}
51
52export type PullRequestTargetError =
53 | PullRequestUrlInvalidError
54 | PullRequestTargetIncompleteError
55 | PullRequestHostRequiredError
56 | PullRequestUrlUnavailableError;
57
58export interface ResolvedTarget {
59 readonly host: string;
60 readonly repository: string;
61 readonly number: number;
62 readonly url: string;
63}
64
65function projectHostAndProvider(project: OrchestrationProjectShell | undefined): {
66 readonly host: string | null;
67 readonly kind: SourceControlProviderKind | null;
68} {
69 const identity = project?.repositoryIdentity;
70 const kind = identity?.provider ?? null;
71 if (!identity || kind === null) return { host: null, kind: null };
72 return {
73 host: sourceControlDefinitions.changeRequestHost(identity, kind),
74 kind,
75 };
76}
77
78export const resolveTarget = Effect.fn("PullRequestsToolkit.resolveTarget")(function* (
79 input: PullRequestTarget,
80 project: OrchestrationProjectShell | undefined,
81) {
82 if (input.url !== undefined) {
83 const parsed = sourceControlDefinitions.parseChangeRequestUrl(input.url)?.link;
84 if (parsed === undefined) {
85 return yield* new PullRequestUrlInvalidError({});
86 }
87 return {
88 ...normalizeThreadPullRequestKey(parsed),
89 url: canonicalizeGitCafePullRequestUrl(input.url),
90 } satisfies ResolvedTarget;
91 }
92 if (input.repository === undefined || input.number === undefined) {
93 return yield* new PullRequestTargetIncompleteError({});
94 }
95 const projectHost = projectHostAndProvider(project);
96 const host = (input.host ?? projectHost.host)?.toLowerCase();
97 if (host === undefined) {
98 return yield* new PullRequestHostRequiredError({});
99 }
100 const repository = input.repository.toLowerCase();
101 // Without a URL, a public forge host says what shape its change request URLs take;
102 // any other host is taken to run the project's own forge.
103 const recognizedKind = sourceControlDefinitions.recognizeRemote(`https://${host}`)?.kind;
104 const kind =
105 sourceControlDefinitions.findByPublicHost(host)?.kind ??
106 (recognizedKind === "gitcafe" ? recognizedKind : projectHost.kind);
107 const url =
108 kind === null
109 ? null
110 : sourceControlDefinitions.get(kind).changeRequestUrl({
111 host,
112 repository,
113 number: input.number,
114 remoteUrl: project?.repositoryIdentity?.locator.remoteUrl,
115 });
116 if (url === null) {
117 return yield* new PullRequestUrlUnavailableError({});
118 }
119 return {
120 ...normalizeThreadPullRequestKey({ host, repository, number: input.number, url }),
121 url: canonicalizeGitCafePullRequestUrl(url),
122 } satisfies ResolvedTarget;
123});