Skip to content

Support Issue forms and PR forms #20987

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 44 commits into from
Sep 2, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
0ed8a3d
feat: extend issue template for yaml
wolfogre Aug 27, 2022
4efe0b4
feat: support yaml template
wolfogre Aug 27, 2022
1539e79
feat: render form to markdown
wolfogre Aug 27, 2022
bf89e5a
feat: support yaml template for pr
wolfogre Aug 27, 2022
3337a6c
chore: rename to Fields
wolfogre Aug 28, 2022
d25217d
feat: template unmarshal
wolfogre Aug 28, 2022
c0a3727
feat: split template
wolfogre Aug 28, 2022
7de200b
feat: render to markdown
wolfogre Aug 28, 2022
a949489
feat: use full name as template file name
wolfogre Aug 28, 2022
64190cd
chore: remove useless file
wolfogre Aug 28, 2022
ad7e58c
feat: use dropdown of fomantic ui
wolfogre Aug 28, 2022
f2d38f1
feat: update input style
wolfogre Aug 28, 2022
03ab606
docs: more comments
wolfogre Aug 28, 2022
64b44e2
fix: render text without render
wolfogre Aug 29, 2022
b0f5472
chore: fix lint error
wolfogre Aug 29, 2022
5e9e60e
fix: support use description as about in markdown
wolfogre Aug 29, 2022
1d1d7dc
fix: add field class in form
wolfogre Aug 29, 2022
3d65d9a
chore: generate swagger
wolfogre Aug 29, 2022
69df477
feat: validate template
wolfogre Aug 29, 2022
fde937f
feat: support is_nummber and regex
wolfogre Aug 29, 2022
b413dce
test: fix broken unit tests
wolfogre Aug 29, 2022
d764ed8
fix: ignore empty body of md template
wolfogre Aug 30, 2022
793b86a
fix: make multiple easymde editors work in one page
wolfogre Aug 30, 2022
4ffde84
feat: better UI
wolfogre Aug 30, 2022
516ccb4
fix: js error in pr form
wolfogre Aug 30, 2022
de06c52
chore: generate swagger
wolfogre Aug 30, 2022
c698b4e
feat: support regex validation
wolfogre Aug 31, 2022
632b86a
chore: generate swagger
wolfogre Aug 31, 2022
d0d7b0d
fix: refresh each markdown editor
wolfogre Aug 31, 2022
645176f
chore: give up required validation
wolfogre Aug 31, 2022
fdd39f4
fix: correct issue template candidates
wolfogre Aug 31, 2022
a0101e8
fix: correct checkboxes style
wolfogre Aug 31, 2022
23da624
chore: ignore .hugo_build.lock in docs
wolfogre Aug 31, 2022
f0ceaf6
docs: separate out a new doc for merge templates
wolfogre Aug 31, 2022
be266dd
docs: introduce syntax of yaml template
wolfogre Aug 31, 2022
59ce6b9
feat: show a alert for invalid templates
wolfogre Sep 1, 2022
a0f3c8e
test: add case for a valid template
wolfogre Sep 1, 2022
ebfbb85
fix: correct attributes of required checkbox
wolfogre Sep 2, 2022
fd14fbe
fix: add class not-under-easymde for dropzone
wolfogre Sep 2, 2022
e2529ee
fix: use more back-quotes
wolfogre Sep 2, 2022
1f3943c
chore: remove translation in zh-CN
wolfogre Sep 2, 2022
233337d
fix EasyMDE statusbar margin
wxiaoguang Sep 2, 2022
1c3b2e4
fix: remove repeated blocks
wolfogre Sep 2, 2022
7018c82
fix: reuse regex for quotes
wolfogre Sep 2, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat: template unmarshal
  • Loading branch information
wolfogre committed Sep 2, 2022
commit d25217d9b65f543a96fd3c850924e507ea9ddccc
62 changes: 14 additions & 48 deletions modules/context/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,9 @@ import (
"context"
"fmt"
"html"
"io"
"net/http"
"net/url"
"path"
"path/filepath"
"strings"

"code.gitea.io/gitea/models"
Expand All @@ -27,6 +25,7 @@ import (
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/git"
code_indexer "code.gitea.io/gitea/modules/indexer/code"
"code.gitea.io/gitea/modules/issue/template"
"code.gitea.io/gitea/modules/log"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
Expand Down Expand Up @@ -1035,8 +1034,8 @@ func UnitTypes() func(ctx *Context) {
}

// IssueTemplatesFromDefaultBranch checks for issue templates in the repo's default branch
func (ctx *Context) IssueTemplatesFromDefaultBranch() []api.IssueTemplate {
var issueTemplates []api.IssueTemplate
func (ctx *Context) IssueTemplatesFromDefaultBranch() []*api.IssueTemplate {
var issueTemplates []*api.IssueTemplate

if ctx.Repo.Repository.IsEmpty {
return issueTemplates
Expand All @@ -1060,52 +1059,19 @@ func (ctx *Context) IssueTemplatesFromDefaultBranch() []api.IssueTemplate {
return issueTemplates
}
for _, entry := range entries {
if t := ctx.extractIssueTemplate(entry); t != nil {
issueTemplates = append(issueTemplates, *t)
it := &api.IssueTemplate{
FileName: entry.Name(),
}
if it.Type() == "" {
continue
}
it, err := template.UnmarshalFromEntry(entry)
if err != nil {
log.Debug("unmarshal template from %s: %v", entry.Name(), err)
} else if it.Valid() {
issueTemplates = append(issueTemplates, it)
}
}
}
return issueTemplates
}

func (ctx *Context) extractIssueTemplate(entry *git.TreeEntry) *api.IssueTemplate {
it := &api.IssueTemplate{
FileName: entry.Name(),
}
if it.Type() == "" {
return nil
}

if name := filepath.Base(it.FileName); name == "config.yaml" || name == "config.yml" {
// ignore config.yaml which is a special configuration file
return nil
}

if entry.Blob().Size() >= setting.UI.MaxDisplayFileSize {
log.Debug("Issue template is too large: %s", entry.Name())
return nil
}

r, err := entry.Blob().DataAsync()
if err != nil {
log.Debug("DataAsync: %v", err)
return nil
}
defer r.Close()

data, err := io.ReadAll(r)
if err != nil {
log.Debug("ReadAll: %v", err)
return nil
}

if err := it.Fill(data); err != nil {
log.Debug("fill template: %v", err)
return nil
}

if !it.Valid() {
return nil
}
return it
}
92 changes: 92 additions & 0 deletions modules/issue/template/unmarshal.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package template

import (
"fmt"
"io"
"strconv"

"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"

"gopkg.in/yaml.v2"
)

// TODO
func Unmarshal(filename string, content []byte) (*api.IssueTemplate, error) {
it := &api.IssueTemplate{
FileName: filename,
}

if typ := it.Type(); typ == "md" {
templateBody, err := markdown.ExtractMetadata(string(content), it)
if err != nil {
return nil, fmt.Errorf("extract metadata: %w", err)
}
it.Content = templateBody
} else if typ == "yaml" {
if err := yaml.Unmarshal(content, it); err != nil {
return nil, fmt.Errorf("yaml unmarshal: %w", err)
}
if it.About == "" {
// Compatible with treating description as about
compatibleTemplate := &struct {
About string `yaml:"description"`
}{}
if err := yaml.Unmarshal(content, compatibleTemplate); err == nil && compatibleTemplate.About != "" {
it.About = compatibleTemplate.About
}
}
for i, v := range it.Fields {
if v.ID == "" {
v.ID = strconv.Itoa(i)
}
}
}

return it, nil
}

// TODO
func UnmarshalFromEntry(entry *git.TreeEntry) (*api.IssueTemplate, error) {
if size := entry.Blob().Size(); size > setting.UI.MaxDisplayFileSize {
return nil, fmt.Errorf("too large: %v > MaxDisplayFileSize", size)
}

r, err := entry.Blob().DataAsync()
if err != nil {
return nil, fmt.Errorf("data async: %w", err)
}
defer r.Close()

content, err := io.ReadAll(r)
if err != nil {
return nil, fmt.Errorf("read all: %w", err)
}

return Unmarshal(entry.Name(), content)
}

// TODO
func UnmarshalFromCommit(commit *git.Commit, filename string) (*api.IssueTemplate, error) {
entry, err := commit.GetTreeEntryByPath(filename)
if err != nil {
return nil, fmt.Errorf("get entry for %q: %w", filename, err)
}
return UnmarshalFromEntry(entry)
}

// TODO
func UnmarshalFromRepo(repo *git.Repository, branch, filename string) (*api.IssueTemplate, error) {
commit, err := repo.GetBranchCommit(branch)
if err != nil {
return nil, fmt.Errorf("get commit on branch %q: %w", branch, err)
}

return UnmarshalFromCommit(commit, filename)
}
77 changes: 5 additions & 72 deletions modules/structs/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,8 @@ package structs
import (
"fmt"
"path/filepath"
"strconv"
"strings"
"time"

"gopkg.in/yaml.v2"
)

// StateType issue state type
Expand Down Expand Up @@ -229,79 +226,15 @@ func (it *IssueTemplate) Valid() bool {

// Type returns the type of IssueTemplate, it could be "md", "yaml" or empty for known
func (it *IssueTemplate) Type() string {
if it.Name == "config.yaml" || it.Name == "config.yml" {
// TODO: should it be?
// ignore config.yaml which is a special configuration file
return ""
}
if ext := filepath.Ext(it.FileName); ext == ".md" {
return "md"
} else if ext == ".yaml" || ext == ".yml" {
return "yaml"
}
return ""
}

func (it *IssueTemplate) Fill(content []byte) error {
if typ := it.Type(); typ == "md" {
templateBody, err := it.extractMetadata(string(content), it)
if err != nil {
return fmt.Errorf("extract metadata: %w", err)
}
it.Content = templateBody
} else if typ == "yaml" {
if err := yaml.Unmarshal(content, it); err != nil {
return fmt.Errorf("yaml unmarshal: %w", err)
}
if it.About == "" {
// Compatible with treating description as about
compatibleTemplate := &struct {
About string `yaml:"description"`
}{}
if err := yaml.Unmarshal(content, compatibleTemplate); err == nil && compatibleTemplate.About != "" {
it.About = compatibleTemplate.About
}
}
for i, v := range it.Fields {
if v.ID == "" {
v.ID = strconv.Itoa(i)
}
}
}
return nil
}

// extractMetadata consumes a markdown file, parses YAML frontmatter,
// and returns the frontmatter metadata separated from the markdown content.
// Copy from markdown.ExtractMetadata to avoid import cycle.
func (*IssueTemplate) extractMetadata(contents string, out interface{}) (string, error) {
isYAMLSeparator := func(line string) bool {
line = strings.TrimSpace(line)
for i := 0; i < len(line); i++ {
if line[i] != '-' {
return false
}
}
return len(line) > 2
}

var front, body []string
lines := strings.Split(contents, "\n")
for idx, line := range lines {
if idx == 0 {
// First line has to be a separator
if !isYAMLSeparator(line) {
return "", fmt.Errorf("frontmatter must start with a separator line")
}
continue
}
if isYAMLSeparator(line) {
front, body = lines[1:idx], lines[idx+1:]
break
}
}

if len(front) == 0 {
return "", fmt.Errorf("could not determine metadata")
}

if err := yaml.Unmarshal([]byte(strings.Join(front, "\n")), out); err != nil {
return "", err
}
return strings.Join(body, "\n"), nil
}
58 changes: 12 additions & 46 deletions routers/web/repo/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
stdCtx "context"
"errors"
"fmt"
"io"
"math/big"
"net/http"
"net/url"
Expand All @@ -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"
Expand Down Expand Up @@ -734,34 +734,6 @@ 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)
if err != nil {
return "", false
}
return string(bytes), true
}

func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleDirs, possibleFiles []string) {
templateCandidates := make([]string, 0, len(possibleDirs)+len(possibleFiles))
if t := ctx.FormString("template"); t != "" {
Expand All @@ -771,23 +743,19 @@ func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleDirs,
}
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 {
template, err := issue_template.UnmarshalFromRepo(ctx.Repo.GitRepo, ctx.Repo.Repository.DefaultBranch, filename)
if err == nil {
continue
}
template := api.IssueTemplate{
FileName: filename,
}
if err := template.Fill([]byte(templateContent)); err != nil {
log.Debug("could fill template from %s [%s]: %v", filename, ctx.Repo.Repository.FullName(), err)
ctx.Data[ctxDataKey] = templateContent
return
if !template.Valid() {
continue
}

ctx.Data[issueTemplateTitleKey] = template.Title
ctx.Data[ctxDataKey] = template.Content

if template.Type() == "yaml" {
ctx.Data["TemplateForm"] = template // TODO: maybe use template.Body
ctx.Data["Fields"] = template.Fields
}
labelIDs := make([]string, 0, len(template.Labels))
if repoLabels, err := issues_model.GetLabelsByRepoID(ctx, ctx.Repo.Repository.ID, "", db.ListOptions{}); err == nil {
Expand Down Expand Up @@ -1107,13 +1075,11 @@ func NewIssuePost(ctx *context.Context) {
// Returns an empty string if user submitted a non-form issue
func renderIssueFormValues(ctx *context.Context, form *url.Values) (string, error) {
filename := form.Get("template-file")
template := api.IssueTemplate{
FileName: filename,
}
if templateContent, found := getFileContentFromDefaultBranch(ctx, form.Get("template-file")); !found {
return "", fmt.Errorf("template file %q not found", filename)
} else if err := template.Fill([]byte(templateContent)); err != nil {
return "", fmt.Errorf("fill template with %q: %w", filename, err)
template, err := issue_template.UnmarshalFromRepo(ctx.Repo.GitRepo, ctx.Repo.Repository.DefaultBranch, form.Get("template-file"))
if err != nil {
return "", fmt.Errorf("unmarshal template %q: %w", filename, err)
} else if !template.Valid() {
return "", fmt.Errorf("invalid template %q", filename)
}

// Render values
Expand Down