loading up the forgejo repo on tangled to test page performance
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add loading yaml label template files (#22976)

Extract from #11669 and enhancement to #22585 to support exclusive
scoped labels in label templates

* Move label template functionality to label module
* Fix handling of color codes
* Add Advanced label template

authored by

Lauris BH and committed by
GitHub
58b41438 de6c718b

+488 -241
+52 -63
models/issues/label.go
··· 7 7 import ( 8 8 "context" 9 9 "fmt" 10 - "regexp" 11 10 "strconv" 12 11 "strings" 13 12 14 13 "code.gitea.io/gitea/models/db" 15 14 user_model "code.gitea.io/gitea/models/user" 15 + "code.gitea.io/gitea/modules/label" 16 16 "code.gitea.io/gitea/modules/timeutil" 17 17 "code.gitea.io/gitea/modules/util" 18 18 ··· 78 78 return util.ErrNotExist 79 79 } 80 80 81 - // LabelColorPattern is a regexp witch can validate LabelColor 82 - var LabelColorPattern = regexp.MustCompile("^#?(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{3})$") 83 - 84 81 // Label represents a label of repository for issues. 85 82 type Label struct { 86 83 ID int64 `xorm:"pk autoincr"` ··· 109 106 } 110 107 111 108 // CalOpenIssues sets the number of open issues of a label based on the already stored number of closed issues. 112 - func (label *Label) CalOpenIssues() { 113 - label.NumOpenIssues = label.NumIssues - label.NumClosedIssues 109 + func (l *Label) CalOpenIssues() { 110 + l.NumOpenIssues = l.NumIssues - l.NumClosedIssues 114 111 } 115 112 116 113 // CalOpenOrgIssues calculates the open issues of a label for a specific repo 117 - func (label *Label) CalOpenOrgIssues(ctx context.Context, repoID, labelID int64) { 114 + func (l *Label) CalOpenOrgIssues(ctx context.Context, repoID, labelID int64) { 118 115 counts, _ := CountIssuesByRepo(ctx, &IssuesOptions{ 119 116 RepoID: repoID, 120 117 LabelIDs: []int64{labelID}, ··· 122 119 }) 123 120 124 121 for _, count := range counts { 125 - label.NumOpenRepoIssues += count 122 + l.NumOpenRepoIssues += count 126 123 } 127 124 } 128 125 129 126 // LoadSelectedLabelsAfterClick calculates the set of selected labels when a label is clicked 130 - func (label *Label) LoadSelectedLabelsAfterClick(currentSelectedLabels []int64, currentSelectedExclusiveScopes []string) { 127 + func (l *Label) LoadSelectedLabelsAfterClick(currentSelectedLabels []int64, currentSelectedExclusiveScopes []string) { 131 128 var labelQuerySlice []string 132 129 labelSelected := false 133 - labelID := strconv.FormatInt(label.ID, 10) 134 - labelScope := label.ExclusiveScope() 130 + labelID := strconv.FormatInt(l.ID, 10) 131 + labelScope := l.ExclusiveScope() 135 132 for i, s := range currentSelectedLabels { 136 - if s == label.ID { 133 + if s == l.ID { 137 134 labelSelected = true 138 - } else if -s == label.ID { 135 + } else if -s == l.ID { 139 136 labelSelected = true 140 - label.IsExcluded = true 137 + l.IsExcluded = true 141 138 } else if s != 0 { 142 139 // Exclude other labels in the same scope from selection 143 140 if s < 0 || labelScope == "" || labelScope != currentSelectedExclusiveScopes[i] { ··· 148 145 if !labelSelected { 149 146 labelQuerySlice = append(labelQuerySlice, labelID) 150 147 } 151 - label.IsSelected = labelSelected 152 - label.QueryString = strings.Join(labelQuerySlice, ",") 148 + l.IsSelected = labelSelected 149 + l.QueryString = strings.Join(labelQuerySlice, ",") 153 150 } 154 151 155 152 // BelongsToOrg returns true if label is an organization label 156 - func (label *Label) BelongsToOrg() bool { 157 - return label.OrgID > 0 153 + func (l *Label) BelongsToOrg() bool { 154 + return l.OrgID > 0 158 155 } 159 156 160 157 // BelongsToRepo returns true if label is a repository label 161 - func (label *Label) BelongsToRepo() bool { 162 - return label.RepoID > 0 158 + func (l *Label) BelongsToRepo() bool { 159 + return l.RepoID > 0 163 160 } 164 161 165 162 // Get color as RGB values in 0..255 range 166 - func (label *Label) ColorRGB() (float64, float64, float64, error) { 167 - color, err := strconv.ParseUint(label.Color[1:], 16, 64) 163 + func (l *Label) ColorRGB() (float64, float64, float64, error) { 164 + color, err := strconv.ParseUint(l.Color[1:], 16, 64) 168 165 if err != nil { 169 166 return 0, 0, 0, err 170 167 } ··· 176 173 } 177 174 178 175 // Determine if label text should be light or dark to be readable on background color 179 - func (label *Label) UseLightTextColor() bool { 180 - if strings.HasPrefix(label.Color, "#") { 181 - if r, g, b, err := label.ColorRGB(); err == nil { 176 + func (l *Label) UseLightTextColor() bool { 177 + if strings.HasPrefix(l.Color, "#") { 178 + if r, g, b, err := l.ColorRGB(); err == nil { 182 179 // Perceived brightness from: https://www.w3.org/TR/AERT/#color-contrast 183 180 // In the future WCAG 3 APCA may be a better solution 184 181 brightness := (0.299*r + 0.587*g + 0.114*b) / 255 ··· 190 187 } 191 188 192 189 // Return scope substring of label name, or empty string if none exists 193 - func (label *Label) ExclusiveScope() string { 194 - if !label.Exclusive { 190 + func (l *Label) ExclusiveScope() string { 191 + if !l.Exclusive { 195 192 return "" 196 193 } 197 - lastIndex := strings.LastIndex(label.Name, "/") 198 - if lastIndex == -1 || lastIndex == 0 || lastIndex == len(label.Name)-1 { 194 + lastIndex := strings.LastIndex(l.Name, "/") 195 + if lastIndex == -1 || lastIndex == 0 || lastIndex == len(l.Name)-1 { 199 196 return "" 200 197 } 201 - return label.Name[:lastIndex] 198 + return l.Name[:lastIndex] 202 199 } 203 200 204 201 // NewLabel creates a new label 205 - func NewLabel(ctx context.Context, label *Label) error { 206 - if !LabelColorPattern.MatchString(label.Color) { 207 - return fmt.Errorf("bad color code: %s", label.Color) 202 + func NewLabel(ctx context.Context, l *Label) error { 203 + color, err := label.NormalizeColor(l.Color) 204 + if err != nil { 205 + return err 208 206 } 207 + l.Color = color 209 208 210 - // normalize case 211 - label.Color = strings.ToLower(label.Color) 212 - 213 - // add leading hash 214 - if label.Color[0] != '#' { 215 - label.Color = "#" + label.Color 216 - } 217 - 218 - // convert 3-character shorthand into 6-character version 219 - if len(label.Color) == 4 { 220 - r := label.Color[1] 221 - g := label.Color[2] 222 - b := label.Color[3] 223 - label.Color = fmt.Sprintf("#%c%c%c%c%c%c", r, r, g, g, b, b) 224 - } 225 - 226 - return db.Insert(ctx, label) 209 + return db.Insert(ctx, l) 227 210 } 228 211 229 212 // NewLabels creates new labels ··· 234 217 } 235 218 defer committer.Close() 236 219 237 - for _, label := range labels { 238 - if !LabelColorPattern.MatchString(label.Color) { 239 - return fmt.Errorf("bad color code: %s", label.Color) 220 + for _, l := range labels { 221 + color, err := label.NormalizeColor(l.Color) 222 + if err != nil { 223 + return err 240 224 } 241 - if err := db.Insert(ctx, label); err != nil { 225 + l.Color = color 226 + 227 + if err := db.Insert(ctx, l); err != nil { 242 228 return err 243 229 } 244 230 } ··· 247 233 248 234 // UpdateLabel updates label information. 249 235 func UpdateLabel(l *Label) error { 250 - if !LabelColorPattern.MatchString(l.Color) { 251 - return fmt.Errorf("bad color code: %s", l.Color) 236 + color, err := label.NormalizeColor(l.Color) 237 + if err != nil { 238 + return err 252 239 } 240 + l.Color = color 241 + 253 242 return updateLabelCols(db.DefaultContext, l, "name", "description", "color", "exclusive") 254 243 } 255 244 256 245 // DeleteLabel delete a label 257 246 func DeleteLabel(id, labelID int64) error { 258 - label, err := GetLabelByID(db.DefaultContext, labelID) 247 + l, err := GetLabelByID(db.DefaultContext, labelID) 259 248 if err != nil { 260 249 if IsErrLabelNotExist(err) { 261 250 return nil ··· 271 260 272 261 sess := db.GetEngine(ctx) 273 262 274 - if label.BelongsToOrg() && label.OrgID != id { 263 + if l.BelongsToOrg() && l.OrgID != id { 275 264 return nil 276 265 } 277 - if label.BelongsToRepo() && label.RepoID != id { 266 + if l.BelongsToRepo() && l.RepoID != id { 278 267 return nil 279 268 } 280 269 ··· 682 671 if err = issue.LoadRepo(ctx); err != nil { 683 672 return err 684 673 } 685 - for _, label := range labels { 674 + for _, l := range labels { 686 675 // Don't add already present labels and invalid labels 687 - if HasIssueLabel(ctx, issue.ID, label.ID) || 688 - (label.RepoID != issue.RepoID && label.OrgID != issue.Repo.OwnerID) { 676 + if HasIssueLabel(ctx, issue.ID, l.ID) || 677 + (l.RepoID != issue.RepoID && l.OrgID != issue.Repo.OwnerID) { 689 678 continue 690 679 } 691 680 692 - if err = newIssueLabel(ctx, issue, label, doer); err != nil { 681 + if err = newIssueLabel(ctx, issue, l, doer); err != nil { 693 682 return fmt.Errorf("newIssueLabel: %w", err) 694 683 } 695 684 }
-2
models/issues/label_test.go
··· 15 15 "github.com/stretchr/testify/assert" 16 16 ) 17 17 18 - // TODO TestGetLabelTemplateFile 19 - 20 18 func TestLabel_CalOpenIssues(t *testing.T) { 21 19 assert.NoError(t, unittest.PrepareTestDatabase()) 22 20 label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1})
+46
modules/label/label.go
··· 1 + // Copyright 2023 The Gitea Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package label 5 + 6 + import ( 7 + "fmt" 8 + "regexp" 9 + "strings" 10 + ) 11 + 12 + // colorPattern is a regexp which can validate label color 13 + var colorPattern = regexp.MustCompile("^#?(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{3})$") 14 + 15 + // Label represents label information loaded from template 16 + type Label struct { 17 + Name string `yaml:"name"` 18 + Color string `yaml:"color"` 19 + Description string `yaml:"description,omitempty"` 20 + Exclusive bool `yaml:"exclusive,omitempty"` 21 + } 22 + 23 + // NormalizeColor normalizes a color string to a 6-character hex code 24 + func NormalizeColor(color string) (string, error) { 25 + // normalize case 26 + color = strings.TrimSpace(strings.ToLower(color)) 27 + 28 + // add leading hash 29 + if len(color) == 6 || len(color) == 3 { 30 + color = "#" + color 31 + } 32 + 33 + if !colorPattern.MatchString(color) { 34 + return "", fmt.Errorf("bad color code: %s", color) 35 + } 36 + 37 + // convert 3-character shorthand into 6-character version 38 + if len(color) == 4 { 39 + r := color[1] 40 + g := color[2] 41 + b := color[3] 42 + color = fmt.Sprintf("#%c%c%c%c%c%c", r, r, g, g, b, b) 43 + } 44 + 45 + return color, nil 46 + }
+126
modules/label/parser.go
··· 1 + // Copyright 2023 The Gitea Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package label 5 + 6 + import ( 7 + "errors" 8 + "fmt" 9 + "strings" 10 + 11 + "code.gitea.io/gitea/modules/options" 12 + 13 + "gopkg.in/yaml.v3" 14 + ) 15 + 16 + type labelFile struct { 17 + Labels []*Label `yaml:"labels"` 18 + } 19 + 20 + // ErrTemplateLoad represents a "ErrTemplateLoad" kind of error. 21 + type ErrTemplateLoad struct { 22 + TemplateFile string 23 + OriginalError error 24 + } 25 + 26 + // IsErrTemplateLoad checks if an error is a ErrTemplateLoad. 27 + func IsErrTemplateLoad(err error) bool { 28 + _, ok := err.(ErrTemplateLoad) 29 + return ok 30 + } 31 + 32 + func (err ErrTemplateLoad) Error() string { 33 + return fmt.Sprintf("Failed to load label template file '%s': %v", err.TemplateFile, err.OriginalError) 34 + } 35 + 36 + // GetTemplateFile loads the label template file by given name, 37 + // then parses and returns a list of name-color pairs and optionally description. 38 + func GetTemplateFile(name string) ([]*Label, error) { 39 + data, err := options.GetRepoInitFile("label", name+".yaml") 40 + if err == nil && len(data) > 0 { 41 + return parseYamlFormat(name+".yaml", data) 42 + } 43 + 44 + data, err = options.GetRepoInitFile("label", name+".yml") 45 + if err == nil && len(data) > 0 { 46 + return parseYamlFormat(name+".yml", data) 47 + } 48 + 49 + data, err = options.GetRepoInitFile("label", name) 50 + if err != nil { 51 + return nil, ErrTemplateLoad{name, fmt.Errorf("GetRepoInitFile: %w", err)} 52 + } 53 + 54 + return parseLegacyFormat(name, data) 55 + } 56 + 57 + func parseYamlFormat(name string, data []byte) ([]*Label, error) { 58 + lf := &labelFile{} 59 + 60 + if err := yaml.Unmarshal(data, lf); err != nil { 61 + return nil, err 62 + } 63 + 64 + // Validate label data and fix colors 65 + for _, l := range lf.Labels { 66 + l.Color = strings.TrimSpace(l.Color) 67 + if len(l.Name) == 0 || len(l.Color) == 0 { 68 + return nil, ErrTemplateLoad{name, errors.New("label name and color are required fields")} 69 + } 70 + color, err := NormalizeColor(l.Color) 71 + if err != nil { 72 + return nil, ErrTemplateLoad{name, fmt.Errorf("bad HTML color code '%s' in label: %s", l.Color, l.Name)} 73 + } 74 + l.Color = color 75 + } 76 + 77 + return lf.Labels, nil 78 + } 79 + 80 + func parseLegacyFormat(name string, data []byte) ([]*Label, error) { 81 + lines := strings.Split(string(data), "\n") 82 + list := make([]*Label, 0, len(lines)) 83 + for i := 0; i < len(lines); i++ { 84 + line := strings.TrimSpace(lines[i]) 85 + if len(line) == 0 { 86 + continue 87 + } 88 + 89 + parts, description, _ := strings.Cut(line, ";") 90 + 91 + color, name, ok := strings.Cut(parts, " ") 92 + if !ok { 93 + return nil, ErrTemplateLoad{name, fmt.Errorf("line is malformed: %s", line)} 94 + } 95 + 96 + color, err := NormalizeColor(color) 97 + if err != nil { 98 + return nil, ErrTemplateLoad{name, fmt.Errorf("bad HTML color code '%s' in line: %s", color, line)} 99 + } 100 + 101 + list = append(list, &Label{ 102 + Name: strings.TrimSpace(name), 103 + Color: color, 104 + Description: strings.TrimSpace(description), 105 + }) 106 + } 107 + 108 + return list, nil 109 + } 110 + 111 + // LoadFormatted loads the labels' list of a template file as a string separated by comma 112 + func LoadFormatted(name string) (string, error) { 113 + var buf strings.Builder 114 + list, err := GetTemplateFile(name) 115 + if err != nil { 116 + return "", err 117 + } 118 + 119 + for i := 0; i < len(list); i++ { 120 + if i > 0 { 121 + buf.WriteString(", ") 122 + } 123 + buf.WriteString(list[i].Name) 124 + } 125 + return buf.String(), nil 126 + }
+72
modules/label/parser_test.go
··· 1 + // Copyright 2023 The Gitea Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package label 5 + 6 + import ( 7 + "testing" 8 + 9 + "github.com/stretchr/testify/assert" 10 + "github.com/stretchr/testify/require" 11 + ) 12 + 13 + func TestYamlParser(t *testing.T) { 14 + data := []byte(`labels: 15 + - name: priority/low 16 + exclusive: true 17 + color: "#0000ee" 18 + description: "Low priority" 19 + - name: priority/medium 20 + exclusive: true 21 + color: "0e0" 22 + description: "Medium priority" 23 + - name: priority/high 24 + exclusive: true 25 + color: "#ee0000" 26 + description: "High priority" 27 + - name: type/bug 28 + color: "#f00" 29 + description: "Bug"`) 30 + 31 + labels, err := parseYamlFormat("test", data) 32 + require.NoError(t, err) 33 + require.Len(t, labels, 4) 34 + assert.Equal(t, "priority/low", labels[0].Name) 35 + assert.True(t, labels[0].Exclusive) 36 + assert.Equal(t, "#0000ee", labels[0].Color) 37 + assert.Equal(t, "Low priority", labels[0].Description) 38 + assert.Equal(t, "priority/medium", labels[1].Name) 39 + assert.True(t, labels[1].Exclusive) 40 + assert.Equal(t, "#00ee00", labels[1].Color) 41 + assert.Equal(t, "Medium priority", labels[1].Description) 42 + assert.Equal(t, "priority/high", labels[2].Name) 43 + assert.True(t, labels[2].Exclusive) 44 + assert.Equal(t, "#ee0000", labels[2].Color) 45 + assert.Equal(t, "High priority", labels[2].Description) 46 + assert.Equal(t, "type/bug", labels[3].Name) 47 + assert.False(t, labels[3].Exclusive) 48 + assert.Equal(t, "#ff0000", labels[3].Color) 49 + assert.Equal(t, "Bug", labels[3].Description) 50 + } 51 + 52 + func TestLegacyParser(t *testing.T) { 53 + data := []byte(`#ee0701 bug ; Something is not working 54 + #cccccc duplicate ; This issue or pull request already exists 55 + #84b6eb enhancement`) 56 + 57 + labels, err := parseLegacyFormat("test", data) 58 + require.NoError(t, err) 59 + require.Len(t, labels, 3) 60 + assert.Equal(t, "bug", labels[0].Name) 61 + assert.False(t, labels[0].Exclusive) 62 + assert.Equal(t, "#ee0701", labels[0].Color) 63 + assert.Equal(t, "Something is not working", labels[0].Description) 64 + assert.Equal(t, "duplicate", labels[1].Name) 65 + assert.False(t, labels[1].Exclusive) 66 + assert.Equal(t, "#cccccc", labels[1].Color) 67 + assert.Equal(t, "This issue or pull request already exists", labels[1].Description) 68 + assert.Equal(t, "enhancement", labels[2].Name) 69 + assert.False(t, labels[2].Exclusive) 70 + assert.Equal(t, "#84b6eb", labels[2].Color) 71 + assert.Empty(t, labels[2].Description) 72 + }
+44
modules/options/repo.go
··· 1 + // Copyright 2023 The Gitea Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package options 5 + 6 + import ( 7 + "fmt" 8 + "os" 9 + "path" 10 + "strings" 11 + 12 + "code.gitea.io/gitea/modules/log" 13 + "code.gitea.io/gitea/modules/setting" 14 + "code.gitea.io/gitea/modules/util" 15 + ) 16 + 17 + // GetRepoInitFile returns repository init files 18 + func GetRepoInitFile(tp, name string) ([]byte, error) { 19 + cleanedName := strings.TrimLeft(path.Clean("/"+name), "/") 20 + relPath := path.Join("options", tp, cleanedName) 21 + 22 + // Use custom file when available. 23 + customPath := path.Join(setting.CustomPath, relPath) 24 + isFile, err := util.IsFile(customPath) 25 + if err != nil { 26 + log.Error("Unable to check if %s is a file. Error: %v", customPath, err) 27 + } 28 + if isFile { 29 + return os.ReadFile(customPath) 30 + } 31 + 32 + switch tp { 33 + case "readme": 34 + return Readme(cleanedName) 35 + case "gitignore": 36 + return Gitignore(cleanedName) 37 + case "license": 38 + return License(cleanedName) 39 + case "label": 40 + return Labels(cleanedName) 41 + default: 42 + return []byte{}, fmt.Errorf("Invalid init file type") 43 + } 44 + }
+2 -1
modules/repository/create.go
··· 23 23 user_model "code.gitea.io/gitea/models/user" 24 24 "code.gitea.io/gitea/models/webhook" 25 25 "code.gitea.io/gitea/modules/git" 26 + "code.gitea.io/gitea/modules/label" 26 27 "code.gitea.io/gitea/modules/log" 27 28 "code.gitea.io/gitea/modules/setting" 28 29 api "code.gitea.io/gitea/modules/structs" ··· 189 190 190 191 // Check if label template exist 191 192 if len(opts.IssueLabels) > 0 { 192 - if _, err := GetLabelTemplateFile(opts.IssueLabels); err != nil { 193 + if _, err := label.GetTemplateFile(opts.IssueLabels); err != nil { 193 194 return nil, err 194 195 } 195 196 }
+18 -116
modules/repository/init.go
··· 18 18 repo_model "code.gitea.io/gitea/models/repo" 19 19 user_model "code.gitea.io/gitea/models/user" 20 20 "code.gitea.io/gitea/modules/git" 21 + "code.gitea.io/gitea/modules/label" 21 22 "code.gitea.io/gitea/modules/log" 22 23 "code.gitea.io/gitea/modules/options" 23 24 "code.gitea.io/gitea/modules/setting" ··· 40 41 LabelTemplates map[string]string 41 42 ) 42 43 43 - // ErrIssueLabelTemplateLoad represents a "ErrIssueLabelTemplateLoad" kind of error. 44 - type ErrIssueLabelTemplateLoad struct { 45 - TemplateFile string 46 - OriginalError error 47 - } 48 - 49 - // IsErrIssueLabelTemplateLoad checks if an error is a ErrIssueLabelTemplateLoad. 50 - func IsErrIssueLabelTemplateLoad(err error) bool { 51 - _, ok := err.(ErrIssueLabelTemplateLoad) 52 - return ok 53 - } 54 - 55 - func (err ErrIssueLabelTemplateLoad) Error() string { 56 - return fmt.Sprintf("Failed to load label template file '%s': %v", err.TemplateFile, err.OriginalError) 57 - } 58 - 59 - // GetRepoInitFile returns repository init files 60 - func GetRepoInitFile(tp, name string) ([]byte, error) { 61 - cleanedName := strings.TrimLeft(path.Clean("/"+name), "/") 62 - relPath := path.Join("options", tp, cleanedName) 63 - 64 - // Use custom file when available. 65 - customPath := path.Join(setting.CustomPath, relPath) 66 - isFile, err := util.IsFile(customPath) 67 - if err != nil { 68 - log.Error("Unable to check if %s is a file. Error: %v", customPath, err) 69 - } 70 - if isFile { 71 - return os.ReadFile(customPath) 72 - } 73 - 74 - switch tp { 75 - case "readme": 76 - return options.Readme(cleanedName) 77 - case "gitignore": 78 - return options.Gitignore(cleanedName) 79 - case "license": 80 - return options.License(cleanedName) 81 - case "label": 82 - return options.Labels(cleanedName) 83 - default: 84 - return []byte{}, fmt.Errorf("Invalid init file type") 85 - } 86 - } 87 - 88 - // GetLabelTemplateFile loads the label template file by given name, 89 - // then parses and returns a list of name-color pairs and optionally description. 90 - func GetLabelTemplateFile(name string) ([][3]string, error) { 91 - data, err := GetRepoInitFile("label", name) 92 - if err != nil { 93 - return nil, ErrIssueLabelTemplateLoad{name, fmt.Errorf("GetRepoInitFile: %w", err)} 94 - } 95 - 96 - lines := strings.Split(string(data), "\n") 97 - list := make([][3]string, 0, len(lines)) 98 - for i := 0; i < len(lines); i++ { 99 - line := strings.TrimSpace(lines[i]) 100 - if len(line) == 0 { 101 - continue 102 - } 103 - 104 - parts := strings.SplitN(line, ";", 2) 105 - 106 - fields := strings.SplitN(parts[0], " ", 2) 107 - if len(fields) != 2 { 108 - return nil, ErrIssueLabelTemplateLoad{name, fmt.Errorf("line is malformed: %s", line)} 109 - } 110 - 111 - color := strings.Trim(fields[0], " ") 112 - if len(color) == 6 { 113 - color = "#" + color 114 - } 115 - if !issues_model.LabelColorPattern.MatchString(color) { 116 - return nil, ErrIssueLabelTemplateLoad{name, fmt.Errorf("bad HTML color code in line: %s", line)} 117 - } 118 - 119 - var description string 120 - 121 - if len(parts) > 1 { 122 - description = strings.TrimSpace(parts[1]) 123 - } 124 - 125 - fields[1] = strings.TrimSpace(fields[1]) 126 - list = append(list, [3]string{fields[1], color, description}) 127 - } 128 - 129 - return list, nil 130 - } 131 - 132 - func loadLabels(labelTemplate string) ([]string, error) { 133 - list, err := GetLabelTemplateFile(labelTemplate) 134 - if err != nil { 135 - return nil, err 136 - } 137 - 138 - labels := make([]string, len(list)) 139 - for i := 0; i < len(list); i++ { 140 - labels[i] = list[i][0] 141 - } 142 - return labels, nil 143 - } 144 - 145 - // LoadLabelsFormatted loads the labels' list of a template file as a string separated by comma 146 - func LoadLabelsFormatted(labelTemplate string) (string, error) { 147 - labels, err := loadLabels(labelTemplate) 148 - return strings.Join(labels, ", "), err 149 - } 150 - 151 44 // LoadRepoConfig loads the repository config 152 45 func LoadRepoConfig() { 153 46 // Load .gitignore and license files and readme templates. ··· 158 51 if err != nil { 159 52 log.Fatal("Failed to get %s files: %v", t, err) 160 53 } 54 + if t == "label" { 55 + for i, f := range files { 56 + ext := strings.ToLower(filepath.Ext(f)) 57 + if ext == ".yaml" || ext == ".yml" { 58 + files[i] = f[:len(f)-len(ext)] 59 + } 60 + } 61 + } 161 62 customPath := path.Join(setting.CustomPath, "options", t) 162 63 isDir, err := util.IsDir(customPath) 163 64 if err != nil { ··· 190 91 // Load label templates 191 92 LabelTemplates = make(map[string]string) 192 93 for _, templateFile := range LabelTemplatesFiles { 193 - labels, err := LoadLabelsFormatted(templateFile) 94 + labels, err := label.LoadFormatted(templateFile) 194 95 if err != nil { 195 96 log.Error("Failed to load labels: %v", err) 196 97 } ··· 235 136 } 236 137 237 138 // README 238 - data, err := GetRepoInitFile("readme", opts.Readme) 139 + data, err := options.GetRepoInitFile("readme", opts.Readme) 239 140 if err != nil { 240 141 return fmt.Errorf("GetRepoInitFile[%s]: %w", opts.Readme, err) 241 142 } ··· 263 164 var buf bytes.Buffer 264 165 names := strings.Split(opts.Gitignores, ",") 265 166 for _, name := range names { 266 - data, err = GetRepoInitFile("gitignore", name) 167 + data, err = options.GetRepoInitFile("gitignore", name) 267 168 if err != nil { 268 169 return fmt.Errorf("GetRepoInitFile[%s]: %w", name, err) 269 170 } ··· 281 182 282 183 // LICENSE 283 184 if len(opts.License) > 0 { 284 - data, err = GetRepoInitFile("license", opts.License) 185 + data, err = options.GetRepoInitFile("license", opts.License) 285 186 if err != nil { 286 187 return fmt.Errorf("GetRepoInitFile[%s]: %w", opts.License, err) 287 188 } ··· 443 344 444 345 // InitializeLabels adds a label set to a repository using a template 445 346 func InitializeLabels(ctx context.Context, id int64, labelTemplate string, isOrg bool) error { 446 - list, err := GetLabelTemplateFile(labelTemplate) 347 + list, err := label.GetTemplateFile(labelTemplate) 447 348 if err != nil { 448 349 return err 449 350 } ··· 451 352 labels := make([]*issues_model.Label, len(list)) 452 353 for i := 0; i < len(list); i++ { 453 354 labels[i] = &issues_model.Label{ 454 - Name: list[i][0], 455 - Description: list[i][2], 456 - Color: list[i][1], 355 + Name: list[i].Name, 356 + Exclusive: list[i].Exclusive, 357 + Description: list[i].Description, 358 + Color: list[i].Color, 457 359 } 458 360 if isOrg { 459 361 labels[i].OrgID = id
+70
options/label/Advanced.yaml
··· 1 + labels: 2 + - name: "Kind/Bug" 3 + color: ee0701 4 + description: Something is not working 5 + - name: "Kind/Feature" 6 + color: 0288d1 7 + description: New functionality 8 + - name: "Kind/Enhancement" 9 + color: 84b6eb 10 + description: Improve existing functionality 11 + - name: "Kind/Security" 12 + color: 9c27b0 13 + description: This is security issue 14 + - name: "Kind/Testing" 15 + color: 795548 16 + description: Issue or pull request related to testing 17 + - name: "Kind/Breaking" 18 + color: c62828 19 + description: Breaking change that won't be backward compatible 20 + - name: "Kind/Documentation" 21 + color: 37474f 22 + description: Documentation changes 23 + - name: "Reviewed/Duplicate" 24 + exclusive: true 25 + color: 616161 26 + description: This issue or pull request already exists 27 + - name: "Reviewed/Invalid" 28 + exclusive: true 29 + color: 546e7a 30 + description: Invalid issue 31 + - name: "Reviewed/Confirmed" 32 + exclusive: true 33 + color: 795548 34 + description: Issue has been confirmed 35 + - name: "Reviewed/Won't Fix" 36 + exclusive: true 37 + color: eeeeee 38 + description: This issue won't be fixed 39 + - name: "Status/Need More Info" 40 + exclusive: true 41 + color: 424242 42 + description: Feedback is required to reproduce issue or to continue work 43 + - name: "Status/Blocked" 44 + exclusive: true 45 + color: 880e4f 46 + description: Something is blocking this issue or pull request 47 + - name: "Status/Abandoned" 48 + exclusive: true 49 + color: "222222" 50 + description: Somebody has started to work on this but abandoned work 51 + - name: "Priority/Critical" 52 + exclusive: true 53 + color: b71c1c 54 + description: The priority is critical 55 + priority: critical 56 + - name: "Priority/High" 57 + exclusive: true 58 + color: d32f2f 59 + description: The priority is high 60 + priority: high 61 + - name: "Priority/Medium" 62 + exclusive: true 63 + color: e64a19 64 + description: The priority is medium 65 + priority: medium 66 + - name: "Priority/Low" 67 + exclusive: true 68 + color: 4caf50 69 + description: The priority is low 70 + priority: low
+15 -18
routers/api/v1/org/label.go
··· 4 4 package org 5 5 6 6 import ( 7 - "fmt" 8 7 "net/http" 9 8 "strconv" 10 9 "strings" 11 10 12 11 issues_model "code.gitea.io/gitea/models/issues" 13 12 "code.gitea.io/gitea/modules/context" 13 + "code.gitea.io/gitea/modules/label" 14 14 api "code.gitea.io/gitea/modules/structs" 15 15 "code.gitea.io/gitea/modules/web" 16 16 "code.gitea.io/gitea/routers/api/v1/utils" ··· 84 84 // "$ref": "#/responses/validationError" 85 85 form := web.GetForm(ctx).(*api.CreateLabelOption) 86 86 form.Color = strings.Trim(form.Color, " ") 87 - if len(form.Color) == 6 { 88 - form.Color = "#" + form.Color 89 - } 90 - if !issues_model.LabelColorPattern.MatchString(form.Color) { 91 - ctx.Error(http.StatusUnprocessableEntity, "ColorPattern", fmt.Errorf("bad color code: %s", form.Color)) 87 + color, err := label.NormalizeColor(form.Color) 88 + if err != nil { 89 + ctx.Error(http.StatusUnprocessableEntity, "Color", err) 92 90 return 93 91 } 92 + form.Color = color 94 93 95 94 label := &issues_model.Label{ 96 95 Name: form.Name, ··· 183 182 // "422": 184 183 // "$ref": "#/responses/validationError" 185 184 form := web.GetForm(ctx).(*api.EditLabelOption) 186 - label, err := issues_model.GetLabelInOrgByID(ctx, ctx.Org.Organization.ID, ctx.ParamsInt64(":id")) 185 + l, err := issues_model.GetLabelInOrgByID(ctx, ctx.Org.Organization.ID, ctx.ParamsInt64(":id")) 187 186 if err != nil { 188 187 if issues_model.IsErrOrgLabelNotExist(err) { 189 188 ctx.NotFound() ··· 194 193 } 195 194 196 195 if form.Name != nil { 197 - label.Name = *form.Name 196 + l.Name = *form.Name 198 197 } 199 198 if form.Exclusive != nil { 200 - label.Exclusive = *form.Exclusive 199 + l.Exclusive = *form.Exclusive 201 200 } 202 201 if form.Color != nil { 203 - label.Color = strings.Trim(*form.Color, " ") 204 - if len(label.Color) == 6 { 205 - label.Color = "#" + label.Color 206 - } 207 - if !issues_model.LabelColorPattern.MatchString(label.Color) { 208 - ctx.Error(http.StatusUnprocessableEntity, "ColorPattern", fmt.Errorf("bad color code: %s", label.Color)) 202 + color, err := label.NormalizeColor(*form.Color) 203 + if err != nil { 204 + ctx.Error(http.StatusUnprocessableEntity, "Color", err) 209 205 return 210 206 } 207 + l.Color = color 211 208 } 212 209 if form.Description != nil { 213 - label.Description = *form.Description 210 + l.Description = *form.Description 214 211 } 215 - if err := issues_model.UpdateLabel(label); err != nil { 212 + if err := issues_model.UpdateLabel(l); err != nil { 216 213 ctx.Error(http.StatusInternalServerError, "UpdateLabel", err) 217 214 return 218 215 } 219 216 220 - ctx.JSON(http.StatusOK, convert.ToLabel(label, nil, ctx.Org.Organization.AsUser())) 217 + ctx.JSON(http.StatusOK, convert.ToLabel(l, nil, ctx.Org.Organization.AsUser())) 221 218 } 222 219 223 220 // DeleteLabel delete a label for an organization
+24 -28
routers/api/v1/repo/label.go
··· 5 5 package repo 6 6 7 7 import ( 8 - "fmt" 9 8 "net/http" 10 9 "strconv" 11 - "strings" 12 10 13 11 issues_model "code.gitea.io/gitea/models/issues" 14 12 "code.gitea.io/gitea/modules/context" 13 + "code.gitea.io/gitea/modules/label" 15 14 api "code.gitea.io/gitea/modules/structs" 16 15 "code.gitea.io/gitea/modules/web" 17 16 "code.gitea.io/gitea/routers/api/v1/utils" ··· 93 92 // "$ref": "#/responses/Label" 94 93 95 94 var ( 96 - label *issues_model.Label 97 - err error 95 + l *issues_model.Label 96 + err error 98 97 ) 99 98 strID := ctx.Params(":id") 100 99 if intID, err2 := strconv.ParseInt(strID, 10, 64); err2 != nil { 101 - label, err = issues_model.GetLabelInRepoByName(ctx, ctx.Repo.Repository.ID, strID) 100 + l, err = issues_model.GetLabelInRepoByName(ctx, ctx.Repo.Repository.ID, strID) 102 101 } else { 103 - label, err = issues_model.GetLabelInRepoByID(ctx, ctx.Repo.Repository.ID, intID) 102 + l, err = issues_model.GetLabelInRepoByID(ctx, ctx.Repo.Repository.ID, intID) 104 103 } 105 104 if err != nil { 106 105 if issues_model.IsErrRepoLabelNotExist(err) { ··· 111 110 return 112 111 } 113 112 114 - ctx.JSON(http.StatusOK, convert.ToLabel(label, ctx.Repo.Repository, nil)) 113 + ctx.JSON(http.StatusOK, convert.ToLabel(l, ctx.Repo.Repository, nil)) 115 114 } 116 115 117 116 // CreateLabel create a label for a repository ··· 145 144 // "$ref": "#/responses/validationError" 146 145 147 146 form := web.GetForm(ctx).(*api.CreateLabelOption) 148 - form.Color = strings.Trim(form.Color, " ") 149 - if len(form.Color) == 6 { 150 - form.Color = "#" + form.Color 151 - } 152 - if !issues_model.LabelColorPattern.MatchString(form.Color) { 153 - ctx.Error(http.StatusUnprocessableEntity, "ColorPattern", fmt.Errorf("bad color code: %s", form.Color)) 147 + 148 + color, err := label.NormalizeColor(form.Color) 149 + if err != nil { 150 + ctx.Error(http.StatusUnprocessableEntity, "StringToColor", err) 154 151 return 155 152 } 153 + form.Color = color 156 154 157 - label := &issues_model.Label{ 155 + l := &issues_model.Label{ 158 156 Name: form.Name, 159 157 Exclusive: form.Exclusive, 160 158 Color: form.Color, 161 159 RepoID: ctx.Repo.Repository.ID, 162 160 Description: form.Description, 163 161 } 164 - if err := issues_model.NewLabel(ctx, label); err != nil { 162 + if err := issues_model.NewLabel(ctx, l); err != nil { 165 163 ctx.Error(http.StatusInternalServerError, "NewLabel", err) 166 164 return 167 165 } 168 166 169 - ctx.JSON(http.StatusCreated, convert.ToLabel(label, ctx.Repo.Repository, nil)) 167 + ctx.JSON(http.StatusCreated, convert.ToLabel(l, ctx.Repo.Repository, nil)) 170 168 } 171 169 172 170 // EditLabel modify a label for a repository ··· 206 204 // "$ref": "#/responses/validationError" 207 205 208 206 form := web.GetForm(ctx).(*api.EditLabelOption) 209 - label, err := issues_model.GetLabelInRepoByID(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":id")) 207 + l, err := issues_model.GetLabelInRepoByID(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":id")) 210 208 if err != nil { 211 209 if issues_model.IsErrRepoLabelNotExist(err) { 212 210 ctx.NotFound() ··· 217 215 } 218 216 219 217 if form.Name != nil { 220 - label.Name = *form.Name 218 + l.Name = *form.Name 221 219 } 222 220 if form.Exclusive != nil { 223 - label.Exclusive = *form.Exclusive 221 + l.Exclusive = *form.Exclusive 224 222 } 225 223 if form.Color != nil { 226 - label.Color = strings.Trim(*form.Color, " ") 227 - if len(label.Color) == 6 { 228 - label.Color = "#" + label.Color 229 - } 230 - if !issues_model.LabelColorPattern.MatchString(label.Color) { 231 - ctx.Error(http.StatusUnprocessableEntity, "ColorPattern", fmt.Errorf("bad color code: %s", label.Color)) 224 + color, err := label.NormalizeColor(*form.Color) 225 + if err != nil { 226 + ctx.Error(http.StatusUnprocessableEntity, "StringToColor", err) 232 227 return 233 228 } 229 + l.Color = color 234 230 } 235 231 if form.Description != nil { 236 - label.Description = *form.Description 232 + l.Description = *form.Description 237 233 } 238 - if err := issues_model.UpdateLabel(label); err != nil { 234 + if err := issues_model.UpdateLabel(l); err != nil { 239 235 ctx.Error(http.StatusInternalServerError, "UpdateLabel", err) 240 236 return 241 237 } 242 238 243 - ctx.JSON(http.StatusOK, convert.ToLabel(label, ctx.Repo.Repository, nil)) 239 + ctx.JSON(http.StatusOK, convert.ToLabel(l, ctx.Repo.Repository, nil)) 244 240 } 245 241 246 242 // DeleteLabel delete a label for a repository
+2 -1
routers/api/v1/repo/repo.go
··· 19 19 user_model "code.gitea.io/gitea/models/user" 20 20 "code.gitea.io/gitea/modules/context" 21 21 "code.gitea.io/gitea/modules/git" 22 + "code.gitea.io/gitea/modules/label" 22 23 "code.gitea.io/gitea/modules/log" 23 24 repo_module "code.gitea.io/gitea/modules/repository" 24 25 "code.gitea.io/gitea/modules/setting" ··· 248 249 ctx.Error(http.StatusConflict, "", "The repository with the same name already exists.") 249 250 } else if db.IsErrNameReserved(err) || 250 251 db.IsErrNamePatternNotAllowed(err) || 251 - repo_module.IsErrIssueLabelTemplateLoad(err) { 252 + label.IsErrTemplateLoad(err) { 252 253 ctx.Error(http.StatusUnprocessableEntity, "", err) 253 254 } else { 254 255 ctx.Error(http.StatusInternalServerError, "CreateRepository", err)
+3 -2
routers/web/org/org_labels.go
··· 9 9 "code.gitea.io/gitea/models/db" 10 10 issues_model "code.gitea.io/gitea/models/issues" 11 11 "code.gitea.io/gitea/modules/context" 12 + "code.gitea.io/gitea/modules/label" 12 13 repo_module "code.gitea.io/gitea/modules/repository" 13 14 "code.gitea.io/gitea/modules/web" 14 15 "code.gitea.io/gitea/services/forms" ··· 103 104 } 104 105 105 106 if err := repo_module.InitializeLabels(ctx, ctx.Org.Organization.ID, form.TemplateName, true); err != nil { 106 - if repo_module.IsErrIssueLabelTemplateLoad(err) { 107 - originalErr := err.(repo_module.ErrIssueLabelTemplateLoad).OriginalError 107 + if label.IsErrTemplateLoad(err) { 108 + originalErr := err.(label.ErrTemplateLoad).OriginalError 108 109 ctx.Flash.Error(ctx.Tr("repo.issues.label_templates.fail_to_load_file", form.TemplateName, originalErr)) 109 110 ctx.Redirect(ctx.Org.OrgLink + "/settings/labels") 110 111 return
+3 -2
routers/web/repo/issue_label.go
··· 11 11 "code.gitea.io/gitea/models/organization" 12 12 "code.gitea.io/gitea/modules/base" 13 13 "code.gitea.io/gitea/modules/context" 14 + "code.gitea.io/gitea/modules/label" 14 15 "code.gitea.io/gitea/modules/log" 15 16 repo_module "code.gitea.io/gitea/modules/repository" 16 17 "code.gitea.io/gitea/modules/web" ··· 41 42 } 42 43 43 44 if err := repo_module.InitializeLabels(ctx, ctx.Repo.Repository.ID, form.TemplateName, false); err != nil { 44 - if repo_module.IsErrIssueLabelTemplateLoad(err) { 45 - originalErr := err.(repo_module.ErrIssueLabelTemplateLoad).OriginalError 45 + if label.IsErrTemplateLoad(err) { 46 + originalErr := err.(label.ErrTemplateLoad).OriginalError 46 47 ctx.Flash.Error(ctx.Tr("repo.issues.label_templates.fail_to_load_file", form.TemplateName, originalErr)) 47 48 ctx.Redirect(ctx.Repo.RepoLink + "/labels") 48 49 return
+11 -8
services/migrations/gitea_uploader.go
··· 21 21 repo_model "code.gitea.io/gitea/models/repo" 22 22 user_model "code.gitea.io/gitea/models/user" 23 23 "code.gitea.io/gitea/modules/git" 24 + "code.gitea.io/gitea/modules/label" 24 25 "code.gitea.io/gitea/modules/log" 25 26 base "code.gitea.io/gitea/modules/migration" 26 27 repo_module "code.gitea.io/gitea/modules/repository" ··· 217 218 // CreateLabels creates labels 218 219 func (g *GiteaLocalUploader) CreateLabels(labels ...*base.Label) error { 219 220 lbs := make([]*issues_model.Label, 0, len(labels)) 220 - for _, label := range labels { 221 - // We must validate color here: 222 - if !issues_model.LabelColorPattern.MatchString("#" + label.Color) { 223 - log.Warn("Invalid label color: #%s for label: %s in migration to %s/%s", label.Color, label.Name, g.repoOwner, g.repoName) 224 - label.Color = "ffffff" 221 + for _, l := range labels { 222 + if color, err := label.NormalizeColor(l.Color); err != nil { 223 + log.Warn("Invalid label color: #%s for label: %s in migration to %s/%s", l.Color, l.Name, g.repoOwner, g.repoName) 224 + l.Color = "#ffffff" 225 + } else { 226 + l.Color = color 225 227 } 226 228 227 229 lbs = append(lbs, &issues_model.Label{ 228 230 RepoID: g.repo.ID, 229 - Name: label.Name, 230 - Description: label.Description, 231 - Color: "#" + label.Color, 231 + Name: l.Name, 232 + Exclusive: l.Exclusive, 233 + Description: l.Description, 234 + Color: l.Color, 232 235 }) 233 236 } 234 237