gitea/services/issue/template.go

192 lines
5.1 KiB
Go

// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package issue
import (
"fmt"
"io"
"net/url"
"path"
"strings"
"code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/issue/template"
"code.gitea.io/gitea/modules/log"
api "code.gitea.io/gitea/modules/structs"
"gopkg.in/yaml.v3"
)
// templateDirCandidates issue templates directory
var templateDirCandidates = []string{
"ISSUE_TEMPLATE",
"issue_template",
".gitea/ISSUE_TEMPLATE",
".gitea/issue_template",
".github/ISSUE_TEMPLATE",
".github/issue_template",
".gitlab/ISSUE_TEMPLATE",
".gitlab/issue_template",
}
var templateConfigCandidates = []string{
".gitea/ISSUE_TEMPLATE/config",
".gitea/issue_template/config",
".github/ISSUE_TEMPLATE/config",
".github/issue_template/config",
}
func GetDefaultTemplateConfig() api.IssueConfig {
return api.IssueConfig{
BlankIssuesEnabled: true,
ContactLinks: make([]api.IssueConfigContactLink, 0),
}
}
// GetTemplateConfig loads the given issue config file.
// It never returns a nil config.
func GetTemplateConfig(gitRepo *git.Repository, path string, commit *git.Commit) (api.IssueConfig, error) {
if gitRepo == nil {
return GetDefaultTemplateConfig(), nil
}
var err error
treeEntry, err := commit.GetTreeEntryByPath(path)
if err != nil {
return GetDefaultTemplateConfig(), err
}
reader, err := treeEntry.Blob().DataAsync()
if err != nil {
log.Debug("DataAsync: %v", err)
return GetDefaultTemplateConfig(), nil
}
defer reader.Close()
configContent, err := io.ReadAll(reader)
if err != nil {
return GetDefaultTemplateConfig(), err
}
issueConfig := GetDefaultTemplateConfig()
if err := yaml.Unmarshal(configContent, &issueConfig); err != nil {
return GetDefaultTemplateConfig(), err
}
for pos, link := range issueConfig.ContactLinks {
if link.Name == "" {
return GetDefaultTemplateConfig(), fmt.Errorf("contact_link at position %d is missing name key", pos+1)
}
if link.URL == "" {
return GetDefaultTemplateConfig(), fmt.Errorf("contact_link at position %d is missing url key", pos+1)
}
if link.About == "" {
return GetDefaultTemplateConfig(), fmt.Errorf("contact_link at position %d is missing about key", pos+1)
}
_, err = url.ParseRequestURI(link.URL)
if err != nil {
return GetDefaultTemplateConfig(), fmt.Errorf("%s is not a valid URL", link.URL)
}
}
return issueConfig, nil
}
// IsTemplateConfig returns if the given path is a issue config file.
func IsTemplateConfig(path string) bool {
for _, configName := range templateConfigCandidates {
if path == configName+".yaml" || path == configName+".yml" {
return true
}
}
return false
}
// ParseTemplatesFromDefaultBranch parses the issue templates in the repo's default branch,
// returns valid templates and the errors of invalid template files (the errors map is guaranteed to be non-nil).
func ParseTemplatesFromDefaultBranch(repo *repo.Repository, gitRepo *git.Repository) (ret struct {
IssueTemplates []*api.IssueTemplate
TemplateErrors map[string]error
},
) {
ret.TemplateErrors = map[string]error{}
if repo.IsEmpty {
return ret
}
commit, err := gitRepo.GetBranchCommit(repo.DefaultBranch)
if err != nil {
return ret
}
for _, dirName := range templateDirCandidates {
tree, err := commit.SubTree(dirName)
if err != nil {
log.Debug("get sub tree of %s: %v", dirName, err)
continue
}
entries, err := tree.ListEntries()
if err != nil {
log.Debug("list entries in %s: %v", dirName, err)
return ret
}
for _, entry := range entries {
if !template.CouldBe(entry.Name()) {
continue
}
fullName := path.Join(dirName, entry.Name())
if it, err := template.UnmarshalFromEntry(entry, dirName); err != nil {
ret.TemplateErrors[fullName] = err
} else {
if !strings.HasPrefix(it.Ref, "refs/") { // Assume that the ref intended is always a branch - for tags users should use refs/tags/<ref>
it.Ref = git.BranchPrefix + it.Ref
}
ret.IssueTemplates = append(ret.IssueTemplates, it)
}
}
}
return ret
}
// GetTemplateConfigFromDefaultBranch returns the issue config for this repo.
// It never returns a nil config.
func GetTemplateConfigFromDefaultBranch(repo *repo.Repository, gitRepo *git.Repository) (api.IssueConfig, error) {
if repo.IsEmpty {
return GetDefaultTemplateConfig(), nil
}
commit, err := gitRepo.GetBranchCommit(repo.DefaultBranch)
if err != nil {
return GetDefaultTemplateConfig(), err
}
for _, configName := range templateConfigCandidates {
if _, err := commit.GetTreeEntryByPath(configName + ".yaml"); err == nil {
return GetTemplateConfig(gitRepo, configName+".yaml", commit)
}
if _, err := commit.GetTreeEntryByPath(configName + ".yml"); err == nil {
return GetTemplateConfig(gitRepo, configName+".yml", commit)
}
}
return GetDefaultTemplateConfig(), nil
}
func HasTemplatesOrContactLinks(repo *repo.Repository, gitRepo *git.Repository) bool {
ret := ParseTemplatesFromDefaultBranch(repo, gitRepo)
if len(ret.IssueTemplates) > 0 {
return true
}
issueConfig, _ := GetTemplateConfigFromDefaultBranch(repo, gitRepo)
return len(issueConfig.ContactLinks) > 0
}