Skip to content

fix(server): find a branch's Forgejo PR by head instead of paging every PR - #44

Merged
pseudoseed merged 1 commit into
mainfrom
fix/forgejo-branch-pr-lookup
Oct 4, 2026
Merged

pseudoseed merged 1 commit into
mainfrom
fix/forgejo-branch-pr-lookup

Conversation

@pseudoseed

Copy link
Copy Markdown
Owner

Why

Fixes #41.

The Forgejo source-control provider found a branch's PR by listing /pulls 50 at a time and comparing head.ref. A branch with no PR read every page. On git.pseudoseed.com (Forgejo 16.0.5) that was 153,134 list requests in one day from one desktop client, about 2 s each on the server.

What changed

apps/server/src/sourceControl/ForgejoSourceControlProvider.ts, listChangeRequests:

  • Every page request now sends head=<branch>. Forgejo 16 narrows /pulls to that head branch, so a lookup takes 1 request, or 2 when the caller asks for more than the PRs that exist.
  • The loop stops after 5 pages (250 PRs). Older Forgejo and Gitea ignore head and return every PR, so this bounds the scan there.
  • The existing head.ref, owner and repository checks are unchanged, so owner:branch selectors still filter forks the same way.

Tests in ForgejoSourceControlProvider.test.ts use a fake server that either honours head or ignores it. They cover: PR found in 1 filtered request (branch name with / and #, to check encoding), no PR in 1 request, owner selector sends the bare branch and keeps the owner check, the 5-page cap when head is ignored, and a recent PR still found when head is ignored.

Why not GET /pulls/{base}/{head}

The issue suggested it. I did not use it, for 2 reasons:

Suggestion 1 (fetch by known PR number) already holds: getChangeRequest calls pulls/{index}. listChangeRequests callers have no number.

How it was verified

Live API, git.pseudoseed.com, repo pseudoseed/infra (1,093 PRs), read-only GETs with curl:

  • /api/v1/version returns 16.0.5+gitea-1.22.0. The served swagger lists head and base query parameters on GET /repos/{owner}/{repo}/pulls.
  • pulls?state=all&head=no-such-branch-zzz returns an empty array, x-total-count: 0, 86 ms. The unfiltered first page took 1.36 s.
  • pulls?state=open&head=fix%2Fgideon-vm-discard returns an array with 1 object: fix(web): apply sticky model when reusing existing draft thread pingdotgg/t3code#1291, state=open, head.ref=fix/gideon-vm-discard.
  • pulls?state=all&head=renovate%2Fghcr.io-shelf-nu-shelf.nu-latest returns 17 objects, all merged. Their head.ref is refs/pull/N/head, because Forgejo reports that once the branch is deleted, so the provider's head.ref check drops them. The old code dropped them too.
  • head=pseudoseed%3Afix%2Fgideon-vm-discard (owner form) returns an empty array, so the provider sends the bare branch.
  • pulls/main/no-such-branch-zzz returns 404 with {"message":"The target couldn't be found.",...}. A missing repository also returns 404.

Patched provider run against the live server through the real ForgejoCli layer (fj credentials), via a throwaway script that is not committed:

Lookup Before After
no-such-branch-zzz, state all, limit 20 23 requests, 31.3 s 1 request, 93 ms
no-such-branch-zzz, state open, limit 1 not run 1 request, 81 ms
fix/gideon-vm-discard, state open, limit 1 not run 1 request, 214 ms, found pingdotgg#1291
fix/gideon-vm-discard, state all, limit 20 not run 2 requests, 163 + 85 ms, found pingdotgg#1291

Times are client round trips from this machine, not server-side.

Commands, run in apps/server with Node 24.20.0 and pnpm 11.10.0:

  • vp test run src/sourceControl/ForgejoSourceControlProvider.test.ts: 11 passed. Against the unpatched provider, 4 of the 5 new tests fail. For example, the no-PR case made 21 requests instead of 1.
  • vp test run src/sourceControl/ src/git/GitManager.test.ts: 20 files, 327 tests passed.
  • vp run typecheck (tsc --noEmit): exit 0, 0 errors.
  • vp lint --report-unused-disable-directives and vp fmt --check on the 2 changed files: exit 0, no findings.

Not verified

  • No desktop or dev-server run. The real user path was checked only through the provider script above.
  • The tea api transport was not run live. Only the fj HTTP path was. Both receive the same path string.
  • No server-side request count after the change. That needs a build in daily use.
  • Older servers were not run live. That Forgejo 11.0 to 15.0 and Gitea (main, release/v1.24, release/v1.22) ignore head on the list endpoint comes from reading their source. On those servers, a PR older than the 250 most recently updated is no longer found.
  • Suggestion 4 (poll idle threads less often) is not in this PR. GitManager already caches lookups for 60 s with an open PR and 5 min without one.

🤖 Generated with Claude Code

…ry PR

The Forgejo provider found a branch's pull request by listing /pulls 50 at a
time and comparing head.ref, so a branch with no PR read every page. On one
Forgejo 16.0.5 server that was 153,134 list requests in one day from one
desktop client, about 2 s each on the server.

Forgejo 16 filters /pulls by a `head` query parameter. Branch lookups now
send it, so a lookup takes 1 or 2 short requests. Older Forgejo and Gitea
ignore the parameter, so the scan now stops after 5 pages instead of
reading the whole history.

Fixes #41

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XS labels Oct 4, 2026
@github-actions

github-actions Bot commented Oct 4, 2026

Copy link
Copy Markdown

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ℹ️ The exact PR base did not have a successful artifact. Baseline uses the latest successful main measurement shown below.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire 13.9 KiB 14.0 KiB +22 B (+0.2%) 15.1 KiB ✅
Codex Thread snapshot wire 7.3 KiB 7.3 KiB +5 B (+0.1%) 7.4 KiB ✅
Codex Live turn WebSocket wire 6.6 KiB 6.7 KiB +17 B (+0.2%) 7.8 KiB ✅
Codex Live turn WebSocket decoded 57.6 KiB 57.6 KiB 0 B (0.0%) 66.4 KiB ✅
Codex Live turn messages 9 9 0 (0.0%) 21 ✅
Claude Total thread wire 14.0 KiB 13.9 KiB −36 B (−0.3%) 15.1 KiB ✅
Claude Thread snapshot wire 7.3 KiB 7.3 KiB −8 B (−0.1%) 7.4 KiB ✅
Claude Live turn WebSocket wire 6.7 KiB 6.6 KiB −28 B (−0.4%) 7.8 KiB ✅
Claude Live turn WebSocket decoded 58.4 KiB 58.3 KiB −44 B (−0.1%) 66.4 KiB ✅
Claude Live turn messages 9 8 −1 (−11.1%) 21 ✅

Baseline: e399c14 · PR result: dc6b9e2 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 116.3 KiB
  • Claude decoded thread snapshot: 117.0 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@pseudoseed
pseudoseed merged commit fb9f5c6 into main Oct 4, 2026
24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XS vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Forgejo provider pages through every PR to find a branch's PR (about 150k API calls a day)

1 participant