Skip to content

Commit d3f850c

Browse files
authored
Support comma-delimited string as labels in issue template (#21831)
The [labels in issue YAML templates](https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-issue-forms#top-level-syntax) can be a string array or a comma-delimited string, so a single string should be valid labels. The old codes committed in #20987 ignore this, that's why the warning is displayed: <img width="618" alt="image" src="https://user-images.githubusercontent.com/9418365/202112642-93dc72d0-71c3-40a2-9720-30fc2d48c97c.png"> Fixes #17877.
1 parent c8f3eb6 commit d3f850c

File tree

7 files changed

+362
-133
lines changed

7 files changed

+362
-133
lines changed

modules/issue/template/template.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ func validateOptions(field *api.IssueFormField, idx int) error {
165165
return position.Errorf("should be a string")
166166
}
167167
case api.IssueFormFieldTypeCheckboxes:
168-
opt, ok := option.(map[interface{}]interface{})
168+
opt, ok := option.(map[string]interface{})
169169
if !ok {
170170
return position.Errorf("should be a dictionary")
171171
}
@@ -351,7 +351,7 @@ func (o *valuedOption) Label() string {
351351
return label
352352
}
353353
case api.IssueFormFieldTypeCheckboxes:
354-
if vs, ok := o.data.(map[interface{}]interface{}); ok {
354+
if vs, ok := o.data.(map[string]interface{}); ok {
355355
if v, ok := vs["label"].(string); ok {
356356
return v
357357
}

modules/issue/template/template_test.go

+217-95
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,21 @@ package template
66

77
import (
88
"net/url"
9-
"reflect"
109
"testing"
1110

1211
"code.gitea.io/gitea/modules/json"
1312
api "code.gitea.io/gitea/modules/structs"
13+
14+
"github.com/stretchr/testify/require"
1415
)
1516

1617
func TestValidate(t *testing.T) {
1718
tests := []struct {
18-
name string
19-
content string
20-
wantErr string
19+
name string
20+
filename string
21+
content string
22+
want *api.IssueTemplate
23+
wantErr string
2124
}{
2225
{
2326
name: "miss name",
@@ -316,21 +319,9 @@ body:
316319
`,
317320
wantErr: "body[0](checkboxes), option[0]: 'required' should be a bool",
318321
},
319-
}
320-
for _, tt := range tests {
321-
t.Run(tt.name, func(t *testing.T) {
322-
tmpl, err := unmarshal("test.yaml", []byte(tt.content))
323-
if err != nil {
324-
t.Fatal(err)
325-
}
326-
if err := Validate(tmpl); (err == nil) != (tt.wantErr == "") || err != nil && err.Error() != tt.wantErr {
327-
t.Errorf("Validate() error = %v, wantErr %q", err, tt.wantErr)
328-
}
329-
})
330-
}
331-
332-
t.Run("valid", func(t *testing.T) {
333-
content := `
322+
{
323+
name: "valid",
324+
content: `
334325
name: Name
335326
title: Title
336327
about: About
@@ -386,96 +377,227 @@ body:
386377
required: false
387378
- label: Option 3 of checkboxes
388379
required: true
389-
`
390-
want := &api.IssueTemplate{
391-
Name: "Name",
392-
Title: "Title",
393-
About: "About",
394-
Labels: []string{"label1", "label2"},
395-
Ref: "Ref",
396-
Fields: []*api.IssueFormField{
397-
{
398-
Type: "markdown",
399-
ID: "id1",
400-
Attributes: map[string]interface{}{
401-
"value": "Value of the markdown",
380+
`,
381+
want: &api.IssueTemplate{
382+
Name: "Name",
383+
Title: "Title",
384+
About: "About",
385+
Labels: []string{"label1", "label2"},
386+
Ref: "Ref",
387+
Fields: []*api.IssueFormField{
388+
{
389+
Type: "markdown",
390+
ID: "id1",
391+
Attributes: map[string]interface{}{
392+
"value": "Value of the markdown",
393+
},
402394
},
403-
},
404-
{
405-
Type: "textarea",
406-
ID: "id2",
407-
Attributes: map[string]interface{}{
408-
"label": "Label of textarea",
409-
"description": "Description of textarea",
410-
"placeholder": "Placeholder of textarea",
411-
"value": "Value of textarea",
412-
"render": "bash",
395+
{
396+
Type: "textarea",
397+
ID: "id2",
398+
Attributes: map[string]interface{}{
399+
"label": "Label of textarea",
400+
"description": "Description of textarea",
401+
"placeholder": "Placeholder of textarea",
402+
"value": "Value of textarea",
403+
"render": "bash",
404+
},
405+
Validations: map[string]interface{}{
406+
"required": true,
407+
},
413408
},
414-
Validations: map[string]interface{}{
415-
"required": true,
409+
{
410+
Type: "input",
411+
ID: "id3",
412+
Attributes: map[string]interface{}{
413+
"label": "Label of input",
414+
"description": "Description of input",
415+
"placeholder": "Placeholder of input",
416+
"value": "Value of input",
417+
},
418+
Validations: map[string]interface{}{
419+
"required": true,
420+
"is_number": true,
421+
"regex": "[a-zA-Z0-9]+",
422+
},
416423
},
417-
},
418-
{
419-
Type: "input",
420-
ID: "id3",
421-
Attributes: map[string]interface{}{
422-
"label": "Label of input",
423-
"description": "Description of input",
424-
"placeholder": "Placeholder of input",
425-
"value": "Value of input",
424+
{
425+
Type: "dropdown",
426+
ID: "id4",
427+
Attributes: map[string]interface{}{
428+
"label": "Label of dropdown",
429+
"description": "Description of dropdown",
430+
"multiple": true,
431+
"options": []interface{}{
432+
"Option 1 of dropdown",
433+
"Option 2 of dropdown",
434+
"Option 3 of dropdown",
435+
},
436+
},
437+
Validations: map[string]interface{}{
438+
"required": true,
439+
},
426440
},
427-
Validations: map[string]interface{}{
428-
"required": true,
429-
"is_number": true,
430-
"regex": "[a-zA-Z0-9]+",
441+
{
442+
Type: "checkboxes",
443+
ID: "id5",
444+
Attributes: map[string]interface{}{
445+
"label": "Label of checkboxes",
446+
"description": "Description of checkboxes",
447+
"options": []interface{}{
448+
map[string]interface{}{"label": "Option 1 of checkboxes", "required": true},
449+
map[string]interface{}{"label": "Option 2 of checkboxes", "required": false},
450+
map[string]interface{}{"label": "Option 3 of checkboxes", "required": true},
451+
},
452+
},
431453
},
432454
},
433-
{
434-
Type: "dropdown",
435-
ID: "id4",
436-
Attributes: map[string]interface{}{
437-
"label": "Label of dropdown",
438-
"description": "Description of dropdown",
439-
"multiple": true,
440-
"options": []interface{}{
441-
"Option 1 of dropdown",
442-
"Option 2 of dropdown",
443-
"Option 3 of dropdown",
455+
FileName: "test.yaml",
456+
},
457+
wantErr: "",
458+
},
459+
{
460+
name: "single label",
461+
content: `
462+
name: Name
463+
title: Title
464+
about: About
465+
labels: label1
466+
ref: Ref
467+
body:
468+
- type: markdown
469+
id: id1
470+
attributes:
471+
value: Value of the markdown
472+
`,
473+
want: &api.IssueTemplate{
474+
Name: "Name",
475+
Title: "Title",
476+
About: "About",
477+
Labels: []string{"label1"},
478+
Ref: "Ref",
479+
Fields: []*api.IssueFormField{
480+
{
481+
Type: "markdown",
482+
ID: "id1",
483+
Attributes: map[string]interface{}{
484+
"value": "Value of the markdown",
444485
},
445486
},
446-
Validations: map[string]interface{}{
447-
"required": true,
487+
},
488+
FileName: "test.yaml",
489+
},
490+
wantErr: "",
491+
},
492+
{
493+
name: "comma-delimited labels",
494+
content: `
495+
name: Name
496+
title: Title
497+
about: About
498+
labels: label1,label2,,label3 ,,
499+
ref: Ref
500+
body:
501+
- type: markdown
502+
id: id1
503+
attributes:
504+
value: Value of the markdown
505+
`,
506+
want: &api.IssueTemplate{
507+
Name: "Name",
508+
Title: "Title",
509+
About: "About",
510+
Labels: []string{"label1", "label2", "label3"},
511+
Ref: "Ref",
512+
Fields: []*api.IssueFormField{
513+
{
514+
Type: "markdown",
515+
ID: "id1",
516+
Attributes: map[string]interface{}{
517+
"value": "Value of the markdown",
518+
},
448519
},
449520
},
450-
{
451-
Type: "checkboxes",
452-
ID: "id5",
453-
Attributes: map[string]interface{}{
454-
"label": "Label of checkboxes",
455-
"description": "Description of checkboxes",
456-
"options": []interface{}{
457-
map[interface{}]interface{}{"label": "Option 1 of checkboxes", "required": true},
458-
map[interface{}]interface{}{"label": "Option 2 of checkboxes", "required": false},
459-
map[interface{}]interface{}{"label": "Option 3 of checkboxes", "required": true},
521+
FileName: "test.yaml",
522+
},
523+
wantErr: "",
524+
},
525+
{
526+
name: "empty string as labels",
527+
content: `
528+
name: Name
529+
title: Title
530+
about: About
531+
labels: ''
532+
ref: Ref
533+
body:
534+
- type: markdown
535+
id: id1
536+
attributes:
537+
value: Value of the markdown
538+
`,
539+
want: &api.IssueTemplate{
540+
Name: "Name",
541+
Title: "Title",
542+
About: "About",
543+
Labels: nil,
544+
Ref: "Ref",
545+
Fields: []*api.IssueFormField{
546+
{
547+
Type: "markdown",
548+
ID: "id1",
549+
Attributes: map[string]interface{}{
550+
"value": "Value of the markdown",
460551
},
461552
},
462553
},
554+
FileName: "test.yaml",
463555
},
464-
FileName: "test.yaml",
465-
}
466-
got, err := unmarshal("test.yaml", []byte(content))
467-
if err != nil {
468-
t.Fatal(err)
469-
}
470-
if err := Validate(got); err != nil {
471-
t.Errorf("Validate() error = %v", err)
472-
}
473-
if !reflect.DeepEqual(want, got) {
474-
jsonWant, _ := json.Marshal(want)
475-
jsonGot, _ := json.Marshal(got)
476-
t.Errorf("want:\n%s\ngot:\n%s", jsonWant, jsonGot)
477-
}
478-
})
556+
wantErr: "",
557+
},
558+
{
559+
name: "comma delimited labels in markdown",
560+
filename: "test.md",
561+
content: `---
562+
name: Name
563+
title: Title
564+
about: About
565+
labels: label1,label2,,label3 ,,
566+
ref: Ref
567+
---
568+
Content
569+
`,
570+
want: &api.IssueTemplate{
571+
Name: "Name",
572+
Title: "Title",
573+
About: "About",
574+
Labels: []string{"label1", "label2", "label3"},
575+
Ref: "Ref",
576+
Fields: nil,
577+
Content: "Content\n",
578+
FileName: "test.md",
579+
},
580+
wantErr: "",
581+
},
582+
}
583+
for _, tt := range tests {
584+
t.Run(tt.name, func(t *testing.T) {
585+
filename := "test.yaml"
586+
if tt.filename != "" {
587+
filename = tt.filename
588+
}
589+
tmpl, err := unmarshal(filename, []byte(tt.content))
590+
require.NoError(t, err)
591+
if tt.wantErr != "" {
592+
require.EqualError(t, Validate(tmpl), tt.wantErr)
593+
} else {
594+
require.NoError(t, Validate(tmpl))
595+
want, _ := json.Marshal(tt.want)
596+
got, _ := json.Marshal(tmpl)
597+
require.JSONEq(t, string(want), string(got))
598+
}
599+
})
600+
}
479601
}
480602

481603
func TestRenderToMarkdown(t *testing.T) {

modules/issue/template/unmarshal.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import (
1616
api "code.gitea.io/gitea/modules/structs"
1717
"code.gitea.io/gitea/modules/util"
1818

19-
"gopkg.in/yaml.v2"
19+
"gopkg.in/yaml.v3"
2020
)
2121

2222
// CouldBe indicates a file with the filename could be a template,

0 commit comments

Comments
 (0)