Skip to content

Commit 15809d8

Browse files
guillep2kzeripath
authored andcommitted
Rewrite reference processing code in preparation for opening/closing from comment references (#8261)
* Add a markdown stripper for mentions and xrefs * Improve comments * Small code simplification * Move reference code to modules/references * Fix typo * Make MarkdownStripper return [][]byte * Implement preliminary keywords parsing * Add FIXME comment * Fix comment * make fmt * Fix permissions check * Fix text assumptions * Fix imports * Fix lint, fmt * Fix unused import * Add missing export comment * Bypass revive on implemented interface * Move mdstripper into its own package * Support alphanumeric patterns * Refactor FindAllMentions * Move mentions test to references * Parse mentions from reference package * Refactor code to implement renderizable references * Fix typo * Move patterns and tests to the references package * Fix nil reference * Preliminary rendering attempt of closing keywords * Normalize names, comments, general tidy-up * Add CSS style for action keywords * Fix permission for admin and owner * Fix golangci-lint * Fix golangci-lint
1 parent 6e3f510 commit 15809d8

16 files changed

+1116
-431
lines changed

integrations/issue_test.go

+7-6
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"testing"
1414

1515
"code.gitea.io/gitea/models"
16+
"code.gitea.io/gitea/modules/references"
1617
"code.gitea.io/gitea/modules/setting"
1718
"code.gitea.io/gitea/modules/test"
1819

@@ -207,7 +208,7 @@ func TestIssueCrossReference(t *testing.T) {
207208
RefIssueID: issueRef.ID,
208209
RefCommentID: 0,
209210
RefIsPull: false,
210-
RefAction: models.XRefActionNone})
211+
RefAction: references.XRefActionNone})
211212

212213
// Edit title, neuter ref
213214
testIssueChangeInfo(t, "user2", issueRefURL, "title", "Title no ref")
@@ -217,7 +218,7 @@ func TestIssueCrossReference(t *testing.T) {
217218
RefIssueID: issueRef.ID,
218219
RefCommentID: 0,
219220
RefIsPull: false,
220-
RefAction: models.XRefActionNeutered})
221+
RefAction: references.XRefActionNeutered})
221222

222223
// Ref from issue content
223224
issueRefURL, issueRef = testIssueWithBean(t, "user2", 1, "TitleXRef", fmt.Sprintf("Description ref #%d", issueBase.Index))
@@ -227,7 +228,7 @@ func TestIssueCrossReference(t *testing.T) {
227228
RefIssueID: issueRef.ID,
228229
RefCommentID: 0,
229230
RefIsPull: false,
230-
RefAction: models.XRefActionNone})
231+
RefAction: references.XRefActionNone})
231232

232233
// Edit content, neuter ref
233234
testIssueChangeInfo(t, "user2", issueRefURL, "content", "Description no ref")
@@ -237,7 +238,7 @@ func TestIssueCrossReference(t *testing.T) {
237238
RefIssueID: issueRef.ID,
238239
RefCommentID: 0,
239240
RefIsPull: false,
240-
RefAction: models.XRefActionNeutered})
241+
RefAction: references.XRefActionNeutered})
241242

242243
// Ref from a comment
243244
session := loginUser(t, "user2")
@@ -248,7 +249,7 @@ func TestIssueCrossReference(t *testing.T) {
248249
RefIssueID: issueRef.ID,
249250
RefCommentID: commentID,
250251
RefIsPull: false,
251-
RefAction: models.XRefActionNone}
252+
RefAction: references.XRefActionNone}
252253
models.AssertExistsAndLoadBean(t, comment)
253254

254255
// Ref from a different repository
@@ -259,7 +260,7 @@ func TestIssueCrossReference(t *testing.T) {
259260
RefIssueID: issueRef.ID,
260261
RefCommentID: 0,
261262
RefIsPull: false,
262-
RefAction: models.XRefActionNone})
263+
RefAction: references.XRefActionNone})
263264
}
264265

265266
func testIssueWithBean(t *testing.T, user string, repoID int64, title, content string) (string, *models.Issue) {

models/action.go

+37-140
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,14 @@ import (
1010
"fmt"
1111
"html"
1212
"path"
13-
"regexp"
1413
"strconv"
1514
"strings"
1615
"time"
17-
"unicode"
1816

1917
"code.gitea.io/gitea/modules/base"
2018
"code.gitea.io/gitea/modules/git"
2119
"code.gitea.io/gitea/modules/log"
20+
"code.gitea.io/gitea/modules/references"
2221
"code.gitea.io/gitea/modules/setting"
2322
api "code.gitea.io/gitea/modules/structs"
2423
"code.gitea.io/gitea/modules/timeutil"
@@ -54,29 +53,6 @@ const (
5453
ActionMirrorSyncDelete // 20
5554
)
5655

57-
var (
58-
// Same as GitHub. See
59-
// https://help.github.com/articles/closing-issues-via-commit-messages
60-
issueCloseKeywords = []string{"close", "closes", "closed", "fix", "fixes", "fixed", "resolve", "resolves", "resolved"}
61-
issueReopenKeywords = []string{"reopen", "reopens", "reopened"}
62-
63-
issueCloseKeywordsPat, issueReopenKeywordsPat *regexp.Regexp
64-
issueReferenceKeywordsPat *regexp.Regexp
65-
)
66-
67-
const issueRefRegexpStr = `(?:([0-9a-zA-Z-_\.]+)/([0-9a-zA-Z-_\.]+))?(#[0-9]+)+`
68-
const issueRefRegexpStrNoKeyword = `(?:\s|^|\(|\[)(?:([0-9a-zA-Z-_\.]+)/([0-9a-zA-Z-_\.]+))?(#[0-9]+)(?:\s|$|\)|\]|:|\.(\s|$))`
69-
70-
func assembleKeywordsPattern(words []string) string {
71-
return fmt.Sprintf(`(?i)(?:%s)(?::?) %s`, strings.Join(words, "|"), issueRefRegexpStr)
72-
}
73-
74-
func init() {
75-
issueCloseKeywordsPat = regexp.MustCompile(assembleKeywordsPattern(issueCloseKeywords))
76-
issueReopenKeywordsPat = regexp.MustCompile(assembleKeywordsPattern(issueReopenKeywords))
77-
issueReferenceKeywordsPat = regexp.MustCompile(issueRefRegexpStrNoKeyword)
78-
}
79-
8056
// Action represents user operation type and other information to
8157
// repository. It implemented interface base.Actioner so that can be
8258
// used in template render.
@@ -351,10 +327,6 @@ func RenameRepoAction(actUser *User, oldRepoName string, repo *Repository) error
351327
return renameRepoAction(x, actUser, oldRepoName, repo)
352328
}
353329

354-
func issueIndexTrimRight(c rune) bool {
355-
return !unicode.IsDigit(c)
356-
}
357-
358330
// PushCommit represents a commit in a push operation.
359331
type PushCommit struct {
360332
Sha1 string
@@ -480,39 +452,9 @@ func (pc *PushCommits) AvatarLink(email string) string {
480452
}
481453

482454
// getIssueFromRef returns the issue referenced by a ref. Returns a nil *Issue
483-
// if the provided ref is misformatted or references a non-existent issue.
484-
func getIssueFromRef(repo *Repository, ref string) (*Issue, error) {
485-
ref = ref[strings.IndexByte(ref, ' ')+1:]
486-
ref = strings.TrimRightFunc(ref, issueIndexTrimRight)
487-
488-
var refRepo *Repository
489-
poundIndex := strings.IndexByte(ref, '#')
490-
if poundIndex < 0 {
491-
return nil, nil
492-
} else if poundIndex == 0 {
493-
refRepo = repo
494-
} else {
495-
slashIndex := strings.IndexByte(ref, '/')
496-
if slashIndex < 0 || slashIndex >= poundIndex {
497-
return nil, nil
498-
}
499-
ownerName := ref[:slashIndex]
500-
repoName := ref[slashIndex+1 : poundIndex]
501-
var err error
502-
refRepo, err = GetRepositoryByOwnerAndName(ownerName, repoName)
503-
if err != nil {
504-
if IsErrRepoNotExist(err) {
505-
return nil, nil
506-
}
507-
return nil, err
508-
}
509-
}
510-
issueIndex, err := strconv.ParseInt(ref[poundIndex+1:], 10, 64)
511-
if err != nil {
512-
return nil, nil
513-
}
514-
515-
issue, err := GetIssueByIndex(refRepo.ID, issueIndex)
455+
// if the provided ref references a non-existent issue.
456+
func getIssueFromRef(repo *Repository, index int64) (*Issue, error) {
457+
issue, err := GetIssueByIndex(repo.ID, index)
516458
if err != nil {
517459
if IsErrIssueNotExist(err) {
518460
return nil, nil
@@ -522,20 +464,7 @@ func getIssueFromRef(repo *Repository, ref string) (*Issue, error) {
522464
return issue, nil
523465
}
524466

525-
func changeIssueStatus(repo *Repository, doer *User, ref string, refMarked map[int64]bool, status bool) error {
526-
issue, err := getIssueFromRef(repo, ref)
527-
if err != nil {
528-
return err
529-
}
530-
531-
if issue == nil || refMarked[issue.ID] {
532-
return nil
533-
}
534-
refMarked[issue.ID] = true
535-
536-
if issue.RepoID != repo.ID || issue.IsClosed == status {
537-
return nil
538-
}
467+
func changeIssueStatus(repo *Repository, issue *Issue, doer *User, status bool) error {
539468

540469
stopTimerIfAvailable := func(doer *User, issue *Issue) error {
541470

@@ -549,7 +478,7 @@ func changeIssueStatus(repo *Repository, doer *User, ref string, refMarked map[i
549478
}
550479

551480
issue.Repo = repo
552-
if err = issue.ChangeStatus(doer, status); err != nil {
481+
if err := issue.ChangeStatus(doer, status); err != nil {
553482
// Don't return an error when dependencies are open as this would let the push fail
554483
if IsErrDependenciesLeft(err) {
555484
return stopTimerIfAvailable(doer, issue)
@@ -566,99 +495,67 @@ func UpdateIssuesCommit(doer *User, repo *Repository, commits []*PushCommit, bra
566495
for i := len(commits) - 1; i >= 0; i-- {
567496
c := commits[i]
568497

569-
refMarked := make(map[int64]bool)
498+
type markKey struct {
499+
ID int64
500+
Action references.XRefAction
501+
}
502+
503+
refMarked := make(map[markKey]bool)
570504
var refRepo *Repository
505+
var refIssue *Issue
571506
var err error
572-
for _, m := range issueReferenceKeywordsPat.FindAllStringSubmatch(c.Message, -1) {
573-
if len(m[3]) == 0 {
574-
continue
575-
}
576-
ref := m[3]
507+
for _, ref := range references.FindAllIssueReferences(c.Message) {
577508

578509
// issue is from another repo
579-
if len(m[1]) > 0 && len(m[2]) > 0 {
580-
refRepo, err = GetRepositoryFromMatch(m[1], m[2])
510+
if len(ref.Owner) > 0 && len(ref.Name) > 0 {
511+
refRepo, err = GetRepositoryFromMatch(ref.Owner, ref.Name)
581512
if err != nil {
582513
continue
583514
}
584515
} else {
585516
refRepo = repo
586517
}
587-
issue, err := getIssueFromRef(refRepo, ref)
588-
if err != nil {
518+
if refIssue, err = getIssueFromRef(refRepo, ref.Index); err != nil {
589519
return err
590520
}
591-
592-
if issue == nil || refMarked[issue.ID] {
521+
if refIssue == nil {
593522
continue
594523
}
595-
refMarked[issue.ID] = true
596524

597-
message := fmt.Sprintf(`<a href="%s/commit/%s">%s</a>`, repo.Link(), c.Sha1, html.EscapeString(c.Message))
598-
if err = CreateRefComment(doer, refRepo, issue, message, c.Sha1); err != nil {
525+
perm, err := GetUserRepoPermission(refRepo, doer)
526+
if err != nil {
599527
return err
600528
}
601-
}
602529

603-
// Change issue status only if the commit has been pushed to the default branch.
604-
// and if the repo is configured to allow only that
605-
if repo.DefaultBranch != branchName && !repo.CloseIssuesViaCommitInAnyBranch {
606-
continue
607-
}
608-
refMarked = make(map[int64]bool)
609-
for _, m := range issueCloseKeywordsPat.FindAllStringSubmatch(c.Message, -1) {
610-
if len(m[3]) == 0 {
530+
key := markKey{ID: refIssue.ID, Action: ref.Action}
531+
if refMarked[key] {
611532
continue
612533
}
613-
ref := m[3]
534+
refMarked[key] = true
614535

615-
// issue is from another repo
616-
if len(m[1]) > 0 && len(m[2]) > 0 {
617-
refRepo, err = GetRepositoryFromMatch(m[1], m[2])
618-
if err != nil {
619-
continue
620-
}
621-
} else {
622-
refRepo = repo
623-
}
624-
625-
perm, err := GetUserRepoPermission(refRepo, doer)
626-
if err != nil {
627-
return err
628-
}
629-
// only close issues in another repo if user has push access
630-
if perm.CanWrite(UnitTypeCode) {
631-
if err := changeIssueStatus(refRepo, doer, ref, refMarked, true); err != nil {
536+
// only create comments for issues if user has permission for it
537+
if perm.IsAdmin() || perm.IsOwner() || perm.CanWrite(UnitTypeIssues) {
538+
message := fmt.Sprintf(`<a href="%s/commit/%s">%s</a>`, repo.Link(), c.Sha1, html.EscapeString(c.Message))
539+
if err = CreateRefComment(doer, refRepo, refIssue, message, c.Sha1); err != nil {
632540
return err
633541
}
634542
}
635-
}
636543

637-
// It is conflict to have close and reopen at same time, so refsMarked doesn't need to reinit here.
638-
for _, m := range issueReopenKeywordsPat.FindAllStringSubmatch(c.Message, -1) {
639-
if len(m[3]) == 0 {
544+
// Process closing/reopening keywords
545+
if ref.Action != references.XRefActionCloses && ref.Action != references.XRefActionReopens {
640546
continue
641547
}
642-
ref := m[3]
643548

644-
// issue is from another repo
645-
if len(m[1]) > 0 && len(m[2]) > 0 {
646-
refRepo, err = GetRepositoryFromMatch(m[1], m[2])
647-
if err != nil {
648-
continue
649-
}
650-
} else {
651-
refRepo = repo
652-
}
653-
654-
perm, err := GetUserRepoPermission(refRepo, doer)
655-
if err != nil {
656-
return err
549+
// Change issue status only if the commit has been pushed to the default branch.
550+
// and if the repo is configured to allow only that
551+
// FIXME: we should be using Issue.ref if set instead of repo.DefaultBranch
552+
if repo.DefaultBranch != branchName && !repo.CloseIssuesViaCommitInAnyBranch {
553+
continue
657554
}
658555

659-
// only reopen issues in another repo if user has push access
660-
if perm.CanWrite(UnitTypeCode) {
661-
if err := changeIssueStatus(refRepo, doer, ref, refMarked, false); err != nil {
556+
// only close issues in another repo if user has push access
557+
if perm.IsAdmin() || perm.IsOwner() || perm.CanWrite(UnitTypeCode) {
558+
if err := changeIssueStatus(refRepo, refIssue, doer, ref.Action == references.XRefActionCloses); err != nil {
662559
return err
663560
}
664561
}

models/action_test.go

+1-52
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package models
22

33
import (
4-
"fmt"
54
"path"
65
"strings"
76
"testing"
@@ -181,56 +180,6 @@ func TestPushCommits_AvatarLink(t *testing.T) {
181180
pushCommits.AvatarLink("[email protected]"))
182181
}
183182

184-
func TestRegExp_issueReferenceKeywordsPat(t *testing.T) {
185-
trueTestCases := []string{
186-
"#2",
187-
"[#2]",
188-
"please see go-gitea/gitea#5",
189-
"#2:",
190-
}
191-
falseTestCases := []string{
192-
"kb#2",
193-
"#2xy",
194-
}
195-
196-
for _, testCase := range trueTestCases {
197-
assert.True(t, issueReferenceKeywordsPat.MatchString(testCase))
198-
}
199-
for _, testCase := range falseTestCases {
200-
assert.False(t, issueReferenceKeywordsPat.MatchString(testCase))
201-
}
202-
}
203-
204-
func Test_getIssueFromRef(t *testing.T) {
205-
assert.NoError(t, PrepareTestDatabase())
206-
repo := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
207-
for _, test := range []struct {
208-
Ref string
209-
ExpectedIssueID int64
210-
}{
211-
{"#2", 2},
212-
{"reopen #2", 2},
213-
{"user2/repo2#1", 4},
214-
{"fixes user2/repo2#1", 4},
215-
{"fixes: user2/repo2#1", 4},
216-
} {
217-
issue, err := getIssueFromRef(repo, test.Ref)
218-
assert.NoError(t, err)
219-
if assert.NotNil(t, issue) {
220-
assert.EqualValues(t, test.ExpectedIssueID, issue.ID)
221-
}
222-
}
223-
224-
for _, badRef := range []string{
225-
"doesnotexist/doesnotexist#1",
226-
fmt.Sprintf("#%d", NonexistentID),
227-
} {
228-
issue, err := getIssueFromRef(repo, badRef)
229-
assert.NoError(t, err)
230-
assert.Nil(t, issue)
231-
}
232-
}
233-
234183
func TestUpdateIssuesCommit(t *testing.T) {
235184
assert.NoError(t, PrepareTestDatabase())
236185
pushCommits := []*PushCommit{
@@ -431,7 +380,7 @@ func TestUpdateIssuesCommit_AnotherRepoNoPermission(t *testing.T) {
431380
AssertNotExistsBean(t, commentBean)
432381
AssertNotExistsBean(t, issueBean, "is_closed=1")
433382
assert.NoError(t, UpdateIssuesCommit(user, repo, pushCommits, repo.DefaultBranch))
434-
AssertExistsAndLoadBean(t, commentBean)
383+
AssertNotExistsBean(t, commentBean)
435384
AssertNotExistsBean(t, issueBean, "is_closed=1")
436385
CheckConsistencyFor(t, &Action{})
437386
}

0 commit comments

Comments
 (0)