From def4956122ea2364f247712b13856383ee496add Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Tue, 9 May 2023 07:30:14 +0800 Subject: [PATCH] Improve Gitea's web context, decouple "issue template" code into service package (#24590) 1. Remove unused fields/methods in web context. 2. Make callers call target function directly instead of the light wrapper like "IsUserRepoReaderSpecific" 3. The "issue template" code shouldn't be put in the "modules/context" package, so move them to the service package. --------- Co-authored-by: Giteabot --- modules/context/context.go | 21 ++-- modules/context/context_cookie.go | 19 --- modules/context/context_model.go | 109 ----------------- modules/context/repo.go | 93 --------------- routers/api/v1/api.go | 6 +- routers/api/v1/repo/repo.go | 13 +- routers/web/repo/issue.go | 16 +-- routers/web/repo/milestone.go | 5 +- routers/web/repo/view.go | 5 +- services/issue/template.go | 189 ++++++++++++++++++++++++++++++ 10 files changed, 227 insertions(+), 249 deletions(-) create mode 100644 services/issue/template.go diff --git a/modules/context/context.go b/modules/context/context.go index 3e1b48dcde..9ba1985f36 100644 --- a/modules/context/context.go +++ b/modules/context/context.go @@ -36,19 +36,20 @@ type Render interface { // Context represents context of a request. type Context struct { - Resp ResponseWriter - Req *http.Request + Resp ResponseWriter + Req *http.Request + Render Render + Data middleware.ContextData // data used by MVC templates PageData map[string]any // data used by JavaScript modules in one page, it's `window.config.pageData` - Render Render - Locale translation.Locale - Cache cache.Cache - Csrf CSRFProtector - Flash *middleware.Flash - Session session.Store - Link string // current request URL - EscapedLink string + Locale translation.Locale + Cache cache.Cache + Csrf CSRFProtector + Flash *middleware.Flash + Session session.Store + + Link string // current request URL (without query string) Doer *user_model.User IsSigned bool IsBasicAuth bool diff --git a/modules/context/context_cookie.go b/modules/context/context_cookie.go index 5cb4ea0aca..9ce67a5298 100644 --- a/modules/context/context_cookie.go +++ b/modules/context/context_cookie.go @@ -6,7 +6,6 @@ package context import ( "encoding/hex" "net/http" - "strconv" "strings" "code.gitea.io/gitea/modules/setting" @@ -85,21 +84,3 @@ func (ctx *Context) CookieEncrypt(secret, value string) string { return hex.EncodeToString(text) } - -// GetCookieInt returns cookie result in int type. -func (ctx *Context) GetCookieInt(name string) int { - r, _ := strconv.Atoi(ctx.GetSiteCookie(name)) - return r -} - -// GetCookieInt64 returns cookie result in int64 type. -func (ctx *Context) GetCookieInt64(name string) int64 { - r, _ := strconv.ParseInt(ctx.GetSiteCookie(name), 10, 64) - return r -} - -// GetCookieFloat64 returns cookie result in float64 type. -func (ctx *Context) GetCookieFloat64(name string) float64 { - v, _ := strconv.ParseFloat(ctx.GetSiteCookie(name), 64) - return v -} diff --git a/modules/context/context_model.go b/modules/context/context_model.go index 5ba98f7e01..4f70aac516 100644 --- a/modules/context/context_model.go +++ b/modules/context/context_model.go @@ -4,14 +4,7 @@ package context import ( - "path" - "strings" - "code.gitea.io/gitea/models/unit" - "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" ) // IsUserSiteAdmin returns true if current user is a site admin @@ -19,11 +12,6 @@ func (ctx *Context) IsUserSiteAdmin() bool { return ctx.IsSigned && ctx.Doer.IsAdmin } -// IsUserRepoOwner returns true if current user owns current repo -func (ctx *Context) IsUserRepoOwner() bool { - return ctx.Repo.IsOwner() -} - // IsUserRepoAdmin returns true if current user is admin in current repo func (ctx *Context) IsUserRepoAdmin() bool { return ctx.Repo.IsAdmin() @@ -39,100 +27,3 @@ func (ctx *Context) IsUserRepoWriter(unitTypes []unit.Type) bool { return false } - -// IsUserRepoReaderSpecific returns true if current user can read current repo's specific part -func (ctx *Context) IsUserRepoReaderSpecific(unitType unit.Type) bool { - return ctx.Repo.CanRead(unitType) -} - -// IsUserRepoReaderAny returns true if current user can read any part of current repo -func (ctx *Context) IsUserRepoReaderAny() bool { - return ctx.Repo.HasAccess() -} - -// IssueTemplatesFromDefaultBranch checks for valid issue templates in the repo's default branch, -func (ctx *Context) IssueTemplatesFromDefaultBranch() []*api.IssueTemplate { - ret, _ := ctx.IssueTemplatesErrorsFromDefaultBranch() - return ret -} - -// IssueTemplatesErrorsFromDefaultBranch checks for issue templates in the repo's default branch, -// returns valid templates and the errors of invalid template files. -func (ctx *Context) IssueTemplatesErrorsFromDefaultBranch() ([]*api.IssueTemplate, map[string]error) { - var issueTemplates []*api.IssueTemplate - - if ctx.Repo.Repository.IsEmpty { - return issueTemplates, nil - } - - if ctx.Repo.Commit == nil { - var err error - ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch) - if err != nil { - return issueTemplates, nil - } - } - - invalidFiles := map[string]error{} - for _, dirName := range IssueTemplateDirCandidates { - tree, err := ctx.Repo.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/ - it.Ref = git.BranchPrefix + it.Ref - } - issueTemplates = append(issueTemplates, it) - } - } - } - return issueTemplates, invalidFiles -} - -// IssueConfigFromDefaultBranch returns the issue config for this repo. -// It never returns a nil config. -func (ctx *Context) IssueConfigFromDefaultBranch() (api.IssueConfig, error) { - if ctx.Repo.Repository.IsEmpty { - return GetDefaultIssueConfig(), nil - } - - commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch) - if err != nil { - return GetDefaultIssueConfig(), err - } - - for _, configName := range IssueConfigCandidates { - if _, err := commit.GetTreeEntryByPath(configName + ".yaml"); err == nil { - return ctx.Repo.GetIssueConfig(configName+".yaml", commit) - } - - if _, err := commit.GetTreeEntryByPath(configName + ".yml"); err == nil { - return ctx.Repo.GetIssueConfig(configName+".yml", commit) - } - } - - return GetDefaultIssueConfig(), nil -} - -func (ctx *Context) HasIssueTemplatesOrContactLinks() bool { - if len(ctx.IssueTemplatesFromDefaultBranch()) > 0 { - return true - } - - issueConfig, _ := ctx.IssueConfigFromDefaultBranch() - return len(issueConfig.ContactLinks) > 0 -} diff --git a/modules/context/repo.go b/modules/context/repo.go index b33341c245..84e07ab422 100644 --- a/modules/context/repo.go +++ b/modules/context/repo.go @@ -8,7 +8,6 @@ import ( "context" "fmt" "html" - "io" "net/http" "net/url" "path" @@ -28,33 +27,12 @@ import ( "code.gitea.io/gitea/modules/log" repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" - api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" asymkey_service "code.gitea.io/gitea/services/asymkey" "github.com/editorconfig/editorconfig-core-go/v2" - "gopkg.in/yaml.v3" ) -// IssueTemplateDirCandidates issue templates directory -var IssueTemplateDirCandidates = []string{ - "ISSUE_TEMPLATE", - "issue_template", - ".gitea/ISSUE_TEMPLATE", - ".gitea/issue_template", - ".github/ISSUE_TEMPLATE", - ".github/issue_template", - ".gitlab/ISSUE_TEMPLATE", - ".gitlab/issue_template", -} - -var IssueConfigCandidates = []string{ - ".gitea/ISSUE_TEMPLATE/config", - ".gitea/issue_template/config", - ".github/ISSUE_TEMPLATE/config", - ".github/issue_template/config", -} - // PullRequest contains information to make a pull request type PullRequest struct { BaseRepo *repo_model.Repository @@ -1061,74 +1039,3 @@ func UnitTypes() func(ctx *Context) { ctx.Data["UnitTypeActions"] = unit_model.TypeActions } } - -func GetDefaultIssueConfig() api.IssueConfig { - return api.IssueConfig{ - BlankIssuesEnabled: true, - ContactLinks: make([]api.IssueConfigContactLink, 0), - } -} - -// GetIssueConfig loads the given issue config file. -// It never returns a nil config. -func (r *Repository) GetIssueConfig(path string, commit *git.Commit) (api.IssueConfig, error) { - if r.GitRepo == nil { - return GetDefaultIssueConfig(), nil - } - - var err error - - treeEntry, err := commit.GetTreeEntryByPath(path) - if err != nil { - return GetDefaultIssueConfig(), err - } - - reader, err := treeEntry.Blob().DataAsync() - if err != nil { - log.Debug("DataAsync: %v", err) - return GetDefaultIssueConfig(), nil - } - - defer reader.Close() - - configContent, err := io.ReadAll(reader) - if err != nil { - return GetDefaultIssueConfig(), err - } - - issueConfig := api.IssueConfig{} - if err := yaml.Unmarshal(configContent, &issueConfig); err != nil { - return GetDefaultIssueConfig(), err - } - - for pos, link := range issueConfig.ContactLinks { - if link.Name == "" { - return GetDefaultIssueConfig(), fmt.Errorf("contact_link at position %d is missing name key", pos+1) - } - - if link.URL == "" { - return GetDefaultIssueConfig(), fmt.Errorf("contact_link at position %d is missing url key", pos+1) - } - - if link.About == "" { - return GetDefaultIssueConfig(), fmt.Errorf("contact_link at position %d is missing about key", pos+1) - } - - _, err = url.ParseRequestURI(link.URL) - if err != nil { - return GetDefaultIssueConfig(), fmt.Errorf("%s is not a valid URL", link.URL) - } - } - - return issueConfig, nil -} - -// IsIssueConfig returns if the given path is a issue config file. -func (r *Repository) IsIssueConfig(path string) bool { - for _, configName := range IssueConfigCandidates { - if path == configName+".yaml" || path == configName+".yml" { - return true - } - } - return false -} diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 9a733b832f..a67a5420ac 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -316,7 +316,7 @@ func reqSiteAdmin() func(ctx *context.APIContext) { // reqOwner user should be the owner of the repo or site admin. func reqOwner() func(ctx *context.APIContext) { return func(ctx *context.APIContext) { - if !ctx.IsUserRepoOwner() && !ctx.IsUserSiteAdmin() { + if !ctx.Repo.IsOwner() && !ctx.IsUserSiteAdmin() { ctx.Error(http.StatusForbidden, "reqOwner", "user should be the owner of the repo") return } @@ -355,7 +355,7 @@ func reqRepoBranchWriter(ctx *context.APIContext) { // reqRepoReader user should have specific read permission or be a repo admin or a site admin func reqRepoReader(unitType unit.Type) func(ctx *context.APIContext) { return func(ctx *context.APIContext) { - if !ctx.IsUserRepoReaderSpecific(unitType) && !ctx.IsUserRepoAdmin() && !ctx.IsUserSiteAdmin() { + if !ctx.Repo.CanRead(unitType) && !ctx.IsUserRepoAdmin() && !ctx.IsUserSiteAdmin() { ctx.Error(http.StatusForbidden, "reqRepoReader", "user should have specific read permission or be a repo admin or a site admin") return } @@ -365,7 +365,7 @@ func reqRepoReader(unitType unit.Type) func(ctx *context.APIContext) { // reqAnyRepoReader user should have any permission to read repository or permissions of site admin func reqAnyRepoReader() func(ctx *context.APIContext) { return func(ctx *context.APIContext) { - if !ctx.IsUserRepoReaderAny() && !ctx.IsUserSiteAdmin() { + if !ctx.Repo.HasAccess() && !ctx.IsUserSiteAdmin() { ctx.Error(http.StatusForbidden, "reqAnyRepoReader", "user should have any permission to read repository or permissions of site admin") return } diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index 480ca397d4..114b93534a 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -30,6 +30,7 @@ import ( "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/services/convert" + "code.gitea.io/gitea/services/issue" repo_service "code.gitea.io/gitea/services/repository" ) @@ -1144,8 +1145,12 @@ func GetIssueTemplates(ctx *context.APIContext) { // responses: // "200": // "$ref": "#/responses/IssueTemplates" - - ctx.JSON(http.StatusOK, ctx.IssueTemplatesFromDefaultBranch()) + ret, err := issue.GetTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetTemplatesFromDefaultBranch", err) + return + } + ctx.JSON(http.StatusOK, ret) } // GetIssueConfig returns the issue config for a repo @@ -1169,7 +1174,7 @@ func GetIssueConfig(ctx *context.APIContext) { // responses: // "200": // "$ref": "#/responses/RepoIssueConfig" - issueConfig, _ := ctx.IssueConfigFromDefaultBranch() + issueConfig, _ := issue.GetTemplateConfigFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo) ctx.JSON(http.StatusOK, issueConfig) } @@ -1194,7 +1199,7 @@ func ValidateIssueConfig(ctx *context.APIContext) { // responses: // "200": // "$ref": "#/responses/RepoIssueConfigValidation" - _, err := ctx.IssueConfigFromDefaultBranch() + _, err := issue.GetTemplateConfigFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo) if err == nil { ctx.JSON(http.StatusOK, api.IssueConfigValidation{Valid: true, Message: ""}) diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index 4efac5c38c..c2f30a01f4 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -431,7 +431,7 @@ func Issues(ctx *context.Context) { } ctx.Data["Title"] = ctx.Tr("repo.issues") ctx.Data["PageIsIssueList"] = true - ctx.Data["NewIssueChooseTemplate"] = ctx.HasIssueTemplatesOrContactLinks() + ctx.Data["NewIssueChooseTemplate"] = issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo) } issues(ctx, ctx.FormInt64("milestone"), ctx.FormInt64("project"), util.OptionalBoolOf(isPullList)) @@ -862,7 +862,7 @@ func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles func NewIssue(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.issues.new") ctx.Data["PageIsIssueList"] = true - ctx.Data["NewIssueChooseTemplate"] = ctx.HasIssueTemplatesOrContactLinks() + ctx.Data["NewIssueChooseTemplate"] = issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo) ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes title := ctx.FormString("title") ctx.Data["TitleQuery"] = title @@ -904,7 +904,7 @@ func NewIssue(ctx *context.Context) { RetrieveRepoMetas(ctx, ctx.Repo.Repository, false) - _, templateErrs := ctx.IssueTemplatesErrorsFromDefaultBranch() + _, templateErrs := issue_service.GetTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo) if errs := setTemplateIfExists(ctx, issueTemplateKey, IssueTemplateCandidates); len(errs) > 0 { for k, v := range errs { templateErrs[k] = v @@ -952,20 +952,20 @@ func NewIssueChooseTemplate(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.issues.new") ctx.Data["PageIsIssueList"] = true - issueTemplates, errs := ctx.IssueTemplatesErrorsFromDefaultBranch() + issueTemplates, errs := issue_service.GetTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo) ctx.Data["IssueTemplates"] = issueTemplates if len(errs) > 0 { ctx.Flash.Warning(renderErrorOfTemplates(ctx, errs), true) } - if !ctx.HasIssueTemplatesOrContactLinks() { + if !issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo) { // 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.Link(), ctx.Req.URL.RawQuery), http.StatusSeeOther) return } - issueConfig, err := ctx.IssueConfigFromDefaultBranch() + issueConfig, err := issue_service.GetTemplateConfigFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo) ctx.Data["IssueConfig"] = issueConfig ctx.Data["IssueConfigError"] = err // ctx.Flash.Err makes problems here @@ -1103,7 +1103,7 @@ func NewIssuePost(ctx *context.Context) { form := web.GetForm(ctx).(*forms.CreateIssueForm) ctx.Data["Title"] = ctx.Tr("repo.issues.new") ctx.Data["PageIsIssueList"] = true - ctx.Data["NewIssueChooseTemplate"] = ctx.HasIssueTemplatesOrContactLinks() + ctx.Data["NewIssueChooseTemplate"] = issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo) ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled upload.AddUploadContext(ctx, "comment") @@ -1297,7 +1297,7 @@ func ViewIssue(ctx *context.Context) { return } ctx.Data["PageIsIssueList"] = true - ctx.Data["NewIssueChooseTemplate"] = ctx.HasIssueTemplatesOrContactLinks() + ctx.Data["NewIssueChooseTemplate"] = issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo) } if issue.IsPull && !ctx.Repo.CanRead(unit.TypeIssues) { diff --git a/routers/web/repo/milestone.go b/routers/web/repo/milestone.go index d712df1001..4b33fbcb16 100644 --- a/routers/web/repo/milestone.go +++ b/routers/web/repo/milestone.go @@ -20,6 +20,7 @@ import ( "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/forms" + "code.gitea.io/gitea/services/issue" "xorm.io/builder" ) @@ -289,7 +290,9 @@ func MilestoneIssuesAndPulls(ctx *context.Context) { ctx.Data["Milestone"] = milestone issues(ctx, milestoneID, 0, util.OptionalBoolNone) - ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0 + + ret, _ := issue.GetTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo) + ctx.Data["NewIssueChooseTemplate"] = len(ret) > 0 ctx.Data["CanWriteIssues"] = ctx.Repo.CanWriteIssuesOrPulls(false) ctx.Data["CanWritePulls"] = ctx.Repo.CanWriteIssuesOrPulls(true) diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 2bf293cbda..2fd893f91c 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -40,6 +40,7 @@ import ( "code.gitea.io/gitea/modules/typesniffer" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers/web/feed" + issue_service "code.gitea.io/gitea/services/issue" "github.com/nektos/act/pkg/model" ) @@ -346,8 +347,8 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st if editorconfigErr != nil { ctx.Data["FileError"] = strings.TrimSpace(editorconfigErr.Error()) } - } else if ctx.Repo.IsIssueConfig(ctx.Repo.TreePath) { - _, issueConfigErr := ctx.Repo.GetIssueConfig(ctx.Repo.TreePath, ctx.Repo.Commit) + } else if issue_service.IsTemplateConfig(ctx.Repo.TreePath) { + _, issueConfigErr := issue_service.GetTemplateConfig(ctx.Repo.GitRepo, ctx.Repo.TreePath, ctx.Repo.Commit) if issueConfigErr != nil { ctx.Data["FileError"] = strings.TrimSpace(issueConfigErr.Error()) } diff --git a/services/issue/template.go b/services/issue/template.go new file mode 100644 index 0000000000..4f1e3d93a0 --- /dev/null +++ b/services/issue/template.go @@ -0,0 +1,189 @@ +// 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 := api.IssueConfig{} + 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/ + 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 +}