Skip to content

Commit 26c4a04

Browse files
jolheiserlunnytechknowlogick
authored
Issue templates directory (#11450)
* Issue templates Signed-off-by: jolheiser <john.olheiser@gmail.com> * Add some comments, appease the linter Signed-off-by: jolheiser <john.olheiser@gmail.com> * Add docs and re-use dir candidates Signed-off-by: jolheiser <john.olheiser@gmail.com> * Add default labels to issue templates Signed-off-by: jolheiser <john.olheiser@gmail.com> * Generate swagger Signed-off-by: jolheiser <john.olheiser@gmail.com> * Suggested changes Signed-off-by: jolheiser <john.olheiser@gmail.com> * Update issue.go * Suggestions Signed-off-by: jolheiser <john.olheiser@gmail.com> * Extract metadata from legacy if possible Signed-off-by: jolheiser <john.olheiser@gmail.com> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: techknowlogick <techknowlogick@gitea.io>
1 parent dd1a651 commit 26c4a04

18 files changed

Lines changed: 381 additions & 17 deletions

File tree

docs/content/doc/usage/issue-pull-request-templates.en-us.md

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,39 @@ Possible file names for PR templates:
4141
* .github/pull_request_template.md
4242

4343

44-
Additionally, the New Issue page URL can be suffixed with `?body=Issue+Text` and the form will be populated with that string. This string will be used instead of the template if there is one.
44+
Additionally, the New Issue page URL can be suffixed with `?title=Issue+Title&body=Issue+Text` and the form will be populated with those strings. Those strings will be used instead of the template if there is one.
45+
46+
# Issue Template Directory
47+
48+
Alternatively, users can create multiple issue templates inside a special directory and allow users to choose one that more specifically
49+
addresses their problem.
50+
51+
Possible directory names for issue templates:
52+
53+
* ISSUE_TEMPLATE
54+
* issue_template
55+
* .gitea/ISSUE_TEMPLATE
56+
* .gitea/issue_template
57+
* .github/ISSUE_TEMPLATE
58+
* .github/issue_template
59+
* .gitlab/ISSUE_TEMPLATE
60+
* .gitlab/issue_template
61+
62+
Inside the directory can be multiple issue templates with the form
63+
64+
```markdown
65+
-----
66+
name: "Template Name"
67+
about: "This template is for testing!"
68+
title: "[TEST] "
69+
labels:
70+
- bug
71+
- "help needed"
72+
-----
73+
This is the template!
74+
```
75+
76+
In the above example, when a user is presented with the list of issues they can submit, this would show as `Template Name` with the description
77+
`This template is for testing!`. When submitting an issue with the above example, the issue title would be pre-populated with
78+
`[TEST] ` while the issue body would be pre-populated with `This is the template!`. The issue would also be assigned two labels,
79+
`bug` and `help needed`.

modules/context/repo.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,27 @@ import (
1616
"code.gitea.io/gitea/modules/cache"
1717
"code.gitea.io/gitea/modules/git"
1818
"code.gitea.io/gitea/modules/log"
19+
"code.gitea.io/gitea/modules/markup/markdown"
1920
"code.gitea.io/gitea/modules/setting"
21+
api "code.gitea.io/gitea/modules/structs"
2022

2123
"gitea.com/macaron/macaron"
2224
"github.com/editorconfig/editorconfig-core-go/v2"
2325
"github.com/unknwon/com"
2426
)
2527

28+
// IssueTemplateDirCandidates issue templates directory
29+
var IssueTemplateDirCandidates = []string{
30+
"ISSUE_TEMPLATE",
31+
"issue_template",
32+
".gitea/ISSUE_TEMPLATE",
33+
".gitea/issue_template",
34+
".github/ISSUE_TEMPLATE",
35+
".github/issue_template",
36+
".gitlab/ISSUE_TEMPLATE",
37+
".gitlab/issue_template",
38+
}
39+
2640
// PullRequest contains informations to make a pull request
2741
type PullRequest struct {
2842
BaseRepo *models.Repository
@@ -821,3 +835,60 @@ func UnitTypes() macaron.Handler {
821835
ctx.Data["UnitTypeProjects"] = models.UnitTypeProjects
822836
}
823837
}
838+
839+
// IssueTemplatesFromDefaultBranch checks for issue templates in the repo's default branch
840+
func (ctx *Context) IssueTemplatesFromDefaultBranch() []api.IssueTemplate {
841+
var issueTemplates []api.IssueTemplate
842+
if ctx.Repo.Commit == nil {
843+
var err error
844+
ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
845+
if err != nil {
846+
return issueTemplates
847+
}
848+
}
849+
850+
for _, dirName := range IssueTemplateDirCandidates {
851+
tree, err := ctx.Repo.Commit.SubTree(dirName)
852+
if err != nil {
853+
continue
854+
}
855+
entries, err := tree.ListEntries()
856+
if err != nil {
857+
return issueTemplates
858+
}
859+
for _, entry := range entries {
860+
if strings.HasSuffix(entry.Name(), ".md") {
861+
if entry.Blob().Size() >= setting.UI.MaxDisplayFileSize {
862+
log.Debug("Issue template is too large: %s", entry.Name())
863+
continue
864+
}
865+
r, err := entry.Blob().DataAsync()
866+
if err != nil {
867+
log.Debug("DataAsync: %v", err)
868+
continue
869+
}
870+
defer r.Close()
871+
data, err := ioutil.ReadAll(r)
872+
if err != nil {
873+
log.Debug("ReadAll: %v", err)
874+
continue
875+
}
876+
var it api.IssueTemplate
877+
content, err := markdown.ExtractMetadata(string(data), &it)
878+
if err != nil {
879+
log.Debug("ExtractMetadata: %v", err)
880+
continue
881+
}
882+
it.Content = content
883+
it.FileName = entry.Name()
884+
if it.Valid() {
885+
issueTemplates = append(issueTemplates, it)
886+
}
887+
}
888+
}
889+
if len(issueTemplates) > 0 {
890+
return issueTemplates
891+
}
892+
}
893+
return issueTemplates
894+
}

modules/markup/markdown/meta.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright 2020 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package markdown
6+
7+
import (
8+
"errors"
9+
"strings"
10+
11+
"gopkg.in/yaml.v2"
12+
)
13+
14+
func isYAMLSeparator(line string) bool {
15+
line = strings.TrimSpace(line)
16+
for i := 0; i < len(line); i++ {
17+
if line[i] != '-' {
18+
return false
19+
}
20+
}
21+
return len(line) > 2
22+
}
23+
24+
// ExtractMetadata consumes a markdown file, parses YAML frontmatter,
25+
// and returns the frontmatter metadata separated from the markdown content
26+
func ExtractMetadata(contents string, out interface{}) (string, error) {
27+
var front, body []string
28+
var seps int
29+
lines := strings.Split(contents, "\n")
30+
for idx, line := range lines {
31+
if seps == 2 {
32+
front, body = lines[:idx], lines[idx:]
33+
break
34+
}
35+
if isYAMLSeparator(line) {
36+
seps++
37+
continue
38+
}
39+
}
40+
41+
if len(front) == 0 && len(body) == 0 {
42+
return "", errors.New("could not determine metadata")
43+
}
44+
45+
if err := yaml.Unmarshal([]byte(strings.Join(front, "\n")), out); err != nil {
46+
return "", err
47+
}
48+
return strings.Join(body, "\n"), nil
49+
}

modules/structs/issue.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package structs
66

77
import (
8+
"strings"
89
"time"
910
)
1011

@@ -119,3 +120,19 @@ type IssueDeadline struct {
119120
// swagger:strfmt date-time
120121
Deadline *time.Time `json:"due_date"`
121122
}
123+
124+
// IssueTemplate represents an issue template for a repository
125+
// swagger:model
126+
type IssueTemplate struct {
127+
Name string `json:"name" yaml:"name"`
128+
Title string `json:"title" yaml:"title"`
129+
About string `json:"about" yaml:"about"`
130+
Labels []string `json:"labels" yaml:"labels"`
131+
Content string `json:"content" yaml:"-"`
132+
FileName string `json:"file_name" yaml:"-"`
133+
}
134+
135+
// Valid checks whether an IssueTemplate is considered valid, e.g. at least name and about
136+
func (it IssueTemplate) Valid() bool {
137+
return strings.TrimSpace(it.Name) != "" && strings.TrimSpace(it.About) != ""
138+
}

options/locale/locale_en-US.ini

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -939,6 +939,8 @@ issues.new.clear_assignees = Clear assignees
939939
issues.new.no_assignees = No Assignees
940940
issues.new.no_reviewers = No reviewers
941941
issues.new.add_reviewer_title = Request review
942+
issues.choose.get_started = Get Started
943+
issues.choose.blank = Open a blank issue
942944
issues.no_ref = No Branch/Tag Specified
943945
issues.create = Create Issue
944946
issues.new_label = New Label

routers/api/v1/api.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -866,6 +866,7 @@ func RegisterRoutes(m *macaron.Macaron) {
866866
Delete(reqToken(), repo.DeleteTopic)
867867
}, reqAdmin())
868868
}, reqAnyRepoReader())
869+
m.Get("/issue_templates", context.ReferencesGitRepo(false), repo.GetIssueTemplates)
869870
m.Get("/languages", reqRepoReader(models.UnitTypeCode), repo.GetLanguages)
870871
}, repoAssignment())
871872
})

routers/api/v1/repo/repo.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -812,3 +812,28 @@ func Delete(ctx *context.APIContext) {
812812
log.Trace("Repository deleted: %s/%s", owner.Name, repo.Name)
813813
ctx.Status(http.StatusNoContent)
814814
}
815+
816+
// GetIssueTemplates returns the issue templates for a repository
817+
func GetIssueTemplates(ctx *context.APIContext) {
818+
// swagger:operation GET /repos/{owner}/{repo}/issue_templates repository repoGetIssueTemplates
819+
// ---
820+
// summary: Get available issue templates for a repository
821+
// produces:
822+
// - application/json
823+
// parameters:
824+
// - name: owner
825+
// in: path
826+
// description: owner of the repo
827+
// type: string
828+
// required: true
829+
// - name: repo
830+
// in: path
831+
// description: name of the repo
832+
// type: string
833+
// required: true
834+
// responses:
835+
// "200":
836+
// "$ref": "#/responses/IssueTemplates"
837+
838+
ctx.JSON(http.StatusOK, ctx.IssueTemplatesFromDefaultBranch())
839+
}

routers/api/v1/swagger/issue.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,13 @@ type swaggerIssueDeadline struct {
8585
Body api.IssueDeadline `json:"body"`
8686
}
8787

88+
// IssueTemplates
89+
// swagger:response IssueTemplates
90+
type swaggerIssueTemplates struct {
91+
// in:body
92+
Body []api.IssueTemplate `json:"body"`
93+
}
94+
8895
// StopWatch
8996
// swagger:response StopWatch
9097
type swaggerResponseStopWatch struct {

routers/repo/compare.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -577,7 +577,7 @@ func CompareDiff(ctx *context.Context) {
577577
ctx.Data["RequireTribute"] = true
578578
ctx.Data["RequireSimpleMDE"] = true
579579
ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
580-
setTemplateIfExists(ctx, pullRequestTemplateKey, pullRequestTemplateCandidates)
580+
setTemplateIfExists(ctx, pullRequestTemplateKey, nil, pullRequestTemplateCandidates)
581581
renderAttachmentSettings(ctx)
582582

583583
ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWrite(models.UnitTypePullRequests)

0 commit comments

Comments
 (0)