mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 21:28:11 +09: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:
		| @@ -38,17 +38,18 @@ type Render interface { | ||||
| type Context struct { | ||||
| 	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 | ||||
| 	Link        string // current request URL (without query string) | ||||
| 	Doer        *user_model.User | ||||
| 	IsSigned    bool | ||||
| 	IsBasicAuth bool | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
| 		} | ||||
|   | ||||
| @@ -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: ""}) | ||||
|   | ||||
| @@ -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) { | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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()) | ||||
| 		} | ||||
|   | ||||
							
								
								
									
										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