feat(api): add base and head query filters to list pull requests endpoint (#12104)

Resolves https://codeberg.org/forgejo/forgejo/issues/6919

Add `base` and `head` filter options to the `repoListPullRequests` API operation.

Co-authored-by: Rahul Gautam Singh <rere0095@Rahuls-MacBook-Air.local>
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/12104
Reviewed-by: Ellen Εμίλια Άννα Zscheile <fogti@noreply.codeberg.org>
Reviewed-by: Cyborus <cyborus@disroot.org>
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Co-authored-by: RahulGautamSingh <rahultesnik@gmail.com>
Co-committed-by: RahulGautamSingh <rahultesnik@gmail.com>
This commit is contained in:
RahulGautamSingh 2026-04-16 19:51:46 +02:00 committed by Gusted
commit 39f677c0db
4 changed files with 85 additions and 0 deletions

View file

@ -27,6 +27,8 @@ type PullRequestsOptions struct {
Labels []int64
MilestoneID int64
PosterID int64
BaseBranch string
HeadBranch string
}
func listPullRequestStatement(ctx context.Context, baseRepoID int64, opts *PullRequestsOptions) *xorm.Session {
@ -51,6 +53,14 @@ func listPullRequestStatement(ctx context.Context, baseRepoID int64, opts *PullR
sess.And("issue.poster_id=?", opts.PosterID)
}
if opts.BaseBranch != "" {
sess.And("pull_request.base_branch=?", opts.BaseBranch)
}
if opts.HeadBranch != "" {
sess.And("pull_request.head_branch=?", opts.HeadBranch)
}
return sess
}

View file

@ -90,6 +90,14 @@ func ListPullRequests(ctx *context.APIContext) {
// in: query
// description: Filter by pull request author
// type: string
// - name: base
// in: query
// description: Filter by base branch name
// type: string
// - name: head
// in: query
// description: Filter by head branch name
// type: string
// - name: page
// in: query
// description: Page number of results to return (1-based)
@ -137,6 +145,8 @@ func ListPullRequests(ctx *context.APIContext) {
Labels: labelIDs,
MilestoneID: ctx.FormInt64("milestone"),
PosterID: posterID,
BaseBranch: ctx.FormTrim("base"),
HeadBranch: ctx.FormTrim("head"),
})
if err != nil {
ctx.Error(http.StatusInternalServerError, "PullRequests", err)

View file

@ -14003,6 +14003,18 @@
"name": "poster",
"in": "query"
},
{
"type": "string",
"description": "Filter by base branch name",
"name": "base",
"in": "query"
},
{
"type": "string",
"description": "Filter by head branch name",
"name": "head",
"in": "query"
},
{
"minimum": 1,
"type": "integer",

View file

@ -178,6 +178,59 @@ func TestAPIPullsFiles(t *testing.T) {
})
}
func TestAPIViewPullsFilterByBaseHead(t *testing.T) {
defer tests.PrepareTestEnv(t)()
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
ctx := NewAPITestContext(t, "user2", repo.Name, auth_model.AccessTokenScopeReadRepository)
t.Run("FilterByBase", func(t *testing.T) {
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/pulls?state=all&base=master", repo.OwnerName, repo.Name).
AddTokenAuth(ctx.Token)
resp := ctx.Session.MakeRequest(t, req, http.StatusOK)
var pulls []*api.PullRequest
DecodeJSON(t, resp, &pulls)
assert.Len(t, pulls, 2)
for _, pr := range pulls {
assert.Equal(t, "master", pr.Base.Name)
}
})
t.Run("FilterByHead", func(t *testing.T) {
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/pulls?state=all&head=branch2", repo.OwnerName, repo.Name).
AddTokenAuth(ctx.Token)
resp := ctx.Session.MakeRequest(t, req, http.StatusOK)
var pulls []*api.PullRequest
DecodeJSON(t, resp, &pulls)
assert.Len(t, pulls, 1)
assert.Equal(t, "branch2", pulls[0].Head.Name)
})
t.Run("FilterByBaseAndHead", func(t *testing.T) {
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/pulls?state=all&base=master&head=branch2", repo.OwnerName, repo.Name).
AddTokenAuth(ctx.Token)
resp := ctx.Session.MakeRequest(t, req, http.StatusOK)
var pulls []*api.PullRequest
DecodeJSON(t, resp, &pulls)
assert.Len(t, pulls, 1)
assert.Equal(t, "master", pulls[0].Base.Name)
assert.Equal(t, "branch2", pulls[0].Head.Name)
})
t.Run("FilterByBaseNoMatch", func(t *testing.T) {
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/pulls?state=all&base=nonexistent", repo.OwnerName, repo.Name).
AddTokenAuth(ctx.Token)
resp := ctx.Session.MakeRequest(t, req, http.StatusOK)
var pulls []*api.PullRequest
DecodeJSON(t, resp, &pulls)
assert.Empty(t, pulls)
})
}
func TestAPIViewPullsByBaseHead(t *testing.T) {
defer tests.PrepareTestEnv(t)()
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})