Skip to content

Commit 6ad5d0a

Browse files
authored
[API] ListReleases add filter for draft and pre-releases (#16175)
* invent ctx.QueryOptionalBool * [API] ListReleases add draft and pre-release filter * Add X-Total-Count header * Add a release to fixtures * Add TEST for API ListReleases
1 parent c9d053f commit 6ad5d0a

File tree

9 files changed

+158
-22
lines changed

9 files changed

+158
-22
lines changed

integrations/api_releases_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package integrations
77
import (
88
"fmt"
99
"net/http"
10+
"net/url"
1011
"testing"
1112

1213
"code.gitea.io/gitea/models"
@@ -16,6 +17,58 @@ import (
1617
"github.com/stretchr/testify/assert"
1718
)
1819

20+
func TestAPIListReleases(t *testing.T) {
21+
defer prepareTestEnv(t)()
22+
23+
repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)
24+
user2 := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User)
25+
session := loginUser(t, user2.LowerName)
26+
token := getTokenForLoggedInUser(t, session)
27+
28+
link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/releases", user2.Name, repo.Name))
29+
link.RawQuery = url.Values{"token": {token}}.Encode()
30+
resp := session.MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK)
31+
var apiReleases []*api.Release
32+
DecodeJSON(t, resp, &apiReleases)
33+
if assert.Len(t, apiReleases, 3) {
34+
for _, release := range apiReleases {
35+
switch release.ID {
36+
case 1:
37+
assert.False(t, release.IsDraft)
38+
assert.False(t, release.IsPrerelease)
39+
case 4:
40+
assert.True(t, release.IsDraft)
41+
assert.False(t, release.IsPrerelease)
42+
case 5:
43+
assert.False(t, release.IsDraft)
44+
assert.True(t, release.IsPrerelease)
45+
default:
46+
assert.NoError(t, fmt.Errorf("unexpected release: %v", release))
47+
}
48+
}
49+
}
50+
51+
// test filter
52+
testFilterByLen := func(auth bool, query url.Values, expectedLength int, msgAndArgs ...string) {
53+
link.RawQuery = query.Encode()
54+
if auth {
55+
query.Set("token", token)
56+
resp = session.MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK)
57+
} else {
58+
resp = MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK)
59+
}
60+
DecodeJSON(t, resp, &apiReleases)
61+
assert.Len(t, apiReleases, expectedLength, msgAndArgs)
62+
}
63+
64+
testFilterByLen(false, url.Values{"draft": {"true"}}, 0, "anon should not see drafts")
65+
testFilterByLen(true, url.Values{"draft": {"true"}}, 1, "repo owner should see drafts")
66+
testFilterByLen(true, url.Values{"draft": {"false"}}, 2, "exclude drafts")
67+
testFilterByLen(true, url.Values{"draft": {"false"}, "pre-release": {"false"}}, 1, "exclude drafts and pre-releases")
68+
testFilterByLen(true, url.Values{"pre-release": {"true"}}, 1, "only get pre-release")
69+
testFilterByLen(true, url.Values{"draft": {"true"}, "pre-release": {"true"}}, 0, "there is no pre-release draft")
70+
}
71+
1972
func createNewReleaseUsingAPI(t *testing.T, session *TestSession, token string, owner *models.User, repo *models.Repository, name, target, title, desc string) *api.Release {
2073
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/releases?token=%s",
2174
owner.Name, repo.Name, token)

integrations/api_repo_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ func TestAPIViewRepo(t *testing.T) {
223223
DecodeJSON(t, resp, &repo)
224224
assert.EqualValues(t, 1, repo.ID)
225225
assert.EqualValues(t, "repo1", repo.Name)
226-
assert.EqualValues(t, 2, repo.Releases)
226+
assert.EqualValues(t, 3, repo.Releases)
227227
assert.EqualValues(t, 1, repo.OpenIssues)
228228
assert.EqualValues(t, 3, repo.OpenPulls)
229229

integrations/release_test.go

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ func TestCreateRelease(t *testing.T) {
8585
session := loginUser(t, "user2")
8686
createNewRelease(t, session, "/user2/repo1", "v0.0.1", "v0.0.1", false, false)
8787

88-
checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", i18n.Tr("en", "repo.release.stable"), 3)
88+
checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", i18n.Tr("en", "repo.release.stable"), 4)
8989
}
9090

9191
func TestCreateReleasePreRelease(t *testing.T) {
@@ -94,7 +94,7 @@ func TestCreateReleasePreRelease(t *testing.T) {
9494
session := loginUser(t, "user2")
9595
createNewRelease(t, session, "/user2/repo1", "v0.0.1", "v0.0.1", true, false)
9696

97-
checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", i18n.Tr("en", "repo.release.prerelease"), 3)
97+
checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", i18n.Tr("en", "repo.release.prerelease"), 4)
9898
}
9999

100100
func TestCreateReleaseDraft(t *testing.T) {
@@ -103,7 +103,7 @@ func TestCreateReleaseDraft(t *testing.T) {
103103
session := loginUser(t, "user2")
104104
createNewRelease(t, session, "/user2/repo1", "v0.0.1", "v0.0.1", false, true)
105105

106-
checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", i18n.Tr("en", "repo.release.draft"), 3)
106+
checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", i18n.Tr("en", "repo.release.draft"), 4)
107107
}
108108

109109
func TestCreateReleasePaging(t *testing.T) {
@@ -142,7 +142,7 @@ func TestViewReleaseListNoLogin(t *testing.T) {
142142

143143
htmlDoc := NewHTMLParser(t, rsp.Body)
144144
releases := htmlDoc.Find("#release-list li.ui.grid")
145-
assert.Equal(t, 1, releases.Length())
145+
assert.Equal(t, 2, releases.Length())
146146

147147
links := make([]string, 0, 5)
148148
releases.Each(func(i int, s *goquery.Selection) {
@@ -153,7 +153,7 @@ func TestViewReleaseListNoLogin(t *testing.T) {
153153
links = append(links, link)
154154
})
155155

156-
assert.EqualValues(t, []string{"/user2/repo1/releases/tag/v1.1"}, links)
156+
assert.EqualValues(t, []string{"/user2/repo1/releases/tag/v1.0", "/user2/repo1/releases/tag/v1.1"}, links)
157157
}
158158

159159
func TestViewReleaseListLogin(t *testing.T) {
@@ -169,7 +169,7 @@ func TestViewReleaseListLogin(t *testing.T) {
169169

170170
htmlDoc := NewHTMLParser(t, rsp.Body)
171171
releases := htmlDoc.Find("#release-list li.ui.grid")
172-
assert.Equal(t, 2, releases.Length())
172+
assert.Equal(t, 3, releases.Length())
173173

174174
links := make([]string, 0, 5)
175175
releases.Each(func(i int, s *goquery.Selection) {
@@ -180,8 +180,11 @@ func TestViewReleaseListLogin(t *testing.T) {
180180
links = append(links, link)
181181
})
182182

183-
assert.EqualValues(t, []string{"/user2/repo1/releases/tag/draft-release",
184-
"/user2/repo1/releases/tag/v1.1"}, links)
183+
assert.EqualValues(t, []string{
184+
"/user2/repo1/releases/tag/draft-release",
185+
"/user2/repo1/releases/tag/v1.0",
186+
"/user2/repo1/releases/tag/v1.1",
187+
}, links)
185188
}
186189

187190
func TestViewTagsList(t *testing.T) {
@@ -197,12 +200,12 @@ func TestViewTagsList(t *testing.T) {
197200

198201
htmlDoc := NewHTMLParser(t, rsp.Body)
199202
tags := htmlDoc.Find(".tag-list tr")
200-
assert.Equal(t, 2, tags.Length())
203+
assert.Equal(t, 3, tags.Length())
201204

202205
tagNames := make([]string, 0, 5)
203206
tags.Each(func(i int, s *goquery.Selection) {
204207
tagNames = append(tagNames, s.Find(".tag a.df.ac").Text())
205208
})
206209

207-
assert.EqualValues(t, []string{"delete-tag", "v1.1"}, tagNames)
210+
assert.EqualValues(t, []string{"v1.0", "delete-tag", "v1.1"}, tagNames)
208211
}

models/fixtures/release.yml

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
-
2-
id: 1
1+
- id: 1
32
repo_id: 1
43
publisher_id: 2
54
tag_name: "v1.1"
@@ -13,8 +12,7 @@
1312
is_tag: false
1413
created_unix: 946684800
1514

16-
-
17-
id: 2
15+
- id: 2
1816
repo_id: 40
1917
publisher_id: 2
2018
tag_name: "v1.1"
@@ -28,8 +26,7 @@
2826
is_tag: false
2927
created_unix: 946684800
3028

31-
-
32-
id: 3
29+
- id: 3
3330
repo_id: 1
3431
publisher_id: 2
3532
tag_name: "delete-tag"
@@ -43,8 +40,7 @@
4340
is_tag: true
4441
created_unix: 946684800
4542

46-
-
47-
id: 4
43+
- id: 4
4844
repo_id: 1
4945
publisher_id: 2
5046
tag_name: "draft-release"
@@ -55,3 +51,18 @@
5551
is_prerelease: false
5652
is_tag: false
5753
created_unix: 1619524806
54+
55+
- id: 5
56+
repo_id: 1
57+
publisher_id: 2
58+
tag_name: "v1.0"
59+
lower_tag_name: "v1.0"
60+
target: "master"
61+
title: "pre-release"
62+
note: "some text for a pre release"
63+
sha1: "65f1bf27bc3bf70f64657658635e66094edbcb4d"
64+
num_commits: 1
65+
is_draft: false
66+
is_prerelease: true
67+
is_tag: false
68+
created_unix: 946684800

models/release.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"code.gitea.io/gitea/modules/setting"
1515
"code.gitea.io/gitea/modules/structs"
1616
"code.gitea.io/gitea/modules/timeutil"
17+
"code.gitea.io/gitea/modules/util"
1718

1819
"xorm.io/builder"
1920
)
@@ -173,6 +174,8 @@ type FindReleasesOptions struct {
173174
ListOptions
174175
IncludeDrafts bool
175176
IncludeTags bool
177+
IsPreRelease util.OptionalBool
178+
IsDraft util.OptionalBool
176179
TagNames []string
177180
}
178181

@@ -189,6 +192,12 @@ func (opts *FindReleasesOptions) toConds(repoID int64) builder.Cond {
189192
if len(opts.TagNames) > 0 {
190193
cond = cond.And(builder.In("tag_name", opts.TagNames))
191194
}
195+
if !opts.IsPreRelease.IsNone() {
196+
cond = cond.And(builder.Eq{"is_prerelease": opts.IsPreRelease.IsTrue()})
197+
}
198+
if !opts.IsDraft.IsNone() {
199+
cond = cond.And(builder.Eq{"is_draft": opts.IsDraft.IsTrue()})
200+
}
192201
return cond
193202
}
194203

@@ -206,6 +215,11 @@ func GetReleasesByRepoID(repoID int64, opts FindReleasesOptions) ([]*Release, er
206215
return rels, sess.Find(&rels)
207216
}
208217

218+
// CountReleasesByRepoID returns a number of releases matching FindReleaseOptions and RepoID.
219+
func CountReleasesByRepoID(repoID int64, opts FindReleasesOptions) (int64, error) {
220+
return x.Where(opts.toConds(repoID)).Count(new(Release))
221+
}
222+
209223
// GetLatestReleaseByRepoID returns the latest release for a repository
210224
func GetLatestReleaseByRepoID(repoID int64) (*Release, error) {
211225
cond := builder.NewCond().

modules/context/context.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"code.gitea.io/gitea/modules/setting"
2828
"code.gitea.io/gitea/modules/templates"
2929
"code.gitea.io/gitea/modules/translation"
30+
"code.gitea.io/gitea/modules/util"
3031
"code.gitea.io/gitea/modules/web/middleware"
3132
"code.gitea.io/gitea/services/auth"
3233

@@ -319,6 +320,11 @@ func (ctx *Context) QueryBool(key string, defaults ...bool) bool {
319320
return (*Forms)(ctx.Req).MustBool(key, defaults...)
320321
}
321322

323+
// QueryOptionalBool returns request form as OptionalBool with default
324+
func (ctx *Context) QueryOptionalBool(key string, defaults ...util.OptionalBool) util.OptionalBool {
325+
return (*Forms)(ctx.Req).MustOptionalBool(key, defaults...)
326+
}
327+
322328
// HandleText handles HTTP status code
323329
func (ctx *Context) HandleText(status int, title string) {
324330
if (status/100 == 4) || (status/100 == 5) {

modules/context/form.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"text/template"
1414

1515
"code.gitea.io/gitea/modules/log"
16+
"code.gitea.io/gitea/modules/util"
1617
)
1718

1819
// Forms a new enhancement of http.Request
@@ -225,3 +226,16 @@ func (f *Forms) MustBool(key string, defaults ...bool) bool {
225226
}
226227
return v
227228
}
229+
230+
// MustOptionalBool returns request form as OptionalBool with default
231+
func (f *Forms) MustOptionalBool(key string, defaults ...util.OptionalBool) util.OptionalBool {
232+
value := (*http.Request)(f).FormValue(key)
233+
if len(value) == 0 {
234+
return util.OptionalBoolNone
235+
}
236+
v, err := strconv.ParseBool((*http.Request)(f).FormValue(key))
237+
if len(defaults) > 0 && err != nil {
238+
return defaults[0]
239+
}
240+
return util.OptionalBoolOf(v)
241+
}

routers/api/v1/repo/release.go

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package repo
66

77
import (
8+
"fmt"
89
"net/http"
910

1011
"code.gitea.io/gitea/models"
@@ -83,6 +84,14 @@ func ListReleases(ctx *context.APIContext) {
8384
// description: name of the repo
8485
// type: string
8586
// required: true
87+
// - name: draft
88+
// in: query
89+
// description: filter (exclude / include) drafts, if you dont have repo write access none will show
90+
// type: boolean
91+
// - name: pre-release
92+
// in: query
93+
// description: filter (exclude / include) pre-releases
94+
// type: boolean
8695
// - name: per_page
8796
// in: query
8897
// description: page size of results, deprecated - use limit
@@ -100,15 +109,19 @@ func ListReleases(ctx *context.APIContext) {
100109
// "200":
101110
// "$ref": "#/responses/ReleaseList"
102111
listOptions := utils.GetListOptions(ctx)
103-
if ctx.QueryInt("per_page") != 0 {
112+
if listOptions.PageSize == 0 && ctx.QueryInt("per_page") != 0 {
104113
listOptions.PageSize = ctx.QueryInt("per_page")
105114
}
106115

107-
releases, err := models.GetReleasesByRepoID(ctx.Repo.Repository.ID, models.FindReleasesOptions{
116+
opts := models.FindReleasesOptions{
108117
ListOptions: listOptions,
109118
IncludeDrafts: ctx.Repo.AccessMode >= models.AccessModeWrite,
110119
IncludeTags: false,
111-
})
120+
IsDraft: ctx.QueryOptionalBool("draft"),
121+
IsPreRelease: ctx.QueryOptionalBool("pre-release"),
122+
}
123+
124+
releases, err := models.GetReleasesByRepoID(ctx.Repo.Repository.ID, opts)
112125
if err != nil {
113126
ctx.Error(http.StatusInternalServerError, "GetReleasesByRepoID", err)
114127
return
@@ -121,6 +134,16 @@ func ListReleases(ctx *context.APIContext) {
121134
}
122135
rels[i] = convert.ToRelease(release)
123136
}
137+
138+
filteredCount, err := models.CountReleasesByRepoID(ctx.Repo.Repository.ID, opts)
139+
if err != nil {
140+
ctx.InternalServerError(err)
141+
return
142+
}
143+
144+
ctx.SetLinkHeader(int(filteredCount), listOptions.PageSize)
145+
ctx.Header().Set("X-Total-Count", fmt.Sprint(filteredCount))
146+
ctx.Header().Set("Access-Control-Expose-Headers", "X-Total-Count, Link")
124147
ctx.JSON(http.StatusOK, rels)
125148
}
126149

templates/swagger/v1_json.tmpl

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8076,6 +8076,18 @@
80768076
"in": "path",
80778077
"required": true
80788078
},
8079+
{
8080+
"type": "boolean",
8081+
"description": "filter (exclude / include) drafts, if you dont have repo write access none will show",
8082+
"name": "draft",
8083+
"in": "query"
8084+
},
8085+
{
8086+
"type": "boolean",
8087+
"description": "filter (exclude / include) pre-releases",
8088+
"name": "pre-release",
8089+
"in": "query"
8090+
},
80798091
{
80808092
"type": "integer",
80818093
"description": "page size of results, deprecated - use limit",

0 commit comments

Comments
 (0)