diff options
author | Jason Song <i@wolfogre.com> | 2022-09-02 15:58:49 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-09-02 15:58:49 +0800 |
commit | 84447df4d366324ab81894b028b00fd66be85caf (patch) | |
tree | 5291442a85faccb6bc17b54ca71a53c16530dfe3 /routers/web/repo | |
parent | b7a4b45ff83dc19febcfb85279215ea6bd224033 (diff) | |
download | gitea-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.go | 6 | ||||
-rw-r--r-- | routers/web/repo/issue.go | 173 | ||||
-rw-r--r-- | routers/web/repo/pull.go | 22 |
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, |