Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/web/src/components/ChatMarkdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3250,7 +3250,7 @@ const CHAT_MARKDOWN_COMPONENTS = {
? resolvePullRequestPreviewTarget({
environmentId,
projects,
pullRequestsEnabled: serverConfig?.environment.capabilities.pullRequests === true,
capabilities: serverConfig?.environment.capabilities,
url: pullRequestCandidateUrl,
})
: null;
Expand Down
8 changes: 4 additions & 4 deletions apps/web/src/components/chat/useResolveComposerObjectLink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ export interface ResolvedComposerObjectLink {
/**
* Turns a link into the chip its attach picker would have made: the issue or pull request is
* fetched and snapshotted the same way. Null when it cannot be read (Linear, Slack, Notion, or Granola
* not connected or turned off, `gh` signed out, a repository no project here is checked out from), so the caller
* not connected or turned off, `gh` signed out, a host no project here is checked out from), so the caller
* keeps the link.
*/
export function useResolveComposerObjectLink(input: {
Expand Down Expand Up @@ -189,7 +189,7 @@ export function useResolveComposerObjectLink(input: {
});
const projects = useProjects();
const serverConfig = useAtomValue(serverEnvironment.configValueAtom(environmentId));
const pullRequestsEnabled = serverConfig?.environment.capabilities.pullRequests === true;
const capabilities = serverConfig?.environment.capabilities;

return useCallback(
async (
Expand Down Expand Up @@ -278,7 +278,7 @@ export function useResolveComposerObjectLink(input: {
const target = resolvePullRequestPreviewTarget({
environmentId,
projects,
pullRequestsEnabled,
capabilities,
url: link.url,
});
if (target === null) return null;
Expand Down Expand Up @@ -359,7 +359,7 @@ export function useResolveComposerObjectLink(input: {
granolaEnabled,
notionEnabled,
projects,
pullRequestsEnabled,
capabilities,
slackEnabled,
threadRef.threadId,
],
Expand Down
43 changes: 42 additions & 1 deletion apps/web/src/lib/openPullRequestLink.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import {
matchesLinkedPullRequestUrl,
parseChangeRequestUrl,
pullRequestCandidateUrlFromReferenceAutolink,
resolvePullRequestPreviewTarget,
shouldOpenPullRequestExternally,
} from "./openPullRequestLink";
import { ProjectId, type RepositoryIdentity } from "@t3tools/contracts";
import { type EnvironmentId, ProjectId, type RepositoryIdentity } from "@t3tools/contracts";
import { normalizeGitRemoteUrl } from "@t3tools/shared/git";

function repositoryIdentity(
Expand Down Expand Up @@ -441,3 +442,43 @@ describe("findProjectForChangeRequest", () => {
).toBeUndefined();
});
});

describe("resolvePullRequestPreviewTarget", () => {
const environmentId = "env-1" as EnvironmentId;
const frontend = {
id: ProjectId.make("frontend"),
environmentId,
repositoryIdentity: {
canonicalKey: "github.com/acme/frontend",
provider: "github",
owner: "acme",
name: "frontend",
},
} as never;
const url = "https://github.com/acme/backend/pull/105";

it("reads a pull request from another repository through a project on its host", () => {
expect(
resolvePullRequestPreviewTarget({
environmentId,
projects: [frontend],
capabilities: { pullRequests: true, threadPullRequests: true } as never,
url,
}),
).toEqual({
environmentId,
input: { projectId: "frontend", host: "github.com", repository: "acme/backend", number: 105 },
});
});

it("keeps the link on a server that cannot route by host", () => {
expect(
resolvePullRequestPreviewTarget({
environmentId,
projects: [frontend],
capabilities: { pullRequests: true } as never,
url,
}),
).toBeNull();
});
});
35 changes: 25 additions & 10 deletions apps/web/src/lib/openPullRequestLink.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import type { EnvironmentId, PullRequestRef, ScopedThreadRef } from "@t3tools/contracts";
import type {
EnvironmentId,
ExecutionEnvironmentCapabilities,
PullRequestRef,
ScopedThreadRef,
} from "@t3tools/contracts";
import { useAtomValue } from "@effect/atom-react";
import { useNavigate } from "@tanstack/react-router";
import { type MouseEvent, useCallback, useMemo } from "react";
Expand Down Expand Up @@ -45,31 +50,41 @@ export function findProjectForChangeRequest(
});
}

/**
* The reference that reads a change request link on one environment. The project only lends
* its checkout and credentials, so any project on the link's host will do, its own first.
* Servers without `threadPullRequests` cannot route by host and read only a project's own
* repository.
*/
export function resolvePullRequestPreviewTarget({
environmentId,
projects,
pullRequestsEnabled,
capabilities,
url,
}: {
environmentId: EnvironmentId | null;
projects: ReadonlyArray<EnvironmentProject>;
pullRequestsEnabled: boolean;
capabilities: ExecutionEnvironmentCapabilities | undefined;
url: string;
}): { environmentId: EnvironmentId; input: PullRequestRef } | null {
if (!pullRequestsEnabled || environmentId === null) return null;
if (capabilities?.pullRequests !== true || environmentId === null) return null;
const parsed = parseChangeRequestUrl(url);
if (parsed === null) return null;
const project = findProjectForChangeRequest(
projects.filter((candidate) => candidate.environmentId === environmentId),
parsed,
);
const candidates = projects.filter((candidate) => candidate.environmentId === environmentId);
const own = findProjectForChangeRequest(candidates, parsed);
const project =
own ??
(capabilities.threadPullRequests === true
? findProjectOnChangeRequestHost(candidates, parsed)
: undefined);
if (project === undefined) return null;
return {
environmentId,
input: {
projectId: project.id,
host: parsed.authority ?? parsed.host,
repository: sourceControlRepositorySelector(project.repositoryIdentity) ?? parsed.repository,
repository:
(own && sourceControlRepositorySelector(own.repositoryIdentity)) ?? parsed.repository,
number: parsed.number,
},
};
Expand All @@ -83,7 +98,7 @@ export function usePullRequestPreviewTarget(environmentId: EnvironmentId | null,
resolvePullRequestPreviewTarget({
environmentId,
projects,
pullRequestsEnabled: serverConfig?.environment.capabilities.pullRequests === true,
capabilities: serverConfig?.environment.capabilities,
url,
}),
[environmentId, projects, serverConfig, url],
Expand Down
2 changes: 1 addition & 1 deletion docs/user/composer.md
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ converts when you type a space or new line after it. The chip replaces the link
loads. A link to one comment on a pull request attaches that comment, with the replies before it
when it is part of a review thread. In the attach menu's pull request picker, right-click a pull
request and choose **Attach a comment…** to pick one of its comments instead. A link T3 Code cannot read stays as text: a Linear issue or Slack message before you connect that service, or a
pull request from a repository none of your projects is checked out from. Paste with
pull request from a host none of your projects is checked out from. Paste with
`Cmd+Shift+V` (`Ctrl+Shift+V` elsewhere) to keep a link as text. A link you turn back into text
with undo, or whose chip you delete, is not converted again as you keep typing.

Expand Down