Skip to content

Forgejo: merged PRs whose source branch was deleted are never found, so threads never learn their PR was merged #33660

Closed

Description

@Cammymoop

What happened

On a self-hosted Forgejo, T3 Code does not recognize merged pull requests once their source branch has been deleted after merging, which is the common "delete branch on merge" setup. The branch lookup reports "no PR" for those threads, so they never record that their PR was merged.

We noticed this through its side effect. After a T3 update, every server start caused a heavy load spike on our Forgejo 15 server for about half an hour, while T3 re-checked hundreds of settled threads and found nothing for any of them.

Diagnosis

For a PR merged with "delete branch", Forgejo's API reports head.ref as refs/pull/<N>/head. The branch name remains only in head.label. This was seen on Forgejo 15.0.10 and on Codeberg (Forgejo 17.0.0), so it is not version-specific.

ForgejoSourceControlProvider.listChangeRequests drops every item where item.head.ref !== branch (packages/source-control-forgejo/src/server/ForgejoSourceControlProvider.ts:347 at v0.1.8, unchanged on main @ 53bcb9a). So findLatestPrForHeadContext / branchPullRequest return null for any branch whose PR was merged and deleted.

Consequences, from code reading at v0.1.8:

  • The thread never gets branchPullRequest. The startup backfill in ThreadPullRequestServiceV2 (apps/server/src/orchestration-v2/ThreadPullRequestService.ts:391, :146) re-checks every settled thread with branchPullRequest == null on each server start. The result stays null, so the same threads are re-checked after every restart.
  • The merged-PR worktree cleanup rule (worktreeOnMerge) cannot fire. It calls branchPullRequest(...) and requires state === "merged". It also compares pullRequest.headRef === worktree.branch unless the branch is integrated, and that comparison would fail too, because headRef is refs/pull/<N>/head.

Of 377 branches looked up after one restart, 339 do have a PR when matched by head.label.

On Forgejo < 16, each of these lookups also pages through the whole PR list, because head= is ignored there (#23551). That is what made the problem visible as server load. Upgrading Forgejo to 16+ removes that cost, but the lookups still return "no PR".

Suggested fix: when head.ref starts with refs/pull/, match the branch against head.label, or the stored head branch, and use that name as the change request's headRef.

Steps to reproduce

  1. On a Forgejo repository with "delete branch after merge", merge a PR from branch <branch>.
  2. GET /api/v1/repos/<owner>/<repo>/pulls?state=all shows that PR with "head": {"ref": "refs/pull/<N>/head", "label": "<branch>"}.
  3. In T3 Code, have a thread on <branch>, and let it settle or restart the server.
  4. The thread's branch PR stays empty (branchPullRequest: null in the thread projection), and findLatestPrForHeadContext for <branch> returns nothing. The merged-PR cleanup does not remove its worktree.

Version

0.1.7 and 0.1.8 (stable); code unchanged on main @ 53bcb9a

Environment

Ubuntu Linux, T3 Code background service (systemd user unit), fj 0.6.0; self-hosted Forgejo 15.0.10 (also checked against Codeberg, Forgejo 17.0.0)

Evidence

# Forgejo list response for a merged PR with deleted branch (public example, Codeberg 17.0.0)
"head": { "ref": "refs/pull/15209/head", "label": "<branch>" }

# Our repository: 595 PRs, 592 with head.ref = refs/pull/<N>/head
# After one server start: 377 distinct branches looked up, all "no PR";
# 339 of them have a PR when matched by head.label

Related issues

#23551 / #19347: head= ignored on Forgejo < 16 and Gitea, so each lookup is a full scan (the cost side, and how we noticed). This issue is the wrong answer, on any version. #25569, #29514: also scan cost. Nothing existing covers the head.ref mismatch.

Fix applied or workaround

None in T3. Upgrading Forgejo to 16+ removes the scan cost but not the wrong answer. Archiving old settled threads shrinks the startup re-check.

Filed by

Claude Code (claude-opus-5-5) in a T3 Code thread, following .github/triage/PLAYBOOK.md manually (not via t3 triage)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions