Skip to content

Commit f9944c0

Browse files
guillep2klafriks
authored andcommitted
Configurable close and reopen keywords for PRs (#8120)
* Add settings for CloseKeywords and ReopenKeywords * Fix and improve tests * Use sync.Once() for initialization * Fix unintended exported function
1 parent ac6acce commit f9944c0

File tree

5 files changed

+198
-76
lines changed

5 files changed

+198
-76
lines changed

custom/conf/app.ini.sample

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ MAX_FILES = 5
6969
[repository.pull-request]
7070
; List of prefixes used in Pull Request title to mark them as Work In Progress
7171
WORK_IN_PROGRESS_PREFIXES=WIP:,[WIP]
72+
; List of keywords used in Pull Request comments to automatically close a related issue
73+
CLOSE_KEYWORDS=close,closes,closed,fix,fixes,fixed,resolve,resolves,resolved
74+
; List of keywords used in Pull Request comments to automatically reopen a related issue
75+
REOPEN_KEYWORDS=reopen,reopens,reopened
7276

7377
[repository.issue]
7478
; List of reasons why a Pull Request or Issue can be locked

docs/content/doc/advanced/config-cheat-sheet.en-us.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
7171

7272
- `WORK_IN_PROGRESS_PREFIXES`: **WIP:,\[WIP\]**: List of prefixes used in Pull Request
7373
title to mark them as Work In Progress
74+
- `CLOSE_KEYWORDS`: **close**, **closes**, **closed**, **fix**, **fixes**, **fixed**, **resolve**, **resolves**, **resolved**: List of
75+
keywords used in Pull Request comments to automatically close a related issue
76+
- `REOPEN_KEYWORDS`: **reopen**, **reopens**, **reopened**: List of keywords used in Pull Request comments to automatically reopen
77+
a related issue
7478

7579
### Repository - Issue (`repository.issue`)
7680

modules/references/references.go

Lines changed: 46 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"strings"
1212
"sync"
1313

14+
"code.gitea.io/gitea/modules/log"
1415
"code.gitea.io/gitea/modules/markup/mdstripper"
1516
"code.gitea.io/gitea/modules/setting"
1617
)
@@ -35,12 +36,8 @@ var (
3536
// e.g. gogits/gogs#12345
3637
crossReferenceIssueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-zA-Z-_\.]+/[0-9a-zA-Z-_\.]+#[0-9]+)(?:\s|$|\)|\]|\.(\s|$))`)
3738

38-
// Same as GitHub. See
39-
// https://help.github.com/articles/closing-issues-via-commit-messages
40-
issueCloseKeywords = []string{"close", "closes", "closed", "fix", "fixes", "fixed", "resolve", "resolves", "resolved"}
41-
issueReopenKeywords = []string{"reopen", "reopens", "reopened"}
42-
4339
issueCloseKeywordsPat, issueReopenKeywordsPat *regexp.Regexp
40+
issueKeywordsOnce sync.Once
4441

4542
giteaHostInit sync.Once
4643
giteaHost string
@@ -107,13 +104,40 @@ type RefSpan struct {
107104
End int
108105
}
109106

110-
func makeKeywordsPat(keywords []string) *regexp.Regexp {
111-
return regexp.MustCompile(`(?i)(?:\s|^|\(|\[)(` + strings.Join(keywords, `|`) + `):? $`)
107+
func makeKeywordsPat(words []string) *regexp.Regexp {
108+
acceptedWords := parseKeywords(words)
109+
if len(acceptedWords) == 0 {
110+
// Never match
111+
return nil
112+
}
113+
return regexp.MustCompile(`(?i)(?:\s|^|\(|\[)(` + strings.Join(acceptedWords, `|`) + `):? $`)
112114
}
113115

114-
func init() {
115-
issueCloseKeywordsPat = makeKeywordsPat(issueCloseKeywords)
116-
issueReopenKeywordsPat = makeKeywordsPat(issueReopenKeywords)
116+
func parseKeywords(words []string) []string {
117+
acceptedWords := make([]string, 0, 5)
118+
wordPat := regexp.MustCompile(`^[\pL]+$`)
119+
for _, word := range words {
120+
word = strings.ToLower(strings.TrimSpace(word))
121+
// Accept Unicode letter class runes (a-z, á, à, ä, )
122+
if wordPat.MatchString(word) {
123+
acceptedWords = append(acceptedWords, word)
124+
} else {
125+
log.Info("Invalid keyword: %s", word)
126+
}
127+
}
128+
return acceptedWords
129+
}
130+
131+
func newKeywords() {
132+
issueKeywordsOnce.Do(func() {
133+
// Delay initialization until after the settings module is initialized
134+
doNewKeywords(setting.Repository.PullRequest.CloseKeywords, setting.Repository.PullRequest.ReopenKeywords)
135+
})
136+
}
137+
138+
func doNewKeywords(close []string, reopen []string) {
139+
issueCloseKeywordsPat = makeKeywordsPat(close)
140+
issueReopenKeywordsPat = makeKeywordsPat(reopen)
117141
}
118142

119143
// getGiteaHostName returns a normalized string with the local host name, with no scheme or port information
@@ -310,13 +334,19 @@ func getCrossReference(content []byte, start, end int, fromLink bool) *rawRefere
310334
}
311335

312336
func findActionKeywords(content []byte, start int) (XRefAction, *RefSpan) {
313-
m := issueCloseKeywordsPat.FindSubmatchIndex(content[:start])
314-
if m != nil {
315-
return XRefActionCloses, &RefSpan{Start: m[2], End: m[3]}
337+
newKeywords()
338+
var m []int
339+
if issueCloseKeywordsPat != nil {
340+
m = issueCloseKeywordsPat.FindSubmatchIndex(content[:start])
341+
if m != nil {
342+
return XRefActionCloses, &RefSpan{Start: m[2], End: m[3]}
343+
}
316344
}
317-
m = issueReopenKeywordsPat.FindSubmatchIndex(content[:start])
318-
if m != nil {
319-
return XRefActionReopens, &RefSpan{Start: m[2], End: m[3]}
345+
if issueReopenKeywordsPat != nil {
346+
m = issueReopenKeywordsPat.FindSubmatchIndex(content[:start])
347+
if m != nil {
348+
return XRefActionReopens, &RefSpan{Start: m[2], End: m[3]}
349+
}
320350
}
321351
return XRefActionNone, nil
322352
}

0 commit comments

Comments
 (0)