Repository navigation
Description
Note
Written by Claude Opus 5.5 on behalf of @erannave
Before submitting
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
Related, not duplicates: #19347 (closed, Forgejo 15) is the same loop. It was closed with "upgrade to Forgejo 16" as the workaround, and that workaround does not exist for Gitea. #19657 and #21620 multiply the number of lookups; this issue is about the cost of each one.
Area
apps/server, packages/source-control-forgejo
Steps to reproduce
- Use a Gitea server (28.2.0, the latest release, 2026-10-18) with a repository that has a few thousand pull requests. Ours has 3,089.
- Request
GET /api/v1/repos/<owner>/<repo>/pulls?state=all&head=no-such-branch&sort=recentupdate&limit=50&page=2. It returns 50 pull requests, none with that head: Gitea ignoreshead. - Connect T3 to the repository and open threads on a few branches that have no pull request.
- Count
ForgejoCli.apispans inserver.trace.ndjson, ortea api …/pulls?…&page=Nprocesses under the T3 server.
Gitea does not support head on this endpoint and has no plans to:
routers/api/v1/repo/pull.goon Giteamain(checked 2026-10-22) readsbase_branch,state,sort,milestone,labels,poster,pageandlimit. There is nohead.- go-gitea/gitea has no issue or pull request, open or closed, that asks for or adds a head filter on the list endpoint.
Expected behavior
A branch PR lookup costs a bounded number of requests on Gitea, as it does on GitHub, GitLab and Forgejo 16.
Actual behavior
findLatestPrForHeadContext calls listChangeRequests with state: "all" and the default limit: 20. The Forgejo provider has no headBranchProbe. The loop in ForgejoSourceControlProvider.listChangeRequests (line ~316 on main) stops only after 20 matches or an empty page. A branch never has 20 pull requests, so every lookup reads every page.
Measured against our server:
| Pages per lookup | 63 (62 full pages of 50, then the empty one) |
| One page | 656 KB, about 1.1 s on an idle server |
| One lookup | about 40 MB and over a minute idle; 431–695 s per lookup under load |
| Sustained rate | 300–500 tea api requests per minute, about 200–300 MB of JSON per minute |
The rate holds because the "no PR" result is cached for 5 minutes (PR_LOOKUP_NO_OPEN_PR_CACHE_TTL), so each branch's 63-request scan starts again. Our instance had 97 distinct (project, worktree, branch) groups to look up; most came from hidden subagent threads (#19657). After a T3 restart, the startup backfill (#21620) added 621 more groups from settled threads.
This is one T3 instance. Every developer who runs T3 against the same Gitea server adds the same load. With five developers on one repository, Gitea serves roughly 1,500–2,500 full-page list requests per minute, about 1–1.5 GB of JSON per minute, to answer "does this branch have a PR?".
A Gitea restart makes it worse. Every lookup that failed during the outage retries at once: 40 lookups reached page 5 within the first minute after our server came back.
Impact
Major degradation or frequent failure
The load is continuous for as long as T3 runs, and it grows with the number of open branches, subagent threads, and developers. Each list page makes Gitea convert 50 pull requests to API objects. Pull requests whose refs are missing (ours: 58 old pull requests whose refs went away in a history rewrite) log GetRefCommitID[refs/pull/N/head]: object does not exist on every page that contains them. That log is how we noticed this.
Version or commit
T3 Code 0.1.1 (server 0.1.1). The loop is unchanged on main as of a8e8e7e (2026-10-20).
Environment
Windows + WSL2 (T3 server in WSL), tea 0.16.0, Gitea 28.2.0 (self-hosted).
Logs or stack traces
tea api --include --login <login> --repo <owner>/<repo> --method GET https://<gitea-host>/api/v1/repos/<owner>/<repo>/pulls?state=all&head=<branch-a>&sort=recentupdate&limit=50&page=22
tea api --include --login <login> --repo <owner>/<repo> --method GET https://<gitea-host>/api/v1/repos/<owner>/<repo>/pulls?state=all&head=<branch-b>&sort=recentupdate&limit=50&page=17
In a 3-minute sample, 951 tea processes had the T3 server as parent, covering 44 branches at pages 1–46. In the traces, every completed lookup on the 3,089-PR repository made exactly 63 ForgejoCli.api calls.
Suggested fix
Gitea has a direct lookup: GET /repos/{owner}/{repo}/pulls/{base}/{head} (present in 28.2.0). Against our server it answers in one request: about 1 s with the pull request, and 404 in 0.32 s when the branch has none.
It has a known limitation, go-gitea/gitea#35217: it always returns the first-created pull request for that base and head, and it needs the base branch. So it can't replace the list on its own. One option that doesn't trade correctness for speed, unlike the page caps rejected in #17201 and #19679:
- Detect that the server ignored
head(page 1 contains a pull request with another head ref). - Then ask
pulls/{base}/{head}for the candidate bases T3 already resolves (default branch, configured merge base). Fall back to the full scan only when the result is closed or merged and a newer pull request might exist.
Workaround
None removes the per-lookup cost. Lookups become fewer if you settle or archive threads (including hidden subagent threads, through inactivity auto-settle) and avoid restarting T3.
Activity
0pilatos0 commentedon Nov 2, 2026
Same issue here on Gitea 28.2.0 with T3 Code 0.1.7-nightly.20261102.4066 (details in #29514). Our nextgen repo has 2,651 PRs, so each lookup for a branch without a PR walks 55 pages: about 25k calls in 72 h, roughly 650 an hour per idle thread, 23 to 29 s per lookup on the client. Every walk also logs
unable to resolve PR head reffor imported PRs missing refs/pull/N/head (17k times in 72 h). Hoping #25569 lands soon.Cammymoop commentedon Nov 10, 2026
Data point from Forgejo 15.0.10, where
head=is ignored exactly as on Gitea. We have T3 0.1.7 and 0.1.8 and a 595-PR repository, so a lookup takes 13 list requests.Each T3 server start re-checks every settled thread that has no
branchPullRequest(ThreadPullRequestServiceV2startup backfill,concurrency: 32). With about 480 such threads, that produced about 26–28 minutes of 85–205pulls?state=all&head=…requests per minute on the forge. Median latency rose from about 1.6 s per page on an idle server to about 11 s. Archiving most of those threads reduced it to one 2-minute burst.For Forgejo users, upgrading to 16+ (where
head=works) removes most of this. Gitea has no such upgrade.The number of threads re-checked on every start is inflated by a separate bug, #33660: merged PRs whose branch was deleted report
head.ref = refs/pull/<N>/headand are never matched, so those threads never get abranchPullRequest.</opus>
I'll be updating my private forgejo so I wont be dealing with this anymore but posting this info for anyone it may concern
test(forgejo): Gitea branch PR lookups read the whole pull request li…