Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
4 changes: 4 additions & 0 deletions custom/conf/app.ini.sample
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ MAX_FILES = 5
[repository.pull-request]
; List of prefixes used in Pull Request title to mark them as Work In Progress
WORK_IN_PROGRESS_PREFIXES=WIP:,[WIP]
; List of keywords used in Pull Request comments to automatically close a related issue
CLOSE_KEYWORDS=close,closes,closed,fix,fixes,fixed,resolve,resolves,resolved
; List of keywords used in Pull Request comments to automatically reopen a related issue
REOPEN_KEYWORDS=reopen,reopens,reopened

[repository.issue]
; List of reasons why a Pull Request or Issue can be locked
Expand Down
4 changes: 4 additions & 0 deletions docs/content/doc/advanced/config-cheat-sheet.en-us.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.

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

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

Expand Down
131 changes: 79 additions & 52 deletions models/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"regexp"
"strconv"
"strings"
"sync"
"time"
"unicode"

Expand Down Expand Up @@ -55,28 +56,25 @@ const (
)

var (
// Same as GitHub. See
// https://help.github.com/articles/closing-issues-via-commit-messages
issueCloseKeywords = []string{"close", "closes", "closed", "fix", "fixes", "fixed", "resolve", "resolves", "resolved"}
issueReopenKeywords = []string{"reopen", "reopens", "reopened"}

issueCloseKeywordsPat, issueReopenKeywordsPat *regexp.Regexp
issueReferenceKeywordsPat *regexp.Regexp
issueKeywordsOnce sync.Once
)

const issueRefRegexpStr = `(?:([0-9a-zA-Z-_\.]+)/([0-9a-zA-Z-_\.]+))?(#[0-9]+)+`
const issueRefRegexpStrNoKeyword = `(?:\s|^|\(|\[)(?:([0-9a-zA-Z-_\.]+)/([0-9a-zA-Z-_\.]+))?(#[0-9]+)(?:\s|$|\)|\]|:|\.(\s|$))`

func assembleKeywordsPattern(words []string) string {
return fmt.Sprintf(`(?i)(?:%s)(?::?) %s`, strings.Join(words, "|"), issueRefRegexpStr)
}

func init() {
issueCloseKeywordsPat = regexp.MustCompile(assembleKeywordsPattern(issueCloseKeywords))
issueReopenKeywordsPat = regexp.MustCompile(assembleKeywordsPattern(issueReopenKeywords))
issueReferenceKeywordsPat = regexp.MustCompile(issueRefRegexpStrNoKeyword)
}

func initKeywordsRegexp() {
issueKeywordsOnce.Do(func() {
issueCloseKeywordsPat = buildKeywordsRegexp(setting.Repository.PullRequest.CloseKeywords)
issueReopenKeywordsPat = buildKeywordsRegexp(setting.Repository.PullRequest.ReopenKeywords)
})
}

// Action represents user operation type and other information to
// repository. It implemented interface base.Actioner so that can be
// used in template render.
Expand Down Expand Up @@ -562,6 +560,7 @@ func changeIssueStatus(repo *Repository, doer *User, ref string, refMarked map[i

// UpdateIssuesCommit checks if issues are manipulated by commit message.
func UpdateIssuesCommit(doer *User, repo *Repository, commits []*PushCommit, branchName string) error {
initKeywordsRegexp()
// Commits are appended in the reverse order.
for i := len(commits) - 1; i >= 0; i-- {
c := commits[i]
Expand Down Expand Up @@ -606,61 +605,65 @@ func UpdateIssuesCommit(doer *User, repo *Repository, commits []*PushCommit, bra
continue
}
refMarked = make(map[int64]bool)
for _, m := range issueCloseKeywordsPat.FindAllStringSubmatch(c.Message, -1) {
if len(m[3]) == 0 {
continue
}
ref := m[3]

// issue is from another repo
if len(m[1]) > 0 && len(m[2]) > 0 {
refRepo, err = GetRepositoryFromMatch(m[1], m[2])
if err != nil {
if issueCloseKeywordsPat != nil {
for _, m := range issueCloseKeywordsPat.FindAllStringSubmatch(c.Message, -1) {
if len(m[3]) == 0 {
continue
}
} else {
refRepo = repo
}
ref := m[3]

// issue is from another repo
if len(m[1]) > 0 && len(m[2]) > 0 {
refRepo, err = GetRepositoryFromMatch(m[1], m[2])
if err != nil {
continue
}
} else {
refRepo = repo
}

perm, err := GetUserRepoPermission(refRepo, doer)
if err != nil {
return err
}
// only close issues in another repo if user has push access
if perm.CanWrite(UnitTypeCode) {
if err := changeIssueStatus(refRepo, doer, ref, refMarked, true); err != nil {
perm, err := GetUserRepoPermission(refRepo, doer)
if err != nil {
return err
}
// only close issues in another repo if user has push access
if perm.CanWrite(UnitTypeCode) {
if err := changeIssueStatus(refRepo, doer, ref, refMarked, true); err != nil {
return err
}
}
}
}

// It is conflict to have close and reopen at same time, so refsMarked doesn't need to reinit here.
for _, m := range issueReopenKeywordsPat.FindAllStringSubmatch(c.Message, -1) {
if len(m[3]) == 0 {
continue
}
ref := m[3]

// issue is from another repo
if len(m[1]) > 0 && len(m[2]) > 0 {
refRepo, err = GetRepositoryFromMatch(m[1], m[2])
if err != nil {
if issueReopenKeywordsPat != nil {
for _, m := range issueReopenKeywordsPat.FindAllStringSubmatch(c.Message, -1) {
if len(m[3]) == 0 {
continue
}
} else {
refRepo = repo
}

perm, err := GetUserRepoPermission(refRepo, doer)
if err != nil {
return err
}
ref := m[3]

// issue is from another repo
if len(m[1]) > 0 && len(m[2]) > 0 {
refRepo, err = GetRepositoryFromMatch(m[1], m[2])
if err != nil {
continue
}
} else {
refRepo = repo
}

// only reopen issues in another repo if user has push access
if perm.CanWrite(UnitTypeCode) {
if err := changeIssueStatus(refRepo, doer, ref, refMarked, false); err != nil {
perm, err := GetUserRepoPermission(refRepo, doer)
if err != nil {
return err
}

// only reopen issues in another repo if user has push access
if perm.CanWrite(UnitTypeCode) {
if err := changeIssueStatus(refRepo, doer, ref, refMarked, false); err != nil {
return err
}
}
}
}
}
Expand Down Expand Up @@ -837,3 +840,27 @@ func GetFeeds(opts GetFeedsOptions) ([]*Action, error) {

return actions, nil
}

func parseKeywords(words []string) []string {
acceptedWords := make([]string, 0, 5)
wordPat := regexp.MustCompile(`^[\pL]+$`)
for _, word := range words {
word = strings.ToLower(strings.TrimSpace(word))
// Accept Unicode letter class runes (a-z, á, à, ä, )
if wordPat.MatchString(word) {
acceptedWords = append(acceptedWords, word)
} else {
log.Info("Invalid keyword: %s", word)
}
}
return acceptedWords
}

func buildKeywordsRegexp(words []string) *regexp.Regexp {
acceptedWords := parseKeywords(words)
if len(acceptedWords) == 0 {
// Never match
return nil
}
return regexp.MustCompile(fmt.Sprintf(`(?i)(?:%s)(?::?) %s`, strings.Join(acceptedWords, "|"), issueRefRegexpStr))
}
25 changes: 25 additions & 0 deletions models/action_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -542,3 +542,28 @@ func TestGetFeeds2(t *testing.T) {
assert.NoError(t, err)
assert.Len(t, actions, 0)
}

func TestParseCloseKeywords(t *testing.T) {
// Test parsing of CloseKeywords and ReopenKeywords
assert.Len(t, parseKeywords([]string{""}), 0)
assert.Len(t, parseKeywords([]string{" aa ", " bb ", "99", "#", "", "this is", "cc"}), 3)

for _, test := range []struct {
pattern string
match string
}{
{"close", "Close #123"},
{"cerró", "cerró #123"},
{"cerró", "CERRÓ #123"},
{"закрывается", "закрывается #123"},
{"κλείνει", "κλείνει #123"},
{"关闭", "关闭 #123"},
{"閉じます", "閉じます #123"},
} {
pat := buildKeywordsRegexp([]string{test.pattern})
assert.NotNil(t, pat)
res := pat.FindAllStringSubmatch(test.match, -1)
assert.Len(t, res, 1)
assert.EqualValues(t, [][]string([][]string{{test.match, "", "", "#123"}}), res)
}
}
8 changes: 8 additions & 0 deletions modules/setting/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ var (
// Pull request settings
PullRequest struct {
WorkInProgressPrefixes []string
CloseKeywords []string
ReopenKeywords []string
} `ini:"repository.pull-request"`

// Issue Setting
Expand Down Expand Up @@ -112,8 +114,14 @@ var (
// Pull request settings
PullRequest: struct {
WorkInProgressPrefixes []string
CloseKeywords []string
ReopenKeywords []string
}{
WorkInProgressPrefixes: []string{"WIP:", "[WIP]"},
// Same as GitHub. See
// https://help.github.com/articles/closing-issues-via-commit-messages
CloseKeywords: strings.Split("close,closes,closed,fix,fixes,fixed,resolve,resolves,resolved", ","),
ReopenKeywords: strings.Split("reopen,reopens,reopened", ","),
},

// Issue settings
Expand Down