authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-22 20:50:50-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-22 20:50:50-07:00
logeafb4a9340bd2c2271b7c8fac7eddd55beea3972
tree018bd6a7573dbb9f39077a250d8f40681f605c29
parent18de6bb328059703b5d1b0661610076e3c0fa2d4
signature Signed by PGP key B5690EEEBB952194

fix(server): GitHub PR lookups stop probing owner-qualified heads (#13200)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

2 files changed, 51 insertions(+), 104 deletions(-)

apps/server/src/git/GitManager.test.ts+21-98
...@@ -1309,7 +1309,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -1309,7 +1309,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
1309 prListByHeadSelector: {1309 prListByHeadSelector: {
1310 // Fake gh returns raw JSON stdout, matching the CLI boundary under test.1310 // Fake gh returns raw JSON stdout, matching the CLI boundary under test.
1311 // @effect-diagnostics-next-line preferSchemaOverJson:off1311 // @effect-diagnostics-next-line preferSchemaOverJson:off
1312 "contributor:feature/deleted-fork-branch": JSON.stringify([1312 "feature/deleted-fork-branch": JSON.stringify([
1313 {1313 {
1314 number: 218,1314 number: 218,
1315 title: "Deleted fork branch PR",1315 title: "Deleted fork branch PR",
...@@ -1339,8 +1339,9 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -1339,8 +1339,9 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
1339 updatedAt: "2026-04-05T15:00:00.000Z",1339 updatedAt: "2026-04-05T15:00:00.000Z",
1340 });1340 });
1341 expect(1341 expect(
1342 ghCalls.some((call) => call.includes("--head contributor:feature/deleted-fork-branch")),1342 ghCalls.some((call) => call.includes("--head feature/deleted-fork-branch --state all")),
1343 ).toBe(true);1343 ).toBe(true);
1344 expect(ghCalls.some((call) => call.includes("--head contributor:"))).toBe(false);
1344 }),1345 }),
1345 );1346 );
13461347
...@@ -1776,8 +1777,6 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -1776,8 +1777,6 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
1776 const { manager } = yield* makeManager({1777 const { manager } = yield* makeManager({
1777 ghScenario: {1778 ghScenario: {
1778 prListByHeadSelector: {1779 prListByHeadSelector: {
1779 "alice:feature": output,
1780 "fork:feature": output,
1781 feature: output,1780 feature: output,
1782 },1781 },
1783 },1782 },
...@@ -1944,10 +1943,6 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -1944,10 +1943,6 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
1944 const { manager, ghCalls } = yield* makeManager({1943 const { manager, ghCalls } = yield* makeManager({
1945 ghScenario: {1944 ghScenario: {
1946 prListSequence: [1945 prListSequence: [
1947 // @effect-diagnostics-next-line preferSchemaOverJson:off
1948 JSON.stringify([]),
1949 // @effect-diagnostics-next-line preferSchemaOverJson:off
1950 JSON.stringify([]),
1951 // @effect-diagnostics-next-line preferSchemaOverJson:off1946 // @effect-diagnostics-next-line preferSchemaOverJson:off
1952 JSON.stringify([1947 JSON.stringify([
1953 {1948 {
...@@ -1983,7 +1978,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -1983,7 +1978,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
1983 updatedAt: "2026-03-10T07:00:00.000Z",1978 updatedAt: "2026-03-10T07:00:00.000Z",
1984 });1979 });
1985 expect(ghCalls).toContain(1980 expect(ghCalls).toContain(
1986 "pr list --head jasonLaster:statemachine --state all --limit 20 --json number,title,url,baseRefName,headRefName,state,isDraft,mergedAt,closedAt,updatedAt,isCrossRepository,headRepository,headRepositoryOwner",1981 "pr list --head statemachine --state all --limit 100 --json number,title,url,baseRefName,headRefName,state,isDraft,mergedAt,closedAt,updatedAt,isCrossRepository,headRepository,headRepositoryOwner",
1987 );1982 );
1988 }),1983 }),
1989 20_000,1984 20_000,
...@@ -2015,7 +2010,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -2015,7 +2010,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
2015 ghScenario: {2010 ghScenario: {
2016 prListByHeadSelector: {2011 prListByHeadSelector: {
2017 // @effect-diagnostics-next-line preferSchemaOverJson:off2012 // @effect-diagnostics-next-line preferSchemaOverJson:off
2018 "contributor:main": JSON.stringify([2013 main: JSON.stringify([
2019 {2014 {
2020 number: 777,2015 number: 777,
2021 title: "Fork PR from main",2016 title: "Fork PR from main",
...@@ -2049,7 +2044,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -2049,7 +2044,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
2049 updatedAt: "2026-03-10T07:00:00.000Z",2044 updatedAt: "2026-03-10T07:00:00.000Z",
2050 });2045 });
2051 expect(ghCalls).toContain(2046 expect(ghCalls).toContain(
2052 "pr list --head contributor:main --state all --limit 20 --json number,title,url,baseRefName,headRefName,state,isDraft,mergedAt,closedAt,updatedAt,isCrossRepository,headRepository,headRepositoryOwner",2047 "pr list --head main --state all --limit 100 --json number,title,url,baseRefName,headRefName,state,isDraft,mergedAt,closedAt,updatedAt,isCrossRepository,headRepository,headRepositoryOwner",
2053 );2048 );
2054 }),2049 }),
2055 20_000,2050 20_000,
...@@ -2114,34 +2109,6 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -2114,34 +2109,6 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
2114 updatedAt: "2026-04-01T10:00:00Z",2109 updatedAt: "2026-04-01T10:00:00Z",
2115 },2110 },
2116 ]),2111 ]),
2117 // @effect-diagnostics-next-line preferSchemaOverJson:off
2118 "pingdotgg:effect-atom": JSON.stringify([]),
2119 // @effect-diagnostics-next-line preferSchemaOverJson:off
2120 "my-org/upstream:effect-atom": JSON.stringify([]),
2121 // @effect-diagnostics-next-line preferSchemaOverJson:off
2122 "pingdotgg:upstream/effect-atom": JSON.stringify([
2123 {
2124 number: 1518,
2125 title: "Wrong PR",
2126 url: "https://github.com/pingdotgg/t3code/pull/1518",
2127 baseRefName: "main",
2128 headRefName: "upstream/effect-atom",
2129 state: "OPEN",
2130 updatedAt: "2026-04-01T10:00:00Z",
2131 },
2132 ]),
2133 // @effect-diagnostics-next-line preferSchemaOverJson:off
2134 "my-org/upstream:upstream/effect-atom": JSON.stringify([
2135 {
2136 number: 1518,
2137 title: "Wrong PR",
2138 url: "https://github.com/pingdotgg/t3code/pull/1518",
2139 baseRefName: "main",
2140 headRefName: "upstream/effect-atom",
2141 state: "OPEN",
2142 updatedAt: "2026-04-01T10:00:00Z",
2143 },
2144 ]),
2145 },2112 },
2146 },2113 },
2147 });2114 });
...@@ -2352,7 +2319,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -2352,7 +2319,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
2352 prListByHeadSelector: {2319 prListByHeadSelector: {
2353 // Fake gh returns raw JSON stdout, matching the CLI boundary under test.2320 // Fake gh returns raw JSON stdout, matching the CLI boundary under test.
2354 // @effect-diagnostics-next-line preferSchemaOverJson:off2321 // @effect-diagnostics-next-line preferSchemaOverJson:off
2355 "contributor:feature/fork-plain": JSON.stringify([2322 "feature/fork-plain": JSON.stringify([
2356 {2323 {
2357 number: 89,2324 number: 89,
2358 title: "Fork PR pushed without -u",2325 title: "Fork PR pushed without -u",
...@@ -2372,9 +2339,8 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -2372,9 +2339,8 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
23722339
2373 const status = yield* manager.status({ cwd: repoDir });2340 const status = yield* manager.status({ cwd: repoDir });
2374 expect(status.pr?.number).toBe(89);2341 expect(status.pr?.number).toBe(89);
2375 expect(ghCalls.some((call) => call.includes("--head contributor:feature/fork-plain"))).toBe(2342 expect(ghCalls.some((call) => call.includes("--head feature/fork-plain"))).toBe(true);
2376 true,2343 expect(ghCalls.some((call) => call.includes("--head contributor:"))).toBe(false);
2377 );
2378 expect(ghCalls.some((call) => call.includes("--head main"))).toBe(false);2344 expect(ghCalls.some((call) => call.includes("--head main"))).toBe(false);
2379 }),2345 }),
2380 );2346 );
...@@ -2409,7 +2375,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -2409,7 +2375,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
2409 prListByHeadSelector: {2375 prListByHeadSelector: {
2410 // Fake gh returns raw JSON stdout, matching the CLI boundary under test.2376 // Fake gh returns raw JSON stdout, matching the CLI boundary under test.
2411 // @effect-diagnostics-next-line preferSchemaOverJson:off2377 // @effect-diagnostics-next-line preferSchemaOverJson:off
2412 "contributor:feature/fork-settle": JSON.stringify([2378 "feature/fork-settle": JSON.stringify([
2413 {2379 {
2414 number: 91,2380 number: 91,
2415 title: "Fork PR to settle",2381 title: "Fork PR to settle",
...@@ -3570,7 +3536,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -3570,7 +3536,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
3570 );3536 );
35713537
3572 it.effect(3538 it.effect(
3573 "returns existing cross-repo PR metadata using the fork owner selector",3539 "returns existing cross-repo PR metadata found under the bare branch name",
3574 () =>3540 () =>
3575 Effect.gen(function* () {3541 Effect.gen(function* () {
3576 const repoDir = yield* makeTempDir("t3code-git-manager-");3542 const repoDir = yield* makeTempDir("t3code-git-manager-");
...@@ -3589,8 +3555,6 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -3589,8 +3555,6 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
3589 const { manager, ghCalls } = yield* makeManager({3555 const { manager, ghCalls } = yield* makeManager({
3590 ghScenario: {3556 ghScenario: {
3591 prListSequence: [3557 prListSequence: [
3592 // @effect-diagnostics-next-line preferSchemaOverJson:off
3593 JSON.stringify([]),
3594 // @effect-diagnostics-next-line preferSchemaOverJson:off3558 // @effect-diagnostics-next-line preferSchemaOverJson:off
3595 JSON.stringify([3559 JSON.stringify([
3596 {3560 {
...@@ -3622,7 +3586,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -3622,7 +3586,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
3622 expect(result.pr.number).toBe(142);3586 expect(result.pr.number).toBe(142);
3623 expect(3587 expect(
3624 ghCalls.some((call) =>3588 ghCalls.some((call) =>
3625 call.includes("pr list --head octocat:statemachine --state open --limit 1"),3589 call.includes("pr list --head statemachine --state open --limit 100"),
3626 ),3590 ),
3627 ).toBe(true);3591 ).toBe(true);
3628 expect(ghCalls.some((call) => call.startsWith("pr create "))).toBe(false);3592 expect(ghCalls.some((call) => call.startsWith("pr create "))).toBe(false);
...@@ -3688,30 +3652,6 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -3688,30 +3652,6 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
3688 headRefName: "upstream/effect-atom",3652 headRefName: "upstream/effect-atom",
3689 },3653 },
3690 ]),3654 ]),
3691 // @effect-diagnostics-next-line preferSchemaOverJson:off
3692 "pingdotgg:effect-atom": JSON.stringify([]),
3693 // @effect-diagnostics-next-line preferSchemaOverJson:off
3694 "my-org/upstream:effect-atom": JSON.stringify([]),
3695 // @effect-diagnostics-next-line preferSchemaOverJson:off
3696 "pingdotgg:upstream/effect-atom": JSON.stringify([
3697 {
3698 number: 1518,
3699 title: "Wrong PR",
3700 url: "https://github.com/pingdotgg/t3code/pull/1518",
3701 baseRefName: "main",
3702 headRefName: "upstream/effect-atom",
3703 },
3704 ]),
3705 // @effect-diagnostics-next-line preferSchemaOverJson:off
3706 "my-org/upstream:upstream/effect-atom": JSON.stringify([
3707 {
3708 number: 1518,
3709 title: "Wrong PR",
3710 url: "https://github.com/pingdotgg/t3code/pull/1518",
3711 baseRefName: "main",
3712 headRefName: "upstream/effect-atom",
3713 },
3714 ]),
3715 },3655 },
3716 },3656 },
3717 });3657 });
...@@ -3731,7 +3671,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -3731,7 +3671,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
3731 );3671 );
37323672
3733 it.effect(3673 it.effect(
3734 "prefers owner-qualified selectors before bare branch names for cross-repo PRs",3674 "picks the fork PR among same-named branches from other repositories",
3735 () =>3675 () =>
3736 Effect.gen(function* () {3676 Effect.gen(function* () {
3737 const repoDir = yield* makeTempDir("t3code-git-manager-");3677 const repoDir = yield* makeTempDir("t3code-git-manager-");
...@@ -3763,9 +3703,6 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -3763,9 +3703,6 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
3763 baseRefName: "main",3703 baseRefName: "main",
3764 headRefName: "statemachine",3704 headRefName: "statemachine",
3765 },3705 },
3766 ]),
3767 // @effect-diagnostics-next-line preferSchemaOverJson:off
3768 "octocat:statemachine": JSON.stringify([
3769 {3706 {
3770 number: 142,3707 number: 142,
3771 title: "Existing fork PR",3708 title: "Existing fork PR",
...@@ -3782,8 +3719,6 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -3782,8 +3719,6 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
3782 },3719 },
3783 },3720 },
3784 ]),3721 ]),
3785 // @effect-diagnostics-next-line preferSchemaOverJson:off
3786 "fork-seed:statemachine": JSON.stringify([]),
3787 },3722 },
3788 },3723 },
3789 });3724 });
...@@ -3795,11 +3730,9 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -3795,11 +3730,9 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
37953730
3796 expect(result.pr.status).toBe("opened_existing");3731 expect(result.pr.status).toBe("opened_existing");
3797 expect(result.pr.number).toBe(142);3732 expect(result.pr.number).toBe(142);
37983733 expect(
3799 const ownerSelectorCallIndex = ghCalls.findIndex((call) =>3734 ghCalls.some((call) => /--head [^ ]*:/u.test(call) && call.startsWith("pr list")),
3800 call.includes("pr list --head octocat:statemachine --state open --limit 1"),3735 ).toBe(false);
3801 );
3802 expect(ownerSelectorCallIndex).toBeGreaterThanOrEqual(0);
3803 expect(ghCalls.some((call) => call.startsWith("pr create "))).toBe(false);3736 expect(ghCalls.some((call) => call.startsWith("pr create "))).toBe(false);
3804 }),3737 }),
3805 12_000,3738 12_000,
...@@ -3828,7 +3761,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -3828,7 +3761,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
3828 ghScenario: {3761 ghScenario: {
3829 prListByHeadSelector: {3762 prListByHeadSelector: {
3830 // @effect-diagnostics-next-line preferSchemaOverJson:off3763 // @effect-diagnostics-next-line preferSchemaOverJson:off
3831 "octocat:statemachine": JSON.stringify([3764 statemachine: JSON.stringify([
3832 {3765 {
3833 number: 142,3766 number: 142,
3834 title: "Existing fork PR",3767 title: "Existing fork PR",
...@@ -3846,11 +3779,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -3846,11 +3779,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
3846 },3779 },
3847 ]),3780 ]),
3848 // @effect-diagnostics-next-line preferSchemaOverJson:off3781 // @effect-diagnostics-next-line preferSchemaOverJson:off
3849 "fork-seed:statemachine": JSON.stringify([]),
3850 // @effect-diagnostics-next-line preferSchemaOverJson:off
3851 "t3code/pr-142/statemachine": JSON.stringify([]),3782 "t3code/pr-142/statemachine": JSON.stringify([]),
3852 // @effect-diagnostics-next-line preferSchemaOverJson:off
3853 statemachine: JSON.stringify([]),
3854 },3783 },
3855 },3784 },
3856 });3785 });
...@@ -3863,10 +3792,10 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -3863,10 +3792,10 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
3863 expect(result.pr.status).toBe("opened_existing");3792 expect(result.pr.status).toBe("opened_existing");
3864 expect(result.pr.number).toBe(142);3793 expect(result.pr.number).toBe(142);
38653794
3866 const openLookupCalls = ghCalls.filter((call) => call.includes("--state open --limit 1"));3795 const openLookupCalls = ghCalls.filter((call) => call.includes("--state open --limit 100"));
3867 expect(openLookupCalls).toHaveLength(1);3796 expect(openLookupCalls).toHaveLength(1);
3868 expect(openLookupCalls[0]).toContain(3797 expect(openLookupCalls[0]).toContain(
3869 "pr list --head octocat:statemachine --state open --limit 1",3798 "pr list --head statemachine --state open --limit 100",
3870 );3799 );
3871 }),3800 }),
3872 12_000,3801 12_000,
...@@ -3891,12 +3820,10 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -3891,12 +3820,10 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
3891 const { manager, ghCalls } = yield* makeManager({3820 const { manager, ghCalls } = yield* makeManager({
3892 ghScenario: {3821 ghScenario: {
3893 prListSequenceByHeadSelector: {3822 prListSequenceByHeadSelector: {
3894 "octocat:statemachine": [3823 statemachine: [
3895 `[{"number":41,"title":"Ambiguous fork PR","url":"https://github.com/pingdotgg/codething-mvp/pull/41","baseRefName":"main","headRefName":"statemachine","state":"OPEN"}]`,3824 `[{"number":41,"title":"Ambiguous fork PR","url":"https://github.com/pingdotgg/codething-mvp/pull/41","baseRefName":"main","headRefName":"statemachine","state":"OPEN"}]`,
3896 `[{"number":142,"title":"Add stacked git actions","url":"https://github.com/pingdotgg/codething-mvp/pull/142","baseRefName":"main","headRefName":"statemachine","state":"OPEN","isCrossRepository":true,"headRepository":{"nameWithOwner":"octocat/codething-mvp"},"headRepositoryOwner":{"login":"octocat"}}]`,3825 `[{"number":142,"title":"Add stacked git actions","url":"https://github.com/pingdotgg/codething-mvp/pull/142","baseRefName":"main","headRefName":"statemachine","state":"OPEN","isCrossRepository":true,"headRepository":{"nameWithOwner":"octocat/codething-mvp"},"headRepositoryOwner":{"login":"octocat"}}]`,
3897 ],3826 ],
3898 "fork-seed:statemachine": ["[]"],
3899 statemachine: ["[]"],
3900 },3827 },
3901 },3828 },
3902 });3829 });
...@@ -4295,7 +4222,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -4295,7 +4222,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
4295 const { manager, ghCalls } = yield* makeManager({4222 const { manager, ghCalls } = yield* makeManager({
4296 ghScenario: {4223 ghScenario: {
4297 prListSequenceByHeadSelector: {4224 prListSequenceByHeadSelector: {
4298 "octocat:statemachine": [4225 statemachine: [
4299 // @effect-diagnostics-next-line preferSchemaOverJson:off4226 // @effect-diagnostics-next-line preferSchemaOverJson:off
4300 JSON.stringify([]),4227 JSON.stringify([]),
4301 // @effect-diagnostics-next-line preferSchemaOverJson:off4228 // @effect-diagnostics-next-line preferSchemaOverJson:off
...@@ -4317,10 +4244,6 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -4317,10 +4244,6 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
4317 },4244 },
4318 ]),4245 ]),
4319 ],4246 ],
4320 // @effect-diagnostics-next-line preferSchemaOverJson:off
4321 "fork-seed:statemachine": [JSON.stringify([])],
4322 // @effect-diagnostics-next-line preferSchemaOverJson:off
4323 statemachine: [JSON.stringify([])],
4324 },4247 },
4325 },4248 },
4326 });4249 });
apps/server/src/git/GitManager.ts+30-6
...@@ -32,6 +32,7 @@ import {...@@ -32,6 +32,7 @@ import {
32 ModelSelection,32 ModelSelection,
33 type ProjectId,33 type ProjectId,
34 SourceControlProviderError,34 SourceControlProviderError,
35 type SourceControlProviderKind,
35 type SourceControlWritingStyleSettings,36 type SourceControlWritingStyleSettings,
36 type ThreadId,37 type ThreadId,
37} from "@t3tools/contracts";38} from "@t3tools/contracts";
...@@ -594,6 +595,26 @@ function parseCustomCommitMessage(raw: string): { subject: string; body: string...@@ -594,6 +595,26 @@ function parseCustomCommitMessage(raw: string): { subject: string; body: string
594 };595 };
595}596}
596597
598// Without the owner selector, a bare branch name also lists same-named
599// branches on other forks (`main`, `patch-1`), so GitHub probes ask for a full
600// page and let matchesBranchHeadContext pick the right head. gh fetches up to
601// 100 in one request, and GitHub prices a first:100 connection like first:1.
602const GITHUB_HEAD_BRANCH_PROBE_LIMIT = 100;
603
604// `gh pr list --head` filters on the head ref name alone and accepts anything, so an
605// `owner:branch` or `remote:branch` selector silently lists zero pull requests
606// while spending a GraphQL call. Git branch names cannot contain ":", and the
607// bare head branch is always among the selectors, so GitHub probes skip them and
608// leave the owner check to matchesBranchHeadContext.
609function probeableHeadSelectors(
610 providerKind: SourceControlProviderKind,
611 headSelectors: ReadonlyArray<string>,
612): ReadonlyArray<string> {
613 return providerKind === "github"
614 ? headSelectors.filter((selector) => !selector.includes(":"))
615 : headSelectors;
616}
617
597function appendUnique(values: string[], next: string | null | undefined): void {618function appendUnique(values: string[], next: string | null | undefined): void {
598 const trimmed = next?.trim() ?? "";619 const trimmed = next?.trim() ?? "";
599 if (trimmed.length === 0 || values.includes(trimmed)) {620 if (trimmed.length === 0 || values.includes(trimmed)) {
...@@ -1604,12 +1625,14 @@ export const make = Effect.gen(function* () {...@@ -1604,12 +1625,14 @@ export const make = Effect.gen(function* () {
1604 | "isCrossRepository"1625 | "isCrossRepository"
1605 >,1626 >,
1606 ) {1627 ) {
1607 for (const headSelector of headContext.headSelectors) {1628 const provider = yield* sourceControlProvider(cwd);
1608 const pullRequests = yield* (yield* sourceControlProvider(cwd)).listChangeRequests({1629 const headSelectors = probeableHeadSelectors(provider.kind, headContext.headSelectors);
1630 for (const headSelector of headSelectors) {
1631 const pullRequests = yield* provider.listChangeRequests({
1609 cwd,1632 cwd,
1610 headSelector,1633 headSelector,
1611 state: "open",1634 state: "open",
1612 limit: 1,1635 limit: provider.kind === "github" ? GITHUB_HEAD_BRANCH_PROBE_LIMIT : 1,
1613 });1636 });
1614 const normalizedPullRequests = pullRequests.map(toPullRequestInfo);1637 const normalizedPullRequests = pullRequests.map(toPullRequestInfo);
16151638
...@@ -1634,12 +1657,13 @@ export const make = Effect.gen(function* () {...@@ -1634,12 +1657,13 @@ export const make = Effect.gen(function* () {
1634 ) {1657 ) {
1635 const parsedByNumber = new Map<number, PullRequestInfo>();1658 const parsedByNumber = new Map<number, PullRequestInfo>();
16361659
1637 for (const headSelector of headContext.headSelectors) {1660 const provider = yield* sourceControlProvider(cwd);
1638 const pullRequests = yield* (yield* sourceControlProvider(cwd)).listChangeRequests({1661 for (const headSelector of probeableHeadSelectors(provider.kind, headContext.headSelectors)) {
1662 const pullRequests = yield* provider.listChangeRequests({
1639 cwd,1663 cwd,
1640 headSelector,1664 headSelector,
1641 state: "all",1665 state: "all",
1642 limit: 20,1666 limit: provider.kind === "github" ? GITHUB_HEAD_BRANCH_PROBE_LIMIT : 20,
1643 });1667 });
16441668
1645 for (const pr of pullRequests.map(toPullRequestInfo)) {1669 for (const pr of pullRequests.map(toPullRequestInfo)) {