summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorwxiaoguang <wxiaoguang@gmail.com>2023-05-09 07:30:14 +0800
committerGitHub <noreply@github.com>2023-05-09 01:30:14 +0200
commitdef4956122ea2364f247712b13856383ee496add (patch)
tree311e7a077aba83825815881b7bf35fff28930533
parentc4303efc23ea19f16ee826809f43888ee4583ebb (diff)
downloadgitea-def4956122ea2364f247712b13856383ee496add.tar.gz
gitea-def4956122ea2364f247712b13856383ee496add.zip
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 <teabot@gitea.io>
-rw-r--r--modules/context/context.go23
-rw-r--r--modules/context/context_cookie.go19
-rw-r--r--modules/context/context_model.go109
-rw-r--r--modules/context/repo.go93
-rw-r--r--routers/api/v1/api.go6
-rw-r--r--routers/api/v1/repo/repo.go13
-rw-r--r--routers/web/repo/issue.go16
-rw-r--r--routers/web/repo/milestone.go5
-rw-r--r--routers/web/repo/view.go5
-rw-r--r--services/issue/template.go189
10 files changed, 228 insertions, 250 deletions
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/<ref>
- 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/<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
+}