1import { useMemo } from "react";
2import { sourceControlRepositorySelector } from "@t3tools/shared/sourceControl";
3import type {
4 EnvironmentId,
5 ScopedThreadRef,
6 ThreadLinkedPullRequest,
7 ThreadPullRequestLink,
8} from "@t3tools/contracts";
9import type { EnvironmentProject } from "@t3tools/client-runtime/state/shell";
10import {
11 isAtomCommandInterrupted,
12 squashAtomCommandFailure,
13} from "@t3tools/client-runtime/state/runtime";
14import {
15 planThreadPullRequestMutation,
16 threadPullRequestLinkMode,
17} from "@t3tools/client-runtime/thread-pull-request-compatibility";
18import {
19 threadPullRequestKeysEqual,
20 visibleThreadPullRequests,
21} from "@t3tools/shared/threadPullRequests";
22import {
23 findProjectForChangeRequest,
24 matchesLinkedPullRequestUrl,
25 parseChangeRequestUrl,
26 type ChangeRequestLink,
27} from "~/lib/openPullRequestLink";
28import { useProjects, useServerConfigs } from "~/state/entities";
29import { threadEnvironment } from "~/state/threads";
30import { useAtomCommand } from "~/state/use-atom-command";
31
32/**
33 * Whether a parsed change-request link can be linked to a thread. `multiple` mode (the
34 * `threadPullRequests` capability) stores a link's host, repository and number on the thread
35 * directly and needs no project at all; a freshly linked pull request already starts with a
36 * null snapshot until some project can sync it, so a host nothing here is checked out from is
37 * the same case, not a reason to refuse the link. `single` mode still needs a matching project,
38 * since that is where its required `projectId` comes from.
39 */
40export function canLinkChangeRequest(
41 mode: ReturnType<typeof threadPullRequestLinkMode>,
42 projects: ReadonlyArray<EnvironmentProject>,
43 parsed: ChangeRequestLink,
44): boolean {
45 if (mode === "unsupported") return false;
46 if (mode === "multiple") return true;
47 return findProjectForChangeRequest(projects, parsed) !== undefined;
48}
49
50/** Routes link actions through the command advertised by this environment. */
51export function usePullRequestLinking(environmentId: EnvironmentId | null | undefined) {
52 const configs = useServerConfigs();
53 const projects = useProjects();
54 const capabilities =
55 environmentId == null ? undefined : configs.get(environmentId)?.environment.capabilities;
56 const mode = threadPullRequestLinkMode(capabilities);
57 const link = useAtomCommand(threadEnvironment.linkPullRequest, { reportFailure: false });
58 const unlink = useAtomCommand(threadEnvironment.unlinkPullRequest, { reportFailure: false });
59 const updateMetadata = useAtomCommand(threadEnvironment.updateMetadata, { reportFailure: false });
60 return useMemo(() => {
61 const environmentProjects = projects.filter(
62 (project) => project.environmentId === environmentId,
63 );
64 const canLink = (url: string) => {
65 const parsed = parseChangeRequestUrl(url);
66 return parsed !== null && canLinkChangeRequest(mode, environmentProjects, parsed);
67 };
68 const isLinked = (
69 thread: {
70 readonly pullRequests?: readonly ThreadPullRequestLink[];
71 readonly linkedPullRequest?: ThreadLinkedPullRequest | null | undefined;
72 } | null,
73 url: string,
74 ) => {
75 if (thread === null || mode === "unsupported") return false;
76 if (mode !== "multiple")
77 return (
78 thread.linkedPullRequest != null &&
79 matchesLinkedPullRequestUrl(thread.linkedPullRequest, url)
80 );
81 const parsed = parseChangeRequestUrl(url);
82 return (
83 parsed !== null &&
84 visibleThreadPullRequests(thread.pullRequests ?? []).some((entry) =>
85 threadPullRequestKeysEqual(entry, parsed),
86 )
87 );
88 };
89 const changeLink = async (threadRef: ScopedThreadRef, url: string, linked: boolean) => {
90 const parsed = parseChangeRequestUrl(url);
91 if (parsed === null || threadRef.environmentId !== environmentId || (linked && !canLink(url)))
92 throw new Error("The pull request is not available in this environment.");
93 const legacyProject = findProjectForChangeRequest(environmentProjects, parsed);
94 const mutation = planThreadPullRequestMutation({
95 capabilities,
96 threadId: threadRef.threadId,
97 reference: { ...parsed, url },
98 legacyProjectId: legacyProject?.id ?? null,
99 legacyRepository:
100 sourceControlRepositorySelector(legacyProject?.repositoryIdentity) ?? undefined,
101 linked,
102 });
103 if (mutation === null)
104 throw new Error("This environment does not support linking this pull request.");
105 const result = await (mutation.type === "thread.meta.update"
106 ? updateMetadata({ environmentId: threadRef.environmentId, input: mutation.input })
107 : mutation.type === "thread.pull-request.link"
108 ? link({ environmentId: threadRef.environmentId, input: mutation.input })
109 : unlink({ environmentId: threadRef.environmentId, input: mutation.input }));
110 if (result._tag === "Failure") {
111 if (isAtomCommandInterrupted(result)) throw new Error("Link update interrupted.");
112 throw squashAtomCommandFailure(result);
113 }
114 };
115 return { mode, canLink, isLinked, changeLink };
116 }, [capabilities, environmentId, link, mode, projects, unlink, updateMetadata]);
117}