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>
This commit is contained in:
parent
b7a4b45ff8
commit
84447df4d3
30 changed files with 1776 additions and 176 deletions
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
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 nil
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
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
|
||||
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...)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
templateErrs := map[string]error{}
|
||||
for _, filename := range templateCandidates {
|
||||
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 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,
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue