Skip to content

Commit 4e33481

Browse files
authored
Make label templates have consistent behavior and priority (#23749)
Fix #23715 Other related PRs: * #23717 * #23716 * #23719 This PR is different from others, it tries to resolve the problem fundamentally (and brings more benefits) Although it looks like some more lines are added, actually many new lines are for tests. ---- Before, the code was just "guessing" the file type and try to parse them. <details> ![image](https://user-images.githubusercontent.com/2114189/228002245-57d58e27-1078-4da9-bf42-5bc0b264c6ce.png) </details> This PR: * Always remember the original option file names, and always use correct parser for them. * Another benefit is that we can sort the Label Templates now (before there was a map, its key order is undefined) ![image](https://user-images.githubusercontent.com/2114189/228002432-931b9f18-3908-484b-a36b-04760c9ad132.png)
1 parent bb6c670 commit 4e33481

File tree

11 files changed

+133
-87
lines changed

11 files changed

+133
-87
lines changed

modules/label/parser.go

+20-28
Original file line numberDiff line numberDiff line change
@@ -30,31 +30,23 @@ func IsErrTemplateLoad(err error) bool {
3030
}
3131

3232
func (err ErrTemplateLoad) Error() string {
33-
return fmt.Sprintf("Failed to load label template file '%s': %v", err.TemplateFile, err.OriginalError)
33+
return fmt.Sprintf("failed to load label template file %q: %v", err.TemplateFile, err.OriginalError)
3434
}
3535

36-
// GetTemplateFile loads the label template file by given name,
37-
// then parses and returns a list of name-color pairs and optionally description.
38-
func GetTemplateFile(name string) ([]*Label, error) {
39-
data, err := options.Labels(name + ".yaml")
40-
if err == nil && len(data) > 0 {
41-
return parseYamlFormat(name+".yaml", data)
42-
}
43-
44-
data, err = options.Labels(name + ".yml")
45-
if err == nil && len(data) > 0 {
46-
return parseYamlFormat(name+".yml", data)
47-
}
48-
49-
data, err = options.Labels(name)
36+
// LoadTemplateFile loads the label template file by given file name, returns a slice of Label structs.
37+
func LoadTemplateFile(fileName string) ([]*Label, error) {
38+
data, err := options.Labels(fileName)
5039
if err != nil {
51-
return nil, ErrTemplateLoad{name, fmt.Errorf("GetRepoInitFile: %w", err)}
40+
return nil, ErrTemplateLoad{fileName, fmt.Errorf("LoadTemplateFile: %w", err)}
5241
}
5342

54-
return parseLegacyFormat(name, data)
43+
if strings.HasSuffix(fileName, ".yaml") || strings.HasSuffix(fileName, ".yml") {
44+
return parseYamlFormat(fileName, data)
45+
}
46+
return parseLegacyFormat(fileName, data)
5547
}
5648

57-
func parseYamlFormat(name string, data []byte) ([]*Label, error) {
49+
func parseYamlFormat(fileName string, data []byte) ([]*Label, error) {
5850
lf := &labelFile{}
5951

6052
if err := yaml.Unmarshal(data, lf); err != nil {
@@ -65,19 +57,19 @@ func parseYamlFormat(name string, data []byte) ([]*Label, error) {
6557
for _, l := range lf.Labels {
6658
l.Color = strings.TrimSpace(l.Color)
6759
if len(l.Name) == 0 || len(l.Color) == 0 {
68-
return nil, ErrTemplateLoad{name, errors.New("label name and color are required fields")}
60+
return nil, ErrTemplateLoad{fileName, errors.New("label name and color are required fields")}
6961
}
7062
color, err := NormalizeColor(l.Color)
7163
if err != nil {
72-
return nil, ErrTemplateLoad{name, fmt.Errorf("bad HTML color code '%s' in label: %s", l.Color, l.Name)}
64+
return nil, ErrTemplateLoad{fileName, fmt.Errorf("bad HTML color code '%s' in label: %s", l.Color, l.Name)}
7365
}
7466
l.Color = color
7567
}
7668

7769
return lf.Labels, nil
7870
}
7971

80-
func parseLegacyFormat(name string, data []byte) ([]*Label, error) {
72+
func parseLegacyFormat(fileName string, data []byte) ([]*Label, error) {
8173
lines := strings.Split(string(data), "\n")
8274
list := make([]*Label, 0, len(lines))
8375
for i := 0; i < len(lines); i++ {
@@ -88,18 +80,18 @@ func parseLegacyFormat(name string, data []byte) ([]*Label, error) {
8880

8981
parts, description, _ := strings.Cut(line, ";")
9082

91-
color, name, ok := strings.Cut(parts, " ")
83+
color, labelName, ok := strings.Cut(parts, " ")
9284
if !ok {
93-
return nil, ErrTemplateLoad{name, fmt.Errorf("line is malformed: %s", line)}
85+
return nil, ErrTemplateLoad{fileName, fmt.Errorf("line is malformed: %s", line)}
9486
}
9587

9688
color, err := NormalizeColor(color)
9789
if err != nil {
98-
return nil, ErrTemplateLoad{name, fmt.Errorf("bad HTML color code '%s' in line: %s", color, line)}
90+
return nil, ErrTemplateLoad{fileName, fmt.Errorf("bad HTML color code '%s' in line: %s", color, line)}
9991
}
10092

10193
list = append(list, &Label{
102-
Name: strings.TrimSpace(name),
94+
Name: strings.TrimSpace(labelName),
10395
Color: color,
10496
Description: strings.TrimSpace(description),
10597
})
@@ -108,10 +100,10 @@ func parseLegacyFormat(name string, data []byte) ([]*Label, error) {
108100
return list, nil
109101
}
110102

111-
// LoadFormatted loads the labels' list of a template file as a string separated by comma
112-
func LoadFormatted(name string) (string, error) {
103+
// LoadTemplateDescription loads the labels from a template file, returns a description string by joining each Label.Name with comma
104+
func LoadTemplateDescription(fileName string) (string, error) {
113105
var buf strings.Builder
114-
list, err := GetTemplateFile(name)
106+
list, err := LoadTemplateFile(fileName)
115107
if err != nil {
116108
return "", err
117109
}

modules/repository/create.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import (
2323
user_model "code.gitea.io/gitea/models/user"
2424
"code.gitea.io/gitea/models/webhook"
2525
"code.gitea.io/gitea/modules/git"
26-
"code.gitea.io/gitea/modules/label"
2726
"code.gitea.io/gitea/modules/log"
2827
"code.gitea.io/gitea/modules/setting"
2928
api "code.gitea.io/gitea/modules/structs"
@@ -190,7 +189,7 @@ func CreateRepository(doer, u *user_model.User, opts CreateRepoOptions) (*repo_m
190189

191190
// Check if label template exist
192191
if len(opts.IssueLabels) > 0 {
193-
if _, err := label.GetTemplateFile(opts.IssueLabels); err != nil {
192+
if _, err := LoadTemplateLabelsByDisplayName(opts.IssueLabels); err != nil {
194193
return nil, err
195194
}
196195
}

modules/repository/init.go

+69-48
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"context"
99
"fmt"
1010
"os"
11-
"path"
1211
"path/filepath"
1312
"sort"
1413
"strings"
@@ -27,6 +26,11 @@ import (
2726
asymkey_service "code.gitea.io/gitea/services/asymkey"
2827
)
2928

29+
type OptionFile struct {
30+
DisplayName string
31+
Description string
32+
}
33+
3034
var (
3135
// Gitignores contains the gitiginore files
3236
Gitignores []string
@@ -37,65 +41,73 @@ var (
3741
// Readmes contains the readme files
3842
Readmes []string
3943

40-
// LabelTemplates contains the label template files and the list of labels for each file
41-
LabelTemplates map[string]string
44+
// LabelTemplateFiles contains the label template files, each item has its DisplayName and Description
45+
LabelTemplateFiles []OptionFile
46+
labelTemplateFileMap = map[string]string{} // DisplayName => FileName mapping
4247
)
4348

49+
type optionFileList struct {
50+
all []string // all files provided by bindata & custom-path. Sorted.
51+
custom []string // custom files provided by custom-path. Non-sorted, internal use only.
52+
}
53+
54+
// mergeCustomLabelFiles merges the custom label files. Always use the file's main name (DisplayName) as the key to de-duplicate.
55+
func mergeCustomLabelFiles(fl optionFileList) []string {
56+
exts := map[string]int{"": 0, ".yml": 1, ".yaml": 2} // "yaml" file has the highest priority to be used.
57+
58+
m := map[string]string{}
59+
merge := func(list []string) {
60+
sort.Slice(list, func(i, j int) bool { return exts[filepath.Ext(list[i])] < exts[filepath.Ext(list[j])] })
61+
for _, f := range list {
62+
m[strings.TrimSuffix(f, filepath.Ext(f))] = f
63+
}
64+
}
65+
merge(fl.all)
66+
merge(fl.custom)
67+
68+
files := make([]string, 0, len(m))
69+
for _, f := range m {
70+
files = append(files, f)
71+
}
72+
sort.Strings(files)
73+
return files
74+
}
75+
4476
// LoadRepoConfig loads the repository config
45-
func LoadRepoConfig() {
46-
// Load .gitignore and license files and readme templates.
47-
types := []string{"gitignore", "license", "readme", "label"}
48-
typeFiles := make([][]string, 4)
77+
func LoadRepoConfig() error {
78+
types := []string{"gitignore", "license", "readme", "label"} // option file directories
79+
typeFiles := make([]optionFileList, len(types))
4980
for i, t := range types {
50-
files, err := options.Dir(t)
51-
if err != nil {
52-
log.Fatal("Failed to get %s files: %v", t, err)
81+
var err error
82+
if typeFiles[i].all, err = options.Dir(t); err != nil {
83+
return fmt.Errorf("failed to list %s files: %w", t, err)
5384
}
54-
if t == "label" {
55-
for i, f := range files {
56-
ext := strings.ToLower(filepath.Ext(f))
57-
if ext == ".yaml" || ext == ".yml" {
58-
files[i] = f[:len(f)-len(ext)]
59-
}
85+
sort.Strings(typeFiles[i].all)
86+
customPath := filepath.Join(setting.CustomPath, "options", t)
87+
if isDir, err := util.IsDir(customPath); err != nil {
88+
return fmt.Errorf("failed to check custom %s dir: %w", t, err)
89+
} else if isDir {
90+
if typeFiles[i].custom, err = util.StatDir(customPath); err != nil {
91+
return fmt.Errorf("failed to list custom %s files: %w", t, err)
6092
}
6193
}
62-
customPath := path.Join(setting.CustomPath, "options", t)
63-
isDir, err := util.IsDir(customPath)
64-
if err != nil {
65-
log.Fatal("Failed to get custom %s files: %v", t, err)
66-
}
67-
if isDir {
68-
customFiles, err := util.StatDir(customPath)
69-
if err != nil {
70-
log.Fatal("Failed to get custom %s files: %v", t, err)
71-
}
72-
73-
for _, f := range customFiles {
74-
if !util.SliceContainsString(files, f, true) {
75-
files = append(files, f)
76-
}
77-
}
78-
}
79-
typeFiles[i] = files
8094
}
8195

82-
Gitignores = typeFiles[0]
83-
Licenses = typeFiles[1]
84-
Readmes = typeFiles[2]
85-
LabelTemplatesFiles := typeFiles[3]
86-
sort.Strings(Gitignores)
87-
sort.Strings(Licenses)
88-
sort.Strings(Readmes)
89-
sort.Strings(LabelTemplatesFiles)
96+
Gitignores = typeFiles[0].all
97+
Licenses = typeFiles[1].all
98+
Readmes = typeFiles[2].all
9099

91100
// Load label templates
92-
LabelTemplates = make(map[string]string)
93-
for _, templateFile := range LabelTemplatesFiles {
94-
labels, err := label.LoadFormatted(templateFile)
101+
LabelTemplateFiles = nil
102+
labelTemplateFileMap = map[string]string{}
103+
for _, file := range mergeCustomLabelFiles(typeFiles[3]) {
104+
description, err := label.LoadTemplateDescription(file)
95105
if err != nil {
96-
log.Error("Failed to load labels: %v", err)
106+
return fmt.Errorf("failed to load labels: %w", err)
97107
}
98-
LabelTemplates[templateFile] = labels
108+
displayName := strings.TrimSuffix(file, filepath.Ext(file))
109+
labelTemplateFileMap[displayName] = file
110+
LabelTemplateFiles = append(LabelTemplateFiles, OptionFile{DisplayName: displayName, Description: description})
99111
}
100112

101113
// Filter out invalid names and promote preferred licenses.
@@ -111,6 +123,7 @@ func LoadRepoConfig() {
111123
}
112124
}
113125
Licenses = sortedLicenses
126+
return nil
114127
}
115128

116129
func prepareRepoCommit(ctx context.Context, repo *repo_model.Repository, tmpDir, repoPath string, opts CreateRepoOptions) error {
@@ -344,7 +357,7 @@ func initRepository(ctx context.Context, repoPath string, u *user_model.User, re
344357

345358
// InitializeLabels adds a label set to a repository using a template
346359
func InitializeLabels(ctx context.Context, id int64, labelTemplate string, isOrg bool) error {
347-
list, err := label.GetTemplateFile(labelTemplate)
360+
list, err := LoadTemplateLabelsByDisplayName(labelTemplate)
348361
if err != nil {
349362
return err
350363
}
@@ -370,3 +383,11 @@ func InitializeLabels(ctx context.Context, id int64, labelTemplate string, isOrg
370383
}
371384
return nil
372385
}
386+
387+
// LoadTemplateLabelsByDisplayName loads a label template by its display name
388+
func LoadTemplateLabelsByDisplayName(displayName string) ([]*label.Label, error) {
389+
if fileName, ok := labelTemplateFileMap[displayName]; ok {
390+
return label.LoadTemplateFile(fileName)
391+
}
392+
return nil, label.ErrTemplateLoad{TemplateFile: displayName, OriginalError: fmt.Errorf("label template %q not found", displayName)}
393+
}

modules/repository/init_test.go

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright 2023 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package repository
5+
6+
import (
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestMergeCustomLabels(t *testing.T) {
13+
files := mergeCustomLabelFiles(optionFileList{
14+
all: []string{"a", "a.yaml", "a.yml"},
15+
custom: nil,
16+
})
17+
assert.EqualValues(t, []string{"a.yaml"}, files, "yaml file should win")
18+
19+
files = mergeCustomLabelFiles(optionFileList{
20+
all: []string{"a", "a.yaml"},
21+
custom: []string{"a"},
22+
})
23+
assert.EqualValues(t, []string{"a"}, files, "custom file should win")
24+
25+
files = mergeCustomLabelFiles(optionFileList{
26+
all: []string{"a", "a.yml", "a.yaml"},
27+
custom: []string{"a", "a.yml"},
28+
})
29+
assert.EqualValues(t, []string{"a.yml"}, files, "custom yml file should win if no yaml")
30+
}

routers/web/org/setting.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,6 @@ func Labels(ctx *context.Context) {
246246
ctx.Data["Title"] = ctx.Tr("repo.labels")
247247
ctx.Data["PageIsOrgSettings"] = true
248248
ctx.Data["PageIsOrgSettingsLabels"] = true
249-
ctx.Data["LabelTemplates"] = repo_module.LabelTemplates
249+
ctx.Data["LabelTemplateFiles"] = repo_module.LabelTemplateFiles
250250
ctx.HTML(http.StatusOK, tplSettingsLabels)
251251
}

routers/web/repo/issue_label.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ func Labels(ctx *context.Context) {
2828
ctx.Data["Title"] = ctx.Tr("repo.labels")
2929
ctx.Data["PageIsIssueList"] = true
3030
ctx.Data["PageIsLabels"] = true
31-
ctx.Data["LabelTemplates"] = repo_module.LabelTemplates
31+
ctx.Data["LabelTemplateFiles"] = repo_module.LabelTemplateFiles
3232
ctx.HTML(http.StatusOK, tplLabels)
3333
}
3434

routers/web/repo/issue_label_test.go

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
issues_model "code.gitea.io/gitea/models/issues"
1212
"code.gitea.io/gitea/models/unittest"
13+
"code.gitea.io/gitea/modules/repository"
1314
"code.gitea.io/gitea/modules/test"
1415
"code.gitea.io/gitea/modules/web"
1516
"code.gitea.io/gitea/services/forms"
@@ -30,6 +31,7 @@ func int64SliceToCommaSeparated(a []int64) string {
3031

3132
func TestInitializeLabels(t *testing.T) {
3233
unittest.PrepareTestEnv(t)
34+
assert.NoError(t, repository.LoadRepoConfig())
3335
ctx := test.MockContext(t, "user2/repo1/labels/initialize")
3436
test.LoadUser(t, ctx, 2)
3537
test.LoadRepo(t, ctx, 2)

routers/web/repo/repo.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ func Create(ctx *context.Context) {
132132

133133
// Give default value for template to render.
134134
ctx.Data["Gitignores"] = repo_module.Gitignores
135-
ctx.Data["LabelTemplates"] = repo_module.LabelTemplates
135+
ctx.Data["LabelTemplateFiles"] = repo_module.LabelTemplateFiles
136136
ctx.Data["Licenses"] = repo_module.Licenses
137137
ctx.Data["Readmes"] = repo_module.Readmes
138138
ctx.Data["readme"] = "Default"
@@ -200,7 +200,7 @@ func CreatePost(ctx *context.Context) {
200200
ctx.Data["Title"] = ctx.Tr("new_repo")
201201

202202
ctx.Data["Gitignores"] = repo_module.Gitignores
203-
ctx.Data["LabelTemplates"] = repo_module.LabelTemplates
203+
ctx.Data["LabelTemplateFiles"] = repo_module.LabelTemplateFiles
204204
ctx.Data["Licenses"] = repo_module.Licenses
205205
ctx.Data["Readmes"] = repo_module.Readmes
206206

services/repository/repository.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,9 @@ func PushCreateRepo(ctx context.Context, authUser, owner *user_model.User, repoN
8181

8282
// Init start repository service
8383
func Init() error {
84-
repo_module.LoadRepoConfig()
84+
if err := repo_module.LoadRepoConfig(); err != nil {
85+
return err
86+
}
8587
system_model.RemoveAllWithNotice(db.DefaultContext, "Clean up temporary repository uploads", setting.Repository.Upload.TempPath)
8688
system_model.RemoveAllWithNotice(db.DefaultContext, "Clean up temporary repositories", repo_module.LocalCopyPath())
8789
return initPushQueue()

templates/repo/create.tmpl

+2-2
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,8 @@
117117
<div class="default text">{{.locale.Tr "repo.issue_labels_helper"}}</div>
118118
<div class="menu">
119119
<div class="item" data-value="">{{.locale.Tr "repo.issue_labels_helper"}}</div>
120-
{{range $template, $labels := .LabelTemplates}}
121-
<div class="item" data-value="{{$template}}">{{$template}}<br><i>({{$labels}})</i></div>
120+
{{range .LabelTemplateFiles}}
121+
<div class="item" data-value="{{.DisplayName}}">{{.DisplayName}}<br><i>({{.Description}})</i></div>
122122
{{end}}
123123
</div>
124124
</div>

0 commit comments

Comments
 (0)