| author | |
| committer | |
| log | a4c9494b0e3606775cc5fc929fc138399288bd43 |
| tree | 609c13c28aad07e1a0bb9972367025e632273ddd |
| parent | 5e2225671f705fcd33f1ea5591b79ba612fb6974 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: PR Batch Tester <agent@local.test>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>3 files changed, 51 insertions(+), 12 deletions(-)
apps/web/src/components/pullRequest/LinkPullRequestDialog.tsx+3-4| ... | ... | @@ -75,8 +75,7 @@ interface ResolvedLink { |
| 75 | 75 | |
| 76 | 76 | /** |
| 77 | 77 | * Which pull request an input names, or why it cannot. A URL carries its own host and |
| 78 | * repository and may point at any repository on a host this environment has a project for; a | |
| 79 | * bare `#123` can only mean the thread's own repository. | |
| 78 | * repository; a bare `#123` can only mean the thread's own repository. | |
| 80 | 79 | */ |
| 81 | 80 | export function resolveLinkPullRequestInput(input: { |
| 82 | 81 | readonly reference: string; |
| ... | ... | @@ -204,8 +203,8 @@ function LinkPullRequestDialog({ |
| 204 | 203 | <DialogHeader> |
| 205 | 204 | <DialogTitle>Link pull request</DialogTitle> |
| 206 | 205 | <DialogDescription> |
| 207 | Attach a pull request to this thread. A full URL can point at any repository on a host | |
| 208 | this environment has a project for. | |
| 206 | Attach a pull request to this thread by its URL, or by its number for this thread's | |
| 207 | repository. | |
| 209 | 208 | </DialogDescription> |
| 210 | 209 | </DialogHeader> |
| 211 | 210 | <DialogPanel> |
apps/web/src/hooks/usePullRequestLinking.test.ts created+27| ... | ... | @@ -0,0 +1,27 @@ |
| 1 | import { describe, expect, it } from "vite-plus/test"; | |
| 2 | ||
| 3 | import { canLinkChangeRequest } from "./usePullRequestLinking"; | |
| 4 | import { parseChangeRequestUrl } from "~/lib/openPullRequestLink"; | |
| 5 | ||
| 6 | describe("canLinkChangeRequest (#9435 / #9440)", () => { | |
| 7 | // A non-Git parent workspace (a folder of several child repositories that is not itself a | |
| 8 | // checkout) registers as a project with no `repositoryIdentity` at all. | |
| 9 | const nonGitProject = { id: "scratch-project" } as never; | |
| 10 | const link = parseChangeRequestUrl("https://github.com/pingdotgg/t3code/pull/15111")!; | |
| 11 | ||
| 12 | it("links a pasted pull request URL when the server tracks links on the thread, even with no matching project", () => { | |
| 13 | // `multiple` mode (the `threadPullRequests` capability) stores a link's host, repository | |
| 14 | // and number on the thread directly, so no project needs to match for the link itself. | |
| 15 | expect(canLinkChangeRequest("multiple", [nonGitProject], link)).toBe(true); | |
| 16 | expect(canLinkChangeRequest("multiple", [], link)).toBe(true); | |
| 17 | }); | |
| 18 | ||
| 19 | it("still requires a matching project in single-link mode, where the link needs a projectId", () => { | |
| 20 | expect(canLinkChangeRequest("single", [nonGitProject], link)).toBe(false); | |
| 21 | expect(canLinkChangeRequest("single", [], link)).toBe(false); | |
| 22 | }); | |
| 23 | ||
| 24 | it("never links when the environment does not support pull request linking", () => { | |
| 25 | expect(canLinkChangeRequest("unsupported", [nonGitProject], link)).toBe(false); | |
| 26 | }); | |
| 27 | }); |
apps/web/src/hooks/usePullRequestLinking.ts+21-8| ... | ... | @@ -6,6 +6,7 @@ import type { |
| 6 | 6 | ThreadLinkedPullRequest, |
| 7 | 7 | ThreadPullRequestLink, |
| 8 | 8 | } from "@t3tools/contracts"; |
| 9 | import type { EnvironmentProject } from "@t3tools/client-runtime/state/shell"; | |
| 9 | 10 | import { |
| 10 | 11 | isAtomCommandInterrupted, |
| 11 | 12 | squashAtomCommandFailure, |
| ... | ... | @@ -20,14 +21,32 @@ import { |
| 20 | 21 | } from "@t3tools/shared/threadPullRequests"; |
| 21 | 22 | import { |
| 22 | 23 | findProjectForChangeRequest, |
| 23 | findProjectOnChangeRequestHost, | |
| 24 | 24 | matchesLinkedPullRequestUrl, |
| 25 | 25 | parseChangeRequestUrl, |
| 26 | type ChangeRequestLink, | |
| 26 | 27 | } from "~/lib/openPullRequestLink"; |
| 27 | 28 | import { useProjects, useServerConfigs } from "~/state/entities"; |
| 28 | 29 | import { threadEnvironment } from "~/state/threads"; |
| 29 | 30 | import { useAtomCommand } from "~/state/use-atom-command"; |
| 30 | 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 | */ | |
| 40 | export 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 | ||
| 31 | 50 | /** Routes link actions through the command advertised by this environment. */ |
| 32 | 51 | export function usePullRequestLinking(environmentId: EnvironmentId | null | undefined) { |
| 33 | 52 | const configs = useServerConfigs(); |
| ... | ... | @@ -44,13 +63,7 @@ export function usePullRequestLinking(environmentId: EnvironmentId | null | unde |
| 44 | 63 | ); |
| 45 | 64 | const canLink = (url: string) => { |
| 46 | 65 | const parsed = parseChangeRequestUrl(url); |
| 47 | if (parsed === null || mode === "unsupported") return false; | |
| 48 | return ( | |
| 49 | (mode === "multiple" ? findProjectOnChangeRequestHost : findProjectForChangeRequest)( | |
| 50 | environmentProjects, | |
| 51 | parsed, | |
| 52 | ) !== undefined | |
| 53 | ); | |
| 66 | return parsed !== null && canLinkChangeRequest(mode, environmentProjects, parsed); | |
| 54 | 67 | }; |
| 55 | 68 | const isLinked = ( |
| 56 | 69 | thread: { |