From b3beaed147466739de0c24fd80206b5af8b71617 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. --- 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 | 53 ++++++++++++++++++++++- 4 files changed, 84 insertions(+), 10 deletions(-) diff --git a/modules/structs/issue_label.go b/modules/structs/issue_label.go index bf68726d79..942cc0b3a1 100644 --- a/modules/structs/issue_label.go +++ b/modules/structs/issue_label.go @@ -47,8 +47,9 @@ type EditLabelOption 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"` } // LabelTemplate info of a Label template diff --git a/routers/api/v1/repo/issue_label.go b/routers/api/v1/repo/issue_label.go index 7d9f85d2aa..413693c5ed 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" @@ -317,7 +319,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 5ca499e708..b1255f1289 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -21897,12 +21897,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" } }, diff --git a/tests/integration/api_issue_label_test.go b/tests/integration/api_issue_label_test.go index 35c0718263..0e4cd8243b 100644 --- a/tests/integration/api_issue_label_test.go +++ b/tests/integration/api_issue_label_test.go @@ -104,7 +104,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 @@ -114,6 +114,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 TestAPIReplaceIssueLabels(t *testing.T) { assert.NoError(t, unittest.LoadFixtures()) @@ -127,7 +153,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 @@ -140,6 +166,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())