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>
This commit is contained in:
parent
c4303efc23
commit
def4956122
|
@ -38,17 +38,18 @@ type Render interface {
|
||||||
type Context struct {
|
type Context struct {
|
||||||
Resp ResponseWriter
|
Resp ResponseWriter
|
||||||
Req *http.Request
|
Req *http.Request
|
||||||
|
Render Render
|
||||||
|
|
||||||
Data middleware.ContextData // data used by MVC templates
|
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`
|
PageData map[string]any // data used by JavaScript modules in one page, it's `window.config.pageData`
|
||||||
Render Render
|
|
||||||
Locale translation.Locale
|
Locale translation.Locale
|
||||||
Cache cache.Cache
|
Cache cache.Cache
|
||||||
Csrf CSRFProtector
|
Csrf CSRFProtector
|
||||||
Flash *middleware.Flash
|
Flash *middleware.Flash
|
||||||
Session session.Store
|
Session session.Store
|
||||||
|
|
||||||
Link string // current request URL
|
Link string // current request URL (without query string)
|
||||||
EscapedLink string
|
|
||||||
Doer *user_model.User
|
Doer *user_model.User
|
||||||
IsSigned bool
|
IsSigned bool
|
||||||
IsBasicAuth bool
|
IsBasicAuth bool
|
||||||
|
|
|
@ -6,7 +6,6 @@ package context
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
@ -85,21 +84,3 @@ func (ctx *Context) CookieEncrypt(secret, value string) string {
|
||||||
|
|
||||||
return hex.EncodeToString(text)
|
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
|
|
||||||
}
|
|
||||||
|
|
|
@ -4,14 +4,7 @@
|
||||||
package context
|
package context
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/unit"
|
"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
|
// 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
|
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
|
// IsUserRepoAdmin returns true if current user is admin in current repo
|
||||||
func (ctx *Context) IsUserRepoAdmin() bool {
|
func (ctx *Context) IsUserRepoAdmin() bool {
|
||||||
return ctx.Repo.IsAdmin()
|
return ctx.Repo.IsAdmin()
|
||||||
|
@ -39,100 +27,3 @@ func (ctx *Context) IsUserRepoWriter(unitTypes []unit.Type) bool {
|
||||||
|
|
||||||
return false
|
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
|
|
||||||
}
|
|
||||||
|
|
|
@ -8,7 +8,6 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html"
|
"html"
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
|
@ -28,33 +27,12 @@ import (
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
repo_module "code.gitea.io/gitea/modules/repository"
|
repo_module "code.gitea.io/gitea/modules/repository"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
asymkey_service "code.gitea.io/gitea/services/asymkey"
|
asymkey_service "code.gitea.io/gitea/services/asymkey"
|
||||||
|
|
||||||
"github.com/editorconfig/editorconfig-core-go/v2"
|
"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
|
// PullRequest contains information to make a pull request
|
||||||
type PullRequest struct {
|
type PullRequest struct {
|
||||||
BaseRepo *repo_model.Repository
|
BaseRepo *repo_model.Repository
|
||||||
|
@ -1061,74 +1039,3 @@ func UnitTypes() func(ctx *Context) {
|
||||||
ctx.Data["UnitTypeActions"] = unit_model.TypeActions
|
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
|
|
||||||
}
|
|
||||||
|
|
|
@ -316,7 +316,7 @@ func reqSiteAdmin() func(ctx *context.APIContext) {
|
||||||
// reqOwner user should be the owner of the repo or site admin.
|
// reqOwner user should be the owner of the repo or site admin.
|
||||||
func reqOwner() func(ctx *context.APIContext) {
|
func reqOwner() func(ctx *context.APIContext) {
|
||||||
return 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")
|
ctx.Error(http.StatusForbidden, "reqOwner", "user should be the owner of the repo")
|
||||||
return
|
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
|
// 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) {
|
func reqRepoReader(unitType unit.Type) func(ctx *context.APIContext) {
|
||||||
return 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")
|
ctx.Error(http.StatusForbidden, "reqRepoReader", "user should have specific read permission or be a repo admin or a site admin")
|
||||||
return
|
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
|
// reqAnyRepoReader user should have any permission to read repository or permissions of site admin
|
||||||
func reqAnyRepoReader() func(ctx *context.APIContext) {
|
func reqAnyRepoReader() func(ctx *context.APIContext) {
|
||||||
return 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")
|
ctx.Error(http.StatusForbidden, "reqAnyRepoReader", "user should have any permission to read repository or permissions of site admin")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,7 @@ import (
|
||||||
"code.gitea.io/gitea/modules/web"
|
"code.gitea.io/gitea/modules/web"
|
||||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||||
"code.gitea.io/gitea/services/convert"
|
"code.gitea.io/gitea/services/convert"
|
||||||
|
"code.gitea.io/gitea/services/issue"
|
||||||
repo_service "code.gitea.io/gitea/services/repository"
|
repo_service "code.gitea.io/gitea/services/repository"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1144,8 +1145,12 @@ func GetIssueTemplates(ctx *context.APIContext) {
|
||||||
// responses:
|
// responses:
|
||||||
// "200":
|
// "200":
|
||||||
// "$ref": "#/responses/IssueTemplates"
|
// "$ref": "#/responses/IssueTemplates"
|
||||||
|
ret, err := issue.GetTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
|
||||||
ctx.JSON(http.StatusOK, ctx.IssueTemplatesFromDefaultBranch())
|
if err != nil {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "GetTemplatesFromDefaultBranch", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.JSON(http.StatusOK, ret)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetIssueConfig returns the issue config for a repo
|
// GetIssueConfig returns the issue config for a repo
|
||||||
|
@ -1169,7 +1174,7 @@ func GetIssueConfig(ctx *context.APIContext) {
|
||||||
// responses:
|
// responses:
|
||||||
// "200":
|
// "200":
|
||||||
// "$ref": "#/responses/RepoIssueConfig"
|
// "$ref": "#/responses/RepoIssueConfig"
|
||||||
issueConfig, _ := ctx.IssueConfigFromDefaultBranch()
|
issueConfig, _ := issue.GetTemplateConfigFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
|
||||||
ctx.JSON(http.StatusOK, issueConfig)
|
ctx.JSON(http.StatusOK, issueConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1194,7 +1199,7 @@ func ValidateIssueConfig(ctx *context.APIContext) {
|
||||||
// responses:
|
// responses:
|
||||||
// "200":
|
// "200":
|
||||||
// "$ref": "#/responses/RepoIssueConfigValidation"
|
// "$ref": "#/responses/RepoIssueConfigValidation"
|
||||||
_, err := ctx.IssueConfigFromDefaultBranch()
|
_, err := issue.GetTemplateConfigFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
ctx.JSON(http.StatusOK, api.IssueConfigValidation{Valid: true, Message: ""})
|
ctx.JSON(http.StatusOK, api.IssueConfigValidation{Valid: true, Message: ""})
|
||||||
|
|
|
@ -431,7 +431,7 @@ func Issues(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
ctx.Data["Title"] = ctx.Tr("repo.issues")
|
ctx.Data["Title"] = ctx.Tr("repo.issues")
|
||||||
ctx.Data["PageIsIssueList"] = true
|
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))
|
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) {
|
func NewIssue(ctx *context.Context) {
|
||||||
ctx.Data["Title"] = ctx.Tr("repo.issues.new")
|
ctx.Data["Title"] = ctx.Tr("repo.issues.new")
|
||||||
ctx.Data["PageIsIssueList"] = true
|
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["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
|
||||||
title := ctx.FormString("title")
|
title := ctx.FormString("title")
|
||||||
ctx.Data["TitleQuery"] = title
|
ctx.Data["TitleQuery"] = title
|
||||||
|
@ -904,7 +904,7 @@ func NewIssue(ctx *context.Context) {
|
||||||
|
|
||||||
RetrieveRepoMetas(ctx, ctx.Repo.Repository, false)
|
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 {
|
if errs := setTemplateIfExists(ctx, issueTemplateKey, IssueTemplateCandidates); len(errs) > 0 {
|
||||||
for k, v := range errs {
|
for k, v := range errs {
|
||||||
templateErrs[k] = v
|
templateErrs[k] = v
|
||||||
|
@ -952,20 +952,20 @@ func NewIssueChooseTemplate(ctx *context.Context) {
|
||||||
ctx.Data["Title"] = ctx.Tr("repo.issues.new")
|
ctx.Data["Title"] = ctx.Tr("repo.issues.new")
|
||||||
ctx.Data["PageIsIssueList"] = true
|
ctx.Data["PageIsIssueList"] = true
|
||||||
|
|
||||||
issueTemplates, errs := ctx.IssueTemplatesErrorsFromDefaultBranch()
|
issueTemplates, errs := issue_service.GetTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
|
||||||
ctx.Data["IssueTemplates"] = issueTemplates
|
ctx.Data["IssueTemplates"] = issueTemplates
|
||||||
|
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
ctx.Flash.Warning(renderErrorOfTemplates(ctx, errs), true)
|
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.
|
// 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)
|
ctx.Redirect(fmt.Sprintf("%s/issues/new?%s", ctx.Repo.Repository.Link(), ctx.Req.URL.RawQuery), http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
issueConfig, err := ctx.IssueConfigFromDefaultBranch()
|
issueConfig, err := issue_service.GetTemplateConfigFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
|
||||||
ctx.Data["IssueConfig"] = issueConfig
|
ctx.Data["IssueConfig"] = issueConfig
|
||||||
ctx.Data["IssueConfigError"] = err // ctx.Flash.Err makes problems here
|
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)
|
form := web.GetForm(ctx).(*forms.CreateIssueForm)
|
||||||
ctx.Data["Title"] = ctx.Tr("repo.issues.new")
|
ctx.Data["Title"] = ctx.Tr("repo.issues.new")
|
||||||
ctx.Data["PageIsIssueList"] = true
|
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["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
|
||||||
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
|
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
|
||||||
upload.AddUploadContext(ctx, "comment")
|
upload.AddUploadContext(ctx, "comment")
|
||||||
|
@ -1297,7 +1297,7 @@ func ViewIssue(ctx *context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.Data["PageIsIssueList"] = true
|
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) {
|
if issue.IsPull && !ctx.Repo.CanRead(unit.TypeIssues) {
|
||||||
|
|
|
@ -20,6 +20,7 @@ import (
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/modules/web"
|
"code.gitea.io/gitea/modules/web"
|
||||||
"code.gitea.io/gitea/services/forms"
|
"code.gitea.io/gitea/services/forms"
|
||||||
|
"code.gitea.io/gitea/services/issue"
|
||||||
|
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
)
|
)
|
||||||
|
@ -289,7 +290,9 @@ func MilestoneIssuesAndPulls(ctx *context.Context) {
|
||||||
ctx.Data["Milestone"] = milestone
|
ctx.Data["Milestone"] = milestone
|
||||||
|
|
||||||
issues(ctx, milestoneID, 0, util.OptionalBoolNone)
|
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["CanWriteIssues"] = ctx.Repo.CanWriteIssuesOrPulls(false)
|
||||||
ctx.Data["CanWritePulls"] = ctx.Repo.CanWriteIssuesOrPulls(true)
|
ctx.Data["CanWritePulls"] = ctx.Repo.CanWriteIssuesOrPulls(true)
|
||||||
|
|
|
@ -40,6 +40,7 @@ import (
|
||||||
"code.gitea.io/gitea/modules/typesniffer"
|
"code.gitea.io/gitea/modules/typesniffer"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/routers/web/feed"
|
"code.gitea.io/gitea/routers/web/feed"
|
||||||
|
issue_service "code.gitea.io/gitea/services/issue"
|
||||||
|
|
||||||
"github.com/nektos/act/pkg/model"
|
"github.com/nektos/act/pkg/model"
|
||||||
)
|
)
|
||||||
|
@ -346,8 +347,8 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st
|
||||||
if editorconfigErr != nil {
|
if editorconfigErr != nil {
|
||||||
ctx.Data["FileError"] = strings.TrimSpace(editorconfigErr.Error())
|
ctx.Data["FileError"] = strings.TrimSpace(editorconfigErr.Error())
|
||||||
}
|
}
|
||||||
} else if ctx.Repo.IsIssueConfig(ctx.Repo.TreePath) {
|
} else if issue_service.IsTemplateConfig(ctx.Repo.TreePath) {
|
||||||
_, issueConfigErr := ctx.Repo.GetIssueConfig(ctx.Repo.TreePath, ctx.Repo.Commit)
|
_, issueConfigErr := issue_service.GetTemplateConfig(ctx.Repo.GitRepo, ctx.Repo.TreePath, ctx.Repo.Commit)
|
||||||
if issueConfigErr != nil {
|
if issueConfigErr != nil {
|
||||||
ctx.Data["FileError"] = strings.TrimSpace(issueConfigErr.Error())
|
ctx.Data["FileError"] = strings.TrimSpace(issueConfigErr.Error())
|
||||||
}
|
}
|
||||||
|
|
189
services/issue/template.go
Normal file
189
services/issue/template.go
Normal file
|
@ -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
|
||||||
|
}
|
Loading…
Reference in a new issue