code.gitea.io/gitea@v1.21.7/services/issue/template.go (about) 1 // Copyright 2023 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package issue 5 6 import ( 7 "fmt" 8 "io" 9 "net/url" 10 "path" 11 "strings" 12 13 "code.gitea.io/gitea/models/repo" 14 "code.gitea.io/gitea/modules/git" 15 "code.gitea.io/gitea/modules/issue/template" 16 "code.gitea.io/gitea/modules/log" 17 api "code.gitea.io/gitea/modules/structs" 18 19 "gopkg.in/yaml.v3" 20 ) 21 22 // templateDirCandidates issue templates directory 23 var templateDirCandidates = []string{ 24 "ISSUE_TEMPLATE", 25 "issue_template", 26 ".gitea/ISSUE_TEMPLATE", 27 ".gitea/issue_template", 28 ".github/ISSUE_TEMPLATE", 29 ".github/issue_template", 30 ".gitlab/ISSUE_TEMPLATE", 31 ".gitlab/issue_template", 32 } 33 34 var templateConfigCandidates = []string{ 35 ".gitea/ISSUE_TEMPLATE/config", 36 ".gitea/issue_template/config", 37 ".github/ISSUE_TEMPLATE/config", 38 ".github/issue_template/config", 39 } 40 41 func GetDefaultTemplateConfig() api.IssueConfig { 42 return api.IssueConfig{ 43 BlankIssuesEnabled: true, 44 ContactLinks: make([]api.IssueConfigContactLink, 0), 45 } 46 } 47 48 // GetTemplateConfig loads the given issue config file. 49 // It never returns a nil config. 50 func GetTemplateConfig(gitRepo *git.Repository, path string, commit *git.Commit) (api.IssueConfig, error) { 51 if gitRepo == nil { 52 return GetDefaultTemplateConfig(), nil 53 } 54 55 var err error 56 57 treeEntry, err := commit.GetTreeEntryByPath(path) 58 if err != nil { 59 return GetDefaultTemplateConfig(), err 60 } 61 62 reader, err := treeEntry.Blob().DataAsync() 63 if err != nil { 64 log.Debug("DataAsync: %v", err) 65 return GetDefaultTemplateConfig(), nil 66 } 67 68 defer reader.Close() 69 70 configContent, err := io.ReadAll(reader) 71 if err != nil { 72 return GetDefaultTemplateConfig(), err 73 } 74 75 issueConfig := GetDefaultTemplateConfig() 76 if err := yaml.Unmarshal(configContent, &issueConfig); err != nil { 77 return GetDefaultTemplateConfig(), err 78 } 79 80 for pos, link := range issueConfig.ContactLinks { 81 if link.Name == "" { 82 return GetDefaultTemplateConfig(), fmt.Errorf("contact_link at position %d is missing name key", pos+1) 83 } 84 85 if link.URL == "" { 86 return GetDefaultTemplateConfig(), fmt.Errorf("contact_link at position %d is missing url key", pos+1) 87 } 88 89 if link.About == "" { 90 return GetDefaultTemplateConfig(), fmt.Errorf("contact_link at position %d is missing about key", pos+1) 91 } 92 93 _, err = url.ParseRequestURI(link.URL) 94 if err != nil { 95 return GetDefaultTemplateConfig(), fmt.Errorf("%s is not a valid URL", link.URL) 96 } 97 } 98 99 return issueConfig, nil 100 } 101 102 // IsTemplateConfig returns if the given path is a issue config file. 103 func IsTemplateConfig(path string) bool { 104 for _, configName := range templateConfigCandidates { 105 if path == configName+".yaml" || path == configName+".yml" { 106 return true 107 } 108 } 109 return false 110 } 111 112 // ParseTemplatesFromDefaultBranch parses the issue templates in the repo's default branch, 113 // returns valid templates and the errors of invalid template files (the errors map is guaranteed to be non-nil). 114 func ParseTemplatesFromDefaultBranch(repo *repo.Repository, gitRepo *git.Repository) (ret struct { 115 IssueTemplates []*api.IssueTemplate 116 TemplateErrors map[string]error 117 }, 118 ) { 119 ret.TemplateErrors = map[string]error{} 120 if repo.IsEmpty { 121 return ret 122 } 123 124 commit, err := gitRepo.GetBranchCommit(repo.DefaultBranch) 125 if err != nil { 126 return ret 127 } 128 129 for _, dirName := range templateDirCandidates { 130 tree, err := commit.SubTree(dirName) 131 if err != nil { 132 log.Debug("get sub tree of %s: %v", dirName, err) 133 continue 134 } 135 entries, err := tree.ListEntries() 136 if err != nil { 137 log.Debug("list entries in %s: %v", dirName, err) 138 return ret 139 } 140 for _, entry := range entries { 141 if !template.CouldBe(entry.Name()) { 142 continue 143 } 144 fullName := path.Join(dirName, entry.Name()) 145 if it, err := template.UnmarshalFromEntry(entry, dirName); err != nil { 146 ret.TemplateErrors[fullName] = err 147 } else { 148 if !strings.HasPrefix(it.Ref, "refs/") { // Assume that the ref intended is always a branch - for tags users should use refs/tags/<ref> 149 it.Ref = git.BranchPrefix + it.Ref 150 } 151 ret.IssueTemplates = append(ret.IssueTemplates, it) 152 } 153 } 154 } 155 return ret 156 } 157 158 // GetTemplateConfigFromDefaultBranch returns the issue config for this repo. 159 // It never returns a nil config. 160 func GetTemplateConfigFromDefaultBranch(repo *repo.Repository, gitRepo *git.Repository) (api.IssueConfig, error) { 161 if repo.IsEmpty { 162 return GetDefaultTemplateConfig(), nil 163 } 164 165 commit, err := gitRepo.GetBranchCommit(repo.DefaultBranch) 166 if err != nil { 167 return GetDefaultTemplateConfig(), err 168 } 169 170 for _, configName := range templateConfigCandidates { 171 if _, err := commit.GetTreeEntryByPath(configName + ".yaml"); err == nil { 172 return GetTemplateConfig(gitRepo, configName+".yaml", commit) 173 } 174 175 if _, err := commit.GetTreeEntryByPath(configName + ".yml"); err == nil { 176 return GetTemplateConfig(gitRepo, configName+".yml", commit) 177 } 178 } 179 180 return GetDefaultTemplateConfig(), nil 181 } 182 183 func HasTemplatesOrContactLinks(repo *repo.Repository, gitRepo *git.Repository) bool { 184 ret := ParseTemplatesFromDefaultBranch(repo, gitRepo) 185 if len(ret.IssueTemplates) > 0 { 186 return true 187 } 188 189 issueConfig, _ := GetTemplateConfigFromDefaultBranch(repo, gitRepo) 190 return len(issueConfig.ContactLinks) > 0 191 }