From 2660b4d2c43578c2d495ffb71736362587124bca Mon Sep 17 00:00:00 2001 From: Jorge <97680247+JorgeDLMV@users.noreply.github.com> Date: Fri, 30 Oct 2026 09:42:49 +0100 Subject: [PATCH] fix(api)!: list only open pull requests when no state is given (#40078) `GET /repos/{owner}/{repo}/pulls` is documented with `state` defaulting to `open`, but without `state` it returned every pull request, closed and merged ones included. Now it parses `state` with `ParseIssueFilterStateIsClosed`, like the issues and milestones endpoints, so no state means open, as documented. `closed` and `all` work as before. GitHub's pulls endpoint also defaults to open. --- models/issues/pull_list.go | 8 ++++---- models/issues/pull_test.go | 7 ++++--- routers/api/v1/repo/pull.go | 2 +- tests/integration/api_pull_test.go | 22 ++++++++++++++++++++++ 4 files changed, 31 insertions(+), 8 deletions(-) diff --git a/models/issues/pull_list.go b/models/issues/pull_list.go index cdd5cb54eeb..2b0d7d72d90 100644 --- a/models/issues/pull_list.go +++ b/models/issues/pull_list.go @@ -14,6 +14,7 @@ import ( user_model "gitea.dev/models/user" "gitea.dev/modules/container" "gitea.dev/modules/log" + "gitea.dev/modules/optional" "gitea.dev/modules/timeutil" "gitea.dev/modules/util" @@ -23,7 +24,7 @@ import ( // PullRequestsOptions holds the options for PRs type PullRequestsOptions struct { db.ListOptions - State string + IsClosed optional.Option[bool] SortType string Labels []int64 MilestoneID int64 @@ -39,9 +40,8 @@ func listPullRequestStatement(ctx context.Context, baseRepoID int64, opts *PullR } sess.Join("INNER", "issue", "pull_request.issue_id = issue.id") - switch opts.State { - case "closed", "open": - sess.And("issue.is_closed=?", opts.State == "closed") + if opts.IsClosed.Has() { + sess.And("issue.is_closed=?", opts.IsClosed.Value()) } if len(opts.Labels) > 0 { diff --git a/models/issues/pull_test.go b/models/issues/pull_test.go index bd7499fa80b..0364acebef1 100644 --- a/models/issues/pull_test.go +++ b/models/issues/pull_test.go @@ -13,6 +13,7 @@ import ( repo_model "gitea.dev/models/repo" "gitea.dev/models/unittest" user_model "gitea.dev/models/user" + "gitea.dev/modules/optional" "gitea.dev/modules/setting" "github.com/stretchr/testify/assert" @@ -92,7 +93,7 @@ func testPullRequestsNewest(t *testing.T) { ListOptions: db.ListOptions{ Page: 1, }, - State: "open", + IsClosed: optional.Some(false), SortType: "newest", }) assert.NoError(t, err) @@ -130,7 +131,7 @@ func testPullRequestsClosedRecentSortType(t *testing.T) { ListOptions: db.ListOptions{ Page: 1, }, - State: "closed", + IsClosed: optional.Some(true), SortType: test.sortType, }) require.NoError(t, err) @@ -175,7 +176,7 @@ func testPullRequestsOldest(t *testing.T) { ListOptions: db.ListOptions{ Page: 1, }, - State: "open", + IsClosed: optional.Some(false), SortType: "oldest", }) assert.NoError(t, err) diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index ab21b6d8015..8190ee4f1ff 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -131,7 +131,7 @@ func ListPullRequests(ctx *context.APIContext) { listOptions := utils.GetListOptions(ctx) prs, maxResults, err := issues_model.PullRequests(ctx, ctx.Repo.Repository.ID, &issues_model.PullRequestsOptions{ ListOptions: listOptions, - State: ctx.FormTrim("state"), + IsClosed: common.ParseIssueFilterStateIsClosed(ctx.FormTrim("state")), SortType: ctx.FormTrim("sort"), Labels: labelIDs, MilestoneID: ctx.FormInt64("milestone"), diff --git a/tests/integration/api_pull_test.go b/tests/integration/api_pull_test.go index d7ddced4ba7..4d4746b4dcb 100644 --- a/tests/integration/api_pull_test.go +++ b/tests/integration/api_pull_test.go @@ -144,6 +144,28 @@ func TestAPIViewPulls(t *testing.T) { })) } + t.Run("DefaultState", func(t *testing.T) { + writeToken := getTokenForLoggedInUser(t, ctx.Session, auth_model.AccessTokenScopeWriteRepository) + req := NewRequestWithJSON(t, http.MethodPatch, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/3", owner.Name, repo.Name), &api.EditPullRequestOption{ + State: new(string(api.StateClosed)), + }).AddTokenAuth(writeToken) + MakeRequest(t, req, http.StatusCreated) + + for query, want := range map[string][]int64{ + "": {2, 5}, + "state=closed": {3}, + "state=all": {2, 3, 5}, + } { + req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/pulls?%s", owner.Name, repo.Name, query).AddTokenAuth(ctx.Token) + listed := DecodeJSON(t, ctx.Session.MakeRequest(t, req, http.StatusOK), []*api.PullRequest{}) + indexes := make([]int64, 0, len(listed)) + for _, pr := range listed { + indexes = append(indexes, pr.Index) + } + assert.ElementsMatch(t, want, indexes, query) + } + }) + t.Run("APIViewPullsByBaseHead", func(t *testing.T) { req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/pulls/master/branch2", owner.Name, repo.Name). AddTokenAuth(ctx.Token)