mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-11-04 00:02:20 -05:00 
			
		
		
		
	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
This commit is contained in:
		
							parent
							
								
									de6c718b46
								
							
						
					
					
						commit
						58b4143803
					
				@ -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