From 8e1de85980f1e4ae05b240cafbf9eaf33c94a203 Mon Sep 17 00:00:00 2001 From: Zettat123 Date: Mon, 13 May 2024 12:28:53 +0800 Subject: [PATCH] Support using label names when changing issue labels (#30943) Resolve #30917 Make the APIs for adding labels and replacing labels support both label IDs and label names so the [`actions/labeler`](https://github.com/actions/labeler) action can work in Gitea. (cherry picked from commit b3beaed147466739de0c24fd80206b5af8b71617) Conflicts: - modules/structs/issue_label.go Resolved by applying the Gitea change by hand. - tests/integration/api_issue_label_test.go Resolved by copying the new tests. --- modules/structs/issue_label.go | 5 +- routers/api/v1/repo/issue_label.go | 29 +++++++++++- templates/swagger/v1_json.tmpl | 7 +-- tests/integration/api_issue_label_test.go | 57 +++++++++++++++++++++-- 4 files changed, 86 insertions(+), 12 deletions(-) diff --git a/modules/structs/issue_label.go b/modules/structs/issue_label.go index b64e375961..153c412678 100644 --- a/modules/structs/issue_label.go +++ b/modules/structs/issue_label.go @@ -57,8 +57,9 @@ type DeleteLabelsOption struct { // IssueLabelsOption a collection of labels type IssueLabelsOption struct { - // list of label IDs - Labels []int64 `json:"labels"` + // Labels can be a list of integers representing label IDs + // or a list of strings representing label names + Labels []any `json:"labels"` // swagger:strfmt date-time Updated *time.Time `json:"updated_at"` } diff --git a/routers/api/v1/repo/issue_label.go b/routers/api/v1/repo/issue_label.go index fd9625c0fb..ae05544365 100644 --- a/routers/api/v1/repo/issue_label.go +++ b/routers/api/v1/repo/issue_label.go @@ -5,7 +5,9 @@ package repo import ( + "fmt" "net/http" + "reflect" issues_model "code.gitea.io/gitea/models/issues" api "code.gitea.io/gitea/modules/structs" @@ -337,7 +339,32 @@ func prepareForReplaceOrAdd(ctx *context.APIContext, form api.IssueLabelsOption) return nil, nil, err } - labels, err := issues_model.GetLabelsByIDs(ctx, form.Labels, "id", "repo_id", "org_id", "name", "exclusive") + var ( + labelIDs []int64 + labelNames []string + ) + for _, label := range form.Labels { + rv := reflect.ValueOf(label) + switch rv.Kind() { + case reflect.Float64: + labelIDs = append(labelIDs, int64(rv.Float())) + case reflect.String: + labelNames = append(labelNames, rv.String()) + } + } + if len(labelIDs) > 0 && len(labelNames) > 0 { + ctx.Error(http.StatusBadRequest, "InvalidLabels", "labels should be an array of strings or integers") + return nil, nil, fmt.Errorf("invalid labels") + } + if len(labelNames) > 0 { + labelIDs, err = issues_model.GetLabelIDsInRepoByNames(ctx, ctx.Repo.Repository.ID, labelNames) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetLabelIDsInRepoByNames", err) + return nil, nil, err + } + } + + labels, err := issues_model.GetLabelsByIDs(ctx, labelIDs, "id", "repo_id", "org_id", "name", "exclusive") if err != nil { ctx.Error(http.StatusInternalServerError, "GetLabelsByIDs", err) return nil, nil, err diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index e7b140d1d3..dc6aa7e46d 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -22330,12 +22330,9 @@ "type": "object", "properties": { "labels": { - "description": "list of label IDs", + "description": "Labels can be a list of integers representing label IDs\nor a list of strings representing label names", "type": "array", - "items": { - "type": "integer", - "format": "int64" - }, + "items": {}, "x-go-name": "Labels" }, "updated_at": { diff --git a/tests/integration/api_issue_label_test.go b/tests/integration/api_issue_label_test.go index 905e79902b..e99c3dfd86 100644 --- a/tests/integration/api_issue_label_test.go +++ b/tests/integration/api_issue_label_test.go @@ -106,7 +106,7 @@ func TestAPIAddIssueLabels(t *testing.T) { urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/labels", repo.OwnerName, repo.Name, issue.Index) req := NewRequestWithJSON(t, "POST", urlStr, &api.IssueLabelsOption{ - Labels: []int64{1, 2}, + Labels: []any{1, 2}, }).AddTokenAuth(token) resp := MakeRequest(t, req, http.StatusOK) var apiLabels []*api.Label @@ -116,6 +116,32 @@ func TestAPIAddIssueLabels(t *testing.T) { unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issue.ID, LabelID: 2}) } +func TestAPIAddIssueLabelsWithLabelNames(t *testing.T) { + assert.NoError(t, unittest.LoadFixtures()) + + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: repo.ID}) + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + + session := loginUser(t, owner.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue) + urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/labels", + repo.OwnerName, repo.Name, issue.Index) + req := NewRequestWithJSON(t, "POST", urlStr, &api.IssueLabelsOption{ + Labels: []any{"label1", "label2"}, + }).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + var apiLabels []*api.Label + DecodeJSON(t, resp, &apiLabels) + assert.Len(t, apiLabels, unittest.GetCount(t, &issues_model.IssueLabel{IssueID: issue.ID})) + + var apiLabelNames []string + for _, label := range apiLabels { + apiLabelNames = append(apiLabelNames, label.Name) + } + assert.ElementsMatch(t, apiLabelNames, []string{"label1", "label2"}) +} + func TestAPIAddIssueLabelsAutoDate(t *testing.T) { defer tests.PrepareTestEnv(t)() @@ -132,7 +158,7 @@ func TestAPIAddIssueLabelsAutoDate(t *testing.T) { defer tests.PrintCurrentTest(t)() req := NewRequestWithJSON(t, "POST", urlStr, &api.IssueLabelsOption{ - Labels: []int64{1}, + Labels: []any{1}, }).AddTokenAuth(token) MakeRequest(t, req, http.StatusOK) @@ -147,7 +173,7 @@ func TestAPIAddIssueLabelsAutoDate(t *testing.T) { updatedAt := time.Now().Add(-time.Hour).Truncate(time.Second) req := NewRequestWithJSON(t, "POST", urlStr, &api.IssueLabelsOption{ - Labels: []int64{2}, + Labels: []any{2}, Updated: &updatedAt, }).AddTokenAuth(token) MakeRequest(t, req, http.StatusOK) @@ -172,7 +198,7 @@ func TestAPIReplaceIssueLabels(t *testing.T) { urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/labels", owner.Name, repo.Name, issue.Index) req := NewRequestWithJSON(t, "PUT", urlStr, &api.IssueLabelsOption{ - Labels: []int64{label.ID}, + Labels: []any{label.ID}, }).AddTokenAuth(token) resp := MakeRequest(t, req, http.StatusOK) var apiLabels []*api.Label @@ -185,6 +211,29 @@ func TestAPIReplaceIssueLabels(t *testing.T) { unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issue.ID, LabelID: label.ID}) } +func TestAPIReplaceIssueLabelsWithLabelNames(t *testing.T) { + assert.NoError(t, unittest.LoadFixtures()) + + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: repo.ID}) + label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{RepoID: repo.ID}) + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + + session := loginUser(t, owner.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue) + urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/labels", + owner.Name, repo.Name, issue.Index) + req := NewRequestWithJSON(t, "PUT", urlStr, &api.IssueLabelsOption{ + Labels: []any{label.Name}, + }).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + var apiLabels []*api.Label + DecodeJSON(t, resp, &apiLabels) + if assert.Len(t, apiLabels, 1) { + assert.EqualValues(t, label.Name, apiLabels[0].Name) + } +} + func TestAPIModifyOrgLabels(t *testing.T) { assert.NoError(t, unittest.LoadFixtures())