Skip to content

Commit 58b4143

Browse files
authored
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
1 parent de6c718 commit 58b4143

File tree

15 files changed

+488
-241
lines changed

15 files changed

+488
-241
lines changed

models/issues/label.go

+52-63
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ package issues
77
import (
88
"context"
99
"fmt"
10-
"regexp"
1110
"strconv"
1211
"strings"
1312

1413
"code.gitea.io/gitea/models/db"
1514
user_model "code.gitea.io/gitea/models/user"
15+
"code.gitea.io/gitea/modules/label"
1616
"code.gitea.io/gitea/modules/timeutil"
1717
"code.gitea.io/gitea/modules/util"
1818

@@ -78,9 +78,6 @@ func (err ErrLabelNotExist) Unwrap() error {
7878
return util.ErrNotExist
7979
}
8080

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-
8481
// Label represents a label of repository for issues.
8582
type Label struct {
8683
ID int64 `xorm:"pk autoincr"`
@@ -109,35 +106,35 @@ func init() {
109106
}
110107

111108
// 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
114111
}
115112

116113
// 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) {
118115
counts, _ := CountIssuesByRepo(ctx, &IssuesOptions{
119116
RepoID: repoID,
120117
LabelIDs: []int64{labelID},
121118
IsClosed: util.OptionalBoolFalse,
122119
})
123120

124121
for _, count := range counts {
125-
label.NumOpenRepoIssues += count
122+
l.NumOpenRepoIssues += count
126123
}
127124
}
128125

129126
// 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) {
131128
var labelQuerySlice []string
132129
labelSelected := false
133-
labelID := strconv.FormatInt(label.ID, 10)
134-
labelScope := label.ExclusiveScope()
130+
labelID := strconv.FormatInt(l.ID, 10)
131+
labelScope := l.ExclusiveScope()
135132
for i, s := range currentSelectedLabels {
136-
if s == label.ID {
133+
if s == l.ID {
137134
labelSelected = true
138-
} else if -s == label.ID {
135+
} else if -s == l.ID {
139136
labelSelected = true
140-
label.IsExcluded = true
137+
l.IsExcluded = true
141138
} else if s != 0 {
142139
// Exclude other labels in the same scope from selection
143140
if s < 0 || labelScope == "" || labelScope != currentSelectedExclusiveScopes[i] {
@@ -148,23 +145,23 @@ func (label *Label) LoadSelectedLabelsAfterClick(currentSelectedLabels []int64,
148145
if !labelSelected {
149146
labelQuerySlice = append(labelQuerySlice, labelID)
150147
}
151-
label.IsSelected = labelSelected
152-
label.QueryString = strings.Join(labelQuerySlice, ",")
148+
l.IsSelected = labelSelected
149+
l.QueryString = strings.Join(labelQuerySlice, ",")
153150
}
154151

155152
// 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
158155
}
159156

160157
// 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
163160
}
164161

165162
// 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)
168165
if err != nil {
169166
return 0, 0, 0, err
170167
}
@@ -176,9 +173,9 @@ func (label *Label) ColorRGB() (float64, float64, float64, error) {
176173
}
177174

178175
// 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 {
182179
// Perceived brightness from: https://www.w3.org/TR/AERT/#color-contrast
183180
// In the future WCAG 3 APCA may be a better solution
184181
brightness := (0.299*r + 0.587*g + 0.114*b) / 255
@@ -190,40 +187,26 @@ func (label *Label) UseLightTextColor() bool {
190187
}
191188

192189
// 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 {
195192
return ""
196193
}
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 {
199196
return ""
200197
}
201-
return label.Name[:lastIndex]
198+
return l.Name[:lastIndex]
202199
}
203200

204201
// 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)
208-
}
209-
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)
202+
func NewLabel(ctx context.Context, l *Label) error {
203+
color, err := label.NormalizeColor(l.Color)
204+
if err != nil {
205+
return err
224206
}
207+
l.Color = color
225208

226-
return db.Insert(ctx, label)
209+
return db.Insert(ctx, l)
227210
}
228211

229212
// NewLabels creates new labels
@@ -234,11 +217,14 @@ func NewLabels(labels ...*Label) error {
234217
}
235218
defer committer.Close()
236219

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
240224
}
241-
if err := db.Insert(ctx, label); err != nil {
225+
l.Color = color
226+
227+
if err := db.Insert(ctx, l); err != nil {
242228
return err
243229
}
244230
}
@@ -247,15 +233,18 @@ func NewLabels(labels ...*Label) error {
247233

248234
// UpdateLabel updates label information.
249235
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
252239
}
240+
l.Color = color
241+
253242
return updateLabelCols(db.DefaultContext, l, "name", "description", "color", "exclusive")
254243
}
255244

256245
// DeleteLabel delete a label
257246
func DeleteLabel(id, labelID int64) error {
258-
label, err := GetLabelByID(db.DefaultContext, labelID)
247+
l, err := GetLabelByID(db.DefaultContext, labelID)
259248
if err != nil {
260249
if IsErrLabelNotExist(err) {
261250
return nil
@@ -271,10 +260,10 @@ func DeleteLabel(id, labelID int64) error {
271260

272261
sess := db.GetEngine(ctx)
273262

274-
if label.BelongsToOrg() && label.OrgID != id {
263+
if l.BelongsToOrg() && l.OrgID != id {
275264
return nil
276265
}
277-
if label.BelongsToRepo() && label.RepoID != id {
266+
if l.BelongsToRepo() && l.RepoID != id {
278267
return nil
279268
}
280269

@@ -682,14 +671,14 @@ func newIssueLabels(ctx context.Context, issue *Issue, labels []*Label, doer *us
682671
if err = issue.LoadRepo(ctx); err != nil {
683672
return err
684673
}
685-
for _, label := range labels {
674+
for _, l := range labels {
686675
// 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) {
689678
continue
690679
}
691680

692-
if err = newIssueLabel(ctx, issue, label, doer); err != nil {
681+
if err = newIssueLabel(ctx, issue, l, doer); err != nil {
693682
return fmt.Errorf("newIssueLabel: %w", err)
694683
}
695684
}

models/issues/label_test.go

-2
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ import (
1515
"github.com/stretchr/testify/assert"
1616
)
1717

18-
// TODO TestGetLabelTemplateFile
19-
2018
func TestLabel_CalOpenIssues(t *testing.T) {
2119
assert.NoError(t, unittest.PrepareTestDatabase())
2220
label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1})

modules/label/label.go

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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+
}

0 commit comments

Comments
 (0)