forked from gitea/gitea
		
	Backport #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 Co-authored-by: Lauris BH <lauris@nix.lv>
This commit is contained in:
		
							parent
							
								
									39178b5756
								
							
						
					
					
						commit
						5d5f907e7f
					
				| @ -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) | ||||
| func NewLabel(ctx context.Context, l *Label) error { | ||||
| 	color, err := label.NormalizeColor(l.Color) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	l.Color = color | ||||
| 
 | ||||
| 	// normalize case | ||||
| 	label.Color = strings.ToLower(label.Color) | ||||
| 
 | ||||
| 	// add leading hash | ||||
| 	if label.Color[0] != '#' { | ||||
| 		label.Color = "#" + label.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) | ||||
| 	} | ||||
| 
 | ||||
| 	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 { | ||||
| 		color, err := label.NormalizeColor(l.Color) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		if err := db.Insert(ctx, label); err != nil { | ||||
| 		l.Color = 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) { | ||||
| 		return fmt.Errorf("bad color code: %s", l.Color) | ||||
| 	color, err := label.NormalizeColor(l.Color) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	l.Color = 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) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| @ -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}) | ||||
|  | ||||
							
								
								
									
										46
									
								
								modules/label/label.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								modules/label/label.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,46 @@ | ||||
| // Copyright 2023 The Gitea Authors. All rights reserved. | ||||
| // SPDX-License-Identifier: MIT | ||||
| 
 | ||||
| package label | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
| ) | ||||
| 
 | ||||
| // 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 | ||||
| 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, error) { | ||||
| 	// normalize case | ||||
| 	color = strings.TrimSpace(strings.ToLower(color)) | ||||
| 
 | ||||
| 	// add leading hash | ||||
| 	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] | ||||
| 		g := color[2] | ||||
| 		b := color[3] | ||||
| 		color = fmt.Sprintf("#%c%c%c%c%c%c", r, r, g, g, b, b) | ||||
| 	} | ||||
| 
 | ||||
| 	return color, nil | ||||
| } | ||||
							
								
								
									
										126
									
								
								modules/label/parser.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								modules/label/parser.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,126 @@ | ||||
| // 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 parseLegacyFormat(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")} | ||||
| 		} | ||||
| 		color, err := NormalizeColor(l.Color) | ||||
| 		if err != nil { | ||||
| 			return nil, ErrTemplateLoad{name, fmt.Errorf("bad HTML color code '%s' in label: %s", l.Color, l.Name)} | ||||
| 		} | ||||
| 		l.Color = color | ||||
| 	} | ||||
| 
 | ||||
| 	return lf.Labels, nil | ||||
| } | ||||
| 
 | ||||
| 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++ { | ||||
| 		line := strings.TrimSpace(lines[i]) | ||||
| 		if len(line) == 0 { | ||||
| 			continue | ||||
| 		} | ||||
| 
 | ||||
| 		parts, description, _ := strings.Cut(line, ";") | ||||
| 
 | ||||
| 		color, name, ok := strings.Cut(parts, " ") | ||||
| 		if !ok { | ||||
| 			return nil, ErrTemplateLoad{name, fmt.Errorf("line is malformed: %s", line)} | ||||
| 		} | ||||
| 
 | ||||
| 		color, err := NormalizeColor(color) | ||||
| 		if err != nil { | ||||
| 			return nil, ErrTemplateLoad{name, fmt.Errorf("bad HTML color code '%s' in line: %s", color, line)} | ||||
| 		} | ||||
| 
 | ||||
| 		list = append(list, &Label{ | ||||
| 			Name:        strings.TrimSpace(name), | ||||
| 			Color:       color, | ||||
| 			Description: strings.TrimSpace(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 | ||||
| } | ||||
							
								
								
									
										72
									
								
								modules/label/parser_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								modules/label/parser_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -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 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 := parseLegacyFormat("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.Empty(t, labels[2].Description) | ||||
| } | ||||
							
								
								
									
										44
									
								
								modules/options/repo.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								modules/options/repo.go
									
									
									
									
									
										Normal file
									
								
							| @ -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") | ||||
| 	} | ||||
| } | ||||
| @ -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 | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| @ -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) | ||||
| 		} | ||||
| @ -443,7 +344,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 | ||||
| 	} | ||||
| @ -451,9 +352,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 | ||||
|  | ||||
							
								
								
									
										70
									
								
								options/label/Advanced.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								options/label/Advanced.yaml
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||
| @ -4,13 +4,13 @@ | ||||
| package org | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	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" | ||||
| @ -84,13 +84,12 @@ 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 { | ||||
| 		form.Color = "#" + form.Color | ||||
| 	} | ||||
| 	if !issues_model.LabelColorPattern.MatchString(form.Color) { | ||||
| 		ctx.Error(http.StatusUnprocessableEntity, "ColorPattern", fmt.Errorf("bad color code: %s", form.Color)) | ||||
| 	color, err := label.NormalizeColor(form.Color) | ||||
| 	if err != nil { | ||||
| 		ctx.Error(http.StatusUnprocessableEntity, "Color", err) | ||||
| 		return | ||||
| 	} | ||||
| 	form.Color = color | ||||
| 
 | ||||
| 	label := &issues_model.Label{ | ||||
| 		Name:        form.Name, | ||||
| @ -183,7 +182,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 +193,28 @@ 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 | ||||
| 		} | ||||
| 		if !issues_model.LabelColorPattern.MatchString(label.Color) { | ||||
| 			ctx.Error(http.StatusUnprocessableEntity, "ColorPattern", fmt.Errorf("bad color code: %s", label.Color)) | ||||
| 		color, err := label.NormalizeColor(*form.Color) | ||||
| 		if err != nil { | ||||
| 			ctx.Error(http.StatusUnprocessableEntity, "Color", err) | ||||
| 			return | ||||
| 		} | ||||
| 		l.Color = color | ||||
| 	} | ||||
| 	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 | ||||
|  | ||||
| @ -5,13 +5,12 @@ | ||||
| package repo | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	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 +92,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 +110,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 | ||||
| @ -145,28 +144,27 @@ 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 { | ||||
| 		form.Color = "#" + form.Color | ||||
| 	} | ||||
| 	if !issues_model.LabelColorPattern.MatchString(form.Color) { | ||||
| 		ctx.Error(http.StatusUnprocessableEntity, "ColorPattern", fmt.Errorf("bad color code: %s", form.Color)) | ||||
| 
 | ||||
| 	color, err := label.NormalizeColor(form.Color) | ||||
| 	if err != nil { | ||||
| 		ctx.Error(http.StatusUnprocessableEntity, "StringToColor", err) | ||||
| 		return | ||||
| 	} | ||||
| 	form.Color = color | ||||
| 
 | ||||
| 	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 +204,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 +215,28 @@ 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 | ||||
| 		} | ||||
| 		if !issues_model.LabelColorPattern.MatchString(label.Color) { | ||||
| 			ctx.Error(http.StatusUnprocessableEntity, "ColorPattern", fmt.Errorf("bad color code: %s", label.Color)) | ||||
| 		color, err := label.NormalizeColor(*form.Color) | ||||
| 		if err != nil { | ||||
| 			ctx.Error(http.StatusUnprocessableEntity, "StringToColor", err) | ||||
| 			return | ||||
| 		} | ||||
| 		l.Color = color | ||||
| 	} | ||||
| 	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 | ||||
|  | ||||
| @ -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) | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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,20 @@ 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 { | ||||
| 		// 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" | ||||
| 	for _, l := range labels { | ||||
| 		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" | ||||
| 		} else { | ||||
| 			l.Color = color | ||||
| 		} | ||||
| 
 | ||||
| 		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, | ||||
| 		}) | ||||
| 	} | ||||
| 
 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Giteabot
						Giteabot