Browse Source

Fix ref parsing in commit messages (#3067)

Ethan Koenig 1 year ago
parent
commit
3163abedd6
3 changed files with 87 additions and 102 deletions
  1. 57 70
      models/action.go
  2. 30 0
      models/action_test.go
  3. 0 32
      models/issue.go

+ 57 - 70
models/action.go

@@ -14,15 +14,14 @@ import (
14 14
 	"time"
15 15
 	"unicode"
16 16
 
17
-	"github.com/Unknwon/com"
18
-	"github.com/go-xorm/builder"
19
-
20 17
 	"code.gitea.io/git"
21
-	api "code.gitea.io/sdk/gitea"
22
-
23 18
 	"code.gitea.io/gitea/modules/base"
24 19
 	"code.gitea.io/gitea/modules/log"
25 20
 	"code.gitea.io/gitea/modules/setting"
21
+	api "code.gitea.io/sdk/gitea"
22
+
23
+	"github.com/Unknwon/com"
24
+	"github.com/go-xorm/builder"
26 25
 )
27 26
 
28 27
 // ActionType represents the type of an action.
@@ -59,14 +58,16 @@ var (
59 58
 	issueReferenceKeywordsPat                     *regexp.Regexp
60 59
 )
61 60
 
61
+const issueRefRegexpStr = `(?:\S+/\S=)?#\d+`
62
+
62 63
 func assembleKeywordsPattern(words []string) string {
63
-	return fmt.Sprintf(`(?i)(?:%s) \S+`, strings.Join(words, "|"))
64
+	return fmt.Sprintf(`(?i)(?:%s) %s`, strings.Join(words, "|"), issueRefRegexpStr)
64 65
 }
65 66
 
66 67
 func init() {
67 68
 	issueCloseKeywordsPat = regexp.MustCompile(assembleKeywordsPattern(issueCloseKeywords))
68 69
 	issueReopenKeywordsPat = regexp.MustCompile(assembleKeywordsPattern(issueReopenKeywords))
69
-	issueReferenceKeywordsPat = regexp.MustCompile(`(?i)(?:)(^| )\S+`)
70
+	issueReferenceKeywordsPat = regexp.MustCompile(issueRefRegexpStr)
70 71
 }
71 72
 
72 73
 // Action represents user operation type and other information to
@@ -390,6 +391,49 @@ func (pc *PushCommits) AvatarLink(email string) string {
390 391
 	return pc.avatars[email]
391 392
 }
392 393
 
394
+// getIssueFromRef returns the issue referenced by a ref. Returns a nil *Issue
395
+// if the provided ref is misformatted or references a non-existent issue.
396
+func getIssueFromRef(repo *Repository, ref string) (*Issue, error) {
397
+	ref = ref[strings.IndexByte(ref, ' ')+1:]
398
+	ref = strings.TrimRightFunc(ref, issueIndexTrimRight)
399
+
400
+	var refRepo *Repository
401
+	poundIndex := strings.IndexByte(ref, '#')
402
+	if poundIndex < 0 {
403
+		return nil, nil
404
+	} else if poundIndex == 0 {
405
+		refRepo = repo
406
+	} else {
407
+		slashIndex := strings.IndexByte(ref, '/')
408
+		if slashIndex < 0 || slashIndex >= poundIndex {
409
+			return nil, nil
410
+		}
411
+		ownerName := ref[:slashIndex]
412
+		repoName := ref[slashIndex+1 : poundIndex]
413
+		var err error
414
+		refRepo, err = GetRepositoryByOwnerAndName(ownerName, repoName)
415
+		if err != nil {
416
+			if IsErrRepoNotExist(err) {
417
+				return nil, nil
418
+			}
419
+			return nil, err
420
+		}
421
+	}
422
+	issueIndex, err := strconv.ParseInt(ref[poundIndex+1:], 10, 64)
423
+	if err != nil {
424
+		return nil, nil
425
+	}
426
+
427
+	issue, err := GetIssueByIndex(refRepo.ID, int64(issueIndex))
428
+	if err != nil {
429
+		if IsErrIssueNotExist(err) {
430
+			return nil, nil
431
+		}
432
+		return nil, err
433
+	}
434
+	return issue, nil
435
+}
436
+
393 437
 // UpdateIssuesCommit checks if issues are manipulated by commit message.
394 438
 func UpdateIssuesCommit(doer *User, repo *Repository, commits []*PushCommit) error {
395 439
 	// Commits are appended in the reverse order.
@@ -398,31 +442,12 @@ func UpdateIssuesCommit(doer *User, repo *Repository, commits []*PushCommit) err
398 442
 
399 443
 		refMarked := make(map[int64]bool)
400 444
 		for _, ref := range issueReferenceKeywordsPat.FindAllString(c.Message, -1) {
401
-			ref = ref[strings.IndexByte(ref, byte(' '))+1:]
402
-			ref = strings.TrimRightFunc(ref, issueIndexTrimRight)
403
-
404
-			if len(ref) == 0 {
405
-				continue
406
-			}
407
-
408
-			// Add repo name if missing
409
-			if ref[0] == '#' {
410
-				ref = fmt.Sprintf("%s%s", repo.FullName(), ref)
411
-			} else if !strings.Contains(ref, "/") {
412
-				// FIXME: We don't support User#ID syntax yet
413
-				// return ErrNotImplemented
414
-				continue
415
-			}
416
-
417
-			issue, err := GetIssueByRef(ref)
445
+			issue, err := getIssueFromRef(repo, ref)
418 446
 			if err != nil {
419
-				if IsErrIssueNotExist(err) || err == errMissingIssueNumber || err == errInvalidIssueNumber {
420
-					continue
421
-				}
422 447
 				return err
423 448
 			}
424 449
 
425
-			if refMarked[issue.ID] {
450
+			if issue == nil || refMarked[issue.ID] {
426 451
 				continue
427 452
 			}
428 453
 			refMarked[issue.ID] = true
@@ -436,31 +461,12 @@ func UpdateIssuesCommit(doer *User, repo *Repository, commits []*PushCommit) err
436 461
 		refMarked = make(map[int64]bool)
437 462
 		// FIXME: can merge this one and next one to a common function.
438 463
 		for _, ref := range issueCloseKeywordsPat.FindAllString(c.Message, -1) {
439
-			ref = ref[strings.IndexByte(ref, byte(' '))+1:]
440
-			ref = strings.TrimRightFunc(ref, issueIndexTrimRight)
441
-
442
-			if len(ref) == 0 {
443
-				continue
444
-			}
445
-
446
-			// Add repo name if missing
447
-			if ref[0] == '#' {
448
-				ref = fmt.Sprintf("%s%s", repo.FullName(), ref)
449
-			} else if !strings.Contains(ref, "/") {
450
-				// We don't support User#ID syntax yet
451
-				// return ErrNotImplemented
452
-				continue
453
-			}
454
-
455
-			issue, err := GetIssueByRef(ref)
464
+			issue, err := getIssueFromRef(repo, ref)
456 465
 			if err != nil {
457
-				if IsErrIssueNotExist(err) || err == errMissingIssueNumber || err == errInvalidIssueNumber {
458
-					continue
459
-				}
460 466
 				return err
461 467
 			}
462 468
 
463
-			if refMarked[issue.ID] {
469
+			if issue == nil || refMarked[issue.ID] {
464 470
 				continue
465 471
 			}
466 472
 			refMarked[issue.ID] = true
@@ -476,31 +482,12 @@ func UpdateIssuesCommit(doer *User, repo *Repository, commits []*PushCommit) err
476 482
 
477 483
 		// It is conflict to have close and reopen at same time, so refsMarked doesn't need to reinit here.
478 484
 		for _, ref := range issueReopenKeywordsPat.FindAllString(c.Message, -1) {
479
-			ref = ref[strings.IndexByte(ref, byte(' '))+1:]
480
-			ref = strings.TrimRightFunc(ref, issueIndexTrimRight)
481
-
482
-			if len(ref) == 0 {
483
-				continue
484
-			}
485
-
486
-			// Add repo name if missing
487
-			if ref[0] == '#' {
488
-				ref = fmt.Sprintf("%s%s", repo.FullName(), ref)
489
-			} else if !strings.Contains(ref, "/") {
490
-				// We don't support User#ID syntax yet
491
-				// return ErrNotImplemented
492
-				continue
493
-			}
494
-
495
-			issue, err := GetIssueByRef(ref)
485
+			issue, err := getIssueFromRef(repo, ref)
496 486
 			if err != nil {
497
-				if IsErrIssueNotExist(err) || err == errMissingIssueNumber || err == errInvalidIssueNumber {
498
-					continue
499
-				}
500 487
 				return err
501 488
 			}
502 489
 
503
-			if refMarked[issue.ID] {
490
+			if issue == nil || refMarked[issue.ID] {
504 491
 				continue
505 492
 			}
506 493
 			refMarked[issue.ID] = true

+ 30 - 0
models/action_test.go

@@ -1,6 +1,7 @@
1 1
 package models
2 2
 
3 3
 import (
4
+	"fmt"
4 5
 	"path"
5 6
 	"strings"
6 7
 	"testing"
@@ -154,6 +155,35 @@ func TestPushCommits_AvatarLink(t *testing.T) {
154 155
 		pushCommits.AvatarLink("nonexistent@example.com"))
155 156
 }
156 157
 
158
+func Test_getIssueFromRef(t *testing.T) {
159
+	assert.NoError(t, PrepareTestDatabase())
160
+	repo := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
161
+	for _, test := range []struct {
162
+		Ref             string
163
+		ExpectedIssueID int64
164
+	}{
165
+		{"#2", 2},
166
+		{"reopen #2", 2},
167
+		{"user2/repo2#1", 4},
168
+		{"fixes user2/repo2#1", 4},
169
+	} {
170
+		issue, err := getIssueFromRef(repo, test.Ref)
171
+		assert.NoError(t, err)
172
+		if assert.NotNil(t, issue) {
173
+			assert.EqualValues(t, test.ExpectedIssueID, issue.ID)
174
+		}
175
+	}
176
+
177
+	for _, badRef := range []string{
178
+		"doesnotexist/doesnotexist#1",
179
+		fmt.Sprintf("#%d", NonexistentID),
180
+	} {
181
+		issue, err := getIssueFromRef(repo, badRef)
182
+		assert.NoError(t, err)
183
+		assert.Nil(t, issue)
184
+	}
185
+}
186
+
157 187
 func TestUpdateIssuesCommit(t *testing.T) {
158 188
 	assert.NoError(t, PrepareTestDatabase())
159 189
 	pushCommits := []*PushCommit{

+ 0 - 32
models/issue.go

@@ -5,7 +5,6 @@
5 5
 package models
6 6
 
7 7
 import (
8
-	"errors"
9 8
 	"fmt"
10 9
 	"path"
11 10
 	"sort"
@@ -22,11 +21,6 @@ import (
22 21
 	"code.gitea.io/gitea/modules/util"
23 22
 )
24 23
 
25
-var (
26
-	errMissingIssueNumber = errors.New("No issue number specified")
27
-	errInvalidIssueNumber = errors.New("Invalid issue number")
28
-)
29
-
30 24
 // Issue represents an issue or pull request of repository.
31 25
 type Issue struct {
32 26
 	ID              int64       `xorm:"pk autoincr"`
@@ -961,32 +955,6 @@ func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, uuids []string)
961 955
 	return nil
962 956
 }
963 957
 
964
-// GetIssueByRef returns an Issue specified by a GFM reference.
965
-// See https://help.github.com/articles/writing-on-github#references for more information on the syntax.
966
-func GetIssueByRef(ref string) (*Issue, error) {
967
-	n := strings.IndexByte(ref, '#')
968
-	if n == -1 {
969
-		return nil, errMissingIssueNumber
970
-	}
971
-
972
-	index, err := com.StrTo(ref[n+1:]).Int64()
973
-	if err != nil {
974
-		return nil, errInvalidIssueNumber
975
-	}
976
-
977
-	i := strings.IndexByte(ref[:n], '/')
978
-	if i < 2 {
979
-		return nil, ErrInvalidReference
980
-	}
981
-
982
-	repo, err := GetRepositoryByOwnerAndName(ref[:i], ref[i+1:n])
983
-	if err != nil {
984
-		return nil, err
985
-	}
986
-
987
-	return GetIssueByIndex(repo.ID, index)
988
-}
989
-
990 958
 // GetRawIssueByIndex returns raw issue without loading attributes by index in a repository.
991 959
 func GetRawIssueByIndex(repoID, index int64) (*Issue, error) {
992 960
 	issue := &Issue{