mirror of
				https://github.com/go-gitea/gitea
				synced 2025-10-31 11:28:24 +00:00 
			
		
		
		
	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:
		| @@ -36,19 +36,20 @@ type Render interface { | |||||||
|  |  | ||||||
| // Context represents context of a request. | // Context represents context of a request. | ||||||
| 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 |  | ||||||
| 	Cache    cache.Cache |  | ||||||
| 	Csrf     CSRFProtector |  | ||||||
| 	Flash    *middleware.Flash |  | ||||||
| 	Session  session.Store |  | ||||||
|  |  | ||||||
| 	Link        string // current request URL | 	Locale  translation.Locale | ||||||
| 	EscapedLink string | 	Cache   cache.Cache | ||||||
|  | 	Csrf    CSRFProtector | ||||||
|  | 	Flash   *middleware.Flash | ||||||
|  | 	Session session.Store | ||||||
|  |  | ||||||
|  | 	Link        string // current request URL (without query 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 | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user