mirror of
https://github.com/go-gitea/gitea.git
synced 2026-07-31 21:48:19 +00:00
feat(api): add tag_filter query parameter to release list API (#38681)
Adds a `tag_filter` query parameter to `GET /repos/{owner}/{repo}/releases` that filters releases by tag name, with `*` as a wildcard (e.g. `v1*`, `*beta`, `*rc*`). Matching is
case-insensitive and done in the database query. Literal `%`, `_` and `\` in the filter are escaped.
Fixes: https://github.com/go-gitea/gitea/issues/38513
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
@@ -266,6 +266,7 @@ type FindReleasesOptions struct {
|
||||
TagNames []string
|
||||
HasSha1 optional.Option[bool] // useful to find draft releases which are created with existing tags
|
||||
NamePattern optional.Option[string]
|
||||
TagFilter string
|
||||
}
|
||||
|
||||
func (opts FindReleasesOptions) ToConds() builder.Cond {
|
||||
@@ -297,7 +298,14 @@ func (opts FindReleasesOptions) ToConds() builder.Cond {
|
||||
if opts.NamePattern.Has() && opts.NamePattern.Value() != "" {
|
||||
cond = cond.And(builder.Like{"lower_tag_name", strings.ToLower(opts.NamePattern.Value())})
|
||||
}
|
||||
|
||||
if opts.TagFilter != "" {
|
||||
pattern := strings.ToLower(opts.TagFilter)
|
||||
pattern = strings.ReplaceAll(pattern, "\\", "\\\\")
|
||||
pattern = strings.ReplaceAll(pattern, "_", "\\_")
|
||||
pattern = strings.ReplaceAll(pattern, "%", "\\%")
|
||||
pattern = strings.ReplaceAll(pattern, "*", "%")
|
||||
cond = cond.And(builder.Like{"lower_tag_name", pattern})
|
||||
}
|
||||
return cond
|
||||
}
|
||||
|
||||
|
||||
@@ -154,6 +154,10 @@ func ListReleases(ctx *context.APIContext) {
|
||||
// in: query
|
||||
// description: filter (exclude / include) pre-releases
|
||||
// type: boolean
|
||||
// - name: tag_filter
|
||||
// in: query
|
||||
// description: 'filter releases by tag. supports "*" as a wildcard (for example: v1*, *beta, *rc*).'
|
||||
// type: string
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number of results to return (1-based)
|
||||
@@ -178,6 +182,7 @@ func ListReleases(ctx *context.APIContext) {
|
||||
IsDraft: ctx.FormOptionalBool("draft"),
|
||||
IsPreRelease: ctx.FormOptionalBool("pre-release"),
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
TagFilter: ctx.FormString("tag_filter"),
|
||||
}
|
||||
|
||||
releases, err := db.Find[repo_model.Release](ctx, opts)
|
||||
|
||||
Generated
+6
@@ -16196,6 +16196,12 @@
|
||||
"name": "pre-release",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "filter releases by tag. supports \"*\" as a wildcard (for example: v1*, *beta, *rc*).",
|
||||
"name": "tag_filter",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "page number of results to return (1-based)",
|
||||
|
||||
Generated
+8
@@ -28109,6 +28109,14 @@
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "filter releases by tag. supports \"*\" as a wildcard (for example: v1*, *beta, *rc*).",
|
||||
"in": "query",
|
||||
"name": "tag_filter",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "page number of results to return (1-based)",
|
||||
"in": "query",
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -41,13 +40,24 @@ func TestAPIReleaseRead(t *testing.T) {
|
||||
t.Run("EditReleaseAttachmentWithUnallowedFile", testAPIEditReleaseAttachmentWithUnallowedFile) // failed attempt, so it is also a read test
|
||||
}
|
||||
|
||||
func testAPIListReleasesTagNames(t *testing.T, queryURL, authToken string, expectedTagNames []string, msgAndArgs ...string) {
|
||||
t.Helper()
|
||||
resp := MakeRequest(t, NewRequest(t, "GET", queryURL).AddTokenAuth(authToken), http.StatusOK)
|
||||
apiReleases := DecodeJSON(t, resp, []*api.Release{})
|
||||
tagNames := make([]string, 0, len(apiReleases))
|
||||
for _, release := range apiReleases {
|
||||
tagNames = append(tagNames, release.TagName)
|
||||
}
|
||||
assert.ElementsMatch(t, expectedTagNames, tagNames, msgAndArgs)
|
||||
}
|
||||
|
||||
func testAPIListReleasesWithWriteToken(t *testing.T) {
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
token := getUserToken(t, user2.LowerName, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/releases", user2.Name, repo.Name))
|
||||
resp := MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(token), http.StatusOK)
|
||||
link := fmt.Sprintf("/api/v1/repos/%s/%s/releases", user2.Name, repo.Name)
|
||||
resp := MakeRequest(t, NewRequest(t, "GET", link).AddTokenAuth(token), http.StatusOK)
|
||||
apiReleases := DecodeJSON(t, resp, []*api.Release{})
|
||||
if assert.Len(t, apiReleases, 3) {
|
||||
for _, release := range apiReleases {
|
||||
@@ -70,24 +80,16 @@ func testAPIListReleasesWithWriteToken(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// test filter
|
||||
testFilterByLen := func(auth bool, query url.Values, expectedLength int, msgAndArgs ...string) {
|
||||
link.RawQuery = query.Encode()
|
||||
req := NewRequest(t, "GET", link.String())
|
||||
if auth {
|
||||
req.AddTokenAuth(token)
|
||||
}
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
apiReleases = DecodeJSON(t, resp, []*api.Release{})
|
||||
assert.Len(t, apiReleases, expectedLength, msgAndArgs)
|
||||
}
|
||||
|
||||
testFilterByLen(false, url.Values{"draft": {"true"}}, 0, "anon should not see drafts")
|
||||
testFilterByLen(true, url.Values{"draft": {"true"}}, 1, "repo owner should see drafts")
|
||||
testFilterByLen(true, url.Values{"draft": {"false"}}, 2, "exclude drafts")
|
||||
testFilterByLen(true, url.Values{"draft": {"false"}, "pre-release": {"false"}}, 1, "exclude drafts and pre-releases")
|
||||
testFilterByLen(true, url.Values{"pre-release": {"true"}}, 1, "only get pre-release")
|
||||
testFilterByLen(true, url.Values{"draft": {"true"}, "pre-release": {"true"}}, 0, "there is no pre-release draft")
|
||||
testAPIListReleasesTagNames(t, link+"?draft=true", "", nil, "anon should not see drafts")
|
||||
testAPIListReleasesTagNames(t, link+"?draft=true", token, []string{"draft-release"}, "repo owner should see drafts")
|
||||
testAPIListReleasesTagNames(t, link+"?draft=false", token, []string{"v1.0", "v1.1"}, "exclude drafts")
|
||||
testAPIListReleasesTagNames(t, link+"?draft=false&pre-release=false", token, []string{"v1.1"}, "exclude drafts and pre-releases")
|
||||
testAPIListReleasesTagNames(t, link+"?pre-release=true", token, []string{"v1.0"}, "only get pre-release")
|
||||
testAPIListReleasesTagNames(t, link+"?draft=true&pre-release=true", token, nil, "there is no pre-release draft")
|
||||
testAPIListReleasesTagNames(t, link+"?tag_filter=v1*", token, []string{"v1.0", "v1.1"}, "prefix tag filter")
|
||||
testAPIListReleasesTagNames(t, link+"?tag_filter=*1.0", token, []string{"v1.0"}, "suffix tag filter")
|
||||
testAPIListReleasesTagNames(t, link+"?tag_filter=*raft*", token, []string{"draft-release"}, "substring tag filter")
|
||||
testAPIListReleasesTagNames(t, link+"?tag_filter=v1._", token, nil, "underscore is not a wildcard")
|
||||
}
|
||||
|
||||
func testAPIListReleasesWithReadToken(t *testing.T) {
|
||||
@@ -95,8 +97,8 @@ func testAPIListReleasesWithReadToken(t *testing.T) {
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
token := getUserToken(t, user2.LowerName, auth_model.AccessTokenScopeReadRepository)
|
||||
|
||||
link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/releases", user2.Name, repo.Name))
|
||||
resp := MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(token), http.StatusOK)
|
||||
link := fmt.Sprintf("/api/v1/repos/%s/%s/releases", user2.Name, repo.Name)
|
||||
resp := MakeRequest(t, NewRequest(t, "GET", link).AddTokenAuth(token), http.StatusOK)
|
||||
apiReleases := DecodeJSON(t, resp, []*api.Release{})
|
||||
if assert.Len(t, apiReleases, 2) {
|
||||
for _, release := range apiReleases {
|
||||
@@ -115,24 +117,12 @@ func testAPIListReleasesWithReadToken(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// test filter
|
||||
testFilterByLen := func(auth bool, query url.Values, expectedLength int, msgAndArgs ...string) {
|
||||
link.RawQuery = query.Encode()
|
||||
req := NewRequest(t, "GET", link.String())
|
||||
if auth {
|
||||
req.AddTokenAuth(token)
|
||||
}
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
apiReleases = DecodeJSON(t, resp, []*api.Release{})
|
||||
assert.Len(t, apiReleases, expectedLength, msgAndArgs)
|
||||
}
|
||||
|
||||
testFilterByLen(false, url.Values{"draft": {"true"}}, 0, "anon should not see drafts")
|
||||
testFilterByLen(true, url.Values{"draft": {"true"}}, 0, "repo owner with read token should not see drafts")
|
||||
testFilterByLen(true, url.Values{"draft": {"false"}}, 2, "exclude drafts")
|
||||
testFilterByLen(true, url.Values{"draft": {"false"}, "pre-release": {"false"}}, 1, "exclude drafts and pre-releases")
|
||||
testFilterByLen(true, url.Values{"pre-release": {"true"}}, 1, "only get pre-release")
|
||||
testFilterByLen(true, url.Values{"draft": {"true"}, "pre-release": {"true"}}, 0, "there is no pre-release draft")
|
||||
testAPIListReleasesTagNames(t, link+"?draft=true", "", nil, "anon should not see drafts")
|
||||
testAPIListReleasesTagNames(t, link+"?draft=true", token, nil, "repo owner with read token should not see drafts")
|
||||
testAPIListReleasesTagNames(t, link+"?draft=false", token, []string{"v1.0", "v1.1"}, "exclude drafts")
|
||||
testAPIListReleasesTagNames(t, link+"?draft=false&pre-release=false", token, []string{"v1.1"}, "exclude drafts and pre-releases")
|
||||
testAPIListReleasesTagNames(t, link+"?pre-release=true", token, []string{"v1.0"}, "only get pre-release")
|
||||
testAPIListReleasesTagNames(t, link+"?draft=true&pre-release=true", token, nil, "there is no pre-release draft")
|
||||
}
|
||||
|
||||
func testAPIGetDraftRelease(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user