mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-11-24 08:57:03 -05:00
Implement Issue Config (#20956)
Closes #20955 This PR adds the possibility to disable blank Issues, when the Repo has templates. This can be done by creating the file `.gitea/issue_config.yaml` with the content `blank_issues_enabled` in the Repo.
This commit is contained in:
parent
5cd1d6c93b
commit
f384b13f1c
12 changed files with 464 additions and 14 deletions
|
@ -50,6 +50,17 @@ Possible file names for issue templates:
|
||||||
- `.github/issue_template.yaml`
|
- `.github/issue_template.yaml`
|
||||||
- `.github/issue_template.yml`
|
- `.github/issue_template.yml`
|
||||||
|
|
||||||
|
Possible file names for issue config:
|
||||||
|
|
||||||
|
- `.gitea/ISSUE_TEMPLATE/config.yaml`
|
||||||
|
- `.gitea/ISSUE_TEMPLATE/config.yml`
|
||||||
|
- `.gitea/issue_template/config.yaml`
|
||||||
|
- `.gitea/issue_template/config.yml`
|
||||||
|
- `.github/ISSUE_TEMPLATE/config.yaml`
|
||||||
|
- `.github/ISSUE_TEMPLATE/config.yml`
|
||||||
|
- `.github/issue_template/config.yaml`
|
||||||
|
- `.github/issue_template/config.yml`
|
||||||
|
|
||||||
Possible file names for PR templates:
|
Possible file names for PR templates:
|
||||||
|
|
||||||
- `PULL_REQUEST_TEMPLATE.md`
|
- `PULL_REQUEST_TEMPLATE.md`
|
||||||
|
@ -267,3 +278,30 @@ For each value in the options array, you can set the following keys.
|
||||||
|----------|------------------------------------------------------------------------------------------------------------------------------------------|----------|---------|---------|---------|
|
|----------|------------------------------------------------------------------------------------------------------------------------------------------|----------|---------|---------|---------|
|
||||||
| label | The identifier for the option, which is displayed in the form. Markdown is supported for bold or italic text formatting, and hyperlinks. | Required | String | - | - |
|
| label | The identifier for the option, which is displayed in the form. Markdown is supported for bold or italic text formatting, and hyperlinks. | Required | String | - | - |
|
||||||
| required | Prevents form submission until element is completed. | Optional | Boolean | false | - |
|
| required | Prevents form submission until element is completed. | Optional | Boolean | false | - |
|
||||||
|
|
||||||
|
## Syntax for issue config
|
||||||
|
|
||||||
|
This is a example for a issue config file
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
blank_issues_enabled: true
|
||||||
|
contact_links:
|
||||||
|
- name: Gitea
|
||||||
|
url: https://gitea.io
|
||||||
|
about: Visit the Gitea Website
|
||||||
|
```
|
||||||
|
|
||||||
|
### Possible Options
|
||||||
|
|
||||||
|
| Key | Description | Type | Default |
|
||||||
|
|----------------------|-------------------------------------------------------------------------------------------------------|--------------------|----------------|
|
||||||
|
| blank_issues_enabled | If set to false, the User is forced to use a Template | Boolean | true |
|
||||||
|
| contact_links | Custom Links to show in the Choose Box | Contact Link Array | Empty Array |
|
||||||
|
|
||||||
|
### Contact Link
|
||||||
|
|
||||||
|
| Key | Description | Type | Required |
|
||||||
|
|----------------------|-------------------------------------------------------------------------------------------------------|---------|----------|
|
||||||
|
| name | the name of your link | String | true |
|
||||||
|
| url | The URL of your Link | String | true |
|
||||||
|
| about | A short description of your Link | String | true |
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html"
|
"html"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
|
@ -33,6 +34,7 @@ import (
|
||||||
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
|
// IssueTemplateDirCandidates issue templates directory
|
||||||
|
@ -47,6 +49,13 @@ var IssueTemplateDirCandidates = []string{
|
||||||
".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
|
||||||
|
@ -1088,3 +1097,108 @@ func (ctx *Context) IssueTemplatesErrorsFromDefaultBranch() ([]*api.IssueTemplat
|
||||||
}
|
}
|
||||||
return issueTemplates, invalidFiles
|
return issueTemplates, invalidFiles
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *Context) HasIssueTemplatesOrContactLinks() bool {
|
||||||
|
if len(ctx.IssueTemplatesFromDefaultBranch()) > 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
issueConfig, _ := ctx.IssueConfigFromDefaultBranch()
|
||||||
|
return len(issueConfig.ContactLinks) > 0
|
||||||
|
}
|
||||||
|
|
|
@ -190,6 +190,22 @@ func (l *IssueTemplateLabels) UnmarshalYAML(value *yaml.Node) error {
|
||||||
return fmt.Errorf("line %d: cannot unmarshal %s into IssueTemplateLabels", value.Line, value.ShortTag())
|
return fmt.Errorf("line %d: cannot unmarshal %s into IssueTemplateLabels", value.Line, value.ShortTag())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type IssueConfigContactLink struct {
|
||||||
|
Name string `json:"name" yaml:"name"`
|
||||||
|
URL string `json:"url" yaml:"url"`
|
||||||
|
About string `json:"about" yaml:"about"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type IssueConfig struct {
|
||||||
|
BlankIssuesEnabled bool `json:"blank_issues_enabled" yaml:"blank_issues_enabled"`
|
||||||
|
ContactLinks []IssueConfigContactLink `json:"contact_links" yaml:"contact_links"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type IssueConfigValidation struct {
|
||||||
|
Valid bool `json:"valid"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
// IssueTemplateType defines issue template type
|
// IssueTemplateType defines issue template type
|
||||||
type IssueTemplateType string
|
type IssueTemplateType string
|
||||||
|
|
||||||
|
|
|
@ -1272,10 +1272,12 @@ issues.new.no_assignees = No Assignees
|
||||||
issues.new.no_reviewers = No reviewers
|
issues.new.no_reviewers = No reviewers
|
||||||
issues.new.add_reviewer_title = Request review
|
issues.new.add_reviewer_title = Request review
|
||||||
issues.choose.get_started = Get Started
|
issues.choose.get_started = Get Started
|
||||||
|
issues.choose.open_external_link = Open
|
||||||
issues.choose.blank = Default
|
issues.choose.blank = Default
|
||||||
issues.choose.blank_about = Create an issue from default template.
|
issues.choose.blank_about = Create an issue from default template.
|
||||||
issues.choose.ignore_invalid_templates = Invalid templates have been ignored
|
issues.choose.ignore_invalid_templates = Invalid templates have been ignored
|
||||||
issues.choose.invalid_templates = %v invalid template(s) found
|
issues.choose.invalid_templates = %v invalid template(s) found
|
||||||
|
issues.choose.invalid_config = The issue config contains errors:
|
||||||
issues.no_ref = No Branch/Tag Specified
|
issues.no_ref = No Branch/Tag Specified
|
||||||
issues.create = Create Issue
|
issues.create = Create Issue
|
||||||
issues.new_label = New Label
|
issues.new_label = New Label
|
||||||
|
|
|
@ -1169,6 +1169,8 @@ func Routes(ctx gocontext.Context) *web.Route {
|
||||||
}, reqAdmin())
|
}, reqAdmin())
|
||||||
}, reqAnyRepoReader())
|
}, reqAnyRepoReader())
|
||||||
m.Get("/issue_templates", context.ReferencesGitRepo(), repo.GetIssueTemplates)
|
m.Get("/issue_templates", context.ReferencesGitRepo(), repo.GetIssueTemplates)
|
||||||
|
m.Get("/issue_config", context.ReferencesGitRepo(), repo.GetIssueConfig)
|
||||||
|
m.Get("/issue_config/validate", context.ReferencesGitRepo(), repo.ValidateIssueConfig)
|
||||||
m.Get("/languages", reqRepoReader(unit.TypeCode), repo.GetLanguages)
|
m.Get("/languages", reqRepoReader(unit.TypeCode), repo.GetLanguages)
|
||||||
}, repoAssignment())
|
}, repoAssignment())
|
||||||
})
|
})
|
||||||
|
|
|
@ -1144,3 +1144,58 @@ func GetIssueTemplates(ctx *context.APIContext) {
|
||||||
|
|
||||||
ctx.JSON(http.StatusOK, ctx.IssueTemplatesFromDefaultBranch())
|
ctx.JSON(http.StatusOK, ctx.IssueTemplatesFromDefaultBranch())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetIssueConfig returns the issue config for a repo
|
||||||
|
func GetIssueConfig(ctx *context.APIContext) {
|
||||||
|
// swagger:operation GET /repos/{owner}/{repo}/issue_config repository repoGetIssueConfig
|
||||||
|
// ---
|
||||||
|
// summary: Returns the issue config for a repo
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: owner
|
||||||
|
// in: path
|
||||||
|
// description: owner of the repo
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: repo
|
||||||
|
// in: path
|
||||||
|
// description: name of the repo
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// responses:
|
||||||
|
// "200":
|
||||||
|
// "$ref": "#/responses/RepoIssueConfig"
|
||||||
|
issueConfig, _ := ctx.IssueConfigFromDefaultBranch()
|
||||||
|
ctx.JSON(http.StatusOK, issueConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateIssueConfig returns validation errors for the issue config
|
||||||
|
func ValidateIssueConfig(ctx *context.APIContext) {
|
||||||
|
// swagger:operation GET /repos/{owner}/{repo}/issue_config/validate repository repoValidateIssueConfig
|
||||||
|
// ---
|
||||||
|
// summary: Returns the validation information for a issue config
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: owner
|
||||||
|
// in: path
|
||||||
|
// description: owner of the repo
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: repo
|
||||||
|
// in: path
|
||||||
|
// description: name of the repo
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// responses:
|
||||||
|
// "200":
|
||||||
|
// "$ref": "#/responses/RepoIssueConfigValidation"
|
||||||
|
_, err := ctx.IssueConfigFromDefaultBranch()
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
ctx.JSON(http.StatusOK, api.IssueConfigValidation{Valid: true, Message: ""})
|
||||||
|
} else {
|
||||||
|
ctx.JSON(http.StatusOK, api.IssueConfigValidation{Valid: false, Message: err.Error()})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -386,3 +386,17 @@ type swaggerRepoCollaboratorPermission struct {
|
||||||
// in:body
|
// in:body
|
||||||
Body api.RepoCollaboratorPermission `json:"body"`
|
Body api.RepoCollaboratorPermission `json:"body"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RepoIssueConfig
|
||||||
|
// swagger:response RepoIssueConfig
|
||||||
|
type swaggerRepoIssueConfig struct {
|
||||||
|
// in:body
|
||||||
|
Body api.IssueConfig `json:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RepoIssueConfigValidation
|
||||||
|
// swagger:response RepoIssueConfigValidation
|
||||||
|
type swaggerRepoIssueConfigValidation struct {
|
||||||
|
// in:body
|
||||||
|
Body api.IssueConfigValidation `json:"body"`
|
||||||
|
}
|
||||||
|
|
|
@ -435,7 +435,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"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0
|
ctx.Data["NewIssueChooseTemplate"] = ctx.HasIssueTemplatesOrContactLinks()
|
||||||
}
|
}
|
||||||
|
|
||||||
issues(ctx, ctx.FormInt64("milestone"), ctx.FormInt64("project"), util.OptionalBoolOf(isPullList))
|
issues(ctx, ctx.FormInt64("milestone"), ctx.FormInt64("project"), util.OptionalBoolOf(isPullList))
|
||||||
|
@ -848,7 +848,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"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0
|
ctx.Data["NewIssueChooseTemplate"] = ctx.HasIssueTemplatesOrContactLinks()
|
||||||
ctx.Data["RequireTribute"] = true
|
ctx.Data["RequireTribute"] = true
|
||||||
ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
|
ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
|
||||||
title := ctx.FormString("title")
|
title := ctx.FormString("title")
|
||||||
|
@ -946,12 +946,16 @@ func NewIssueChooseTemplate(ctx *context.Context) {
|
||||||
ctx.Flash.Warning(renderErrorOfTemplates(ctx, errs), true)
|
ctx.Flash.Warning(renderErrorOfTemplates(ctx, errs), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(issueTemplates) == 0 {
|
if !ctx.HasIssueTemplatesOrContactLinks() {
|
||||||
// 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()
|
||||||
|
ctx.Data["IssueConfig"] = issueConfig
|
||||||
|
ctx.Data["IssueConfigError"] = err // ctx.Flash.Err makes problems here
|
||||||
|
|
||||||
ctx.Data["milestone"] = ctx.FormInt64("milestone")
|
ctx.Data["milestone"] = ctx.FormInt64("milestone")
|
||||||
ctx.Data["project"] = ctx.FormInt64("project")
|
ctx.Data["project"] = ctx.FormInt64("project")
|
||||||
|
|
||||||
|
@ -1086,7 +1090,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"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0
|
ctx.Data["NewIssueChooseTemplate"] = ctx.HasIssueTemplatesOrContactLinks()
|
||||||
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")
|
||||||
|
@ -1280,7 +1284,7 @@ func ViewIssue(ctx *context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.Data["PageIsIssueList"] = true
|
ctx.Data["PageIsIssueList"] = true
|
||||||
ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0
|
ctx.Data["NewIssueChooseTemplate"] = ctx.HasIssueTemplatesOrContactLinks()
|
||||||
}
|
}
|
||||||
|
|
||||||
if issue.IsPull && !ctx.Repo.CanRead(unit.TypeIssues) {
|
if issue.IsPull && !ctx.Repo.CanRead(unit.TypeIssues) {
|
||||||
|
|
|
@ -348,6 +348,9 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st
|
||||||
if ctx.Repo.TreePath == ".editorconfig" {
|
if ctx.Repo.TreePath == ".editorconfig" {
|
||||||
_, editorconfigErr := ctx.Repo.GetEditorconfig(ctx.Repo.Commit)
|
_, editorconfigErr := ctx.Repo.GetEditorconfig(ctx.Repo.Commit)
|
||||||
ctx.Data["FileError"] = editorconfigErr
|
ctx.Data["FileError"] = editorconfigErr
|
||||||
|
} else if ctx.Repo.IsIssueConfig(ctx.Repo.TreePath) {
|
||||||
|
_, issueConfigErr := ctx.Repo.GetIssueConfig(ctx.Repo.TreePath, ctx.Repo.Commit)
|
||||||
|
ctx.Data["FileError"] = issueConfigErr
|
||||||
}
|
}
|
||||||
|
|
||||||
isDisplayingSource := ctx.FormString("display") == "source"
|
isDisplayingSource := ctx.FormString("display") == "source"
|
||||||
|
|
|
@ -20,17 +20,40 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
<div class="ui attached segment">
|
{{range .IssueConfig.ContactLinks}}
|
||||||
<div class="ui two column grid">
|
<div class="ui attached segment">
|
||||||
<div class="column left aligned">
|
<div class="ui two column grid">
|
||||||
<strong>{{.locale.Tr "repo.issues.choose.blank"}}</strong>
|
<div class="column left aligned">
|
||||||
<br>{{.locale.Tr "repo.issues.choose.blank_about"}}
|
<strong>{{.Name | RenderEmojiPlain}}</strong>
|
||||||
</div>
|
<br>{{.About | RenderEmojiPlain}}
|
||||||
<div class="column right aligned">
|
</div>
|
||||||
<a href="{{.RepoLink}}/issues/new?{{if .milestone}}&milestone={{.milestone}}{{end}}{{if $.project}}&project={{$.project}}{{end}}" class="ui green button">{{$.locale.Tr "repo.issues.choose.get_started"}}</a>
|
<div class="column right aligned">
|
||||||
|
<a href="{{.URL}}" class="ui green button">{{svg "octicon-link-external"}} {{$.locale.Tr "repo.issues.choose.open_external_link"}}</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{{end}}
|
||||||
|
{{if .IssueConfig.BlankIssuesEnabled}}
|
||||||
|
<div class="ui attached segment">
|
||||||
|
<div class="ui two column grid">
|
||||||
|
<div class="column left aligned">
|
||||||
|
<strong>{{.locale.Tr "repo.issues.choose.blank"}}</strong>
|
||||||
|
<br/>{{.locale.Tr "repo.issues.choose.blank_about"}}
|
||||||
|
</div>
|
||||||
|
<div class="column right aligned">
|
||||||
|
<a href="{{.RepoLink}}/issues/new?{{if .milestone}}&milestone={{.milestone}}{{end}}{{if $.project}}&project={{$.project}}{{end}}" class="ui green button">{{$.locale.Tr "repo.issues.choose.get_started"}}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{- if .IssueConfigError}}{{/* normal warning flash makes problems here*/}}
|
||||||
|
<div class="ui warning message">
|
||||||
|
<div class="text left">
|
||||||
|
<div>{{.locale.Tr "repo.issues.choose.invalid_config"}}</div>
|
||||||
|
<diy>{{.IssueConfigError}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{template "base/footer" .}}
|
{{template "base/footer" .}}
|
||||||
|
|
|
@ -5013,6 +5013,72 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/repos/{owner}/{repo}/issue_config": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"repository"
|
||||||
|
],
|
||||||
|
"summary": "Returns the issue config for a repo",
|
||||||
|
"operationId": "repoGetIssueConfig",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "owner of the repo",
|
||||||
|
"name": "owner",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "name of the repo",
|
||||||
|
"name": "repo",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"$ref": "#/responses/RepoIssueConfig"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/repos/{owner}/{repo}/issue_config/validate": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"repository"
|
||||||
|
],
|
||||||
|
"summary": "Returns the validation information for a issue config",
|
||||||
|
"operationId": "repoValidateIssueConfig",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "owner of the repo",
|
||||||
|
"name": "owner",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "name of the repo",
|
||||||
|
"name": "repo",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"$ref": "#/responses/RepoIssueConfigValidation"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/repos/{owner}/{repo}/issue_templates": {
|
"/repos/{owner}/{repo}/issue_templates": {
|
||||||
"get": {
|
"get": {
|
||||||
"produces": [
|
"produces": [
|
||||||
|
@ -18165,6 +18231,55 @@
|
||||||
},
|
},
|
||||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||||
},
|
},
|
||||||
|
"IssueConfig": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"blank_issues_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"x-go-name": "BlankIssuesEnabled"
|
||||||
|
},
|
||||||
|
"contact_links": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/IssueConfigContactLink"
|
||||||
|
},
|
||||||
|
"x-go-name": "ContactLinks"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||||
|
},
|
||||||
|
"IssueConfigContactLink": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"about": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "About"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "Name"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "URL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||||
|
},
|
||||||
|
"IssueConfigValidation": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "Message"
|
||||||
|
},
|
||||||
|
"valid": {
|
||||||
|
"type": "boolean",
|
||||||
|
"x-go-name": "Valid"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||||
|
},
|
||||||
"IssueDeadline": {
|
"IssueDeadline": {
|
||||||
"description": "IssueDeadline represents an issue deadline",
|
"description": "IssueDeadline represents an issue deadline",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
@ -21444,6 +21559,18 @@
|
||||||
"$ref": "#/definitions/RepoCollaboratorPermission"
|
"$ref": "#/definitions/RepoCollaboratorPermission"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"RepoIssueConfig": {
|
||||||
|
"description": "RepoIssueConfig",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/IssueConfig"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"RepoIssueConfigValidation": {
|
||||||
|
"description": "RepoIssueConfigValidation",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/IssueConfigValidation"
|
||||||
|
}
|
||||||
|
},
|
||||||
"Repository": {
|
"Repository": {
|
||||||
"description": "Repository",
|
"description": "Repository",
|
||||||
"schema": {
|
"schema": {
|
||||||
|
|
52
tests/integration/api_issue_config_test.go
Normal file
52
tests/integration/api_issue_config_test.go
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
|
"code.gitea.io/gitea/tests"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAPIReposGetDefaultIssueConfig(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
|
||||||
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||||
|
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||||
|
|
||||||
|
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issue_config", owner.Name, repo.Name)
|
||||||
|
req := NewRequest(t, "GET", urlStr)
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
var issueConfig api.IssueConfig
|
||||||
|
DecodeJSON(t, resp, &issueConfig)
|
||||||
|
|
||||||
|
assert.True(t, issueConfig.BlankIssuesEnabled)
|
||||||
|
assert.Equal(t, issueConfig.ContactLinks, make([]api.IssueConfigContactLink, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIReposValidateDefaultIssueConfig(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
|
||||||
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||||
|
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||||
|
|
||||||
|
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issue_config/validate", owner.Name, repo.Name)
|
||||||
|
req := NewRequest(t, "GET", urlStr)
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
var issueConfigValidation api.IssueConfigValidation
|
||||||
|
DecodeJSON(t, resp, &issueConfigValidation)
|
||||||
|
|
||||||
|
assert.True(t, issueConfigValidation.Valid)
|
||||||
|
assert.Equal(t, issueConfigValidation.Message, "")
|
||||||
|
}
|
Loading…
Reference in a new issue