Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite reference processing code in preparation for opening/closing from comment references #8261

Merged
merged 50 commits into from
Oct 13, 2019
Merged
Show file tree
Hide file tree
Changes from 47 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
9dbabcd
Merge go-gitea/master into master
guillep2k Sep 14, 2019
889c619
Merge branch 'master' of github.com:go-gitea/gitea
guillep2k Sep 14, 2019
d7c46c8
Merge branch 'master' of github.com:go-gitea/gitea
guillep2k Sep 15, 2019
8eaacbf
Merge remote-tracking branch 'refs/remotes/origin/master'
guillep2k Sep 19, 2019
de5aa64
Merge branch 'master' of github.com:go-gitea/gitea
guillep2k Sep 19, 2019
80c6f2b
Merge branch 'master' of github.com:go-gitea/gitea
guillep2k Sep 20, 2019
7d7bd6c
Add a markdown stripper for mentions and xrefs
guillep2k Sep 20, 2019
c20afe1
Improve comments
guillep2k Sep 20, 2019
69ef1c3
Small code simplification
guillep2k Sep 21, 2019
ae66b14
Move reference code to modules/references
guillep2k Sep 21, 2019
999bc87
Fix typo
guillep2k Sep 21, 2019
06f092e
Make MarkdownStripper return [][]byte
guillep2k Sep 21, 2019
16c6ab4
Implement preliminary keywords parsing
guillep2k Sep 22, 2019
dadadfa
Add FIXME comment
guillep2k Sep 22, 2019
ac40f7f
Merge branch 'master' of github.com:go-gitea/gitea
guillep2k Sep 22, 2019
a118be2
Fix comment
guillep2k Sep 22, 2019
9206849
Merge branch 'master' of github.com:go-gitea/gitea into strip-markdown
guillep2k Sep 22, 2019
debb01a
make fmt
guillep2k Sep 22, 2019
163ec65
Fix permissions check
guillep2k Sep 22, 2019
4796f43
Fix text assumptions
guillep2k Sep 22, 2019
4c75723
Fix imports
guillep2k Sep 22, 2019
79a7275
Fix lint, fmt
guillep2k Sep 22, 2019
98ea87e
Fix unused import
guillep2k Sep 22, 2019
361ba2e
Add missing export comment
guillep2k Sep 22, 2019
ac57ac5
Bypass revive on implemented interface
guillep2k Sep 23, 2019
f6ac46b
Merge branch 'master' of github.com:go-gitea/gitea
guillep2k Sep 23, 2019
caa53b7
Move mdstripper into its own package
guillep2k Sep 24, 2019
b936015
Support alphanumeric patterns
guillep2k Sep 24, 2019
c4c467c
Refactor FindAllMentions
guillep2k Sep 24, 2019
fdb45e6
Move mentions test to references
guillep2k Sep 24, 2019
70684f5
Parse mentions from reference package
guillep2k Sep 24, 2019
b9d709b
Refactor code to implement renderizable references
guillep2k Sep 25, 2019
b763968
Fix typo
guillep2k Sep 25, 2019
06b0f51
Move patterns and tests to the references package
guillep2k Sep 25, 2019
a00c798
Fix nil reference
guillep2k Sep 25, 2019
b563158
Merge branch 'master' of github.com:go-gitea/gitea
guillep2k Sep 25, 2019
5acbf34
Merge 'master' into strip-markdown
guillep2k Sep 25, 2019
8668772
Preliminary rendering attempt of closing keywords
guillep2k Sep 26, 2019
fc7e278
Normalize names, comments, general tidy-up
guillep2k Sep 26, 2019
3f9cd80
Add CSS style for action keywords
guillep2k Sep 27, 2019
cd31aad
Merge branch 'master' of github.com:go-gitea/gitea into strip-markdown
guillep2k Sep 27, 2019
44d1758
Merge branch 'master' into strip-markdown
guillep2k Sep 29, 2019
02b6eb2
Fix permission for admin and owner
guillep2k Sep 30, 2019
549ef80
Merge branch 'strip-markdown' of github.com:guillep2k/gitea into stri…
guillep2k Sep 30, 2019
4acf06f
Merge branch 'master' of github.com:go-gitea/gitea into strip-markdown
guillep2k Oct 9, 2019
1faece0
Fix golangci-lint
guillep2k Oct 9, 2019
b0735c3
Fix golangci-lint
guillep2k Oct 9, 2019
d18ade7
merge from master, resolve conflicts
guillep2k Oct 10, 2019
4927471
Merge branch 'master' into strip-markdown
lafriks Oct 11, 2019
4780e47
Merge branch 'master' into strip-markdown
zeripath Oct 13, 2019
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
13 changes: 7 additions & 6 deletions integrations/issue_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"testing"

"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/references"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"

Expand Down Expand Up @@ -207,7 +208,7 @@ func TestIssueCrossReference(t *testing.T) {
RefIssueID: issueRef.ID,
RefCommentID: 0,
RefIsPull: false,
RefAction: models.XRefActionNone})
RefAction: references.XRefActionNone})

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

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

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

// Ref from a comment
session := loginUser(t, "user2")
Expand All @@ -248,7 +249,7 @@ func TestIssueCrossReference(t *testing.T) {
RefIssueID: issueRef.ID,
RefCommentID: commentID,
RefIsPull: false,
RefAction: models.XRefActionNone}
RefAction: references.XRefActionNone}
models.AssertExistsAndLoadBean(t, comment)

// Ref from a different repository
Expand All @@ -259,7 +260,7 @@ func TestIssueCrossReference(t *testing.T) {
RefIssueID: issueRef.ID,
RefCommentID: 0,
RefIsPull: false,
RefAction: models.XRefActionNone})
RefAction: references.XRefActionNone})
}

func testIssueWithBean(t *testing.T, user string, repoID int64, title, content string) (string, *models.Issue) {
Expand Down
177 changes: 37 additions & 140 deletions models/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,14 @@ import (
"fmt"
"html"
"path"
"regexp"
"strconv"
"strings"
"time"
"unicode"

"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/references"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
Expand Down Expand Up @@ -54,29 +53,6 @@ const (
ActionMirrorSyncDelete // 20
)

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
)

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)
}

// 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 @@ -351,10 +327,6 @@ func RenameRepoAction(actUser *User, oldRepoName string, repo *Repository) error
return renameRepoAction(x, actUser, oldRepoName, repo)
}

func issueIndexTrimRight(c rune) bool {
return !unicode.IsDigit(c)
}

// PushCommit represents a commit in a push operation.
type PushCommit struct {
Sha1 string
Expand Down Expand Up @@ -480,39 +452,9 @@ func (pc *PushCommits) AvatarLink(email string) string {
}

// 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, issueIndex)
// if the provided ref references a non-existent issue.
func getIssueFromRef(repo *Repository, index int64) (*Issue, error) {
issue, err := GetIssueByIndex(repo.ID, index)
if err != nil {
if IsErrIssueNotExist(err) {
return nil, nil
Expand All @@ -522,20 +464,7 @@ func getIssueFromRef(repo *Repository, ref string) (*Issue, error) {
return issue, nil
}

func changeIssueStatus(repo *Repository, doer *User, ref string, refMarked map[int64]bool, status bool) error {
issue, err := getIssueFromRef(repo, ref)
if err != nil {
return err
}

if issue == nil || refMarked[issue.ID] {
return nil
}
refMarked[issue.ID] = true

if issue.RepoID != repo.ID || issue.IsClosed == status {
return nil
}
func changeIssueStatus(repo *Repository, issue *Issue, doer *User, status bool) error {

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

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

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

refMarked := make(map[int64]bool)
type markKey struct {
ID int64
Action references.XRefAction
}

refMarked := make(map[markKey]bool)
var refRepo *Repository
var refIssue *Issue
var err error
for _, m := range issueReferenceKeywordsPat.FindAllStringSubmatch(c.Message, -1) {
if len(m[3]) == 0 {
continue
}
ref := m[3]
for _, ref := range references.FindAllIssueReferences(c.Message) {

// issue is from another repo
if len(m[1]) > 0 && len(m[2]) > 0 {
refRepo, err = GetRepositoryFromMatch(m[1], m[2])
if len(ref.Owner) > 0 && len(ref.Name) > 0 {
refRepo, err = GetRepositoryFromMatch(ref.Owner, ref.Name)
if err != nil {
continue
}
} else {
refRepo = repo
}
issue, err := getIssueFromRef(refRepo, ref)
if err != nil {
if refIssue, err = getIssueFromRef(refRepo, ref.Index); err != nil {
return err
}

if issue == nil || refMarked[issue.ID] {
if refIssue == nil {
continue
}
refMarked[issue.ID] = true

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

// Change issue status only if the commit has been pushed to the default branch.
// and if the repo is configured to allow only that
if repo.DefaultBranch != branchName && !repo.CloseIssuesViaCommitInAnyBranch {
continue
}
refMarked = make(map[int64]bool)
for _, m := range issueCloseKeywordsPat.FindAllStringSubmatch(c.Message, -1) {
if len(m[3]) == 0 {
key := markKey{ID: refIssue.ID, Action: ref.Action}
if refMarked[key] {
continue
}
ref := m[3]
refMarked[key] = true

// 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 {
// only create comments for issues if user has permission for it
if perm.IsAdmin() || perm.IsOwner() || perm.CanWrite(UnitTypeIssues) {
message := fmt.Sprintf(`<a href="%s/commit/%s">%s</a>`, repo.Link(), c.Sha1, html.EscapeString(c.Message))
if err = CreateRefComment(doer, refRepo, refIssue, message, c.Sha1); 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 {
// Process closing/reopening keywords
if ref.Action != references.XRefActionCloses && ref.Action != references.XRefActionReopens {
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 {
continue
}
} else {
refRepo = repo
}

perm, err := GetUserRepoPermission(refRepo, doer)
if err != nil {
return err
// Change issue status only if the commit has been pushed to the default branch.
// and if the repo is configured to allow only that
// FIXME: we should be using Issue.ref if set instead of repo.DefaultBranch
if repo.DefaultBranch != branchName && !repo.CloseIssuesViaCommitInAnyBranch {
continue
}

// 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 {
// only close issues in another repo if user has push access
if perm.IsAdmin() || perm.IsOwner() || perm.CanWrite(UnitTypeCode) {
if err := changeIssueStatus(refRepo, refIssue, doer, ref.Action == references.XRefActionCloses); err != nil {
return err
}
}
Expand Down
53 changes: 1 addition & 52 deletions models/action_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package models

import (
"fmt"
"path"
"strings"
"testing"
Expand Down Expand Up @@ -181,56 +180,6 @@ func TestPushCommits_AvatarLink(t *testing.T) {
pushCommits.AvatarLink("nonexistent@example.com"))
}

func TestRegExp_issueReferenceKeywordsPat(t *testing.T) {
trueTestCases := []string{
"#2",
"[#2]",
"please see go-gitea/gitea#5",
"#2:",
}
falseTestCases := []string{
"kb#2",
"#2xy",
}

for _, testCase := range trueTestCases {
assert.True(t, issueReferenceKeywordsPat.MatchString(testCase))
}
for _, testCase := range falseTestCases {
assert.False(t, issueReferenceKeywordsPat.MatchString(testCase))
}
}

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},
{"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) {
assert.NoError(t, PrepareTestDatabase())
pushCommits := []*PushCommit{
Expand Down Expand Up @@ -431,7 +380,7 @@ func TestUpdateIssuesCommit_AnotherRepoNoPermission(t *testing.T) {
AssertNotExistsBean(t, commentBean)
AssertNotExistsBean(t, issueBean, "is_closed=1")
assert.NoError(t, UpdateIssuesCommit(user, repo, pushCommits, repo.DefaultBranch))
AssertExistsAndLoadBean(t, commentBean)
AssertNotExistsBean(t, commentBean)
lafriks marked this conversation as resolved.
Show resolved Hide resolved
AssertNotExistsBean(t, issueBean, "is_closed=1")
CheckConsistencyFor(t, &Action{})
}
Expand Down
Loading