diff --git a/apps/mobile/src/lib/githubIssueUrl.ts b/apps/mobile/src/lib/githubIssueUrl.ts index 1214102c81..f7101d4fde 100644 --- a/apps/mobile/src/lib/githubIssueUrl.ts +++ b/apps/mobile/src/lib/githubIssueUrl.ts @@ -9,23 +9,28 @@ export interface ParsedGithubIssueUrl { } const GITHUB_ISSUE_URL_PATTERN = - /^https?:\/\/github\.com\/([^/\s]+)\/([^/\s]+)\/(issues|pull)\/(\d+)(?:[/?#].*)?$/; + /^https?:\/\/github\.com\/([^/\s]+)\/([^/\s]+)\/(issues|pull)\/(\d+)([/?#].*)?$/; export function parseGithubIssueUrl(text: string): ParsedGithubIssueUrl | null { const trimmed = text.trim(); const match = trimmed.match(GITHUB_ISSUE_URL_PATTERN); if (!match) return null; - const [, owner, repo, segment, rawNumber] = match; + const [, owner, repo, segment, rawNumber, suffix = ""] = match; const number = Number(rawNumber); if (!Number.isInteger(number) || number <= 0) return null; const kind: GithubRefKind = segment === "pull" ? "pr" : "issue"; + // A fragment like #discussion_r123 anchors a specific comment, so keep the + // whole suffix — anchors such as #r123 only resolve on the /files subpage + // they were copied from. Without a fragment the tab/query suffix is noise. + const hashIndex = suffix.indexOf("#"); + const hasFragment = hashIndex !== -1 && hashIndex < suffix.length - 1; return { kind, owner, repo, number, - normalizedUrl: `https://github.com/${owner}/${repo}/${segment}/${number}`, + normalizedUrl: `https://github.com/${owner}/${repo}/${segment}/${number}${hasFragment ? suffix : ""}`, }; } diff --git a/packages/core/src/message-editor/githubIssueUrl.test.ts b/packages/core/src/message-editor/githubIssueUrl.test.ts index 039d639b05..c62db88f34 100644 --- a/packages/core/src/message-editor/githubIssueUrl.test.ts +++ b/packages/core/src/message-editor/githubIssueUrl.test.ts @@ -55,8 +55,45 @@ describe("parseGithubIssueUrl", () => { }, }, { - name: "fragment is stripped from normalized URL", + name: "issue comment fragment is preserved", input: "https://github.com/PostHog/code/issues/1808#issuecomment-123", + expected: { + kind: "issue", + owner: "PostHog", + repo: "code", + number: 1808, + normalizedUrl: + "https://github.com/PostHog/code/issues/1808#issuecomment-123", + }, + }, + { + name: "PR review comment fragment is preserved", + input: + "https://github.com/PostHog/posthog/pull/72409#discussion_r3647131256", + expected: { + kind: "pr", + owner: "PostHog", + repo: "posthog", + number: 72409, + normalizedUrl: + "https://github.com/PostHog/posthog/pull/72409#discussion_r3647131256", + }, + }, + { + name: "files tab suffix is kept when a comment fragment needs it", + input: "https://github.com/PostHog/code/pull/1454/files#r3647131256", + expected: { + kind: "pr", + owner: "PostHog", + repo: "code", + number: 1454, + normalizedUrl: + "https://github.com/PostHog/code/pull/1454/files#r3647131256", + }, + }, + { + name: "empty fragment is stripped from normalized URL", + input: "https://github.com/PostHog/code/issues/1808#", expected: { kind: "issue", owner: "PostHog", diff --git a/packages/core/src/message-editor/githubIssueUrl.ts b/packages/core/src/message-editor/githubIssueUrl.ts index 094eab0c1d..a905692050 100644 --- a/packages/core/src/message-editor/githubIssueUrl.ts +++ b/packages/core/src/message-editor/githubIssueUrl.ts @@ -11,23 +11,28 @@ export interface ParsedGithubIssueUrl { } const GITHUB_ISSUE_URL_PATTERN = - /^https?:\/\/github\.com\/([^/\s]+)\/([^/\s]+)\/(issues|pull)\/(\d+)(?:[/?#].*)?$/; + /^https?:\/\/github\.com\/([^/\s]+)\/([^/\s]+)\/(issues|pull)\/(\d+)([/?#].*)?$/; export function parseGithubIssueUrl(text: string): ParsedGithubIssueUrl | null { const trimmed = text.trim(); const match = trimmed.match(GITHUB_ISSUE_URL_PATTERN); if (!match) return null; - const [, owner, repo, segment, rawNumber] = match; + const [, owner, repo, segment, rawNumber, suffix = ""] = match; const number = Number(rawNumber); if (!Number.isInteger(number) || number <= 0) return null; const kind: GithubRefKind = segment === "pull" ? "pr" : "issue"; + // A fragment like #discussion_r123 anchors a specific comment, so keep the + // whole suffix — anchors such as #r123 only resolve on the /files subpage + // they were copied from. Without a fragment the tab/query suffix is noise. + const hashIndex = suffix.indexOf("#"); + const hasFragment = hashIndex !== -1 && hashIndex < suffix.length - 1; return { kind, owner, repo, number, - normalizedUrl: `https://github.com/${owner}/${repo}/${segment}/${number}`, + normalizedUrl: `https://github.com/${owner}/${repo}/${segment}/${number}${hasFragment ? suffix : ""}`, }; }