Skip to content
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
fac2cb6
Allow disabling blank Issues
JakobDev Aug 25, 2022
adb916e
Change supported paths
JakobDev Aug 29, 2022
c7a8ee4
Change config paths
JakobDev Aug 29, 2022
4d008fc
Merge branch 'main' into noblankissues
JakobDev Sep 2, 2022
ecbbbe9
Fix bugs
JakobDev Sep 2, 2022
7b4415b
Return error when getting default branch fails
JakobDev Sep 4, 2022
bdd3c2a
Merge branch 'main' into noblankissues
JakobDev Dec 13, 2022
146bd6b
Add documentation
JakobDev Dec 13, 2022
bb72aea
Refactor code
JakobDev Dec 20, 2022
8624c96
Improve documentation
JakobDev Dec 26, 2022
714e92e
Add API
JakobDev Jan 2, 2023
18fe929
Make error text translatable
JakobDev Jan 2, 2023
099def7
Apply suggestion from wolfogre
JakobDev Jan 9, 2023
fe94b82
Merge branch 'main' into noblankissues
JakobDev Jan 23, 2023
5ae2ed0
Fix linting
JakobDev Jan 23, 2023
0564f5c
Run make generate-swagger
JakobDev Jan 23, 2023
b34913f
Add defer reader.Close()
JakobDev Jan 27, 2023
c1dc3e4
Merge branch 'main' into noblankissues
JakobDev Feb 6, 2023
abf14a5
Implement Contact Links
JakobDev Feb 6, 2023
2a16447
Show choose page when contact links exists
JakobDev Feb 6, 2023
579b8e5
Validate ContactLinks
JakobDev Feb 7, 2023
1f52bb7
Add API to validate issue config
JakobDev Feb 7, 2023
ed09834
Fix lint
JakobDev Feb 7, 2023
0e01deb
Add API tests
JakobDev Feb 7, 2023
b054f49
Merge branch 'main' into noblankissues
JakobDev Feb 7, 2023
17185ab
Changes requested by wolfogre
JakobDev Feb 8, 2023
062375f
Run make fmt
JakobDev Feb 8, 2023
c820b91
Merge branch 'main' into noblankissues
JakobDev Feb 8, 2023
2ad3111
Merge branch 'main' into noblankissues
JakobDev Feb 13, 2023
b9750dc
Merge branch 'main' into noblankissues
JakobDev Feb 14, 2023
7a4c6f8
Merge branch 'main' into noblankissues
JakobDev Mar 27, 2023
159c0f7
Rename IssueConfigValidate to IssueConfigValidation
JakobDev Mar 27, 2023
68f508b
Missed test
JakobDev Mar 27, 2023
609840c
Typo
JakobDev Mar 27, 2023
31d1353
Merge branch 'main' into noblankissues
JakobDev Mar 28, 2023
d4a69d5
Merge branch 'main' into noblankissues
jolheiser Mar 28, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions docs/content/doc/usage/issue-pull-request-templates.en-us.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,17 @@ Possible file names for issue templates:
- `.github/issue_template.yaml`
- `.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:

- `PULL_REQUEST_TEMPLATE.md`
Expand Down Expand Up @@ -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 | - | - |
| 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 |
114 changes: 114 additions & 0 deletions modules/context/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"context"
"fmt"
"html"
"io"
"net/http"
"net/url"
"path"
Expand All @@ -33,6 +34,7 @@ import (
asymkey_service "code.gitea.io/gitea/services/asymkey"

"github.com/editorconfig/editorconfig-core-go/v2"
"gopkg.in/yaml.v3"
)

// IssueTemplateDirCandidates issue templates directory
Expand All @@ -47,6 +49,13 @@ var IssueTemplateDirCandidates = []string{
".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
Expand Down Expand Up @@ -1099,3 +1108,108 @@ func (ctx *Context) IssueTemplatesErrorsFromDefaultBranch() ([]*api.IssueTemplat
}
return issueTemplates, invalidFiles
}

func GetDefaultIssueConfig() api.IssueConfig {
return api.IssueConfig{
BlankIssuesEnabled: true,
Comment thread
JakobDev marked this conversation as resolved.
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) {
Comment thread
JakobDev marked this conversation as resolved.
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
}

Comment thread
JakobDev marked this conversation as resolved.
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)
}
Comment thread
delvh marked this conversation as resolved.

_, err = url.ParseRequestURI(link.URL)
if err != nil {
return GetDefaultIssueConfig(), fmt.Errorf("%s is not a valid URL", link.URL)
}
}

return issueConfig, nil
Comment thread
wolfogre marked this conversation as resolved.
}

// 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 {
Comment thread
JakobDev marked this conversation as resolved.
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
}
16 changes: 16 additions & 0 deletions modules/structs/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}

type IssueConfigContactLink struct {
Comment thread
delvh marked this conversation as resolved.
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 IssueConfigValidate struct {
Comment thread
delvh marked this conversation as resolved.
Outdated
Valid bool `json:"valid"`
Message string `json:"message"`
}
Comment on lines +193 to +207
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I've noticed from having to work with the GitHub templates already:
Collaborators who already have a lot of in-depth knowledge about a project tend to specify their issues with enough detail that they don't necessarily need to input all troubleshooting information.
Having to fill out the template can be pretty tiresome, especially for large projects like Gitea itself, and often leads to entering unnecessary information simply to satisfy the validation.
This means that later on, another value for-contributors, for example, could be added.
So, we could think about converting the type to (bool/)string, to support more use cases later on.
You don't need to implement the additional functionality I mentioned yet as that can be postponed.
However, I think converting it into a string can already be done in this PR.
As far as I know, it should not make a difference for the yaml parser.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that having such a setting would be useful, but @lunny requested compatibility with GitHub above, that's the reason it also searches in .github for the config. On GitHub, blank_issues_enabled is a boolean. That doesn't mean we can't add our own settings, but if we want compatibility with GitHub, we should not use a different type for the same setting as GitHub. Another setting would be the better solution here.

Copy link
Copy Markdown
Member

@delvh delvh Dec 26, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it would accept the same things as GitHub: true, yes, on, off, no, false (-> strconv.ParseBool),
and then more (i.e. for-contributors).
So it would still be compatible with GitHub?
As far as I know, you don't need " around a YAML string…

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I know, you don't need " around a YAML string…

That's true in most cases, but reserved keywords like true or false or numbers needs quotes to be accepted as string.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh. Didn't know that.
But can our YAML Parser perhaps reconvert a boolean to a string?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fwiw the parser seems to gracefully handle it, though I would need to test it in this context specifically.
https://go.dev/play/p/jNdWPWtK-Jz

Copy link
Copy Markdown
Member

@delvh delvh Dec 27, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's way better and easier to just add another setting like enable_blank_issues_for_contributors.

I strongly disagree there.
That simply allows for an ungodly amount of edge cases and illegal states.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fwiw the parser seems to gracefully handle it, though I would need to test it in this context specifically. https://go.dev/play/p/jNdWPWtK-Jz

That's exactly what I hoped for.
This means we can still convert it without repercussions.
But I agree with @jolheiser, we should certainly ensure with a test that this behavior is fulfilled.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think this is a bad idea. Even is that could parsed, this will confuse users and is still not comptaible with GitHub. I will keep the current behaviour for this PR. It is better to do is in another PR.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can just keep it only allow true and false in this PR. And in future PR, it can be allowed other values but we don't need to discuss it in this PR.


// IssueTemplateType defines issue template type
type IssueTemplateType string

Expand Down
2 changes: 2 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -1267,10 +1267,12 @@ issues.new.no_assignees = No Assignees
issues.new.no_reviewers = No reviewers
issues.new.add_reviewer_title = Request review
issues.choose.get_started = Get Started
issues.choose.open_external_link = Open
issues.choose.blank = Default
issues.choose.blank_about = Create an issue from default template.
issues.choose.ignore_invalid_templates = Invalid templates have been ignored
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.create = Create Issue
issues.new_label = New Label
Expand Down
2 changes: 2 additions & 0 deletions routers/api/v1/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -1152,6 +1152,8 @@ func Routes(ctx gocontext.Context) *web.Route {
}, reqAdmin())
}, reqAnyRepoReader())
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)
}, repoAssignment())
})
Expand Down
55 changes: 55 additions & 0 deletions routers/api/v1/repo/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -1099,3 +1099,58 @@ func GetIssueTemplates(ctx *context.APIContext) {

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/RepoIssueConfigValidate"
_, err := ctx.IssueConfigFromDefaultBranch()

if err == nil {
ctx.JSON(http.StatusOK, api.IssueConfigValidate{Valid: true, Message: ""})
} else {
ctx.JSON(http.StatusOK, api.IssueConfigValidate{Valid: false, Message: err.Error()})
}
}
14 changes: 14 additions & 0 deletions routers/api/v1/swagger/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -386,3 +386,17 @@ type swaggerRepoCollaboratorPermission struct {
// in:body
Body api.RepoCollaboratorPermission `json:"body"`
}

// RepoIssueConfig
// swagger:response RepoIssueConfig
type swaggerRepoIssueConfig struct {
// in:body
Body api.IssueConfig `json:"body"`
}

// RepoIssueConfigValidate
// swagger:response RepoIssueConfigValidate
type swaggerRepoIssueConfigValidate struct {
// in:body
Body api.IssueConfigValidate `json:"body"`
}
14 changes: 9 additions & 5 deletions routers/web/repo/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,7 @@ func Issues(ctx *context.Context) {
}
ctx.Data["Title"] = ctx.Tr("repo.issues")
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))
Expand Down Expand Up @@ -827,7 +827,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"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0
ctx.Data["NewIssueChooseTemplate"] = ctx.HasIssueTemplatesOrContactLinks()
ctx.Data["RequireTribute"] = true
ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
title := ctx.FormString("title")
Expand Down Expand Up @@ -925,12 +925,16 @@ func NewIssueChooseTemplate(ctx *context.Context) {
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.
ctx.Redirect(fmt.Sprintf("%s/issues/new?%s", ctx.Repo.Repository.Link(), ctx.Req.URL.RawQuery), http.StatusSeeOther)
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["project"] = ctx.FormInt64("project")

Expand Down Expand Up @@ -1065,7 +1069,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"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0
ctx.Data["NewIssueChooseTemplate"] = ctx.HasIssueTemplatesOrContactLinks()
ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
upload.AddUploadContext(ctx, "comment")
Expand Down Expand Up @@ -1255,7 +1259,7 @@ func ViewIssue(ctx *context.Context) {
return
}
ctx.Data["PageIsIssueList"] = true
ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0
ctx.Data["NewIssueChooseTemplate"] = ctx.HasIssueTemplatesOrContactLinks()
}

if issue.IsPull && !ctx.Repo.CanRead(unit.TypeIssues) {
Expand Down
3 changes: 3 additions & 0 deletions routers/web/repo/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,9 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st
if ctx.Repo.TreePath == ".editorconfig" {
_, editorconfigErr := ctx.Repo.GetEditorconfig(ctx.Repo.Commit)
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"
Expand Down
Loading