From 4e9476994183ac9fab6dccdda5f24c7bb4134fc0 Mon Sep 17 00:00:00 2001 From: shawn Date: Thu, 6 Aug 2026 21:18:16 +0800 Subject: [PATCH] fix: ignore merged pull request conflicts --- src/features/pr-conflict-indicator.test.ts | 31 +++++++++++++++++----- src/features/pr-conflict-indicator.ts | 8 +++--- src/lib/messages.ts | 1 + src/service-worker.test.ts | 13 ++++----- src/service-worker.ts | 9 +++++-- 5 files changed, 44 insertions(+), 18 deletions(-) diff --git a/src/features/pr-conflict-indicator.test.ts b/src/features/pr-conflict-indicator.test.ts index eb4902c..4df2643 100644 --- a/src/features/pr-conflict-indicator.test.ts +++ b/src/features/pr-conflict-indicator.test.ts @@ -48,8 +48,8 @@ describe("injectPRConflictIndicator", () => { it("checks only visible PRs and renders a conflict status without creating a label", async () => { vi.mocked(fetchPRConflictStatuses).mockResolvedValue([ - { number: 7, mergeable: "CONFLICTING" }, - { number: 8, mergeable: "MERGEABLE" }, + { number: 7, state: "OPEN", mergeable: "CONFLICTING" }, + { number: 8, state: "OPEN", mergeable: "MERGEABLE" }, ]); injectPRConflictIndicator(); @@ -102,7 +102,7 @@ describe("injectPRConflictIndicator", () => { await vi.waitFor(() => expect(fetchPRConflictStatuses).toHaveBeenCalledTimes(1)); injectPRConflictIndicator(); - resolveStatuses([{ number: 7, mergeable: "CONFLICTING" }]); + resolveStatuses([{ number: 7, state: "OPEN", mergeable: "CONFLICTING" }]); await vi.waitFor(() => expect(row7.querySelector(".better-github-conflict-indicator")).not.toBeNull(), @@ -111,10 +111,10 @@ describe("injectPRConflictIndicator", () => { it("retries unknown and missing statuses on the next polling pass", async () => { vi.mocked(fetchPRConflictStatuses) - .mockResolvedValueOnce([{ number: 7, mergeable: "UNKNOWN" }]) + .mockResolvedValueOnce([{ number: 7, state: "OPEN", mergeable: "UNKNOWN" }]) .mockResolvedValueOnce([ - { number: 7, mergeable: "CONFLICTING" }, - { number: 8, mergeable: "MERGEABLE" }, + { number: 7, state: "OPEN", mergeable: "CONFLICTING" }, + { number: 8, state: "OPEN", mergeable: "MERGEABLE" }, ]); injectPRConflictIndicator(); @@ -137,4 +137,23 @@ describe("injectPRConflictIndicator", () => { await vi.waitFor(() => expect(fetchPRConflictStatuses).toHaveBeenCalledTimes(2)); expect(row7.querySelector(".better-github-conflict-indicator")).not.toBeNull(); }); + + it("ignores merged stacked PR conflicts without retrying them", async () => { + vi.mocked(fetchPRConflictStatuses).mockResolvedValue([ + { number: 7, state: "MERGED", mergeable: "CONFLICTING" }, + ]); + + injectPRConflictIndicator(); + + const row7 = document.getElementById("issue_7")!; + observerCallback( + [{ target: row7, isIntersecting: true }] as unknown as IntersectionObserverEntry[], + {} as IntersectionObserver, + ); + await vi.waitFor(() => expect(fetchPRConflictStatuses).toHaveBeenCalledTimes(1)); + + injectPRConflictIndicator(); + expect(observe.mock.calls.filter(([row]) => row === row7)).toHaveLength(1); + expect(row7.querySelector(".better-github-conflict-indicator")).toBeNull(); + }); }); diff --git a/src/features/pr-conflict-indicator.ts b/src/features/pr-conflict-indicator.ts index 7d699c2..068cadf 100644 --- a/src/features/pr-conflict-indicator.ts +++ b/src/features/pr-conflict-indicator.ts @@ -39,14 +39,14 @@ async function checkRows( const statuses = await fetchPRConflictStatuses(owner, repo, [...rowByNumber.keys()]); if (currentGeneration !== generation) return; - const statusByNumber = new Map(statuses.map(({ number, mergeable }) => [number, mergeable])); + const statusByNumber = new Map(statuses.map((status) => [status.number, status])); for (const [number, row] of rowByNumber) { - const mergeable = statusByNumber.get(number); - if (mergeable !== "MERGEABLE" && mergeable !== "CONFLICTING") { + const status = statusByNumber.get(number); + if (!status || (status.state === "OPEN" && status.mergeable === "UNKNOWN")) { checkedRows.delete(row); continue; } - if (mergeable !== "CONFLICTING") continue; + if (status.state !== "OPEN" || status.mergeable !== "CONFLICTING") continue; if (!row?.isConnected || row.querySelector(`.${INDICATOR_CLASS}`) || hasConflictLabel(row)) { continue; diff --git a/src/lib/messages.ts b/src/lib/messages.ts index 2fdc8f5..5a912ac 100644 --- a/src/lib/messages.ts +++ b/src/lib/messages.ts @@ -5,6 +5,7 @@ export interface PRBranchInfo { export interface PRConflictStatus { number: number; + state: "OPEN" | "CLOSED" | "MERGED"; mergeable: "CONFLICTING" | "MERGEABLE" | "UNKNOWN"; } diff --git a/src/service-worker.test.ts b/src/service-worker.test.ts index 849df6d..0426c40 100644 --- a/src/service-worker.test.ts +++ b/src/service-worker.test.ts @@ -382,9 +382,9 @@ describe("service worker", () => { jsonResponse({ data: { repository: { - pr_1: { mergeable: "CONFLICTING" }, - pr_2: { mergeable: "MERGEABLE" }, - pr_3: { mergeable: "UNKNOWN" }, + pr_1: { state: "OPEN", mergeable: "CONFLICTING" }, + pr_2: { state: "OPEN", mergeable: "MERGEABLE" }, + pr_3: { state: "OPEN", mergeable: "UNKNOWN" }, }, }, }), @@ -400,12 +400,13 @@ describe("service worker", () => { expect(response).toEqual({ ok: true, data: [ - { number: 1, mergeable: "CONFLICTING" }, - { number: 2, mergeable: "MERGEABLE" }, - { number: 3, mergeable: "UNKNOWN" }, + { number: 1, state: "OPEN", mergeable: "CONFLICTING" }, + { number: 2, state: "OPEN", mergeable: "MERGEABLE" }, + { number: 3, state: "OPEN", mergeable: "UNKNOWN" }, ], }); const query = JSON.parse(vi.mocked(fetch).mock.calls[0][1]?.body as string).query as string; + expect(query).toContain("state"); expect(query).toContain("mergeable"); expect(query).not.toContain("mergeStateStatus"); }); diff --git a/src/service-worker.ts b/src/service-worker.ts index abdd9b6..e9aa3c4 100644 --- a/src/service-worker.ts +++ b/src/service-worker.ts @@ -273,14 +273,19 @@ async function fetchPRConflictStatuses( keys: [...prNumbers].sort((a, b) => a - b), aliasFor: (n) => `pr_${n}`, buildNodeQuery: (n) => `pullRequest(number: ${n}) { + state mergeable }`, parseNode: (n, pr) => { + const state = pr.state; const mergeable = pr.mergeable; - if (mergeable !== "CONFLICTING" && mergeable !== "MERGEABLE" && mergeable !== "UNKNOWN") { + if ( + (state !== "OPEN" && state !== "CLOSED" && state !== "MERGED") || + (mergeable !== "CONFLICTING" && mergeable !== "MERGEABLE" && mergeable !== "UNKNOWN") + ) { return null; } - return { number: n, mergeable }; + return { number: n, state, mergeable }; }, }); }