summaryrefslogtreecommitdiffstats
path: root/routers/web/repo
diff options
context:
space:
mode:
authorJason Song <i@wolfogre.com>2022-09-02 15:58:49 +0800
committerGitHub <noreply@github.com>2022-09-02 15:58:49 +0800
commit84447df4d366324ab81894b028b00fd66be85caf (patch)
tree5291442a85faccb6bc17b54ca71a53c16530dfe3 /routers/web/repo
parentb7a4b45ff83dc19febcfb85279215ea6bd224033 (diff)
downloadgitea-84447df4d366324ab81894b028b00fd66be85caf.tar.gz
gitea-84447df4d366324ab81894b028b00fd66be85caf.zip
Support Issue forms and PR forms (#20987)
* feat: extend issue template for yaml * feat: support yaml template * feat: render form to markdown * feat: support yaml template for pr * chore: rename to Fields * feat: template unmarshal * feat: split template * feat: render to markdown * feat: use full name as template file name * chore: remove useless file * feat: use dropdown of fomantic ui * feat: update input style * docs: more comments * fix: render text without render * chore: fix lint error * fix: support use description as about in markdown * fix: add field class in form * chore: generate swagger * feat: validate template * feat: support is_nummber and regex * test: fix broken unit tests * fix: ignore empty body of md template * fix: make multiple easymde editors work in one page * feat: better UI * fix: js error in pr form * chore: generate swagger * feat: support regex validation * chore: generate swagger * fix: refresh each markdown editor * chore: give up required validation * fix: correct issue template candidates * fix: correct checkboxes style * chore: ignore .hugo_build.lock in docs * docs: separate out a new doc for merge templates * docs: introduce syntax of yaml template * feat: show a alert for invalid templates * test: add case for a valid template * fix: correct attributes of required checkbox * fix: add class not-under-easymde for dropzone * fix: use more back-quotes * chore: remove translation in zh-CN * fix EasyMDE statusbar margin * fix: remove repeated blocks * fix: reuse regex for quotes Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Diffstat (limited to 'routers/web/repo')
-rw-r--r--routers/web/repo/compare.go6
-rw-r--r--routers/web/repo/issue.go173
-rw-r--r--routers/web/repo/pull.go22
3 files changed, 132 insertions, 69 deletions
diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go
index f34d3a5203..e35af31724 100644
--- a/routers/web/repo/compare.go
+++ b/routers/web/repo/compare.go
@@ -784,7 +784,11 @@ func CompareDiff(ctx *context.Context) {
ctx.Data["IsRepoToolbarCommits"] = true
ctx.Data["IsDiffCompare"] = true
ctx.Data["RequireTribute"] = true
- setTemplateIfExists(ctx, pullRequestTemplateKey, nil, pullRequestTemplateCandidates)
+ templateErrs := setTemplateIfExists(ctx, pullRequestTemplateKey, pullRequestTemplateCandidates)
+
+ if len(templateErrs) > 0 {
+ ctx.Flash.Warning(renderErrorOfTemplates(ctx, templateErrs), true)
+ }
// If a template content is set, prepend the "content". In this case that's only
// applicable if you have one commit to compare and that commit has a message.
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index 3f14416e48..06e003bff2 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -10,11 +10,10 @@ import (
stdCtx "context"
"errors"
"fmt"
- "io"
"math/big"
"net/http"
"net/url"
- "path"
+ "sort"
"strconv"
"strings"
"time"
@@ -35,6 +34,7 @@ import (
"code.gitea.io/gitea/modules/convert"
"code.gitea.io/gitea/modules/git"
issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
+ issue_template "code.gitea.io/gitea/modules/issue/template"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
@@ -45,6 +45,7 @@ import (
"code.gitea.io/gitea/modules/upload"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/routers/utils"
asymkey_service "code.gitea.io/gitea/services/asymkey"
comment_service "code.gitea.io/gitea/services/comments"
"code.gitea.io/gitea/services/forms"
@@ -70,11 +71,23 @@ const (
// IssueTemplateCandidates issue templates
var IssueTemplateCandidates = []string{
"ISSUE_TEMPLATE.md",
+ "ISSUE_TEMPLATE.yaml",
+ "ISSUE_TEMPLATE.yml",
"issue_template.md",
+ "issue_template.yaml",
+ "issue_template.yml",
".gitea/ISSUE_TEMPLATE.md",
+ ".gitea/ISSUE_TEMPLATE.yaml",
+ ".gitea/ISSUE_TEMPLATE.yml",
+ ".gitea/issue_template.md",
+ ".gitea/issue_template.yaml",
".gitea/issue_template.md",
".github/ISSUE_TEMPLATE.md",
+ ".github/ISSUE_TEMPLATE.yaml",
+ ".github/ISSUE_TEMPLATE.yml",
".github/issue_template.md",
+ ".github/issue_template.yaml",
+ ".github/issue_template.yml",
}
// MustAllowUserComment checks to make sure if an issue is locked.
@@ -722,81 +735,62 @@ func RetrieveRepoMetas(ctx *context.Context, repo *repo_model.Repository, isPull
return labels
}
-func getFileContentFromDefaultBranch(ctx *context.Context, filename string) (string, bool) {
- if ctx.Repo.Commit == nil {
- var err error
- ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
- if err != nil {
- return "", false
- }
- }
-
- entry, err := ctx.Repo.Commit.GetTreeEntryByPath(filename)
- if err != nil {
- return "", false
- }
- if entry.Blob().Size() >= setting.UI.MaxDisplayFileSize {
- return "", false
- }
- r, err := entry.Blob().DataAsync()
- if err != nil {
- return "", false
- }
- defer r.Close()
- bytes, err := io.ReadAll(r)
+func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles []string) map[string]error {
+ commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
if err != nil {
- return "", false
+ return nil
}
- return string(bytes), true
-}
-func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleDirs, possibleFiles []string) {
- templateCandidates := make([]string, 0, len(possibleFiles))
- if ctx.FormString("template") != "" {
- for _, dirName := range possibleDirs {
- templateCandidates = append(templateCandidates, path.Join(dirName, ctx.FormString("template")))
- }
+ templateCandidates := make([]string, 0, 1+len(possibleFiles))
+ if t := ctx.FormString("template"); t != "" {
+ templateCandidates = append(templateCandidates, t)
}
templateCandidates = append(templateCandidates, possibleFiles...) // Append files to the end because they should be fallback
+
+ templateErrs := map[string]error{}
for _, filename := range templateCandidates {
- templateContent, found := getFileContentFromDefaultBranch(ctx, filename)
- if found {
- var meta api.IssueTemplate
- templateBody, err := markdown.ExtractMetadata(templateContent, &meta)
- if err != nil {
- log.Debug("could not extract metadata from %s [%s]: %v", filename, ctx.Repo.Repository.FullName(), err)
- ctx.Data[ctxDataKey] = templateContent
- return
- }
- ctx.Data[issueTemplateTitleKey] = meta.Title
- ctx.Data[ctxDataKey] = templateBody
- labelIDs := make([]string, 0, len(meta.Labels))
- if repoLabels, err := issues_model.GetLabelsByRepoID(ctx, ctx.Repo.Repository.ID, "", db.ListOptions{}); err == nil {
- ctx.Data["Labels"] = repoLabels
- if ctx.Repo.Owner.IsOrganization() {
- if orgLabels, err := issues_model.GetLabelsByOrgID(ctx, ctx.Repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{}); err == nil {
- ctx.Data["OrgLabels"] = orgLabels
- repoLabels = append(repoLabels, orgLabels...)
- }
+ if ok, _ := commit.HasFile(filename); !ok {
+ continue
+ }
+ template, err := issue_template.UnmarshalFromCommit(commit, filename)
+ if err != nil {
+ templateErrs[filename] = err
+ continue
+ }
+ ctx.Data[issueTemplateTitleKey] = template.Title
+ ctx.Data[ctxDataKey] = template.Content
+
+ if template.Type() == api.IssueTemplateTypeYaml {
+ ctx.Data["Fields"] = template.Fields
+ ctx.Data["TemplateFile"] = template.FileName
+ }
+ labelIDs := make([]string, 0, len(template.Labels))
+ if repoLabels, err := issues_model.GetLabelsByRepoID(ctx, ctx.Repo.Repository.ID, "", db.ListOptions{}); err == nil {
+ ctx.Data["Labels"] = repoLabels
+ if ctx.Repo.Owner.IsOrganization() {
+ if orgLabels, err := issues_model.GetLabelsByOrgID(ctx, ctx.Repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{}); err == nil {
+ ctx.Data["OrgLabels"] = orgLabels
+ repoLabels = append(repoLabels, orgLabels...)
}
+ }
- for _, metaLabel := range meta.Labels {
- for _, repoLabel := range repoLabels {
- if strings.EqualFold(repoLabel.Name, metaLabel) {
- repoLabel.IsChecked = true
- labelIDs = append(labelIDs, strconv.FormatInt(repoLabel.ID, 10))
- break
- }
+ for _, metaLabel := range template.Labels {
+ for _, repoLabel := range repoLabels {
+ if strings.EqualFold(repoLabel.Name, metaLabel) {
+ repoLabel.IsChecked = true
+ labelIDs = append(labelIDs, strconv.FormatInt(repoLabel.ID, 10))
+ break
}
}
}
- ctx.Data["HasSelectedLabel"] = len(labelIDs) > 0
- ctx.Data["label_ids"] = strings.Join(labelIDs, ",")
- ctx.Data["Reference"] = meta.Ref
- ctx.Data["RefEndName"] = git.RefEndName(meta.Ref)
- return
}
+ ctx.Data["HasSelectedLabel"] = len(labelIDs) > 0
+ ctx.Data["label_ids"] = strings.Join(labelIDs, ",")
+ ctx.Data["Reference"] = template.Ref
+ ctx.Data["RefEndName"] = git.RefEndName(template.Ref)
+ return templateErrs
}
+ return templateErrs
}
// NewIssue render creating issue page
@@ -845,24 +839,62 @@ func NewIssue(ctx *context.Context) {
}
RetrieveRepoMetas(ctx, ctx.Repo.Repository, false)
- setTemplateIfExists(ctx, issueTemplateKey, context.IssueTemplateDirCandidates, IssueTemplateCandidates)
+
+ _, templateErrs := ctx.IssueTemplatesErrorsFromDefaultBranch()
+ if errs := setTemplateIfExists(ctx, issueTemplateKey, IssueTemplateCandidates); len(errs) > 0 {
+ for k, v := range errs {
+ templateErrs[k] = v
+ }
+ }
if ctx.Written() {
return
}
+ if len(templateErrs) > 0 {
+ ctx.Flash.Warning(renderErrorOfTemplates(ctx, templateErrs), true)
+ }
+
ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWrite(unit.TypeIssues)
ctx.HTML(http.StatusOK, tplIssueNew)
}
+func renderErrorOfTemplates(ctx *context.Context, errs map[string]error) string {
+ var files []string
+ for k := range errs {
+ files = append(files, k)
+ }
+ sort.Strings(files) // keep the output stable
+
+ var lines []string
+ for _, file := range files {
+ lines = append(lines, fmt.Sprintf("%s: %v", file, errs[file]))
+ }
+
+ flashError, err := ctx.RenderToString(tplAlertDetails, map[string]interface{}{
+ "Message": ctx.Tr("repo.issues.choose.ignore_invalid_templates"),
+ "Summary": ctx.Tr("repo.issues.choose.invalid_templates", len(errs)),
+ "Details": utils.SanitizeFlashErrorString(strings.Join(lines, "\n")),
+ })
+ if err != nil {
+ log.Debug("render flash error: %v", err)
+ flashError = ctx.Tr("repo.issues.choose.ignore_invalid_templates")
+ }
+ return flashError
+}
+
// NewIssueChooseTemplate render creating issue from template page
func NewIssueChooseTemplate(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.issues.new")
ctx.Data["PageIsIssueList"] = true
- issueTemplates := ctx.IssueTemplatesFromDefaultBranch()
+ issueTemplates, errs := ctx.IssueTemplatesErrorsFromDefaultBranch()
ctx.Data["IssueTemplates"] = issueTemplates
+ if len(errs) > 0 {
+ ctx.Flash.Warning(renderErrorOfTemplates(ctx, errs), true)
+ }
+
if len(issueTemplates) == 0 {
// The "issues/new" and "issues/new/choose" share the same query parameters "project" and "milestone", if no template here, just redirect to the "issues/new" page with these parameters.
ctx.Redirect(fmt.Sprintf("%s/issues/new?%s", ctx.Repo.Repository.HTMLURL(), ctx.Req.URL.RawQuery), http.StatusSeeOther)
@@ -1031,6 +1063,13 @@ func NewIssuePost(ctx *context.Context) {
return
}
+ content := form.Content
+ if filename := ctx.Req.Form.Get("template-file"); filename != "" {
+ if template, err := issue_template.UnmarshalFromRepo(ctx.Repo.GitRepo, ctx.Repo.Repository.DefaultBranch, filename); err == nil {
+ content = issue_template.RenderToMarkdown(template, ctx.Req.Form)
+ }
+ }
+
issue := &issues_model.Issue{
RepoID: repo.ID,
Repo: repo,
@@ -1038,7 +1077,7 @@ func NewIssuePost(ctx *context.Context) {
PosterID: ctx.Doer.ID,
Poster: ctx.Doer,
MilestoneID: milestoneID,
- Content: form.Content,
+ Content: content,
Ref: form.Ref,
}
diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go
index 9b2c7c02cb..aa2c4cdb53 100644
--- a/routers/web/repo/pull.go
+++ b/routers/web/repo/pull.go
@@ -30,6 +30,7 @@ import (
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
+ issue_template "code.gitea.io/gitea/modules/issue/template"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/notification"
"code.gitea.io/gitea/modules/setting"
@@ -58,11 +59,23 @@ const (
var pullRequestTemplateCandidates = []string{
"PULL_REQUEST_TEMPLATE.md",
+ "PULL_REQUEST_TEMPLATE.yaml",
+ "PULL_REQUEST_TEMPLATE.yml",
"pull_request_template.md",
+ "pull_request_template.yaml",
+ "pull_request_template.yml",
".gitea/PULL_REQUEST_TEMPLATE.md",
+ ".gitea/PULL_REQUEST_TEMPLATE.yaml",
+ ".gitea/PULL_REQUEST_TEMPLATE.yml",
".gitea/pull_request_template.md",
+ ".gitea/pull_request_template.yaml",
+ ".gitea/pull_request_template.yml",
".github/PULL_REQUEST_TEMPLATE.md",
+ ".github/PULL_REQUEST_TEMPLATE.yaml",
+ ".github/PULL_REQUEST_TEMPLATE.yml",
".github/pull_request_template.md",
+ ".github/pull_request_template.yaml",
+ ".github/pull_request_template.yml",
}
func getRepository(ctx *context.Context, repoID int64) *repo_model.Repository {
@@ -1194,6 +1207,13 @@ func CompareAndPullRequestPost(ctx *context.Context) {
return
}
+ content := form.Content
+ if filename := ctx.Req.Form.Get("template-file"); filename != "" {
+ if template, err := issue_template.UnmarshalFromRepo(ctx.Repo.GitRepo, ctx.Repo.Repository.DefaultBranch, filename); err == nil {
+ content = issue_template.RenderToMarkdown(template, ctx.Req.Form)
+ }
+ }
+
pullIssue := &issues_model.Issue{
RepoID: repo.ID,
Repo: repo,
@@ -1202,7 +1222,7 @@ func CompareAndPullRequestPost(ctx *context.Context) {
Poster: ctx.Doer,
MilestoneID: milestoneID,
IsPull: true,
- Content: form.Content,
+ Content: content,
}
pullRequest := &issues_model.PullRequest{
HeadRepoID: ci.HeadRepo.ID,