mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-11-28 09:21:13 -05:00
Fix ref parsing in commit messages (#3067)
This commit is contained in:
parent
b0971ae37c
commit
3163abedd6
3 changed files with 87 additions and 102 deletions
127
models/action.go
127
models/action.go
|
@ -14,15 +14,14 @@ import (
|
||||||
"time"
|
"time"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
"github.com/Unknwon/com"
|
|
||||||
"github.com/go-xorm/builder"
|
|
||||||
|
|
||||||
"code.gitea.io/git"
|
"code.gitea.io/git"
|
||||||
api "code.gitea.io/sdk/gitea"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/base"
|
"code.gitea.io/gitea/modules/base"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
api "code.gitea.io/sdk/gitea"
|
||||||
|
|
||||||
|
"github.com/Unknwon/com"
|
||||||
|
"github.com/go-xorm/builder"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ActionType represents the type of an action.
|
// ActionType represents the type of an action.
|
||||||
|
@ -59,14 +58,16 @@ var (
|
||||||
issueReferenceKeywordsPat *regexp.Regexp
|
issueReferenceKeywordsPat *regexp.Regexp
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const issueRefRegexpStr = `(?:\S+/\S=)?#\d+`
|
||||||
|
|
||||||
func assembleKeywordsPattern(words []string) string {
|
func assembleKeywordsPattern(words []string) string {
|
||||||
return fmt.Sprintf(`(?i)(?:%s) \S+`, strings.Join(words, "|"))
|
return fmt.Sprintf(`(?i)(?:%s) %s`, strings.Join(words, "|"), issueRefRegexpStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
issueCloseKeywordsPat = regexp.MustCompile(assembleKeywordsPattern(issueCloseKeywords))
|
issueCloseKeywordsPat = regexp.MustCompile(assembleKeywordsPattern(issueCloseKeywords))
|
||||||
issueReopenKeywordsPat = regexp.MustCompile(assembleKeywordsPattern(issueReopenKeywords))
|
issueReopenKeywordsPat = regexp.MustCompile(assembleKeywordsPattern(issueReopenKeywords))
|
||||||
issueReferenceKeywordsPat = regexp.MustCompile(`(?i)(?:)(^| )\S+`)
|
issueReferenceKeywordsPat = regexp.MustCompile(issueRefRegexpStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Action represents user operation type and other information to
|
// Action represents user operation type and other information to
|
||||||
|
@ -390,6 +391,49 @@ func (pc *PushCommits) AvatarLink(email string) string {
|
||||||
return pc.avatars[email]
|
return pc.avatars[email]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getIssueFromRef returns the issue referenced by a ref. Returns a nil *Issue
|
||||||
|
// if the provided ref is misformatted or references a non-existent issue.
|
||||||
|
func getIssueFromRef(repo *Repository, ref string) (*Issue, error) {
|
||||||
|
ref = ref[strings.IndexByte(ref, ' ')+1:]
|
||||||
|
ref = strings.TrimRightFunc(ref, issueIndexTrimRight)
|
||||||
|
|
||||||
|
var refRepo *Repository
|
||||||
|
poundIndex := strings.IndexByte(ref, '#')
|
||||||
|
if poundIndex < 0 {
|
||||||
|
return nil, nil
|
||||||
|
} else if poundIndex == 0 {
|
||||||
|
refRepo = repo
|
||||||
|
} else {
|
||||||
|
slashIndex := strings.IndexByte(ref, '/')
|
||||||
|
if slashIndex < 0 || slashIndex >= poundIndex {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
ownerName := ref[:slashIndex]
|
||||||
|
repoName := ref[slashIndex+1 : poundIndex]
|
||||||
|
var err error
|
||||||
|
refRepo, err = GetRepositoryByOwnerAndName(ownerName, repoName)
|
||||||
|
if err != nil {
|
||||||
|
if IsErrRepoNotExist(err) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
issueIndex, err := strconv.ParseInt(ref[poundIndex+1:], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
issue, err := GetIssueByIndex(refRepo.ID, int64(issueIndex))
|
||||||
|
if err != nil {
|
||||||
|
if IsErrIssueNotExist(err) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return issue, nil
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateIssuesCommit checks if issues are manipulated by commit message.
|
// UpdateIssuesCommit checks if issues are manipulated by commit message.
|
||||||
func UpdateIssuesCommit(doer *User, repo *Repository, commits []*PushCommit) error {
|
func UpdateIssuesCommit(doer *User, repo *Repository, commits []*PushCommit) error {
|
||||||
// Commits are appended in the reverse order.
|
// Commits are appended in the reverse order.
|
||||||
|
@ -398,31 +442,12 @@ func UpdateIssuesCommit(doer *User, repo *Repository, commits []*PushCommit) err
|
||||||
|
|
||||||
refMarked := make(map[int64]bool)
|
refMarked := make(map[int64]bool)
|
||||||
for _, ref := range issueReferenceKeywordsPat.FindAllString(c.Message, -1) {
|
for _, ref := range issueReferenceKeywordsPat.FindAllString(c.Message, -1) {
|
||||||
ref = ref[strings.IndexByte(ref, byte(' '))+1:]
|
issue, err := getIssueFromRef(repo, ref)
|
||||||
ref = strings.TrimRightFunc(ref, issueIndexTrimRight)
|
|
||||||
|
|
||||||
if len(ref) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add repo name if missing
|
|
||||||
if ref[0] == '#' {
|
|
||||||
ref = fmt.Sprintf("%s%s", repo.FullName(), ref)
|
|
||||||
} else if !strings.Contains(ref, "/") {
|
|
||||||
// FIXME: We don't support User#ID syntax yet
|
|
||||||
// return ErrNotImplemented
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
issue, err := GetIssueByRef(ref)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if IsErrIssueNotExist(err) || err == errMissingIssueNumber || err == errInvalidIssueNumber {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if refMarked[issue.ID] {
|
if issue == nil || refMarked[issue.ID] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
refMarked[issue.ID] = true
|
refMarked[issue.ID] = true
|
||||||
|
@ -436,31 +461,12 @@ func UpdateIssuesCommit(doer *User, repo *Repository, commits []*PushCommit) err
|
||||||
refMarked = make(map[int64]bool)
|
refMarked = make(map[int64]bool)
|
||||||
// FIXME: can merge this one and next one to a common function.
|
// FIXME: can merge this one and next one to a common function.
|
||||||
for _, ref := range issueCloseKeywordsPat.FindAllString(c.Message, -1) {
|
for _, ref := range issueCloseKeywordsPat.FindAllString(c.Message, -1) {
|
||||||
ref = ref[strings.IndexByte(ref, byte(' '))+1:]
|
issue, err := getIssueFromRef(repo, ref)
|
||||||
ref = strings.TrimRightFunc(ref, issueIndexTrimRight)
|
|
||||||
|
|
||||||
if len(ref) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add repo name if missing
|
|
||||||
if ref[0] == '#' {
|
|
||||||
ref = fmt.Sprintf("%s%s", repo.FullName(), ref)
|
|
||||||
} else if !strings.Contains(ref, "/") {
|
|
||||||
// We don't support User#ID syntax yet
|
|
||||||
// return ErrNotImplemented
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
issue, err := GetIssueByRef(ref)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if IsErrIssueNotExist(err) || err == errMissingIssueNumber || err == errInvalidIssueNumber {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if refMarked[issue.ID] {
|
if issue == nil || refMarked[issue.ID] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
refMarked[issue.ID] = true
|
refMarked[issue.ID] = true
|
||||||
|
@ -476,31 +482,12 @@ func UpdateIssuesCommit(doer *User, repo *Repository, commits []*PushCommit) err
|
||||||
|
|
||||||
// It is conflict to have close and reopen at same time, so refsMarked doesn't need to reinit here.
|
// It is conflict to have close and reopen at same time, so refsMarked doesn't need to reinit here.
|
||||||
for _, ref := range issueReopenKeywordsPat.FindAllString(c.Message, -1) {
|
for _, ref := range issueReopenKeywordsPat.FindAllString(c.Message, -1) {
|
||||||
ref = ref[strings.IndexByte(ref, byte(' '))+1:]
|
issue, err := getIssueFromRef(repo, ref)
|
||||||
ref = strings.TrimRightFunc(ref, issueIndexTrimRight)
|
|
||||||
|
|
||||||
if len(ref) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add repo name if missing
|
|
||||||
if ref[0] == '#' {
|
|
||||||
ref = fmt.Sprintf("%s%s", repo.FullName(), ref)
|
|
||||||
} else if !strings.Contains(ref, "/") {
|
|
||||||
// We don't support User#ID syntax yet
|
|
||||||
// return ErrNotImplemented
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
issue, err := GetIssueByRef(ref)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if IsErrIssueNotExist(err) || err == errMissingIssueNumber || err == errInvalidIssueNumber {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if refMarked[issue.ID] {
|
if issue == nil || refMarked[issue.ID] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
refMarked[issue.ID] = true
|
refMarked[issue.ID] = true
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -154,6 +155,35 @@ func TestPushCommits_AvatarLink(t *testing.T) {
|
||||||
pushCommits.AvatarLink("nonexistent@example.com"))
|
pushCommits.AvatarLink("nonexistent@example.com"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_getIssueFromRef(t *testing.T) {
|
||||||
|
assert.NoError(t, PrepareTestDatabase())
|
||||||
|
repo := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
|
||||||
|
for _, test := range []struct {
|
||||||
|
Ref string
|
||||||
|
ExpectedIssueID int64
|
||||||
|
}{
|
||||||
|
{"#2", 2},
|
||||||
|
{"reopen #2", 2},
|
||||||
|
{"user2/repo2#1", 4},
|
||||||
|
{"fixes user2/repo2#1", 4},
|
||||||
|
} {
|
||||||
|
issue, err := getIssueFromRef(repo, test.Ref)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
if assert.NotNil(t, issue) {
|
||||||
|
assert.EqualValues(t, test.ExpectedIssueID, issue.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, badRef := range []string{
|
||||||
|
"doesnotexist/doesnotexist#1",
|
||||||
|
fmt.Sprintf("#%d", NonexistentID),
|
||||||
|
} {
|
||||||
|
issue, err := getIssueFromRef(repo, badRef)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Nil(t, issue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestUpdateIssuesCommit(t *testing.T) {
|
func TestUpdateIssuesCommit(t *testing.T) {
|
||||||
assert.NoError(t, PrepareTestDatabase())
|
assert.NoError(t, PrepareTestDatabase())
|
||||||
pushCommits := []*PushCommit{
|
pushCommits := []*PushCommit{
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"path"
|
"path"
|
||||||
"sort"
|
"sort"
|
||||||
|
@ -22,11 +21,6 @@ import (
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
errMissingIssueNumber = errors.New("No issue number specified")
|
|
||||||
errInvalidIssueNumber = errors.New("Invalid issue number")
|
|
||||||
)
|
|
||||||
|
|
||||||
// Issue represents an issue or pull request of repository.
|
// Issue represents an issue or pull request of repository.
|
||||||
type Issue struct {
|
type Issue struct {
|
||||||
ID int64 `xorm:"pk autoincr"`
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
@ -961,32 +955,6 @@ func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, uuids []string)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetIssueByRef returns an Issue specified by a GFM reference.
|
|
||||||
// See https://help.github.com/articles/writing-on-github#references for more information on the syntax.
|
|
||||||
func GetIssueByRef(ref string) (*Issue, error) {
|
|
||||||
n := strings.IndexByte(ref, '#')
|
|
||||||
if n == -1 {
|
|
||||||
return nil, errMissingIssueNumber
|
|
||||||
}
|
|
||||||
|
|
||||||
index, err := com.StrTo(ref[n+1:]).Int64()
|
|
||||||
if err != nil {
|
|
||||||
return nil, errInvalidIssueNumber
|
|
||||||
}
|
|
||||||
|
|
||||||
i := strings.IndexByte(ref[:n], '/')
|
|
||||||
if i < 2 {
|
|
||||||
return nil, ErrInvalidReference
|
|
||||||
}
|
|
||||||
|
|
||||||
repo, err := GetRepositoryByOwnerAndName(ref[:i], ref[i+1:n])
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return GetIssueByIndex(repo.ID, index)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRawIssueByIndex returns raw issue without loading attributes by index in a repository.
|
// GetRawIssueByIndex returns raw issue without loading attributes by index in a repository.
|
||||||
func GetRawIssueByIndex(repoID, index int64) (*Issue, error) {
|
func GetRawIssueByIndex(repoID, index int64) (*Issue, error) {
|
||||||
issue := &Issue{
|
issue := &Issue{
|
||||||
|
|
Loading…
Reference in a new issue