mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-12-01 09:51:39 -05:00
[API] Fix inconsistent label color format (#10129)
* update and use labelColorPattern * add TestCases * fix lint * # optional for templates * fix typo * some more * fix lint of **master**
This commit is contained in:
parent
74a4a1e17f
commit
e273817154
9 changed files with 170 additions and 54 deletions
|
@ -7,6 +7,7 @@ package integrations
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models"
|
"code.gitea.io/gitea/models"
|
||||||
|
@ -15,6 +16,76 @@ import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestAPIModifyLabels(t *testing.T) {
|
||||||
|
assert.NoError(t, models.LoadFixtures())
|
||||||
|
|
||||||
|
repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 2}).(*models.Repository)
|
||||||
|
owner := models.AssertExistsAndLoadBean(t, &models.User{ID: repo.OwnerID}).(*models.User)
|
||||||
|
session := loginUser(t, owner.Name)
|
||||||
|
token := getTokenForLoggedInUser(t, session)
|
||||||
|
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/labels?token=%s", owner.Name, repo.Name, token)
|
||||||
|
|
||||||
|
// CreateLabel
|
||||||
|
req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateLabelOption{
|
||||||
|
Name: "TestL 1",
|
||||||
|
Color: "abcdef",
|
||||||
|
Description: "test label",
|
||||||
|
})
|
||||||
|
resp := session.MakeRequest(t, req, http.StatusCreated)
|
||||||
|
apiLabel := new(api.Label)
|
||||||
|
DecodeJSON(t, resp, &apiLabel)
|
||||||
|
dbLabel := models.AssertExistsAndLoadBean(t, &models.Label{ID: apiLabel.ID, RepoID: repo.ID}).(*models.Label)
|
||||||
|
assert.EqualValues(t, dbLabel.Name, apiLabel.Name)
|
||||||
|
assert.EqualValues(t, strings.TrimLeft(dbLabel.Color, "#"), apiLabel.Color)
|
||||||
|
|
||||||
|
req = NewRequestWithJSON(t, "POST", urlStr, &api.CreateLabelOption{
|
||||||
|
Name: "TestL 2",
|
||||||
|
Color: "#123456",
|
||||||
|
Description: "jet another test label",
|
||||||
|
})
|
||||||
|
session.MakeRequest(t, req, http.StatusCreated)
|
||||||
|
req = NewRequestWithJSON(t, "POST", urlStr, &api.CreateLabelOption{
|
||||||
|
Name: "WrongTestL",
|
||||||
|
Color: "#12345g",
|
||||||
|
})
|
||||||
|
session.MakeRequest(t, req, http.StatusUnprocessableEntity)
|
||||||
|
|
||||||
|
//ListLabels
|
||||||
|
req = NewRequest(t, "GET", urlStr)
|
||||||
|
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
var apiLabels []*api.Label
|
||||||
|
DecodeJSON(t, resp, &apiLabels)
|
||||||
|
assert.Len(t, apiLabels, 2)
|
||||||
|
|
||||||
|
//GetLabel
|
||||||
|
singleURLStr := fmt.Sprintf("/api/v1/repos/%s/%s/labels/%d?token=%s", owner.Name, repo.Name, dbLabel.ID, token)
|
||||||
|
req = NewRequest(t, "GET", singleURLStr)
|
||||||
|
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
DecodeJSON(t, resp, &apiLabel)
|
||||||
|
assert.EqualValues(t, strings.TrimLeft(dbLabel.Color, "#"), apiLabel.Color)
|
||||||
|
|
||||||
|
//EditLabel
|
||||||
|
newName := "LabelNewName"
|
||||||
|
newColor := "09876a"
|
||||||
|
newColorWrong := "09g76a"
|
||||||
|
req = NewRequestWithJSON(t, "PATCH", singleURLStr, &api.EditLabelOption{
|
||||||
|
Name: &newName,
|
||||||
|
Color: &newColor,
|
||||||
|
})
|
||||||
|
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
DecodeJSON(t, resp, &apiLabel)
|
||||||
|
assert.EqualValues(t, newColor, apiLabel.Color)
|
||||||
|
req = NewRequestWithJSON(t, "PATCH", singleURLStr, &api.EditLabelOption{
|
||||||
|
Color: &newColorWrong,
|
||||||
|
})
|
||||||
|
session.MakeRequest(t, req, http.StatusUnprocessableEntity)
|
||||||
|
|
||||||
|
//DeleteLabel
|
||||||
|
req = NewRequest(t, "DELETE", singleURLStr)
|
||||||
|
resp = session.MakeRequest(t, req, http.StatusNoContent)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func TestAPIAddIssueLabels(t *testing.T) {
|
func TestAPIAddIssueLabels(t *testing.T) {
|
||||||
assert.NoError(t, models.LoadFixtures())
|
assert.NoError(t, models.LoadFixtures())
|
||||||
|
|
||||||
|
|
|
@ -18,47 +18,8 @@ import (
|
||||||
"xorm.io/xorm"
|
"xorm.io/xorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
var labelColorPattern = regexp.MustCompile("#([a-fA-F0-9]{6})")
|
// LabelColorPattern is a regexp witch can validate LabelColor
|
||||||
|
var LabelColorPattern = regexp.MustCompile("^#[0-9a-fA-F]{6}$")
|
||||||
// GetLabelTemplateFile loads the label template file by given name,
|
|
||||||
// then parses and returns a list of name-color pairs and optionally description.
|
|
||||||
func GetLabelTemplateFile(name string) ([][3]string, error) {
|
|
||||||
data, err := GetRepoInitFile("label", name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("GetRepoInitFile: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
lines := strings.Split(string(data), "\n")
|
|
||||||
list := make([][3]string, 0, len(lines))
|
|
||||||
for i := 0; i < len(lines); i++ {
|
|
||||||
line := strings.TrimSpace(lines[i])
|
|
||||||
if len(line) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
parts := strings.SplitN(line, ";", 2)
|
|
||||||
|
|
||||||
fields := strings.SplitN(parts[0], " ", 2)
|
|
||||||
if len(fields) != 2 {
|
|
||||||
return nil, fmt.Errorf("line is malformed: %s", line)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !labelColorPattern.MatchString(fields[0]) {
|
|
||||||
return nil, fmt.Errorf("bad HTML color code in line: %s", line)
|
|
||||||
}
|
|
||||||
|
|
||||||
var description string
|
|
||||||
|
|
||||||
if len(parts) > 1 {
|
|
||||||
description = strings.TrimSpace(parts[1])
|
|
||||||
}
|
|
||||||
|
|
||||||
fields[1] = strings.TrimSpace(fields[1])
|
|
||||||
list = append(list, [3]string{fields[1], fields[0], description})
|
|
||||||
}
|
|
||||||
|
|
||||||
return list, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Label represents a label of repository for issues.
|
// Label represents a label of repository for issues.
|
||||||
type Label struct {
|
type Label struct {
|
||||||
|
@ -86,6 +47,50 @@ func (label *Label) APIFormat() *api.Label {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetLabelTemplateFile loads the label template file by given name,
|
||||||
|
// then parses and returns a list of name-color pairs and optionally description.
|
||||||
|
func GetLabelTemplateFile(name string) ([][3]string, error) {
|
||||||
|
data, err := GetRepoInitFile("label", name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("GetRepoInitFile: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := strings.Split(string(data), "\n")
|
||||||
|
list := make([][3]string, 0, len(lines))
|
||||||
|
for i := 0; i < len(lines); i++ {
|
||||||
|
line := strings.TrimSpace(lines[i])
|
||||||
|
if len(line) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.SplitN(line, ";", 2)
|
||||||
|
|
||||||
|
fields := strings.SplitN(parts[0], " ", 2)
|
||||||
|
if len(fields) != 2 {
|
||||||
|
return nil, fmt.Errorf("line is malformed: %s", line)
|
||||||
|
}
|
||||||
|
|
||||||
|
color := strings.Trim(fields[0], " ")
|
||||||
|
if len(color) == 6 {
|
||||||
|
color = "#" + color
|
||||||
|
}
|
||||||
|
if !LabelColorPattern.MatchString(color) {
|
||||||
|
return nil, fmt.Errorf("bad HTML color code in line: %s", line)
|
||||||
|
}
|
||||||
|
|
||||||
|
var description string
|
||||||
|
|
||||||
|
if len(parts) > 1 {
|
||||||
|
description = strings.TrimSpace(parts[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
fields[1] = strings.TrimSpace(fields[1])
|
||||||
|
list = append(list, [3]string{fields[1], color, description})
|
||||||
|
}
|
||||||
|
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
||||||
// CalOpenIssues calculates the open issues of label.
|
// CalOpenIssues calculates the open issues of label.
|
||||||
func (label *Label) CalOpenIssues() {
|
func (label *Label) CalOpenIssues() {
|
||||||
label.NumOpenIssues = label.NumIssues - label.NumClosedIssues
|
label.NumOpenIssues = label.NumIssues - label.NumClosedIssues
|
||||||
|
@ -152,7 +157,7 @@ func LoadLabelsFormatted(labelTemplate string) (string, error) {
|
||||||
return strings.Join(labels, ", "), err
|
return strings.Join(labels, ", "), err
|
||||||
}
|
}
|
||||||
|
|
||||||
func initalizeLabels(e Engine, repoID int64, labelTemplate string) error {
|
func initializeLabels(e Engine, repoID int64, labelTemplate string) error {
|
||||||
list, err := GetLabelTemplateFile(labelTemplate)
|
list, err := GetLabelTemplateFile(labelTemplate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ErrIssueLabelTemplateLoad{labelTemplate, err}
|
return ErrIssueLabelTemplateLoad{labelTemplate, err}
|
||||||
|
@ -175,9 +180,9 @@ func initalizeLabels(e Engine, repoID int64, labelTemplate string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitalizeLabels adds a label set to a repository using a template
|
// InitializeLabels adds a label set to a repository using a template
|
||||||
func InitalizeLabels(ctx DBContext, repoID int64, labelTemplate string) error {
|
func InitializeLabels(ctx DBContext, repoID int64, labelTemplate string) error {
|
||||||
return initalizeLabels(ctx.e, repoID, labelTemplate)
|
return initializeLabels(ctx.e, repoID, labelTemplate)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newLabel(e Engine, label *Label) error {
|
func newLabel(e Engine, label *Label) error {
|
||||||
|
@ -187,6 +192,9 @@ func newLabel(e Engine, label *Label) error {
|
||||||
|
|
||||||
// NewLabel creates a new label for a repository
|
// NewLabel creates a new label for a repository
|
||||||
func NewLabel(label *Label) error {
|
func NewLabel(label *Label) error {
|
||||||
|
if !LabelColorPattern.MatchString(label.Color) {
|
||||||
|
return fmt.Errorf("bad color code: %s", label.Color)
|
||||||
|
}
|
||||||
return newLabel(x, label)
|
return newLabel(x, label)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -198,6 +206,9 @@ func NewLabels(labels ...*Label) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, label := range labels {
|
for _, label := range labels {
|
||||||
|
if !LabelColorPattern.MatchString(label.Color) {
|
||||||
|
return fmt.Errorf("bad color code: %s", label.Color)
|
||||||
|
}
|
||||||
if err := newLabel(sess, label); err != nil {
|
if err := newLabel(sess, label); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -359,6 +370,9 @@ func updateLabel(e Engine, l *Label) error {
|
||||||
|
|
||||||
// UpdateLabel updates label information.
|
// UpdateLabel updates label information.
|
||||||
func UpdateLabel(l *Label) error {
|
func UpdateLabel(l *Label) error {
|
||||||
|
if !LabelColorPattern.MatchString(l.Color) {
|
||||||
|
return fmt.Errorf("bad color code: %s", l.Color)
|
||||||
|
}
|
||||||
return updateLabel(x, l)
|
return updateLabel(x, l)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -45,8 +45,11 @@ func TestNewLabels(t *testing.T) {
|
||||||
assert.NoError(t, PrepareTestDatabase())
|
assert.NoError(t, PrepareTestDatabase())
|
||||||
labels := []*Label{
|
labels := []*Label{
|
||||||
{RepoID: 2, Name: "labelName2", Color: "#123456"},
|
{RepoID: 2, Name: "labelName2", Color: "#123456"},
|
||||||
{RepoID: 3, Name: "labelName3", Color: "#234567"},
|
{RepoID: 3, Name: "labelName3", Color: "#23456F"},
|
||||||
}
|
}
|
||||||
|
assert.Error(t, NewLabel(&Label{RepoID: 3, Name: "invalid Color", Color: ""}))
|
||||||
|
assert.Error(t, NewLabel(&Label{RepoID: 3, Name: "invalid Color", Color: "123456"}))
|
||||||
|
assert.Error(t, NewLabel(&Label{RepoID: 3, Name: "invalid Color", Color: "#12345G"}))
|
||||||
for _, label := range labels {
|
for _, label := range labels {
|
||||||
AssertNotExistsBean(t, label)
|
AssertNotExistsBean(t, label)
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,15 +58,15 @@ func CreateRepository(doer, u *models.User, opts models.CreateRepoOptions) (_ *m
|
||||||
|
|
||||||
// Initialize Issue Labels if selected
|
// Initialize Issue Labels if selected
|
||||||
if len(opts.IssueLabels) > 0 {
|
if len(opts.IssueLabels) > 0 {
|
||||||
if err = models.InitalizeLabels(ctx, repo.ID, opts.IssueLabels); err != nil {
|
if err = models.InitializeLabels(ctx, repo.ID, opts.IssueLabels); err != nil {
|
||||||
return fmt.Errorf("initalizeLabels: %v", err)
|
return fmt.Errorf("InitializeLabels: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if stdout, err := git.NewCommand("update-server-info").
|
if stdout, err := git.NewCommand("update-server-info").
|
||||||
SetDescription(fmt.Sprintf("CreateRepository(git update-server-info): %s", repoPath)).
|
SetDescription(fmt.Sprintf("CreateRepository(git update-server-info): %s", repoPath)).
|
||||||
RunInDir(repoPath); err != nil {
|
RunInDir(repoPath); err != nil {
|
||||||
log.Error("CreateRepitory(git update-server-info) in %v: Stdout: %s\nError: %v", repo, stdout, err)
|
log.Error("CreateRepository(git update-server-info) in %v: Stdout: %s\nError: %v", repo, stdout, err)
|
||||||
return fmt.Errorf("CreateRepository(git update-server-info): %v", err)
|
return fmt.Errorf("CreateRepository(git update-server-info): %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ type CreateLabelOption struct {
|
||||||
Name string `json:"name" binding:"Required"`
|
Name string `json:"name" binding:"Required"`
|
||||||
// required:true
|
// required:true
|
||||||
// example: #00aabb
|
// example: #00aabb
|
||||||
Color string `json:"color" binding:"Required;Size(7)"`
|
Color string `json:"color" binding:"Required"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,8 +6,10 @@
|
||||||
package repo
|
package repo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models"
|
"code.gitea.io/gitea/models"
|
||||||
"code.gitea.io/gitea/modules/context"
|
"code.gitea.io/gitea/modules/context"
|
||||||
|
@ -135,6 +137,17 @@ func CreateLabel(ctx *context.APIContext, form api.CreateLabelOption) {
|
||||||
// responses:
|
// responses:
|
||||||
// "201":
|
// "201":
|
||||||
// "$ref": "#/responses/Label"
|
// "$ref": "#/responses/Label"
|
||||||
|
// "422":
|
||||||
|
// "$ref": "#/responses/validationError"
|
||||||
|
|
||||||
|
form.Color = strings.Trim(form.Color, " ")
|
||||||
|
if len(form.Color) == 6 {
|
||||||
|
form.Color = "#" + form.Color
|
||||||
|
}
|
||||||
|
if !models.LabelColorPattern.MatchString(form.Color) {
|
||||||
|
ctx.Error(http.StatusUnprocessableEntity, "ColorPattern", fmt.Errorf("bad color code: %s", form.Color))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
label := &models.Label{
|
label := &models.Label{
|
||||||
Name: form.Name,
|
Name: form.Name,
|
||||||
|
@ -182,6 +195,8 @@ func EditLabel(ctx *context.APIContext, form api.EditLabelOption) {
|
||||||
// responses:
|
// responses:
|
||||||
// "200":
|
// "200":
|
||||||
// "$ref": "#/responses/Label"
|
// "$ref": "#/responses/Label"
|
||||||
|
// "422":
|
||||||
|
// "$ref": "#/responses/validationError"
|
||||||
|
|
||||||
label, err := models.GetLabelInRepoByID(ctx.Repo.Repository.ID, ctx.ParamsInt64(":id"))
|
label, err := models.GetLabelInRepoByID(ctx.Repo.Repository.ID, ctx.ParamsInt64(":id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -197,7 +212,14 @@ func EditLabel(ctx *context.APIContext, form api.EditLabelOption) {
|
||||||
label.Name = *form.Name
|
label.Name = *form.Name
|
||||||
}
|
}
|
||||||
if form.Color != nil {
|
if form.Color != nil {
|
||||||
label.Color = *form.Color
|
label.Color = strings.Trim(*form.Color, " ")
|
||||||
|
if len(label.Color) == 6 {
|
||||||
|
label.Color = "#" + label.Color
|
||||||
|
}
|
||||||
|
if !models.LabelColorPattern.MatchString(label.Color) {
|
||||||
|
ctx.Error(http.StatusUnprocessableEntity, "ColorPattern", fmt.Errorf("bad color code: %s", label.Color))
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if form.Description != nil {
|
if form.Description != nil {
|
||||||
label.Description = *form.Description
|
label.Description = *form.Description
|
||||||
|
|
|
@ -35,14 +35,14 @@ func InitializeLabels(ctx *context.Context, form auth.InitializeLabelsForm) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := models.InitalizeLabels(models.DefaultDBContext(), ctx.Repo.Repository.ID, form.TemplateName); err != nil {
|
if err := models.InitializeLabels(models.DefaultDBContext(), ctx.Repo.Repository.ID, form.TemplateName); err != nil {
|
||||||
if models.IsErrIssueLabelTemplateLoad(err) {
|
if models.IsErrIssueLabelTemplateLoad(err) {
|
||||||
originalErr := err.(models.ErrIssueLabelTemplateLoad).OriginalError
|
originalErr := err.(models.ErrIssueLabelTemplateLoad).OriginalError
|
||||||
ctx.Flash.Error(ctx.Tr("repo.issues.label_templates.fail_to_load_file", form.TemplateName, originalErr))
|
ctx.Flash.Error(ctx.Tr("repo.issues.label_templates.fail_to_load_file", form.TemplateName, originalErr))
|
||||||
ctx.Redirect(ctx.Repo.RepoLink + "/labels")
|
ctx.Redirect(ctx.Repo.RepoLink + "/labels")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.ServerError("InitalizeLabels", err)
|
ctx.ServerError("InitializeLabels", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.Redirect(ctx.Repo.RepoLink + "/labels")
|
ctx.Redirect(ctx.Repo.RepoLink + "/labels")
|
||||||
|
|
|
@ -5317,6 +5317,9 @@
|
||||||
"responses": {
|
"responses": {
|
||||||
"201": {
|
"201": {
|
||||||
"$ref": "#/responses/Label"
|
"$ref": "#/responses/Label"
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"$ref": "#/responses/validationError"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5443,6 +5446,9 @@
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"$ref": "#/responses/Label"
|
"$ref": "#/responses/Label"
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"$ref": "#/responses/validationError"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue