From b28aafa1df1a4b7ac9f52d8c7e393a83b2bda4ac Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Mon, 2 Nov 2026 07:32:03 +0100 Subject: [PATCH] fix(forges): isolate repository identity by remote Revalidate pull request sources when a checkout changes forge servers. --- .../server/src/services/gitea-service.test.ts | 33 +++++++++++++++++++ packages/server/src/services/gitea-service.ts | 2 ++ 2 files changed, 35 insertions(+) diff --git a/packages/server/src/services/gitea-service.test.ts b/packages/server/src/services/gitea-service.test.ts index 05cc027307..18b8de9ffa 100644 --- a/packages/server/src/services/gitea-service.test.ts +++ b/packages/server/src/services/gitea-service.test.ts @@ -2316,6 +2316,39 @@ describe("createGiteaService", () => { expect(calls.filter((args) => args[1] === "repos/example-user/sample-repo")).toHaveLength(2); }); + it("revalidates the source repository when the forge remote changes", async () => { + let host = "first.example"; + const { service, calls } = makeService( + (args) => { + if (args[0] === "pr" && args[1] === "list") + return ok(JSON.stringify([{ ...OPEN_PR, index: "8", head: "topic" }])); + if (args[0] === "api" && args[1].endsWith("/pulls/8")) { + const pr = currentPullRequestApi({ + number: 8, + state: "open", + headRef: "topic", + headSha: "1".repeat(40), + }); + if (host === "second.example") pr.head.repo.full_name = "example-user/foreign"; + return ok(JSON.stringify(pr)); + } + if (args[0] === "api" && args[1] === "repos/example-user/sample-repo") + return ok(JSON.stringify({ full_name: "example-user/sample-repo" })); + if (args[0] === "api" && args[1].includes("pulls?state=all")) return ok("[]"); + if (args[0] === "pr" && args[1] === "8") return ok(JSON.stringify(STATUS_PR_VIEW)); + if (args[0] === "api" && args[1].includes("/commits/")) + return ok(JSON.stringify({ state: "", statuses: [] })); + throw new Error(`unexpected call: ${args.join(" ")}`); + }, + { resolveRemoteUrl: async () => `https://${host}/example-user/sample-repo.git` }, + ); + const input = { cwd: "/repo", headRef: "topic" }; + expect((await service.getCurrentPullRequestStatus(input))?.number).toBe(8); + host = "second.example"; + expect(await service.getCurrentPullRequestStatus(input)).toBeNull(); + expect(calls.filter((args) => args[1]?.endsWith("/pulls/8"))).toHaveLength(2); + }); + it.each(["open", "merged"] as const)("matches a transferred fork for an %s PR", async (state) => { const original = currentPullRequestApi({ number: 8, diff --git a/packages/server/src/services/gitea-service.ts b/packages/server/src/services/gitea-service.ts index 32879e3b9c..4764b442de 100644 --- a/packages/server/src/services/gitea-service.ts +++ b/packages/server/src/services/gitea-service.ts @@ -1868,6 +1868,7 @@ export function createGiteaService(options: CreateGiteaServiceOptions = {}): For endpoint?: import("./forge-service.js").HeadRepositoryEndpoint, ): Promise<{ match: GiteaPrListItem | null; exhausted: boolean }> { let identityReads = 0; + const remoteUrl = await resolveRemoteUrl(input.cwd); for (let page = 1; page <= CURRENT_PR_OPEN_LOOKUP_MAX_PAGES; page += 1) { const items = await listPullRequestItems({ cwd: input.cwd, @@ -1879,6 +1880,7 @@ export function createGiteaService(options: CreateGiteaServiceOptions = {}): For if (!(await matchesHead(item))) continue; const identityKey = JSON.stringify([ input.cwd, + remoteUrl, repoPath, item.index, expectedPath.toLowerCase(),