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
57 changes: 56 additions & 1 deletion src/lib/pr-url.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { beforeEach, describe, expect, it, vi } from "vitest";

import { _resetPrLinkIndex, parsePrUrl, publishPrLinkIndex } from "./pr-url";
import {
_resetPrLinkIndex,
parsePrUrl,
publishPrLinkIndex,
resolvePrLink,
resolvePrRoot,
} from "./pr-url";
import type { PrRow } from "./pr-overview";

const openUrl = vi.hoisted(() => vi.fn(() => Promise.resolve()));
Expand Down Expand Up @@ -88,6 +94,45 @@ describe("parsePrUrl", () => {
});
});

describe("repository matching", () => {
const parsed = {
kind: "github" as const,
host: "github.com",
slug: "example/codemux",
number: 285,
};
const otherHostRow = row({
url: "https://git.acme.internal/example/codemux/pull/285",
projectRoot: "/home/dev/projects/enterprise-codemux",
});

it("matches both host and repository for an indexed pull request", () => {
expect(resolvePrLink(parsed, [otherHostRow, row()])).toEqual(row());
expect(resolvePrLink(parsed, [otherHostRow])).toBeNull();
});

it("checks the host when falling back to another pull request in the repository", () => {
expect(resolvePrRoot({ ...parsed, number: 353 }, [otherHostRow])).toBeNull();
});

it("checks the host when falling back to a workspace URL", () => {
expect(resolvePrRoot(parsed, [], [{
url: otherHostRow.url,
projectRoot: otherHostRow.projectRoot,
}])).toBeNull();
expect(resolvePrRoot(parsed, [], [{
url: row().url,
projectRoot: row().projectRoot,
}])).toBe(row().projectRoot);
});

it("accepts the www alias supported by URL parsing", () => {
expect(resolvePrLink(parsed, [row({
url: "https://www.github.com/example/codemux/pull/285",
})])).not.toBeNull();
});
});

describe("openExternalUrl", () => {
beforeEach(() => {
vi.clearAllMocks();
Expand Down Expand Up @@ -135,6 +180,16 @@ describe("openExternalUrl", () => {
});
});

it("opens a same-named repository on another host in the browser", async () => {
setCustomHosts({ "git.acme.internal": "github" });
publishPrLinkIndex([row()]);

const url = "https://git.acme.internal/example/codemux/pull/285";
expect(await openExternalUrl(url)).toBe("browser");
expect(openUrl).toHaveBeenCalledWith(url);
expect(useUIStore.getState().showPullRequests).toBe(false);
});

it("shift-click keeps the browser", async () => {
publishPrLinkIndex([row()]);

Expand Down
26 changes: 20 additions & 6 deletions src/lib/pr-url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,18 +119,28 @@ export function _resetPrLinkIndex(): void {
indexedRows = [];
}

function matchesHost(host: string, url: string): boolean {
try {
// URL parsing recognises www aliases, so repository matching must too.
return new URL(url).hostname.toLowerCase().replace(/^www\./, "") ===
host.toLowerCase().replace(/^www\./, "");
} catch {
return false;
}
}

/**
* The open row this URL names, if any.
*
* Matched on repository slug *and* number: a number alone is only unique
* within one repository, and two open projects can easily both have a
* `#12`.
* Matched on host, repository slug and number: separate servers can host
* repositories with the same slug and pull-request numbers.
*/
export function resolvePrLink(parsed: ParsedPrUrl, rows: PrRow[] = indexedRows): PrRow | null {
const slug = parsed.slug.toLowerCase();
return (
rows.find(
(row) => row.number === parsed.number && (row.repo ?? "").toLowerCase() === slug,
(row) => row.number === parsed.number &&
(row.repo ?? "").toLowerCase() === slug && matchesHost(parsed.host, row.url),
) ?? null
);
}
Expand All @@ -152,7 +162,11 @@ export function resolvePrRoot(
const exact = resolvePrLink(parsed, rows);
if (exact) return exact.projectRoot;
const slug = parsed.slug.toLowerCase();
const sameRepo = rows.find((row) => (row.repo ?? "").toLowerCase() === slug);
const sameRepo = rows.find((row) =>
(row.repo ?? "").toLowerCase() === slug && matchesHost(parsed.host, row.url),
);
if (sameRepo) return sameRepo.projectRoot;
return known.find((k) => repoSlugFromUrl(k.url)?.toLowerCase() === slug)?.projectRoot ?? null;
return known.find((k) =>
repoSlugFromUrl(k.url)?.toLowerCase() === slug && matchesHost(parsed.host, k.url),
)?.projectRoot ?? null;
}
Loading