mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-09-18 16:55:55 +00:00
For pull request templates, Forgejo currently does not look in the `docs` directory, but [GitHub does](https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/creating-a-pull-request-template-for-your-repository#adding-a-pull-request-template). By making Forgejo also look there, it becomes possible to have the same repository work on both sites, without the need for vendor-specific paths. There was duplication in the list of accepted file paths. On the one hand it’s nice for greppability that they are all spelled out, but it does mean adding 6 variants, I thought it would be more maintainable to deduplicate the Cartesian product. I added one fully spelled out path in the comment to still maintain some greppability. Resolves forgejo/forgejo#8284 Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/8863 Reviewed-by: Gusted <gusted@noreply.codeberg.org> Co-authored-by: Ruud van Asseldonk <dev@veniogames.com> Co-committed-by: Ruud van Asseldonk <dev@veniogames.com>
195 lines
5.3 KiB
Go
195 lines
5.3 KiB
Go
// Copyright 2023 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package issue
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/url"
|
|
"path"
|
|
"strings"
|
|
|
|
"forgejo.org/models/repo"
|
|
"forgejo.org/modules/git"
|
|
"forgejo.org/modules/issue/template"
|
|
"forgejo.org/modules/log"
|
|
api "forgejo.org/modules/structs"
|
|
|
|
"go.yaml.in/yaml/v3"
|
|
)
|
|
|
|
// templateDirCandidates issue templates directory
|
|
var templateDirCandidates = []string{
|
|
"ISSUE_TEMPLATE",
|
|
"issue_template",
|
|
".forgejo/ISSUE_TEMPLATE",
|
|
".forgejo/issue_template",
|
|
".gitea/ISSUE_TEMPLATE",
|
|
".gitea/issue_template",
|
|
".github/ISSUE_TEMPLATE",
|
|
".github/issue_template",
|
|
".gitlab/ISSUE_TEMPLATE",
|
|
".gitlab/issue_template",
|
|
"docs/ISSUE_TEMPLATE",
|
|
"docs/issue_template",
|
|
}
|
|
|
|
var templateConfigCandidates = []string{
|
|
".forgejo/ISSUE_TEMPLATE/config",
|
|
".forgejo/issue_template/config",
|
|
".gitea/ISSUE_TEMPLATE/config",
|
|
".gitea/issue_template/config",
|
|
".github/ISSUE_TEMPLATE/config",
|
|
".github/issue_template/config",
|
|
"docs/ISSUE_TEMPLATE/config",
|
|
"docs/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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// GetTemplatesFromDefaultBranch checks for issue templates in the repo's default branch,
|
|
// returns valid templates and the errors of invalid template files.
|
|
func GetTemplatesFromDefaultBranch(repo *repo.Repository, gitRepo *git.Repository) ([]*api.IssueTemplate, map[string]error) {
|
|
var issueTemplates []*api.IssueTemplate
|
|
|
|
if repo.IsEmpty {
|
|
return issueTemplates, nil
|
|
}
|
|
|
|
commit, err := gitRepo.GetBranchCommit(repo.DefaultBranch)
|
|
if err != nil {
|
|
return issueTemplates, nil
|
|
}
|
|
|
|
invalidFiles := map[string]error{}
|
|
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 issueTemplates, nil
|
|
}
|
|
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 {
|
|
invalidFiles[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
|
|
}
|
|
issueTemplates = append(issueTemplates, it)
|
|
}
|
|
}
|
|
}
|
|
return issueTemplates, invalidFiles
|
|
}
|
|
|
|
// 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, _ := GetTemplatesFromDefaultBranch(repo, gitRepo)
|
|
if len(ret) > 0 {
|
|
return true
|
|
}
|
|
|
|
issueConfig, _ := GetTemplateConfigFromDefaultBranch(repo, gitRepo)
|
|
return len(issueConfig.ContactLinks) > 0
|
|
}
|