forge pr find-by-head and branch resolution ignore the head filter, returning the wrong PR #83

Open
opened 2026-07-05 19:45:19 +01:00 by vnprc-agent · 0 comments
Contributor

User story: So that I can submit and manage pull requests by branch name without risking changes to the wrong PR, forge must resolve branch heads from actual PR metadata rather than an ignored server filter.

Part of the "Forge / workspace CLI tooling" arc.


forge pr find-by-head <branch> returns a PR number even when no PR exists for that branch, and returns the wrong PR whenever any other open PR exists. The root cause is that pr_find_by_head — and resolve_pr_target, which resolves a branch-name target for pr edit/pr comment/pr close — query the Forgejo pulls list with a head= filter the API does not honor:

api GET "/repos/$REPO/pulls?state=open&limit=50&head=$(urlencode "$branch")" | jq -r 'first(.[] | .number) // empty'

Forgejo/Gitea's GET /repos/{owner}/{repo}/pulls endpoint does not support a head query-parameter filter (unlike GitHub's API, from which this pattern was likely ported). The unrecognized head= param is silently ignored, so the endpoint returns every open PR and first(.[] | .number) yields the first open PR's number regardless of the branch asked for.

Reproduction

With exactly one open PR, whose head branch is agent/rotation-vmssh-suite (say #78):

$ forge pr find-by-head agent/rotation-vmssh-suite
78
$ forge pr find-by-head agent/rotation-common-lift   # no PR exists for this branch
78

Expected: the second call prints nothing, since no open PR has that head.

Impact

  • allod change submit calls forge pr find-by-head "$branch" to guard against duplicate PRs. Because it returns the first open PR for any branch, submitting a second (e.g. stacked) PR while another PR is open dies with PR #N already exists for '<branch>' and blocks the workflow. Observed while stacking two PRs on vnprc/nexus; worked around by creating the second PR with forge pr create -H <branch> -B <base> directly.
  • More seriously, resolve_pr_target resolves a branch-name target the same way, so forge pr edit <branch>, forge pr comment <branch>, and forge pr close <branch> can act on the wrong PR (the first open one) instead of the one for that branch.

Suggested fix

Filter client-side on .head.ref rather than trusting the server head= param:

api GET "/repos/$REPO/pulls?state=open&limit=50" | jq -r --arg h "$branch" 'first(.[] | select(.head.ref == $h) | .number) // empty'

Both pr_find_by_head and resolve_pr_target share the pattern and should be fixed together. Consider paginating instead of a fixed limit=50 so a branch's PR is not missed once more than 50 PRs are open.

**User story:** So that I can submit and manage pull requests by branch name without risking changes to the wrong PR, `forge` must resolve branch heads from actual PR metadata rather than an ignored server filter. _Part of the "Forge / workspace CLI tooling" arc._ --- `forge pr find-by-head <branch>` returns a PR number even when no PR exists for that branch, and returns the wrong PR whenever any other open PR exists. The root cause is that `pr_find_by_head` — and `resolve_pr_target`, which resolves a branch-name target for `pr edit`/`pr comment`/`pr close` — query the Forgejo pulls list with a `head=` filter the API does not honor: ``` api GET "/repos/$REPO/pulls?state=open&limit=50&head=$(urlencode "$branch")" | jq -r 'first(.[] | .number) // empty' ``` Forgejo/Gitea's `GET /repos/{owner}/{repo}/pulls` endpoint does not support a `head` query-parameter filter (unlike GitHub's API, from which this pattern was likely ported). The unrecognized `head=` param is silently ignored, so the endpoint returns every open PR and `first(.[] | .number)` yields the first open PR's number regardless of the branch asked for. ## Reproduction With exactly one open PR, whose head branch is `agent/rotation-vmssh-suite` (say #78): ``` $ forge pr find-by-head agent/rotation-vmssh-suite 78 $ forge pr find-by-head agent/rotation-common-lift # no PR exists for this branch 78 ``` Expected: the second call prints nothing, since no open PR has that head. ## Impact - `allod change submit` calls `forge pr find-by-head "$branch"` to guard against duplicate PRs. Because it returns the first open PR for any branch, submitting a second (e.g. stacked) PR while another PR is open dies with `PR #N already exists for '<branch>'` and blocks the workflow. Observed while stacking two PRs on `vnprc/nexus`; worked around by creating the second PR with `forge pr create -H <branch> -B <base>` directly. - More seriously, `resolve_pr_target` resolves a branch-name target the same way, so `forge pr edit <branch>`, `forge pr comment <branch>`, and `forge pr close <branch>` can act on the wrong PR (the first open one) instead of the one for that branch. ## Suggested fix Filter client-side on `.head.ref` rather than trusting the server `head=` param: ``` api GET "/repos/$REPO/pulls?state=open&limit=50" | jq -r --arg h "$branch" 'first(.[] | select(.head.ref == $h) | .number) // empty' ``` Both `pr_find_by_head` and `resolve_pr_target` share the pattern and should be fixed together. Consider paginating instead of a fixed `limit=50` so a branch's PR is not missed once more than 50 PRs are open.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
allod/tools#83
No description provided.