From 0595ec4954698d302cd912ad15bd3d5b1438d419 Mon Sep 17 00:00:00 2001 From: Lauris BH Date: Sat, 18 Feb 2023 23:38:12 +0200 Subject: [PATCH 1/6] Add loading yaml label template files * Move label template functionality to label module * Fix handling of color codes * Add Advanced label template --- models/issues/label.go | 111 ++++++++++----------- models/issues/label_test.go | 2 - modules/label/label.go | 42 ++++++++ modules/label/parser.go | 137 ++++++++++++++++++++++++++ modules/label/parser_test.go | 72 ++++++++++++++ modules/options/repo.go | 44 +++++++++ modules/repository/create.go | 3 +- modules/repository/init.go | 134 ++++--------------------- options/label/Advanced.yaml | 70 +++++++++++++ routers/api/v1/org/label.go | 25 ++--- routers/api/v1/repo/label.go | 41 ++++---- routers/api/v1/repo/repo.go | 3 +- routers/web/org/org_labels.go | 5 +- routers/web/repo/issue_label.go | 5 +- services/migrations/gitea_uploader.go | 16 +-- 15 files changed, 486 insertions(+), 224 deletions(-) create mode 100644 modules/label/label.go create mode 100644 modules/label/parser.go create mode 100644 modules/label/parser_test.go create mode 100644 modules/options/repo.go create mode 100644 options/label/Advanced.yaml diff --git a/models/issues/label.go b/models/issues/label.go index 0dd12fb5c9af0..22e33df67f99b 100644 --- a/models/issues/label.go +++ b/models/issues/label.go @@ -7,12 +7,12 @@ package issues import ( "context" "fmt" - "regexp" "strconv" "strings" "code.gitea.io/gitea/models/db" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/label" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" @@ -78,9 +78,6 @@ func (err ErrLabelNotExist) Unwrap() error { return util.ErrNotExist } -// LabelColorPattern is a regexp witch can validate LabelColor -var LabelColorPattern = regexp.MustCompile("^#?(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{3})$") - // Label represents a label of repository for issues. type Label struct { ID int64 `xorm:"pk autoincr"` @@ -109,12 +106,12 @@ func init() { } // CalOpenIssues sets the number of open issues of a label based on the already stored number of closed issues. -func (label *Label) CalOpenIssues() { - label.NumOpenIssues = label.NumIssues - label.NumClosedIssues +func (l *Label) CalOpenIssues() { + l.NumOpenIssues = l.NumIssues - l.NumClosedIssues } // CalOpenOrgIssues calculates the open issues of a label for a specific repo -func (label *Label) CalOpenOrgIssues(ctx context.Context, repoID, labelID int64) { +func (l *Label) CalOpenOrgIssues(ctx context.Context, repoID, labelID int64) { counts, _ := CountIssuesByRepo(ctx, &IssuesOptions{ RepoID: repoID, LabelIDs: []int64{labelID}, @@ -122,22 +119,22 @@ func (label *Label) CalOpenOrgIssues(ctx context.Context, repoID, labelID int64) }) for _, count := range counts { - label.NumOpenRepoIssues += count + l.NumOpenRepoIssues += count } } // LoadSelectedLabelsAfterClick calculates the set of selected labels when a label is clicked -func (label *Label) LoadSelectedLabelsAfterClick(currentSelectedLabels []int64, currentSelectedExclusiveScopes []string) { +func (l *Label) LoadSelectedLabelsAfterClick(currentSelectedLabels []int64, currentSelectedExclusiveScopes []string) { var labelQuerySlice []string labelSelected := false - labelID := strconv.FormatInt(label.ID, 10) - labelScope := label.ExclusiveScope() + labelID := strconv.FormatInt(l.ID, 10) + labelScope := l.ExclusiveScope() for i, s := range currentSelectedLabels { - if s == label.ID { + if s == l.ID { labelSelected = true - } else if -s == label.ID { + } else if -s == l.ID { labelSelected = true - label.IsExcluded = true + l.IsExcluded = true } else if s != 0 { // Exclude other labels in the same scope from selection if s < 0 || labelScope == "" || labelScope != currentSelectedExclusiveScopes[i] { @@ -148,23 +145,23 @@ func (label *Label) LoadSelectedLabelsAfterClick(currentSelectedLabels []int64, if !labelSelected { labelQuerySlice = append(labelQuerySlice, labelID) } - label.IsSelected = labelSelected - label.QueryString = strings.Join(labelQuerySlice, ",") + l.IsSelected = labelSelected + l.QueryString = strings.Join(labelQuerySlice, ",") } // BelongsToOrg returns true if label is an organization label -func (label *Label) BelongsToOrg() bool { - return label.OrgID > 0 +func (l *Label) BelongsToOrg() bool { + return l.OrgID > 0 } // BelongsToRepo returns true if label is a repository label -func (label *Label) BelongsToRepo() bool { - return label.RepoID > 0 +func (l *Label) BelongsToRepo() bool { + return l.RepoID > 0 } // Get color as RGB values in 0..255 range -func (label *Label) ColorRGB() (float64, float64, float64, error) { - color, err := strconv.ParseUint(label.Color[1:], 16, 64) +func (l *Label) ColorRGB() (float64, float64, float64, error) { + color, err := strconv.ParseUint(l.Color[1:], 16, 64) if err != nil { return 0, 0, 0, err } @@ -176,9 +173,9 @@ func (label *Label) ColorRGB() (float64, float64, float64, error) { } // Determine if label text should be light or dark to be readable on background color -func (label *Label) UseLightTextColor() bool { - if strings.HasPrefix(label.Color, "#") { - if r, g, b, err := label.ColorRGB(); err == nil { +func (l *Label) UseLightTextColor() bool { + if strings.HasPrefix(l.Color, "#") { + if r, g, b, err := l.ColorRGB(); err == nil { // Perceived brightness from: https://www.w3.org/TR/AERT/#color-contrast // In the future WCAG 3 APCA may be a better solution brightness := (0.299*r + 0.587*g + 0.114*b) / 255 @@ -190,40 +187,26 @@ func (label *Label) UseLightTextColor() bool { } // Return scope substring of label name, or empty string if none exists -func (label *Label) ExclusiveScope() string { - if !label.Exclusive { +func (l *Label) ExclusiveScope() string { + if !l.Exclusive { return "" } - lastIndex := strings.LastIndex(label.Name, "/") - if lastIndex == -1 || lastIndex == 0 || lastIndex == len(label.Name)-1 { + lastIndex := strings.LastIndex(l.Name, "/") + if lastIndex == -1 || lastIndex == 0 || lastIndex == len(l.Name)-1 { return "" } - return label.Name[:lastIndex] + return l.Name[:lastIndex] } // NewLabel creates a new label -func NewLabel(ctx context.Context, label *Label) error { - if !LabelColorPattern.MatchString(label.Color) { - return fmt.Errorf("bad color code: %s", label.Color) - } - - // normalize case - label.Color = strings.ToLower(label.Color) - - // add leading hash - if label.Color[0] != '#' { - label.Color = "#" + label.Color +func NewLabel(ctx context.Context, l *Label) error { + if !label.ColorPattern.MatchString(l.Color) { + return fmt.Errorf("bad color code: %s", l.Color) } - // convert 3-character shorthand into 6-character version - if len(label.Color) == 4 { - r := label.Color[1] - g := label.Color[2] - b := label.Color[3] - label.Color = fmt.Sprintf("#%c%c%c%c%c%c", r, r, g, g, b, b) - } + l.Color = label.NormalizeColor(l.Color) - return db.Insert(ctx, label) + return db.Insert(ctx, l) } // NewLabels creates new labels @@ -234,11 +217,14 @@ func NewLabels(labels ...*Label) error { } defer committer.Close() - for _, label := range labels { - if !LabelColorPattern.MatchString(label.Color) { - return fmt.Errorf("bad color code: %s", label.Color) + for _, l := range labels { + if !label.ColorPattern.MatchString(l.Color) { + return fmt.Errorf("bad color code: %s", l.Color) } - if err := db.Insert(ctx, label); err != nil { + + l.Color = label.NormalizeColor(l.Color) + + if err := db.Insert(ctx, l); err != nil { return err } } @@ -247,15 +233,18 @@ func NewLabels(labels ...*Label) error { // UpdateLabel updates label information. func UpdateLabel(l *Label) error { - if !LabelColorPattern.MatchString(l.Color) { + if !label.ColorPattern.MatchString(l.Color) { return fmt.Errorf("bad color code: %s", l.Color) } + + l.Color = label.NormalizeColor(l.Color) + return updateLabelCols(db.DefaultContext, l, "name", "description", "color", "exclusive") } // DeleteLabel delete a label func DeleteLabel(id, labelID int64) error { - label, err := GetLabelByID(db.DefaultContext, labelID) + l, err := GetLabelByID(db.DefaultContext, labelID) if err != nil { if IsErrLabelNotExist(err) { return nil @@ -271,10 +260,10 @@ func DeleteLabel(id, labelID int64) error { sess := db.GetEngine(ctx) - if label.BelongsToOrg() && label.OrgID != id { + if l.BelongsToOrg() && l.OrgID != id { return nil } - if label.BelongsToRepo() && label.RepoID != id { + if l.BelongsToRepo() && l.RepoID != id { return nil } @@ -682,14 +671,14 @@ func newIssueLabels(ctx context.Context, issue *Issue, labels []*Label, doer *us if err = issue.LoadRepo(ctx); err != nil { return err } - for _, label := range labels { + for _, l := range labels { // Don't add already present labels and invalid labels - if HasIssueLabel(ctx, issue.ID, label.ID) || - (label.RepoID != issue.RepoID && label.OrgID != issue.Repo.OwnerID) { + if HasIssueLabel(ctx, issue.ID, l.ID) || + (l.RepoID != issue.RepoID && l.OrgID != issue.Repo.OwnerID) { continue } - if err = newIssueLabel(ctx, issue, label, doer); err != nil { + if err = newIssueLabel(ctx, issue, l, doer); err != nil { return fmt.Errorf("newIssueLabel: %w", err) } } diff --git a/models/issues/label_test.go b/models/issues/label_test.go index 0e45e0db0bac6..1f6ce4f42ee78 100644 --- a/models/issues/label_test.go +++ b/models/issues/label_test.go @@ -15,8 +15,6 @@ import ( "github.com/stretchr/testify/assert" ) -// TODO TestGetLabelTemplateFile - func TestLabel_CalOpenIssues(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1}) diff --git a/modules/label/label.go b/modules/label/label.go new file mode 100644 index 0000000000000..7bd89111ef7e1 --- /dev/null +++ b/modules/label/label.go @@ -0,0 +1,42 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package label + +import ( + "fmt" + "regexp" + "strings" +) + +// LabelColorPattern is a regexp witch can validate LabelColor +var ColorPattern = regexp.MustCompile("^#?(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{3})$") + +// Label represents label information loaded from template +type Label struct { + Name string `yaml:"name"` + Color string `yaml:"color"` + Description string `yaml:"description,omitempty"` + Exclusive bool `yaml:"exclusive,omitempty"` +} + +// NormalizeColor normalizes a color string to a 6-character hex code +func NormalizeColor(color string) string { + // normalize case + color = strings.ToLower(color) + + // add leading hash + if color[0] != '#' { + color = "#" + color + } + + // convert 3-character shorthand into 6-character version + if len(color) == 4 { + r := color[1] + g := color[2] + b := color[3] + color = fmt.Sprintf("#%c%c%c%c%c%c", r, r, g, g, b, b) + } + + return color +} diff --git a/modules/label/parser.go b/modules/label/parser.go new file mode 100644 index 0000000000000..9c56ae05204d3 --- /dev/null +++ b/modules/label/parser.go @@ -0,0 +1,137 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package label + +import ( + "errors" + "fmt" + "strings" + + "code.gitea.io/gitea/modules/options" + "gopkg.in/yaml.v3" +) + +type labelFile struct { + Labels []*Label `yaml:"labels"` +} + +// ErrTemplateLoad represents a "ErrTemplateLoad" kind of error. +type ErrTemplateLoad struct { + TemplateFile string + OriginalError error +} + +// IsErrTemplateLoad checks if an error is a ErrTemplateLoad. +func IsErrTemplateLoad(err error) bool { + _, ok := err.(ErrTemplateLoad) + return ok +} + +func (err ErrTemplateLoad) Error() string { + return fmt.Sprintf("Failed to load label template file '%s': %v", err.TemplateFile, err.OriginalError) +} + +// GetTemplateFile loads the label template file by given name, +// then parses and returns a list of name-color pairs and optionally description. +func GetTemplateFile(name string) ([]*Label, error) { + data, err := options.GetRepoInitFile("label", name+".yaml") + if err == nil && len(data) > 0 { + return parseYamlFormat(name+".yaml", data) + } + + data, err = options.GetRepoInitFile("label", name+".yml") + if err == nil && len(data) > 0 { + return parseYamlFormat(name+".yml", data) + } + + data, err = options.GetRepoInitFile("label", name) + if err != nil { + return nil, ErrTemplateLoad{name, fmt.Errorf("GetRepoInitFile: %w", err)} + } + + return parseDefaultFormat(name, data) +} + +func parseYamlFormat(name string, data []byte) ([]*Label, error) { + lf := &labelFile{} + + if err := yaml.Unmarshal(data, lf); err != nil { + return nil, err + } + + // Validate label data and fix colors + for _, l := range lf.Labels { + l.Color = strings.TrimSpace(l.Color) + if len(l.Name) == 0 || len(l.Color) == 0 { + return nil, ErrTemplateLoad{name, errors.New("label name and color are required fields")} + } + if len(l.Color) == 6 || len(l.Color) == 3 { + l.Color = "#" + l.Color + } + if !ColorPattern.MatchString(l.Color) { + return nil, ErrTemplateLoad{name, fmt.Errorf("bad HTML color code in label: %s", l.Name)} + } + l.Color = NormalizeColor(l.Color) + } + + return lf.Labels, nil +} + +func parseDefaultFormat(name string, data []byte) ([]*Label, error) { + lines := strings.Split(string(data), "\n") + list := make([]*Label, 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, ErrTemplateLoad{name, fmt.Errorf("line is malformed: %s", line)} + } + + color := strings.Trim(fields[0], " ") + if len(color) == 6 { + color = "#" + color + } + if !ColorPattern.MatchString(color) { + return nil, ErrTemplateLoad{name, 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, &Label{ + Name: fields[1], + Color: NormalizeColor(color), + Description: description, + }) + } + + return list, nil +} + +// LoadFormatted loads the labels' list of a template file as a string separated by comma +func LoadFormatted(name string) (string, error) { + var buf strings.Builder + list, err := GetTemplateFile(name) + if err != nil { + return "", err + } + + for i := 0; i < len(list); i++ { + if i > 0 { + buf.WriteString(", ") + } + buf.WriteString(list[i].Name) + } + return buf.String(), nil +} diff --git a/modules/label/parser_test.go b/modules/label/parser_test.go new file mode 100644 index 0000000000000..be975c69bc66b --- /dev/null +++ b/modules/label/parser_test.go @@ -0,0 +1,72 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package label + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestYamlParser(t *testing.T) { + data := []byte(`labels: + - name: priority/low + exclusive: true + color: "#0000ee" + description: "Low priority" + - name: priority/medium + exclusive: true + color: "0e0" + description: "Medium priority" + - name: priority/high + exclusive: true + color: "#ee0000" + description: "High priority" + - name: type/bug + color: "#f00" + description: "Bug"`) + + labels, err := parseYamlFormat("test", data) + require.NoError(t, err) + require.Len(t, labels, 4) + assert.Equal(t, "priority/low", labels[0].Name) + assert.True(t, labels[0].Exclusive) + assert.Equal(t, "#0000ee", labels[0].Color) + assert.Equal(t, "Low priority", labels[0].Description) + assert.Equal(t, "priority/medium", labels[1].Name) + assert.True(t, labels[1].Exclusive) + assert.Equal(t, "#00ee00", labels[1].Color) + assert.Equal(t, "Medium priority", labels[1].Description) + assert.Equal(t, "priority/high", labels[2].Name) + assert.True(t, labels[2].Exclusive) + assert.Equal(t, "#ee0000", labels[2].Color) + assert.Equal(t, "High priority", labels[2].Description) + assert.Equal(t, "type/bug", labels[3].Name) + assert.False(t, labels[3].Exclusive) + assert.Equal(t, "#ff0000", labels[3].Color) + assert.Equal(t, "Bug", labels[3].Description) +} + +func TestDefaultParser(t *testing.T) { + data := []byte(`#ee0701 bug ; Something is not working +#cccccc duplicate ; This issue or pull request already exists +#84b6eb enhancement ; New feature`) + + labels, err := parseDefaultFormat("test", data) + require.NoError(t, err) + require.Len(t, labels, 3) + assert.Equal(t, "bug", labels[0].Name) + assert.False(t, labels[0].Exclusive) + assert.Equal(t, "#ee0701", labels[0].Color) + assert.Equal(t, "Something is not working", labels[0].Description) + assert.Equal(t, "duplicate", labels[1].Name) + assert.False(t, labels[1].Exclusive) + assert.Equal(t, "#cccccc", labels[1].Color) + assert.Equal(t, "This issue or pull request already exists", labels[1].Description) + assert.Equal(t, "enhancement", labels[2].Name) + assert.False(t, labels[2].Exclusive) + assert.Equal(t, "#84b6eb", labels[2].Color) + assert.Equal(t, "New feature", labels[2].Description) +} diff --git a/modules/options/repo.go b/modules/options/repo.go new file mode 100644 index 0000000000000..1480f7808176c --- /dev/null +++ b/modules/options/repo.go @@ -0,0 +1,44 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package options + +import ( + "fmt" + "os" + "path" + "strings" + + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" +) + +// GetRepoInitFile returns repository init files +func GetRepoInitFile(tp, name string) ([]byte, error) { + cleanedName := strings.TrimLeft(path.Clean("/"+name), "/") + relPath := path.Join("options", tp, cleanedName) + + // Use custom file when available. + customPath := path.Join(setting.CustomPath, relPath) + isFile, err := util.IsFile(customPath) + if err != nil { + log.Error("Unable to check if %s is a file. Error: %v", customPath, err) + } + if isFile { + return os.ReadFile(customPath) + } + + switch tp { + case "readme": + return Readme(cleanedName) + case "gitignore": + return Gitignore(cleanedName) + case "license": + return License(cleanedName) + case "label": + return Labels(cleanedName) + default: + return []byte{}, fmt.Errorf("Invalid init file type") + } +} diff --git a/modules/repository/create.go b/modules/repository/create.go index 1704ea792cbf8..6a1fa41b6b87d 100644 --- a/modules/repository/create.go +++ b/modules/repository/create.go @@ -23,6 +23,7 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/models/webhook" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/label" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" @@ -189,7 +190,7 @@ func CreateRepository(doer, u *user_model.User, opts CreateRepoOptions) (*repo_m // Check if label template exist if len(opts.IssueLabels) > 0 { - if _, err := GetLabelTemplateFile(opts.IssueLabels); err != nil { + if _, err := label.GetTemplateFile(opts.IssueLabels); err != nil { return nil, err } } diff --git a/modules/repository/init.go b/modules/repository/init.go index 5705fe5b998e4..8cc12bac6bcfe 100644 --- a/modules/repository/init.go +++ b/modules/repository/init.go @@ -18,6 +18,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/label" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/options" "code.gitea.io/gitea/modules/setting" @@ -40,114 +41,6 @@ var ( LabelTemplates map[string]string ) -// ErrIssueLabelTemplateLoad represents a "ErrIssueLabelTemplateLoad" kind of error. -type ErrIssueLabelTemplateLoad struct { - TemplateFile string - OriginalError error -} - -// IsErrIssueLabelTemplateLoad checks if an error is a ErrIssueLabelTemplateLoad. -func IsErrIssueLabelTemplateLoad(err error) bool { - _, ok := err.(ErrIssueLabelTemplateLoad) - return ok -} - -func (err ErrIssueLabelTemplateLoad) Error() string { - return fmt.Sprintf("Failed to load label template file '%s': %v", err.TemplateFile, err.OriginalError) -} - -// GetRepoInitFile returns repository init files -func GetRepoInitFile(tp, name string) ([]byte, error) { - cleanedName := strings.TrimLeft(path.Clean("/"+name), "/") - relPath := path.Join("options", tp, cleanedName) - - // Use custom file when available. - customPath := path.Join(setting.CustomPath, relPath) - isFile, err := util.IsFile(customPath) - if err != nil { - log.Error("Unable to check if %s is a file. Error: %v", customPath, err) - } - if isFile { - return os.ReadFile(customPath) - } - - switch tp { - case "readme": - return options.Readme(cleanedName) - case "gitignore": - return options.Gitignore(cleanedName) - case "license": - return options.License(cleanedName) - case "label": - return options.Labels(cleanedName) - default: - return []byte{}, fmt.Errorf("Invalid init file type") - } -} - -// 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, ErrIssueLabelTemplateLoad{name, fmt.Errorf("GetRepoInitFile: %w", 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, ErrIssueLabelTemplateLoad{name, fmt.Errorf("line is malformed: %s", line)} - } - - color := strings.Trim(fields[0], " ") - if len(color) == 6 { - color = "#" + color - } - if !issues_model.LabelColorPattern.MatchString(color) { - return nil, ErrIssueLabelTemplateLoad{name, 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 -} - -func loadLabels(labelTemplate string) ([]string, error) { - list, err := GetLabelTemplateFile(labelTemplate) - if err != nil { - return nil, err - } - - labels := make([]string, len(list)) - for i := 0; i < len(list); i++ { - labels[i] = list[i][0] - } - return labels, nil -} - -// LoadLabelsFormatted loads the labels' list of a template file as a string separated by comma -func LoadLabelsFormatted(labelTemplate string) (string, error) { - labels, err := loadLabels(labelTemplate) - return strings.Join(labels, ", "), err -} - // LoadRepoConfig loads the repository config func LoadRepoConfig() { // Load .gitignore and license files and readme templates. @@ -158,6 +51,14 @@ func LoadRepoConfig() { if err != nil { log.Fatal("Failed to get %s files: %v", t, err) } + if t == "label" { + for i, f := range files { + ext := strings.ToLower(filepath.Ext(f)) + if ext == ".yaml" || ext == ".yml" { + files[i] = f[:len(f)-len(ext)] + } + } + } customPath := path.Join(setting.CustomPath, "options", t) isDir, err := util.IsDir(customPath) if err != nil { @@ -190,7 +91,7 @@ func LoadRepoConfig() { // Load label templates LabelTemplates = make(map[string]string) for _, templateFile := range LabelTemplatesFiles { - labels, err := LoadLabelsFormatted(templateFile) + labels, err := label.LoadFormatted(templateFile) if err != nil { log.Error("Failed to load labels: %v", err) } @@ -235,7 +136,7 @@ func prepareRepoCommit(ctx context.Context, repo *repo_model.Repository, tmpDir, } // README - data, err := GetRepoInitFile("readme", opts.Readme) + data, err := options.GetRepoInitFile("readme", opts.Readme) if err != nil { return fmt.Errorf("GetRepoInitFile[%s]: %w", opts.Readme, err) } @@ -263,7 +164,7 @@ func prepareRepoCommit(ctx context.Context, repo *repo_model.Repository, tmpDir, var buf bytes.Buffer names := strings.Split(opts.Gitignores, ",") for _, name := range names { - data, err = GetRepoInitFile("gitignore", name) + data, err = options.GetRepoInitFile("gitignore", name) if err != nil { return fmt.Errorf("GetRepoInitFile[%s]: %w", name, err) } @@ -281,7 +182,7 @@ func prepareRepoCommit(ctx context.Context, repo *repo_model.Repository, tmpDir, // LICENSE if len(opts.License) > 0 { - data, err = GetRepoInitFile("license", opts.License) + data, err = options.GetRepoInitFile("license", opts.License) if err != nil { return fmt.Errorf("GetRepoInitFile[%s]: %w", opts.License, err) } @@ -444,7 +345,7 @@ func initRepository(ctx context.Context, repoPath string, u *user_model.User, re // InitializeLabels adds a label set to a repository using a template func InitializeLabels(ctx context.Context, id int64, labelTemplate string, isOrg bool) error { - list, err := GetLabelTemplateFile(labelTemplate) + list, err := label.GetTemplateFile(labelTemplate) if err != nil { return err } @@ -452,9 +353,10 @@ func InitializeLabels(ctx context.Context, id int64, labelTemplate string, isOrg labels := make([]*issues_model.Label, len(list)) for i := 0; i < len(list); i++ { labels[i] = &issues_model.Label{ - Name: list[i][0], - Description: list[i][2], - Color: list[i][1], + Name: list[i].Name, + Exclusive: list[i].Exclusive, + Description: list[i].Description, + Color: list[i].Color, } if isOrg { labels[i].OrgID = id diff --git a/options/label/Advanced.yaml b/options/label/Advanced.yaml new file mode 100644 index 0000000000000..27b2c146372b3 --- /dev/null +++ b/options/label/Advanced.yaml @@ -0,0 +1,70 @@ +labels: + - name: "Kind/Bug" + color: ee0701 + description: Something is not working + - name: "Kind/Feature" + color: 0288d1 + description: New functionality + - name: "Kind/Enhancement" + color: 84b6eb + description: Improve existing functionality + - name: "Kind/Security" + color: 9c27b0 + description: This is security issue + - name: "Kind/Testing" + color: 795548 + description: Issue or pull request related to testing + - name: "Kind/Breaking" + color: c62828 + description: Breaking change that won't be backward compatible + - name: "Kind/Documentation" + color: 37474f + description: Documentation changes + - name: "Reviewed/Duplicate" + exclusive: true + color: 616161 + description: This issue or pull request already exists + - name: "Reviewed/Invalid" + exclusive: true + color: 546e7a + description: Invalid issue + - name: "Reviewed/Confirmed" + exclusive: true + color: 795548 + description: Issue has been confirmed + - name: "Reviewed/Won't Fix" + exclusive: true + color: eeeeee + description: This issue won't be fixed + - name: "Status/Need More Info" + exclusive: true + color: 424242 + description: Feedback is required to reproduce issue or to continue work + - name: "Status/Blocked" + exclusive: true + color: 880e4f + description: Something is blocking this issue or pull request + - name: "Status/Abandoned" + exclusive: true + color: "222222" + description: Somebody has started to work on this but abandoned work + - name: "Priority/Critical" + exclusive: true + color: b71c1c + description: The priority is critical + priority: critical + - name: "Priority/High" + exclusive: true + color: d32f2f + description: The priority is high + priority: high + - name: "Priority/Medium" + exclusive: true + color: e64a19 + description: The priority is medium + priority: medium + - name: "Priority/Low" + exclusive: true + color: 4caf50 + description: The priority is low + priority: low diff --git a/routers/api/v1/org/label.go b/routers/api/v1/org/label.go index 938fe79df64cc..e3f9ac15dabe3 100644 --- a/routers/api/v1/org/label.go +++ b/routers/api/v1/org/label.go @@ -11,6 +11,7 @@ import ( issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/label" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/api/v1/utils" @@ -87,7 +88,7 @@ func CreateLabel(ctx *context.APIContext) { if len(form.Color) == 6 { form.Color = "#" + form.Color } - if !issues_model.LabelColorPattern.MatchString(form.Color) { + if !label.ColorPattern.MatchString(form.Color) { ctx.Error(http.StatusUnprocessableEntity, "ColorPattern", fmt.Errorf("bad color code: %s", form.Color)) return } @@ -183,7 +184,7 @@ func EditLabel(ctx *context.APIContext) { // "422": // "$ref": "#/responses/validationError" form := web.GetForm(ctx).(*api.EditLabelOption) - label, err := issues_model.GetLabelInOrgByID(ctx, ctx.Org.Organization.ID, ctx.ParamsInt64(":id")) + l, err := issues_model.GetLabelInOrgByID(ctx, ctx.Org.Organization.ID, ctx.ParamsInt64(":id")) if err != nil { if issues_model.IsErrOrgLabelNotExist(err) { ctx.NotFound() @@ -194,30 +195,30 @@ func EditLabel(ctx *context.APIContext) { } if form.Name != nil { - label.Name = *form.Name + l.Name = *form.Name } if form.Exclusive != nil { - label.Exclusive = *form.Exclusive + l.Exclusive = *form.Exclusive } if form.Color != nil { - label.Color = strings.Trim(*form.Color, " ") - if len(label.Color) == 6 { - label.Color = "#" + label.Color + l.Color = strings.Trim(*form.Color, " ") + if len(l.Color) == 6 { + l.Color = "#" + l.Color } - if !issues_model.LabelColorPattern.MatchString(label.Color) { - ctx.Error(http.StatusUnprocessableEntity, "ColorPattern", fmt.Errorf("bad color code: %s", label.Color)) + if !label.ColorPattern.MatchString(l.Color) { + ctx.Error(http.StatusUnprocessableEntity, "ColorPattern", fmt.Errorf("bad color code: %s", l.Color)) return } } if form.Description != nil { - label.Description = *form.Description + l.Description = *form.Description } - if err := issues_model.UpdateLabel(label); err != nil { + if err := issues_model.UpdateLabel(l); err != nil { ctx.Error(http.StatusInternalServerError, "UpdateLabel", err) return } - ctx.JSON(http.StatusOK, convert.ToLabel(label, nil, ctx.Org.Organization.AsUser())) + ctx.JSON(http.StatusOK, convert.ToLabel(l, nil, ctx.Org.Organization.AsUser())) } // DeleteLabel delete a label for an organization diff --git a/routers/api/v1/repo/label.go b/routers/api/v1/repo/label.go index a06d26e837335..1cea29d68bca2 100644 --- a/routers/api/v1/repo/label.go +++ b/routers/api/v1/repo/label.go @@ -12,6 +12,7 @@ import ( issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/label" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/api/v1/utils" @@ -93,14 +94,14 @@ func GetLabel(ctx *context.APIContext) { // "$ref": "#/responses/Label" var ( - label *issues_model.Label - err error + l *issues_model.Label + err error ) strID := ctx.Params(":id") if intID, err2 := strconv.ParseInt(strID, 10, 64); err2 != nil { - label, err = issues_model.GetLabelInRepoByName(ctx, ctx.Repo.Repository.ID, strID) + l, err = issues_model.GetLabelInRepoByName(ctx, ctx.Repo.Repository.ID, strID) } else { - label, err = issues_model.GetLabelInRepoByID(ctx, ctx.Repo.Repository.ID, intID) + l, err = issues_model.GetLabelInRepoByID(ctx, ctx.Repo.Repository.ID, intID) } if err != nil { if issues_model.IsErrRepoLabelNotExist(err) { @@ -111,7 +112,7 @@ func GetLabel(ctx *context.APIContext) { return } - ctx.JSON(http.StatusOK, convert.ToLabel(label, ctx.Repo.Repository, nil)) + ctx.JSON(http.StatusOK, convert.ToLabel(l, ctx.Repo.Repository, nil)) } // CreateLabel create a label for a repository @@ -149,24 +150,24 @@ func CreateLabel(ctx *context.APIContext) { if len(form.Color) == 6 { form.Color = "#" + form.Color } - if !issues_model.LabelColorPattern.MatchString(form.Color) { + if !label.ColorPattern.MatchString(form.Color) { ctx.Error(http.StatusUnprocessableEntity, "ColorPattern", fmt.Errorf("bad color code: %s", form.Color)) return } - label := &issues_model.Label{ + l := &issues_model.Label{ Name: form.Name, Exclusive: form.Exclusive, Color: form.Color, RepoID: ctx.Repo.Repository.ID, Description: form.Description, } - if err := issues_model.NewLabel(ctx, label); err != nil { + if err := issues_model.NewLabel(ctx, l); err != nil { ctx.Error(http.StatusInternalServerError, "NewLabel", err) return } - ctx.JSON(http.StatusCreated, convert.ToLabel(label, ctx.Repo.Repository, nil)) + ctx.JSON(http.StatusCreated, convert.ToLabel(l, ctx.Repo.Repository, nil)) } // EditLabel modify a label for a repository @@ -206,7 +207,7 @@ func EditLabel(ctx *context.APIContext) { // "$ref": "#/responses/validationError" form := web.GetForm(ctx).(*api.EditLabelOption) - label, err := issues_model.GetLabelInRepoByID(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":id")) + l, err := issues_model.GetLabelInRepoByID(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":id")) if err != nil { if issues_model.IsErrRepoLabelNotExist(err) { ctx.NotFound() @@ -217,30 +218,30 @@ func EditLabel(ctx *context.APIContext) { } if form.Name != nil { - label.Name = *form.Name + l.Name = *form.Name } if form.Exclusive != nil { - label.Exclusive = *form.Exclusive + l.Exclusive = *form.Exclusive } if form.Color != nil { - label.Color = strings.Trim(*form.Color, " ") - if len(label.Color) == 6 { - label.Color = "#" + label.Color + l.Color = strings.Trim(*form.Color, " ") + if len(l.Color) == 6 { + l.Color = "#" + l.Color } - if !issues_model.LabelColorPattern.MatchString(label.Color) { - ctx.Error(http.StatusUnprocessableEntity, "ColorPattern", fmt.Errorf("bad color code: %s", label.Color)) + if !label.ColorPattern.MatchString(l.Color) { + ctx.Error(http.StatusUnprocessableEntity, "ColorPattern", fmt.Errorf("bad color code: %s", l.Color)) return } } if form.Description != nil { - label.Description = *form.Description + l.Description = *form.Description } - if err := issues_model.UpdateLabel(label); err != nil { + if err := issues_model.UpdateLabel(l); err != nil { ctx.Error(http.StatusInternalServerError, "UpdateLabel", err) return } - ctx.JSON(http.StatusOK, convert.ToLabel(label, ctx.Repo.Repository, nil)) + ctx.JSON(http.StatusOK, convert.ToLabel(l, ctx.Repo.Repository, nil)) } // DeleteLabel delete a label for a repository diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index 0395198e209a3..8490eb99f93ef 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -19,6 +19,7 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/label" "code.gitea.io/gitea/modules/log" repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" @@ -248,7 +249,7 @@ func CreateUserRepo(ctx *context.APIContext, owner *user_model.User, opt api.Cre ctx.Error(http.StatusConflict, "", "The repository with the same name already exists.") } else if db.IsErrNameReserved(err) || db.IsErrNamePatternNotAllowed(err) || - repo_module.IsErrIssueLabelTemplateLoad(err) { + label.IsErrTemplateLoad(err) { ctx.Error(http.StatusUnprocessableEntity, "", err) } else { ctx.Error(http.StatusInternalServerError, "CreateRepository", err) diff --git a/routers/web/org/org_labels.go b/routers/web/org/org_labels.go index e96627762bd54..9ce05680d7bd2 100644 --- a/routers/web/org/org_labels.go +++ b/routers/web/org/org_labels.go @@ -9,6 +9,7 @@ import ( "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/label" repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/forms" @@ -103,8 +104,8 @@ func InitializeLabels(ctx *context.Context) { } if err := repo_module.InitializeLabels(ctx, ctx.Org.Organization.ID, form.TemplateName, true); err != nil { - if repo_module.IsErrIssueLabelTemplateLoad(err) { - originalErr := err.(repo_module.ErrIssueLabelTemplateLoad).OriginalError + if label.IsErrTemplateLoad(err) { + originalErr := err.(label.ErrTemplateLoad).OriginalError ctx.Flash.Error(ctx.Tr("repo.issues.label_templates.fail_to_load_file", form.TemplateName, originalErr)) ctx.Redirect(ctx.Org.OrgLink + "/settings/labels") return diff --git a/routers/web/repo/issue_label.go b/routers/web/repo/issue_label.go index d4fece9f014b7..31bf85fedb29a 100644 --- a/routers/web/repo/issue_label.go +++ b/routers/web/repo/issue_label.go @@ -11,6 +11,7 @@ import ( "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/label" "code.gitea.io/gitea/modules/log" repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/web" @@ -41,8 +42,8 @@ func InitializeLabels(ctx *context.Context) { } if err := repo_module.InitializeLabels(ctx, ctx.Repo.Repository.ID, form.TemplateName, false); err != nil { - if repo_module.IsErrIssueLabelTemplateLoad(err) { - originalErr := err.(repo_module.ErrIssueLabelTemplateLoad).OriginalError + if label.IsErrTemplateLoad(err) { + originalErr := err.(label.ErrTemplateLoad).OriginalError ctx.Flash.Error(ctx.Tr("repo.issues.label_templates.fail_to_load_file", form.TemplateName, originalErr)) ctx.Redirect(ctx.Repo.RepoLink + "/labels") return diff --git a/services/migrations/gitea_uploader.go b/services/migrations/gitea_uploader.go index 20370d99f9824..e60cdd041b337 100644 --- a/services/migrations/gitea_uploader.go +++ b/services/migrations/gitea_uploader.go @@ -21,6 +21,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/label" "code.gitea.io/gitea/modules/log" base "code.gitea.io/gitea/modules/migration" repo_module "code.gitea.io/gitea/modules/repository" @@ -217,18 +218,19 @@ func (g *GiteaLocalUploader) CreateMilestones(milestones ...*base.Milestone) err // CreateLabels creates labels func (g *GiteaLocalUploader) CreateLabels(labels ...*base.Label) error { lbs := make([]*issues_model.Label, 0, len(labels)) - for _, label := range labels { + for _, l := range labels { // We must validate color here: - if !issues_model.LabelColorPattern.MatchString("#" + label.Color) { - log.Warn("Invalid label color: #%s for label: %s in migration to %s/%s", label.Color, label.Name, g.repoOwner, g.repoName) - label.Color = "ffffff" + if !label.ColorPattern.MatchString("#" + l.Color) { + log.Warn("Invalid label color: #%s for label: %s in migration to %s/%s", l.Color, l.Name, g.repoOwner, g.repoName) + l.Color = "ffffff" } lbs = append(lbs, &issues_model.Label{ RepoID: g.repo.ID, - Name: label.Name, - Description: label.Description, - Color: "#" + label.Color, + Name: l.Name, + Exclusive: l.Exclusive, + Description: l.Description, + Color: "#" + l.Color, }) } From 1f079f3d3199f10f95375e6555fbc16507c44e55 Mon Sep 17 00:00:00 2001 From: Lauris BH Date: Sat, 18 Feb 2023 23:50:25 +0200 Subject: [PATCH 2/6] Lint check fixes --- modules/label/label.go | 2 +- modules/label/parser.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/label/label.go b/modules/label/label.go index 7bd89111ef7e1..794f4046f07f2 100644 --- a/modules/label/label.go +++ b/modules/label/label.go @@ -9,7 +9,7 @@ import ( "strings" ) -// LabelColorPattern is a regexp witch can validate LabelColor +// ColorPattern is a regexp witch can validate label color var ColorPattern = regexp.MustCompile("^#?(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{3})$") // Label represents label information loaded from template diff --git a/modules/label/parser.go b/modules/label/parser.go index 9c56ae05204d3..a71ac0ee0d128 100644 --- a/modules/label/parser.go +++ b/modules/label/parser.go @@ -9,6 +9,7 @@ import ( "strings" "code.gitea.io/gitea/modules/options" + "gopkg.in/yaml.v3" ) From 068da385fe41333c50f1e61470b3b587f1a8ecf9 Mon Sep 17 00:00:00 2001 From: Lauris BH Date: Sun, 19 Feb 2023 00:38:46 +0200 Subject: [PATCH 3/6] Add label color normalization for API methods --- routers/api/v1/org/label.go | 7 ++++--- routers/api/v1/repo/label.go | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/routers/api/v1/org/label.go b/routers/api/v1/org/label.go index e3f9ac15dabe3..1d6c01f8a82b4 100644 --- a/routers/api/v1/org/label.go +++ b/routers/api/v1/org/label.go @@ -85,7 +85,7 @@ func CreateLabel(ctx *context.APIContext) { // "$ref": "#/responses/validationError" form := web.GetForm(ctx).(*api.CreateLabelOption) form.Color = strings.Trim(form.Color, " ") - if len(form.Color) == 6 { + if len(form.Color) == 6 || len(form.Color) == 3 { form.Color = "#" + form.Color } if !label.ColorPattern.MatchString(form.Color) { @@ -96,7 +96,7 @@ func CreateLabel(ctx *context.APIContext) { label := &issues_model.Label{ Name: form.Name, Exclusive: form.Exclusive, - Color: form.Color, + Color: label.NormalizeColor(form.Color), OrgID: ctx.Org.Organization.ID, Description: form.Description, } @@ -202,13 +202,14 @@ func EditLabel(ctx *context.APIContext) { } if form.Color != nil { l.Color = strings.Trim(*form.Color, " ") - if len(l.Color) == 6 { + if len(l.Color) == 6 || len(l.Color) == 3 { l.Color = "#" + l.Color } if !label.ColorPattern.MatchString(l.Color) { ctx.Error(http.StatusUnprocessableEntity, "ColorPattern", fmt.Errorf("bad color code: %s", l.Color)) return } + l.Color = label.NormalizeColor(l.Color) } if form.Description != nil { l.Description = *form.Description diff --git a/routers/api/v1/repo/label.go b/routers/api/v1/repo/label.go index 1cea29d68bca2..ca25f54dc6d58 100644 --- a/routers/api/v1/repo/label.go +++ b/routers/api/v1/repo/label.go @@ -147,7 +147,7 @@ func CreateLabel(ctx *context.APIContext) { form := web.GetForm(ctx).(*api.CreateLabelOption) form.Color = strings.Trim(form.Color, " ") - if len(form.Color) == 6 { + if len(form.Color) == 6 || len(form.Color) == 3 { form.Color = "#" + form.Color } if !label.ColorPattern.MatchString(form.Color) { @@ -158,7 +158,7 @@ func CreateLabel(ctx *context.APIContext) { l := &issues_model.Label{ Name: form.Name, Exclusive: form.Exclusive, - Color: form.Color, + Color: label.NormalizeColor(form.Color), RepoID: ctx.Repo.Repository.ID, Description: form.Description, } @@ -225,13 +225,14 @@ func EditLabel(ctx *context.APIContext) { } if form.Color != nil { l.Color = strings.Trim(*form.Color, " ") - if len(l.Color) == 6 { + if len(l.Color) == 6 || len(l.Color) == 3 { l.Color = "#" + l.Color } if !label.ColorPattern.MatchString(l.Color) { ctx.Error(http.StatusUnprocessableEntity, "ColorPattern", fmt.Errorf("bad color code: %s", l.Color)) return } + l.Color = label.NormalizeColor(l.Color) } if form.Description != nil { l.Description = *form.Description From fb035a6c6573f2d55667e5eab97c7b4911b9d6bd Mon Sep 17 00:00:00 2001 From: Lauris BH Date: Mon, 20 Feb 2023 13:55:50 +0200 Subject: [PATCH 4/6] Deduplicate label code color handling and simplify default label template file parsing --- models/issues/label.go | 24 +++++++++---------- modules/label/label.go | 16 ++++++++----- modules/label/parser.go | 34 +++++++++------------------ modules/label/parser_test.go | 8 +++---- routers/api/v1/org/label.go | 23 +++++++----------- routers/api/v1/repo/label.go | 26 ++++++++------------ services/migrations/gitea_uploader.go | 9 +++---- 7 files changed, 61 insertions(+), 79 deletions(-) diff --git a/models/issues/label.go b/models/issues/label.go index 22e33df67f99b..5367018452bd6 100644 --- a/models/issues/label.go +++ b/models/issues/label.go @@ -200,12 +200,12 @@ func (l *Label) ExclusiveScope() string { // NewLabel creates a new label func NewLabel(ctx context.Context, l *Label) error { - if !label.ColorPattern.MatchString(l.Color) { - return fmt.Errorf("bad color code: %s", l.Color) + if color, err := label.NormalizeColor(l.Color); err != nil { + return err + } else { + l.Color = color } - l.Color = label.NormalizeColor(l.Color) - return db.Insert(ctx, l) } @@ -218,12 +218,12 @@ func NewLabels(labels ...*Label) error { defer committer.Close() for _, l := range labels { - if !label.ColorPattern.MatchString(l.Color) { - return fmt.Errorf("bad color code: %s", l.Color) + if color, err := label.NormalizeColor(l.Color); err != nil { + return err + } else { + l.Color = color } - l.Color = label.NormalizeColor(l.Color) - if err := db.Insert(ctx, l); err != nil { return err } @@ -233,12 +233,12 @@ func NewLabels(labels ...*Label) error { // UpdateLabel updates label information. func UpdateLabel(l *Label) error { - if !label.ColorPattern.MatchString(l.Color) { - return fmt.Errorf("bad color code: %s", l.Color) + if color, err := label.NormalizeColor(l.Color); err != nil { + return err + } else { + l.Color = color } - l.Color = label.NormalizeColor(l.Color) - return updateLabelCols(db.DefaultContext, l, "name", "description", "color", "exclusive") } diff --git a/modules/label/label.go b/modules/label/label.go index 794f4046f07f2..994bee3360682 100644 --- a/modules/label/label.go +++ b/modules/label/label.go @@ -9,8 +9,8 @@ import ( "strings" ) -// ColorPattern is a regexp witch can validate label color -var ColorPattern = regexp.MustCompile("^#?(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{3})$") +// colorPattern is a regexp witch can validate label color +var colorPattern = regexp.MustCompile("^#?(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{3})$") // Label represents label information loaded from template type Label struct { @@ -21,15 +21,19 @@ type Label struct { } // NormalizeColor normalizes a color string to a 6-character hex code -func NormalizeColor(color string) string { +func NormalizeColor(color string) (string, error) { // normalize case - color = strings.ToLower(color) + color = strings.TrimSpace(strings.ToLower(color)) // add leading hash - if color[0] != '#' { + if len(color) == 6 || len(color) == 3 { color = "#" + color } + if !colorPattern.MatchString(color) { + return "", fmt.Errorf("bad color code: %s", color) + } + // convert 3-character shorthand into 6-character version if len(color) == 4 { r := color[1] @@ -38,5 +42,5 @@ func NormalizeColor(color string) string { color = fmt.Sprintf("#%c%c%c%c%c%c", r, r, g, g, b, b) } - return color + return color, nil } diff --git a/modules/label/parser.go b/modules/label/parser.go index a71ac0ee0d128..dcd9542893194 100644 --- a/modules/label/parser.go +++ b/modules/label/parser.go @@ -67,13 +67,11 @@ func parseYamlFormat(name string, data []byte) ([]*Label, error) { if len(l.Name) == 0 || len(l.Color) == 0 { return nil, ErrTemplateLoad{name, errors.New("label name and color are required fields")} } - if len(l.Color) == 6 || len(l.Color) == 3 { - l.Color = "#" + l.Color - } - if !ColorPattern.MatchString(l.Color) { + if color, err := NormalizeColor(l.Color); err != nil { return nil, ErrTemplateLoad{name, fmt.Errorf("bad HTML color code in label: %s", l.Name)} + } else { + l.Color = color } - l.Color = NormalizeColor(l.Color) } return lf.Labels, nil @@ -88,32 +86,22 @@ func parseDefaultFormat(name string, data []byte) ([]*Label, error) { continue } - parts := strings.SplitN(line, ";", 2) + parts, description, _ := strings.Cut(line, ";") - fields := strings.SplitN(parts[0], " ", 2) - if len(fields) != 2 { + color, name, ok := strings.Cut(parts, " ") + if !ok { return nil, ErrTemplateLoad{name, fmt.Errorf("line is malformed: %s", line)} } - color := strings.Trim(fields[0], " ") - if len(color) == 6 { - color = "#" + color - } - if !ColorPattern.MatchString(color) { + color, err := NormalizeColor(color) + if err != nil { return nil, ErrTemplateLoad{name, 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, &Label{ - Name: fields[1], - Color: NormalizeColor(color), - Description: description, + Name: strings.TrimSpace(name), + Color: color, + Description: strings.TrimSpace(description), }) } diff --git a/modules/label/parser_test.go b/modules/label/parser_test.go index be975c69bc66b..2d176bb3519b6 100644 --- a/modules/label/parser_test.go +++ b/modules/label/parser_test.go @@ -50,9 +50,9 @@ func TestYamlParser(t *testing.T) { } func TestDefaultParser(t *testing.T) { - data := []byte(`#ee0701 bug ; Something is not working -#cccccc duplicate ; This issue or pull request already exists -#84b6eb enhancement ; New feature`) + data := []byte(`#ee0701 bug ; Something is not working +#cccccc duplicate ; This issue or pull request already exists +#84b6eb enhancement`) labels, err := parseDefaultFormat("test", data) require.NoError(t, err) @@ -68,5 +68,5 @@ func TestDefaultParser(t *testing.T) { assert.Equal(t, "enhancement", labels[2].Name) assert.False(t, labels[2].Exclusive) assert.Equal(t, "#84b6eb", labels[2].Color) - assert.Equal(t, "New feature", labels[2].Description) + assert.Empty(t, labels[2].Description) } diff --git a/routers/api/v1/org/label.go b/routers/api/v1/org/label.go index 1d6c01f8a82b4..9a7c687b9c43d 100644 --- a/routers/api/v1/org/label.go +++ b/routers/api/v1/org/label.go @@ -4,7 +4,6 @@ package org import ( - "fmt" "net/http" "strconv" "strings" @@ -85,18 +84,17 @@ func CreateLabel(ctx *context.APIContext) { // "$ref": "#/responses/validationError" form := web.GetForm(ctx).(*api.CreateLabelOption) form.Color = strings.Trim(form.Color, " ") - if len(form.Color) == 6 || len(form.Color) == 3 { - form.Color = "#" + form.Color - } - if !label.ColorPattern.MatchString(form.Color) { - ctx.Error(http.StatusUnprocessableEntity, "ColorPattern", fmt.Errorf("bad color code: %s", form.Color)) + if color, err := label.NormalizeColor(form.Color); err != nil { + ctx.Error(http.StatusUnprocessableEntity, "Color", err) return + } else { + form.Color = color } label := &issues_model.Label{ Name: form.Name, Exclusive: form.Exclusive, - Color: label.NormalizeColor(form.Color), + Color: form.Color, OrgID: ctx.Org.Organization.ID, Description: form.Description, } @@ -201,15 +199,12 @@ func EditLabel(ctx *context.APIContext) { l.Exclusive = *form.Exclusive } if form.Color != nil { - l.Color = strings.Trim(*form.Color, " ") - if len(l.Color) == 6 || len(l.Color) == 3 { - l.Color = "#" + l.Color - } - if !label.ColorPattern.MatchString(l.Color) { - ctx.Error(http.StatusUnprocessableEntity, "ColorPattern", fmt.Errorf("bad color code: %s", l.Color)) + if color, err := label.NormalizeColor(*form.Color); err != nil { + ctx.Error(http.StatusUnprocessableEntity, "Color", err) return + } else { + l.Color = color } - l.Color = label.NormalizeColor(l.Color) } if form.Description != nil { l.Description = *form.Description diff --git a/routers/api/v1/repo/label.go b/routers/api/v1/repo/label.go index ca25f54dc6d58..0eb3ea22912fe 100644 --- a/routers/api/v1/repo/label.go +++ b/routers/api/v1/repo/label.go @@ -5,10 +5,8 @@ package repo import ( - "fmt" "net/http" "strconv" - "strings" issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/modules/context" @@ -146,19 +144,18 @@ func CreateLabel(ctx *context.APIContext) { // "$ref": "#/responses/validationError" form := web.GetForm(ctx).(*api.CreateLabelOption) - form.Color = strings.Trim(form.Color, " ") - if len(form.Color) == 6 || len(form.Color) == 3 { - form.Color = "#" + form.Color - } - if !label.ColorPattern.MatchString(form.Color) { - ctx.Error(http.StatusUnprocessableEntity, "ColorPattern", fmt.Errorf("bad color code: %s", form.Color)) + + if color, err := label.NormalizeColor(form.Color); err != nil { + ctx.Error(http.StatusUnprocessableEntity, "StringToColor", err) return + } else { + form.Color = color } l := &issues_model.Label{ Name: form.Name, Exclusive: form.Exclusive, - Color: label.NormalizeColor(form.Color), + Color: form.Color, RepoID: ctx.Repo.Repository.ID, Description: form.Description, } @@ -224,15 +221,12 @@ func EditLabel(ctx *context.APIContext) { l.Exclusive = *form.Exclusive } if form.Color != nil { - l.Color = strings.Trim(*form.Color, " ") - if len(l.Color) == 6 || len(l.Color) == 3 { - l.Color = "#" + l.Color - } - if !label.ColorPattern.MatchString(l.Color) { - ctx.Error(http.StatusUnprocessableEntity, "ColorPattern", fmt.Errorf("bad color code: %s", l.Color)) + if color, err := label.NormalizeColor(*form.Color); err != nil { + ctx.Error(http.StatusUnprocessableEntity, "StringToColor", err) return + } else { + l.Color = color } - l.Color = label.NormalizeColor(l.Color) } if form.Description != nil { l.Description = *form.Description diff --git a/services/migrations/gitea_uploader.go b/services/migrations/gitea_uploader.go index e60cdd041b337..8b259a362b1eb 100644 --- a/services/migrations/gitea_uploader.go +++ b/services/migrations/gitea_uploader.go @@ -219,10 +219,11 @@ func (g *GiteaLocalUploader) CreateMilestones(milestones ...*base.Milestone) err func (g *GiteaLocalUploader) CreateLabels(labels ...*base.Label) error { lbs := make([]*issues_model.Label, 0, len(labels)) for _, l := range labels { - // We must validate color here: - if !label.ColorPattern.MatchString("#" + l.Color) { + if color, err := label.NormalizeColor(l.Color); err != nil { log.Warn("Invalid label color: #%s for label: %s in migration to %s/%s", l.Color, l.Name, g.repoOwner, g.repoName) - l.Color = "ffffff" + l.Color = "#ffffff" + } else { + l.Color = color } lbs = append(lbs, &issues_model.Label{ @@ -230,7 +231,7 @@ func (g *GiteaLocalUploader) CreateLabels(labels ...*base.Label) error { Name: l.Name, Exclusive: l.Exclusive, Description: l.Description, - Color: "#" + l.Color, + Color: l.Color, }) } From ed56ef4c9fdd236194fcc06d6f1b508e4b0835e5 Mon Sep 17 00:00:00 2001 From: Lauris BH Date: Tue, 21 Feb 2023 12:27:33 +0200 Subject: [PATCH 5/6] Fix lint --- models/issues/label.go | 18 +++++++++--------- modules/label/parser.go | 6 +++--- routers/api/v1/org/label.go | 12 ++++++------ routers/api/v1/repo/label.go | 12 ++++++------ 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/models/issues/label.go b/models/issues/label.go index 5367018452bd6..c0419cf881dac 100644 --- a/models/issues/label.go +++ b/models/issues/label.go @@ -200,11 +200,11 @@ func (l *Label) ExclusiveScope() string { // NewLabel creates a new label func NewLabel(ctx context.Context, l *Label) error { - if color, err := label.NormalizeColor(l.Color); err != nil { + color, err := label.NormalizeColor(l.Color) + if err != nil { return err - } else { - l.Color = color } + l.Color = color return db.Insert(ctx, l) } @@ -218,11 +218,11 @@ func NewLabels(labels ...*Label) error { defer committer.Close() for _, l := range labels { - if color, err := label.NormalizeColor(l.Color); err != nil { + color, err := label.NormalizeColor(l.Color) + if err != nil { return err - } else { - l.Color = color } + l.Color = color if err := db.Insert(ctx, l); err != nil { return err @@ -233,11 +233,11 @@ func NewLabels(labels ...*Label) error { // UpdateLabel updates label information. func UpdateLabel(l *Label) error { - if color, err := label.NormalizeColor(l.Color); err != nil { + color, err := label.NormalizeColor(l.Color) + if err != nil { return err - } else { - l.Color = color } + l.Color = color return updateLabelCols(db.DefaultContext, l, "name", "description", "color", "exclusive") } diff --git a/modules/label/parser.go b/modules/label/parser.go index dcd9542893194..1769fb6fd26f2 100644 --- a/modules/label/parser.go +++ b/modules/label/parser.go @@ -67,11 +67,11 @@ func parseYamlFormat(name string, data []byte) ([]*Label, error) { if len(l.Name) == 0 || len(l.Color) == 0 { return nil, ErrTemplateLoad{name, errors.New("label name and color are required fields")} } - if color, err := NormalizeColor(l.Color); err != nil { + color, err := NormalizeColor(l.Color) + if err != nil { return nil, ErrTemplateLoad{name, fmt.Errorf("bad HTML color code in label: %s", l.Name)} - } else { - l.Color = color } + l.Color = color } return lf.Labels, nil diff --git a/routers/api/v1/org/label.go b/routers/api/v1/org/label.go index 9a7c687b9c43d..183c1e6cc8a8c 100644 --- a/routers/api/v1/org/label.go +++ b/routers/api/v1/org/label.go @@ -84,12 +84,12 @@ func CreateLabel(ctx *context.APIContext) { // "$ref": "#/responses/validationError" form := web.GetForm(ctx).(*api.CreateLabelOption) form.Color = strings.Trim(form.Color, " ") - if color, err := label.NormalizeColor(form.Color); err != nil { + color, err := label.NormalizeColor(form.Color) + if err != nil { ctx.Error(http.StatusUnprocessableEntity, "Color", err) return - } else { - form.Color = color } + form.Color = color label := &issues_model.Label{ Name: form.Name, @@ -199,12 +199,12 @@ func EditLabel(ctx *context.APIContext) { l.Exclusive = *form.Exclusive } if form.Color != nil { - if color, err := label.NormalizeColor(*form.Color); err != nil { + color, err := label.NormalizeColor(*form.Color) + if err != nil { ctx.Error(http.StatusUnprocessableEntity, "Color", err) return - } else { - l.Color = color } + l.Color = color } if form.Description != nil { l.Description = *form.Description diff --git a/routers/api/v1/repo/label.go b/routers/api/v1/repo/label.go index 0eb3ea22912fe..6cb231f596c8b 100644 --- a/routers/api/v1/repo/label.go +++ b/routers/api/v1/repo/label.go @@ -145,12 +145,12 @@ func CreateLabel(ctx *context.APIContext) { form := web.GetForm(ctx).(*api.CreateLabelOption) - if color, err := label.NormalizeColor(form.Color); err != nil { + color, err := label.NormalizeColor(form.Color) + if err != nil { ctx.Error(http.StatusUnprocessableEntity, "StringToColor", err) return - } else { - form.Color = color } + form.Color = color l := &issues_model.Label{ Name: form.Name, @@ -221,12 +221,12 @@ func EditLabel(ctx *context.APIContext) { l.Exclusive = *form.Exclusive } if form.Color != nil { - if color, err := label.NormalizeColor(*form.Color); err != nil { + color, err := label.NormalizeColor(*form.Color) + if err != nil { ctx.Error(http.StatusUnprocessableEntity, "StringToColor", err) return - } else { - l.Color = color } + l.Color = color } if form.Description != nil { l.Description = *form.Description From 9d79146d4235578a9913a5b8203b5cf2cee64d1c Mon Sep 17 00:00:00 2001 From: Lauris BH Date: Thu, 2 Mar 2023 01:09:58 +0200 Subject: [PATCH 6/6] Small fixes --- modules/label/label.go | 2 +- modules/label/parser.go | 8 ++++---- modules/label/parser_test.go | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/modules/label/label.go b/modules/label/label.go index 994bee3360682..d3ef0e1dc967a 100644 --- a/modules/label/label.go +++ b/modules/label/label.go @@ -9,7 +9,7 @@ import ( "strings" ) -// colorPattern is a regexp witch can validate label color +// colorPattern is a regexp which can validate label color var colorPattern = regexp.MustCompile("^#?(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{3})$") // Label represents label information loaded from template diff --git a/modules/label/parser.go b/modules/label/parser.go index 1769fb6fd26f2..768c72a61b014 100644 --- a/modules/label/parser.go +++ b/modules/label/parser.go @@ -51,7 +51,7 @@ func GetTemplateFile(name string) ([]*Label, error) { return nil, ErrTemplateLoad{name, fmt.Errorf("GetRepoInitFile: %w", err)} } - return parseDefaultFormat(name, data) + return parseLegacyFormat(name, data) } func parseYamlFormat(name string, data []byte) ([]*Label, error) { @@ -69,7 +69,7 @@ func parseYamlFormat(name string, data []byte) ([]*Label, error) { } color, err := NormalizeColor(l.Color) if err != nil { - return nil, ErrTemplateLoad{name, fmt.Errorf("bad HTML color code in label: %s", l.Name)} + return nil, ErrTemplateLoad{name, fmt.Errorf("bad HTML color code '%s' in label: %s", l.Color, l.Name)} } l.Color = color } @@ -77,7 +77,7 @@ func parseYamlFormat(name string, data []byte) ([]*Label, error) { return lf.Labels, nil } -func parseDefaultFormat(name string, data []byte) ([]*Label, error) { +func parseLegacyFormat(name string, data []byte) ([]*Label, error) { lines := strings.Split(string(data), "\n") list := make([]*Label, 0, len(lines)) for i := 0; i < len(lines); i++ { @@ -95,7 +95,7 @@ func parseDefaultFormat(name string, data []byte) ([]*Label, error) { color, err := NormalizeColor(color) if err != nil { - return nil, ErrTemplateLoad{name, fmt.Errorf("bad HTML color code in line: %s", line)} + return nil, ErrTemplateLoad{name, fmt.Errorf("bad HTML color code '%s' in line: %s", color, line)} } list = append(list, &Label{ diff --git a/modules/label/parser_test.go b/modules/label/parser_test.go index 2d176bb3519b6..5c8042f66863c 100644 --- a/modules/label/parser_test.go +++ b/modules/label/parser_test.go @@ -49,12 +49,12 @@ func TestYamlParser(t *testing.T) { assert.Equal(t, "Bug", labels[3].Description) } -func TestDefaultParser(t *testing.T) { +func TestLegacyParser(t *testing.T) { data := []byte(`#ee0701 bug ; Something is not working #cccccc duplicate ; This issue or pull request already exists #84b6eb enhancement`) - labels, err := parseDefaultFormat("test", data) + labels, err := parseLegacyFormat("test", data) require.NoError(t, err) require.Len(t, labels, 3) assert.Equal(t, "bug", labels[0].Name)