This commit is contained in:
krrutkow 2018-07-17 00:42:35 +00:00 committed by GitHub
commit f519972513
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 1087 additions and 145 deletions

View File

@ -49,14 +49,43 @@ const (
ActionDeleteBranch // 17
)
// KeywordMaskType represents the bitmask of types of keywords found in a message.
type KeywordMaskType int
// Possible bitmask types for keywords that can be found.
const (
KeywordReference KeywordMaskType = 1 << iota // 1 = 1 << 0
KeywordReopen // 2 = 1 << 1
KeywordClose // 4 = 1 << 2
)
// IssueKeywordsToFind represents a pairing of a pattern to use to find keywords in message and the keywords bitmask value.
type IssueKeywordsToFind struct {
Pattern *regexp.Regexp
KeywordMask KeywordMaskType
}
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
// populate with details to find keywords for reference, reopen, close
issueKeywordsToFind = []*IssueKeywordsToFind{
{
Pattern: regexp.MustCompile(issueRefRegexpStr),
KeywordMask: KeywordReference,
},
{
Pattern: regexp.MustCompile(assembleKeywordsPattern(issueReopenKeywords)),
KeywordMask: KeywordReopen,
},
{
Pattern: regexp.MustCompile(assembleKeywordsPattern(issueCloseKeywords)),
KeywordMask: KeywordClose,
},
}
)
const issueRefRegexpStr = `(?:\S+/\S=)?#\d+`
@ -65,12 +94,6 @@ 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(issueRefRegexpStr)
}
// Action represents user operation type and other information to
// repository. It implemented interface base.Actioner so that can be
// used in template render.
@ -435,70 +458,125 @@ func getIssueFromRef(repo *Repository, ref string) (*Issue, error) {
return issue, nil
}
// findIssueReferencesInString iterates over the keywords to find in a message and accumulates the findings into refs
func findIssueReferencesInString(message string, repo *Repository) (map[int64]KeywordMaskType, error) {
refs := make(map[int64]KeywordMaskType)
for _, kwToFind := range issueKeywordsToFind {
for _, ref := range kwToFind.Pattern.FindAllString(message, -1) {
issue, err := getIssueFromRef(repo, ref)
if err != nil {
return nil, err
}
if issue != nil {
refs[issue.ID] |= kwToFind.KeywordMask
}
}
}
return refs, nil
}
// changeIssueStatus encapsulates the logic for changing the status of an issue based on what keywords are marked in the keyword mask
func changeIssueStatus(mask KeywordMaskType, doer *User, repo *Repository, issue *Issue) error {
// take no action if both KeywordClose and KeywordOpen are set
switch mask & (KeywordReopen | KeywordClose) {
case KeywordClose:
if issue.RepoID == repo.ID && !issue.IsClosed {
if err := issue.ChangeStatus(doer, repo, true); err != nil {
return err
}
}
case KeywordReopen:
if issue.RepoID == repo.ID && issue.IsClosed {
if err := issue.ChangeStatus(doer, repo, false); err != nil {
return err
}
}
}
return nil
}
// UpdateIssuesComment checks if issues are manipulated by a comment
func UpdateIssuesComment(doer *User, repo *Repository, commentIssue *Issue, comment *Comment, canOpenClose bool) error {
var refString string
if comment != nil {
refString = comment.Content
} else {
refString = commentIssue.Title + ": " + commentIssue.Content
}
uniqueID := fmt.Sprintf("%d", commentIssue.ID)
if comment != nil {
uniqueID += fmt.Sprintf("@%d", comment.ID)
}
refs, err := findIssueReferencesInString(refString, repo)
if err != nil {
return err
}
for id, mask := range refs {
issue, err := GetIssueByID(id)
if err != nil {
return err
}
if issue == nil || issue.ID == commentIssue.ID {
continue
}
if (mask & KeywordReference) == KeywordReference {
if comment != nil {
err = CreateCommentRefComment(doer, repo, issue, fmt.Sprintf(`%d`, comment.ID), base.EncodeSha1(uniqueID))
} else if commentIssue.IsPull {
err = CreatePullRefComment(doer, repo, issue, fmt.Sprintf(`%d`, commentIssue.ID), base.EncodeSha1(uniqueID))
} else {
err = CreateIssueRefComment(doer, repo, issue, fmt.Sprintf(`%d`, commentIssue.ID), base.EncodeSha1(uniqueID))
}
if err != nil {
return err
}
}
if canOpenClose {
if err = changeIssueStatus(mask, doer, repo, issue); err != nil {
return err
}
}
}
return nil
}
// 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, commitsAreMerged bool) error {
// Commits are appended in the reverse order.
for i := len(commits) - 1; i >= 0; i-- {
c := commits[i]
refMarked := make(map[int64]bool)
for _, ref := range issueReferenceKeywordsPat.FindAllString(c.Message, -1) {
issue, err := getIssueFromRef(repo, ref)
if err != nil {
return err
}
if issue == nil || refMarked[issue.ID] {
continue
}
refMarked[issue.ID] = true
message := fmt.Sprintf(`<a href="%s/commit/%s">%s</a>`, repo.Link(), c.Sha1, c.Message)
if err = CreateRefComment(doer, repo, issue, message, c.Sha1); err != nil {
return err
}
refs, err := findIssueReferencesInString(c.Message, repo)
if err != nil {
return err
}
refMarked = make(map[int64]bool)
// FIXME: can merge this one and next one to a common function.
for _, ref := range issueCloseKeywordsPat.FindAllString(c.Message, -1) {
issue, err := getIssueFromRef(repo, ref)
for id, mask := range refs {
issue, err := GetIssueByID(id)
if err != nil {
return err
}
if issue == nil || refMarked[issue.ID] {
continue
}
refMarked[issue.ID] = true
if issue.RepoID != repo.ID || issue.IsClosed {
if issue == nil {
continue
}
if err = issue.ChangeStatus(doer, repo, 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 _, ref := range issueReopenKeywordsPat.FindAllString(c.Message, -1) {
issue, err := getIssueFromRef(repo, ref)
if err != nil {
return err
if (mask & KeywordReference) == KeywordReference {
message := fmt.Sprintf("%d %s", repo.ID, c.Sha1)
if err = CreateCommitRefComment(doer, repo, issue, message, c.Sha1); err != nil {
return err
}
}
if issue == nil || refMarked[issue.ID] {
continue
}
refMarked[issue.ID] = true
if issue.RepoID != repo.ID || !issue.IsClosed {
continue
}
if err = issue.ChangeStatus(doer, repo, false); err != nil {
return err
if commitsAreMerged {
if err = changeIssueStatus(mask, doer, repo, issue); err != nil {
return err
}
}
}
}
@ -560,8 +638,8 @@ func CommitRepoAction(opts CommitRepoActionOptions) error {
opts.Commits.CompareURL = repo.ComposeCompareURL(opts.OldCommitID, opts.NewCommitID)
}
if err = UpdateIssuesCommit(pusher, repo, opts.Commits.Commits); err != nil {
log.Error(4, "updateIssuesCommit: %v", err)
if err = UpdateIssuesCommit(pusher, repo, opts.Commits.Commits, true); err != nil {
log.Error(4, "UpdateIssuesCommit: %v", err)
}
}
@ -715,21 +793,91 @@ func TransferRepoAction(doer, oldOwner *User, repo *Repository) error {
return transferRepoAction(x, doer, oldOwner, repo)
}
func mergePullRequestAction(e Engine, doer *User, repo *Repository, issue *Issue) error {
return notifyWatchers(e, &Action{
// MergePullRequestAction adds new action for merging pull request (including manually merged pull requests).
func MergePullRequestAction(doer *User, repo *Repository, pull *Issue, commits *PushCommits) error {
if commits != nil {
if err := UpdateIssuesCommit(doer, repo, commits.Commits, true); err != nil {
log.Error(4, "UpdateIssuesCommit: %v", err)
}
}
if err := UpdateIssuesComment(doer, repo, pull, nil, true); err != nil {
log.Error(4, "UpdateIssuesComment: %v", err)
}
if err := notifyWatchers(x, &Action{
ActUserID: doer.ID,
ActUser: doer,
OpType: ActionMergePullRequest,
Content: fmt.Sprintf("%d|%s", issue.Index, issue.Title),
Content: fmt.Sprintf("%d|%s", pull.Index, pull.Title),
RepoID: repo.ID,
Repo: repo,
IsPrivate: repo.IsPrivate,
})
}); err != nil {
return fmt.Errorf("notifyWatchers: %v", err)
}
return nil
}
// MergePullRequestAction adds new action for merging pull request.
func MergePullRequestAction(actUser *User, repo *Repository, pull *Issue) error {
return mergePullRequestAction(x, actUser, repo, pull)
// NewPullRequestAction adds new action for creating a new pull request.
func NewPullRequestAction(doer *User, repo *Repository, pull *Issue, commits *PushCommits) error {
if err := UpdateIssuesCommit(doer, repo, commits.Commits, false); err != nil {
log.Error(4, "UpdateIssuesCommit: %v", err)
}
if err := UpdateIssuesComment(doer, repo, pull, nil, false); err != nil {
log.Error(4, "UpdateIssuesComment: %v", err)
}
if err := NotifyWatchers(&Action{
ActUserID: doer.ID,
ActUser: doer,
OpType: ActionCreatePullRequest,
Content: fmt.Sprintf("%d|%s", pull.Index, pull.Title),
RepoID: repo.ID,
Repo: repo,
IsPrivate: repo.IsPrivate,
}); err != nil {
log.Error(4, "NotifyWatchers: %v", err)
} else if err := pull.MailParticipants(); err != nil {
log.Error(4, "MailParticipants: %v", err)
}
return nil
}
// CommitPullRequestAction adds new action for pushed commits tracked by a pull request.
func CommitPullRequestAction(doer *User, repo *Repository, commits *PushCommits) error {
if err := UpdateIssuesCommit(doer, repo, commits.Commits, false); err != nil {
log.Error(4, "UpdateIssuesCommit: %v", err)
}
// no action added
return nil
}
// CreateOrUpdateCommentAction adds new action when creating or updating a comment.
func CreateOrUpdateCommentAction(doer *User, repo *Repository, issue *Issue, comment *Comment) error {
if err := UpdateIssuesComment(doer, repo, issue, comment, false); err != nil {
log.Error(4, "UpdateIssuesComment: %v", err)
}
// no action added
return nil
}
// CreateOrUpdateIssueAction adds new action when creating a new issue or pull request.
func CreateOrUpdateIssueAction(doer *User, repo *Repository, issue *Issue) error {
if err := UpdateIssuesComment(doer, repo, issue, nil, false); err != nil {
log.Error(4, "UpdateIssuesComment: %v", err)
}
// no action added
return nil
}
// GetFeedsOptions options for retrieving feeds

View File

@ -7,6 +7,7 @@ import (
"testing"
"code.gitea.io/git"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/setting"
"github.com/stretchr/testify/assert"
@ -184,53 +185,530 @@ func Test_getIssueFromRef(t *testing.T) {
}
}
func TestUpdateIssuesCommit(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())
pushCommits := []*PushCommit{
{
Sha1: "abcdef1",
CommitterEmail: "user2@example.com",
CommitterName: "User Two",
AuthorEmail: "user4@example.com",
AuthorName: "User Four",
Message: "start working on #FST-1, #1",
},
{
Sha1: "abcdef2",
CommitterEmail: "user2@example.com",
CommitterName: "User Two",
AuthorEmail: "user2@example.com",
AuthorName: "User Two",
Message: "a plain message",
},
{
Sha1: "abcdef2",
CommitterEmail: "user2@example.com",
CommitterName: "User Two",
AuthorEmail: "user2@example.com",
AuthorName: "User Two",
Message: "close #2",
},
}
func TestUpdateIssuesCommentIssues(t *testing.T) {
for _, canOpenClose := range []bool{false, true} {
// if cannot open or close then issue should not change status
isOpen := "is_closed!=1"
isClosed := "is_closed=1"
if !canOpenClose {
isClosed = isOpen
}
assert.NoError(t, PrepareTestDatabase())
user := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
repo := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
repo.Owner = user
commentIssue := AssertExistsAndLoadBean(t, &Issue{RepoID: repo.ID, Index: 1}).(*Issue)
refIssue1 := AssertExistsAndLoadBean(t, &Issue{RepoID: repo.ID, Index: 2}).(*Issue)
refIssue2 := AssertExistsAndLoadBean(t, &Issue{RepoID: repo.ID, Index: 3}).(*Issue)
commentBean := []*Comment{
{
Type: CommentTypeIssueRef,
CommitSHA: base.EncodeSha1(fmt.Sprintf("%d", commentIssue.ID)),
PosterID: user.ID,
IssueID: commentIssue.ID,
},
{
Type: CommentTypeIssueRef,
CommitSHA: base.EncodeSha1(fmt.Sprintf("%d", commentIssue.ID)),
PosterID: user.ID,
IssueID: refIssue1.ID,
},
{
Type: CommentTypeIssueRef,
CommitSHA: base.EncodeSha1(fmt.Sprintf("%d", commentIssue.ID)),
PosterID: user.ID,
IssueID: refIssue2.ID,
},
}
// test issue/pull request closing multiple issues
commentIssue.Title = "close #2"
commentIssue.Content = "close #3"
AssertNotExistsBean(t, commentBean[0])
AssertNotExistsBean(t, commentBean[1])
AssertNotExistsBean(t, commentBean[2])
AssertExistsAndLoadBean(t, &Issue{RepoID: repo.ID, Index: 2}, isOpen)
AssertExistsAndLoadBean(t, &Issue{RepoID: repo.ID, Index: 3}, isOpen)
assert.NoError(t, UpdateIssuesComment(user, repo, commentIssue, nil, canOpenClose))
AssertNotExistsBean(t, commentBean[0])
AssertExistsAndLoadBean(t, commentBean[1])
AssertExistsAndLoadBean(t, commentBean[2])
AssertExistsAndLoadBean(t, &Issue{RepoID: repo.ID, Index: 2}, isClosed)
AssertExistsAndLoadBean(t, &Issue{RepoID: repo.ID, Index: 3}, isClosed)
CheckConsistencyFor(t, &Action{})
// test issue/pull request re-opening multiple issues
commentIssue.Title = "reopen #2"
commentIssue.Content = "reopen #3"
AssertNotExistsBean(t, commentBean[0])
AssertExistsAndLoadBean(t, commentBean[1])
AssertExistsAndLoadBean(t, commentBean[2])
AssertExistsAndLoadBean(t, &Issue{RepoID: repo.ID, Index: 2}, isClosed)
AssertExistsAndLoadBean(t, &Issue{RepoID: repo.ID, Index: 3}, isClosed)
assert.NoError(t, UpdateIssuesComment(user, repo, commentIssue, nil, canOpenClose))
AssertNotExistsBean(t, commentBean[0])
AssertExistsAndLoadBean(t, commentBean[1])
AssertExistsAndLoadBean(t, commentBean[2])
AssertExistsAndLoadBean(t, &Issue{RepoID: repo.ID, Index: 2}, isOpen)
AssertExistsAndLoadBean(t, &Issue{RepoID: repo.ID, Index: 3}, isOpen)
CheckConsistencyFor(t, &Action{})
// test issue/pull request mixing re-opening and closing issue and self-referencing issue
commentIssue.Title = "reopen #2"
commentIssue.Content = "close #2 and reference #1"
AssertNotExistsBean(t, commentBean[0])
AssertExistsAndLoadBean(t, commentBean[1])
AssertExistsAndLoadBean(t, commentBean[2])
AssertExistsAndLoadBean(t, &Issue{RepoID: repo.ID, Index: 2}, isOpen)
assert.NoError(t, UpdateIssuesComment(user, repo, commentIssue, nil, canOpenClose))
AssertNotExistsBean(t, commentBean[0])
AssertExistsAndLoadBean(t, commentBean[1])
AssertExistsAndLoadBean(t, commentBean[2])
AssertExistsAndLoadBean(t, &Issue{RepoID: repo.ID, Index: 2}, isOpen)
CheckConsistencyFor(t, &Action{})
}
}
func TestUpdateIssuesCommentComments(t *testing.T) {
isOpen := "is_closed!=1"
assert.NoError(t, PrepareTestDatabase())
user := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
repo := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
repo.Owner = user
commentBean := &Comment{
Type: CommentTypeCommitRef,
CommitSHA: "abcdef1",
PosterID: user.ID,
IssueID: 1,
}
issueBean := &Issue{RepoID: repo.ID, Index: 2}
commentIssue := AssertExistsAndLoadBean(t, &Issue{RepoID: repo.ID, Index: 1}).(*Issue)
refIssue1 := AssertExistsAndLoadBean(t, &Issue{RepoID: repo.ID, Index: 2}).(*Issue)
refIssue2 := AssertExistsAndLoadBean(t, &Issue{RepoID: repo.ID, Index: 3}).(*Issue)
AssertNotExistsBean(t, commentBean)
AssertNotExistsBean(t, &Issue{RepoID: repo.ID, Index: 2}, "is_closed=1")
assert.NoError(t, UpdateIssuesCommit(user, repo, pushCommits))
AssertExistsAndLoadBean(t, commentBean)
AssertExistsAndLoadBean(t, issueBean, "is_closed=1")
comment := Comment{
ID: 123456789,
Type: CommentTypeComment,
PosterID: user.ID,
Poster: user,
IssueID: commentIssue.ID,
Content: "this is a comment that mentions #2 and #1 too",
}
commentBean := []*Comment{
{
Type: CommentTypeCommentRef,
CommitSHA: base.EncodeSha1(fmt.Sprintf("%d@%d", commentIssue.ID, comment.ID)),
PosterID: user.ID,
IssueID: commentIssue.ID,
},
{
Type: CommentTypeCommentRef,
CommitSHA: base.EncodeSha1(fmt.Sprintf("%d@%d", commentIssue.ID, comment.ID)),
PosterID: user.ID,
IssueID: refIssue1.ID,
},
{
Type: CommentTypeCommentRef,
CommitSHA: base.EncodeSha1(fmt.Sprintf("%d@%d", commentIssue.ID, comment.ID)),
PosterID: user.ID,
IssueID: refIssue2.ID,
},
}
// test comment referencing issue including self-referencing
AssertNotExistsBean(t, commentBean[0])
AssertNotExistsBean(t, commentBean[1])
AssertNotExistsBean(t, commentBean[2])
AssertExistsAndLoadBean(t, &Issue{RepoID: repo.ID, Index: 2}, isOpen)
AssertExistsAndLoadBean(t, &Issue{RepoID: repo.ID, Index: 3}, isOpen)
assert.NoError(t, UpdateIssuesComment(user, repo, commentIssue, &comment, false))
AssertNotExistsBean(t, commentBean[0])
AssertExistsAndLoadBean(t, commentBean[1])
AssertNotExistsBean(t, commentBean[2])
AssertExistsAndLoadBean(t, &Issue{RepoID: repo.ID, Index: 2}, isOpen)
AssertExistsAndLoadBean(t, &Issue{RepoID: repo.ID, Index: 3}, isOpen)
CheckConsistencyFor(t, &Action{})
// test comment updating issue reference
comment.Content = "this is a comment that mentions #2 and #3 too"
AssertNotExistsBean(t, commentBean[0])
AssertExistsAndLoadBean(t, commentBean[1])
AssertNotExistsBean(t, commentBean[2])
AssertExistsAndLoadBean(t, &Issue{RepoID: repo.ID, Index: 2}, isOpen)
AssertExistsAndLoadBean(t, &Issue{RepoID: repo.ID, Index: 3}, isOpen)
assert.NoError(t, UpdateIssuesComment(user, repo, commentIssue, &comment, false))
AssertNotExistsBean(t, commentBean[0])
AssertExistsAndLoadBean(t, commentBean[1])
AssertExistsAndLoadBean(t, commentBean[2])
AssertExistsAndLoadBean(t, &Issue{RepoID: repo.ID, Index: 2}, isOpen)
AssertExistsAndLoadBean(t, &Issue{RepoID: repo.ID, Index: 3}, isOpen)
CheckConsistencyFor(t, &Action{})
}
func TestUpdateIssuesCommit(t *testing.T) {
for _, commitsAreMerged := range []bool{false, true} {
// if commits were not merged then issue should not change status
isOpen := "is_closed!=1"
isClosed := "is_closed=1"
if !commitsAreMerged {
isClosed = isOpen
}
assert.NoError(t, PrepareTestDatabase())
user := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
repo := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
repo.Owner = user
// test re-open of already open issue
pushCommits := []*PushCommit{
{
Sha1: "abcdef1",
CommitterEmail: "user2@example.com",
CommitterName: "User Two",
AuthorEmail: "user2@example.com",
AuthorName: "User Two",
Message: "reoopen #2",
},
}
commentBean := []*Comment{
{
Type: CommentTypeCommitRef,
CommitSHA: pushCommits[0].Sha1,
PosterID: user.ID,
IssueID: 2,
},
}
AssertNotExistsBean(t, commentBean[0])
AssertExistsAndLoadBean(t, &Issue{RepoID: repo.ID, Index: 2}, isOpen)
assert.NoError(t, UpdateIssuesCommit(user, repo, pushCommits, commitsAreMerged))
AssertExistsAndLoadBean(t, commentBean[0])
AssertExistsAndLoadBean(t, &Issue{RepoID: repo.ID, Index: 2}, isOpen)
CheckConsistencyFor(t, &Action{})
// test simultaneous open and close on an already open issue
pushCommits = []*PushCommit{
{
Sha1: "abcdef2",
CommitterEmail: "user2@example.com",
CommitterName: "User Two",
AuthorEmail: "user2@example.com",
AuthorName: "User Two",
Message: "reopen #2 and the close #2",
},
}
commentBean = []*Comment{
{
Type: CommentTypeCommitRef,
CommitSHA: pushCommits[0].Sha1,
PosterID: user.ID,
IssueID: 2,
},
}
AssertNotExistsBean(t, commentBean[0])
AssertExistsAndLoadBean(t, &Issue{RepoID: repo.ID, Index: 2}, isOpen)
assert.NoError(t, UpdateIssuesCommit(user, repo, pushCommits, commitsAreMerged))
AssertExistsAndLoadBean(t, commentBean[0])
AssertExistsAndLoadBean(t, &Issue{RepoID: repo.ID, Index: 2}, isOpen)
CheckConsistencyFor(t, &Action{})
// test close of an open issue
pushCommits = []*PushCommit{
{
Sha1: "abcdef3",
CommitterEmail: "user2@example.com",
CommitterName: "User Two",
AuthorEmail: "user2@example.com",
AuthorName: "User Two",
Message: "closes #2",
},
}
commentBean = []*Comment{
{
Type: CommentTypeCommitRef,
CommitSHA: pushCommits[0].Sha1,
PosterID: user.ID,
IssueID: 2,
},
}
AssertNotExistsBean(t, commentBean[0])
AssertExistsAndLoadBean(t, &Issue{RepoID: repo.ID, Index: 2}, isOpen)
assert.NoError(t, UpdateIssuesCommit(user, repo, pushCommits, commitsAreMerged))
AssertExistsAndLoadBean(t, commentBean[0])
AssertExistsAndLoadBean(t, &Issue{RepoID: repo.ID, Index: 2}, isClosed)
CheckConsistencyFor(t, &Action{})
// test close of an already closed issue
pushCommits = []*PushCommit{
{
Sha1: "abcdef4",
CommitterEmail: "user2@example.com",
CommitterName: "User Two",
AuthorEmail: "user2@example.com",
AuthorName: "User Two",
Message: "close #2",
},
}
commentBean = []*Comment{
{
Type: CommentTypeCommitRef,
CommitSHA: pushCommits[0].Sha1,
PosterID: user.ID,
IssueID: 2,
},
}
AssertNotExistsBean(t, commentBean[0])
AssertExistsAndLoadBean(t, &Issue{RepoID: repo.ID, Index: 2}, isClosed)
assert.NoError(t, UpdateIssuesCommit(user, repo, pushCommits, commitsAreMerged))
AssertExistsAndLoadBean(t, commentBean[0])
AssertExistsAndLoadBean(t, &Issue{RepoID: repo.ID, Index: 2}, isClosed)
CheckConsistencyFor(t, &Action{})
// test simultaneous open and close on a closed issue
pushCommits = []*PushCommit{
{
Sha1: "abcdef5",
CommitterEmail: "user2@example.com",
CommitterName: "User Two",
AuthorEmail: "user2@example.com",
AuthorName: "User Two",
Message: "close #2 and reopen #2",
},
}
commentBean = []*Comment{
{
Type: CommentTypeCommitRef,
CommitSHA: pushCommits[0].Sha1,
PosterID: user.ID,
IssueID: 2,
},
}
AssertNotExistsBean(t, commentBean[0])
AssertExistsAndLoadBean(t, &Issue{RepoID: repo.ID, Index: 2}, isClosed)
assert.NoError(t, UpdateIssuesCommit(user, repo, pushCommits, commitsAreMerged))
AssertExistsAndLoadBean(t, commentBean[0])
AssertExistsAndLoadBean(t, &Issue{RepoID: repo.ID, Index: 2}, isClosed)
CheckConsistencyFor(t, &Action{})
// test referencing an closed issue
pushCommits = []*PushCommit{
{
Sha1: "abcdef6",
CommitterEmail: "user2@example.com",
CommitterName: "User Two",
AuthorEmail: "user2@example.com",
AuthorName: "User Two",
Message: "for details on how to open, see #2",
},
}
commentBean = []*Comment{
{
Type: CommentTypeCommitRef,
CommitSHA: pushCommits[0].Sha1,
PosterID: user.ID,
IssueID: 2,
},
}
AssertNotExistsBean(t, commentBean[0])
AssertExistsAndLoadBean(t, &Issue{RepoID: repo.ID, Index: 2}, isClosed)
assert.NoError(t, UpdateIssuesCommit(user, repo, pushCommits, commitsAreMerged))
AssertExistsAndLoadBean(t, commentBean[0])
AssertExistsAndLoadBean(t, &Issue{RepoID: repo.ID, Index: 2}, isClosed)
CheckConsistencyFor(t, &Action{})
// test re-open a closed issue
pushCommits = []*PushCommit{
{
Sha1: "abcdef7",
CommitterEmail: "user2@example.com",
CommitterName: "User Two",
AuthorEmail: "user2@example.com",
AuthorName: "User Two",
Message: "reopens #2",
},
}
commentBean = []*Comment{
{
Type: CommentTypeCommitRef,
CommitSHA: pushCommits[0].Sha1,
PosterID: user.ID,
IssueID: 2,
},
}
AssertNotExistsBean(t, commentBean[0])
AssertExistsAndLoadBean(t, &Issue{RepoID: repo.ID, Index: 2}, isClosed)
assert.NoError(t, UpdateIssuesCommit(user, repo, pushCommits, commitsAreMerged))
AssertExistsAndLoadBean(t, commentBean[0])
AssertExistsAndLoadBean(t, &Issue{RepoID: repo.ID, Index: 2}, isOpen)
CheckConsistencyFor(t, &Action{})
// test referencing an open issue
pushCommits = []*PushCommit{
{
Sha1: "abcdef8",
CommitterEmail: "user2@example.com",
CommitterName: "User Two",
AuthorEmail: "user2@example.com",
AuthorName: "User Two",
Message: "for details on how to close, see #2",
},
}
commentBean = []*Comment{
{
Type: CommentTypeCommitRef,
CommitSHA: pushCommits[0].Sha1,
PosterID: user.ID,
IssueID: 2,
},
}
AssertNotExistsBean(t, commentBean[0])
AssertExistsAndLoadBean(t, &Issue{RepoID: repo.ID, Index: 2}, isOpen)
assert.NoError(t, UpdateIssuesCommit(user, repo, pushCommits, commitsAreMerged))
AssertExistsAndLoadBean(t, commentBean[0])
AssertExistsAndLoadBean(t, &Issue{RepoID: repo.ID, Index: 2}, isOpen)
CheckConsistencyFor(t, &Action{})
// test close-then-open commit order
pushCommits = []*PushCommit{
{
Sha1: "abcdef10",
CommitterEmail: "user2@example.com",
CommitterName: "User Two",
AuthorEmail: "user2@example.com",
AuthorName: "User Two",
Message: "reopened #2",
},
{
Sha1: "abcdef9",
CommitterEmail: "user2@example.com",
CommitterName: "User Two",
AuthorEmail: "user2@example.com",
AuthorName: "User Two",
Message: "fixes #2",
},
}
commentBean = []*Comment{
{
Type: CommentTypeCommitRef,
CommitSHA: pushCommits[0].Sha1,
PosterID: user.ID,
IssueID: 2,
},
{
Type: CommentTypeCommitRef,
CommitSHA: pushCommits[1].Sha1,
PosterID: user.ID,
IssueID: 2,
},
}
AssertNotExistsBean(t, commentBean[0])
AssertNotExistsBean(t, commentBean[1])
AssertExistsAndLoadBean(t, &Issue{RepoID: repo.ID, Index: 2}, isOpen)
assert.NoError(t, UpdateIssuesCommit(user, repo, pushCommits, commitsAreMerged))
AssertExistsAndLoadBean(t, commentBean[0])
AssertExistsAndLoadBean(t, commentBean[1])
AssertExistsAndLoadBean(t, &Issue{RepoID: repo.ID, Index: 2}, isOpen)
CheckConsistencyFor(t, &Action{})
// test open-then-close commit order
pushCommits = []*PushCommit{
{
Sha1: "abcdef12",
CommitterEmail: "user2@example.com",
CommitterName: "User Two",
AuthorEmail: "user2@example.com",
AuthorName: "User Two",
Message: "resolved #2",
},
{
Sha1: "abcdef11",
CommitterEmail: "user2@example.com",
CommitterName: "User Two",
AuthorEmail: "user2@example.com",
AuthorName: "User Two",
Message: "reopened #2",
},
}
commentBean = []*Comment{
{
Type: CommentTypeCommitRef,
CommitSHA: pushCommits[0].Sha1,
PosterID: user.ID,
IssueID: 2,
},
{
Type: CommentTypeCommitRef,
CommitSHA: pushCommits[1].Sha1,
PosterID: user.ID,
IssueID: 2,
},
}
AssertNotExistsBean(t, commentBean[0])
AssertNotExistsBean(t, commentBean[1])
AssertExistsAndLoadBean(t, &Issue{RepoID: repo.ID, Index: 2}, isOpen)
assert.NoError(t, UpdateIssuesCommit(user, repo, pushCommits, commitsAreMerged))
AssertExistsAndLoadBean(t, commentBean[0])
AssertExistsAndLoadBean(t, commentBean[1])
AssertExistsAndLoadBean(t, &Issue{RepoID: repo.ID, Index: 2}, isClosed)
CheckConsistencyFor(t, &Action{})
// test more complex commit pattern
pushCommits = []*PushCommit{
{
Sha1: "abcdef15",
CommitterEmail: "user2@example.com",
CommitterName: "User Two",
AuthorEmail: "user4@example.com",
AuthorName: "User Four",
Message: "start working on #FST-1, #1",
},
{
Sha1: "abcdef14",
CommitterEmail: "user2@example.com",
CommitterName: "User Two",
AuthorEmail: "user2@example.com",
AuthorName: "User Two",
Message: "reopen #2",
},
{
Sha1: "abcdef13",
CommitterEmail: "user2@example.com",
CommitterName: "User Two",
AuthorEmail: "user2@example.com",
AuthorName: "User Two",
Message: "close #2",
},
}
commentBean = []*Comment{
{
Type: CommentTypeCommitRef,
CommitSHA: pushCommits[0].Sha1,
PosterID: user.ID,
IssueID: 1,
},
{
Type: CommentTypeCommitRef,
CommitSHA: pushCommits[1].Sha1,
PosterID: user.ID,
IssueID: 2,
},
{
Type: CommentTypeCommitRef,
CommitSHA: pushCommits[2].Sha1,
PosterID: user.ID,
IssueID: 2,
},
}
AssertNotExistsBean(t, commentBean[0])
AssertNotExistsBean(t, commentBean[1])
AssertNotExistsBean(t, commentBean[2])
AssertExistsAndLoadBean(t, &Issue{RepoID: repo.ID, Index: 2}, isClosed)
assert.NoError(t, UpdateIssuesCommit(user, repo, pushCommits, commitsAreMerged))
AssertExistsAndLoadBean(t, commentBean[0])
AssertExistsAndLoadBean(t, commentBean[1])
AssertExistsAndLoadBean(t, commentBean[2])
AssertExistsAndLoadBean(t, &Issue{RepoID: repo.ID, Index: 2}, isOpen)
CheckConsistencyFor(t, &Action{})
}
}
func testCorrectRepoAction(t *testing.T, opts CommitRepoActionOptions, actionBean *Action) {
@ -379,6 +857,7 @@ func TestMergePullRequestAction(t *testing.T) {
repo := AssertExistsAndLoadBean(t, &Repository{ID: 1, OwnerID: user.ID}).(*Repository)
repo.Owner = user
issue := AssertExistsAndLoadBean(t, &Issue{ID: 3, RepoID: repo.ID}).(*Issue)
commits := &PushCommits{0, make([]*PushCommit, 0), "", nil}
actionBean := &Action{
OpType: ActionMergePullRequest,
@ -389,7 +868,7 @@ func TestMergePullRequestAction(t *testing.T) {
IsPrivate: repo.IsPrivate,
}
AssertNotExistsBean(t, actionBean)
assert.NoError(t, MergePullRequestAction(user, repo, issue))
assert.NoError(t, MergePullRequestAction(user, repo, issue, commits))
AssertExistsAndLoadBean(t, actionBean)
CheckConsistencyFor(t, &Action{})
}

View File

@ -802,6 +802,10 @@ func (issue *Issue) ChangeTitle(doer *User, title string) (err error) {
go HookQueue.Add(issue.RepoID)
}
if err = CreateOrUpdateIssueAction(doer, issue.Repo, issue); err != nil {
return fmt.Errorf("CreateOrUpdateIssueAction: %v", err)
}
return nil
}
@ -867,6 +871,10 @@ func (issue *Issue) ChangeContent(doer *User, content string) (err error) {
go HookQueue.Add(issue.RepoID)
}
if err = CreateOrUpdateIssueAction(doer, issue.Repo, issue); err != nil {
return fmt.Errorf("CreateOrUpdateIssueAction: %v", err)
}
return nil
}
@ -1037,6 +1045,10 @@ func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, assigneeIDs []in
UpdateIssueIndexer(issue.ID)
if err = CreateOrUpdateIssueAction(issue.Poster, issue.Repo, issue); err != nil {
return fmt.Errorf("CreateOrUpdateIssueAction: %v", err)
}
if err = NotifyWatchers(&Action{
ActUserID: issue.Poster.ID,
ActUser: issue.Poster,

View File

@ -14,6 +14,7 @@ import (
api "code.gitea.io/sdk/gitea"
"code.gitea.io/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/util"
@ -107,7 +108,13 @@ type Comment struct {
CreatedUnix util.TimeStamp `xorm:"INDEX created"`
UpdatedUnix util.TimeStamp `xorm:"INDEX updated"`
// Reference issue in commit message
// Reference issue in commit message, comments, issues, or pull requests
RefExists bool `xorm:"-"`
RefIssue *Issue `xorm:"-"`
RefComment *Comment `xorm:"-"`
RefMessage string `xorm:"-"`
RefURL string `xorm:"-"`
// the commit SHA for commit refs otherwise a SHA of a unique reference identifier
CommitSHA string `xorm:"VARCHAR(40)"`
Attachments []*Attachment `xorm:"-"`
@ -225,6 +232,105 @@ func (c *Comment) EventTag() string {
return "event-" + com.ToStr(c.ID)
}
// LoadReference if comment.Type is CommentType{Issue,Commit,Comment,Pull}Ref, then load RefIssue, RefComment
func (c *Comment) LoadReference() error {
if c.Type == CommentTypeIssueRef || c.Type == CommentTypePullRef {
var issueID int64
n, err := fmt.Sscanf(c.Content, "%d", &issueID)
if err != nil {
return err
}
if n == 1 {
refIssue, err := GetIssueByID(issueID)
if err != nil {
return err
}
pullOrIssue := "issues"
if refIssue.IsPull {
pullOrIssue = "pulls"
}
c.RefIssue = refIssue
c.RefURL = fmt.Sprintf("%s/%s/%d", refIssue.Repo.Link(), pullOrIssue, refIssue.Index)
c.RefExists = true
}
} else if c.Type == CommentTypeCommitRef {
if strings.HasPrefix(c.Content, `<a href="`) && strings.HasSuffix(c.Content, `</a>`) {
// this is an old style commit ref
content := strings.TrimSuffix(strings.TrimPrefix(c.Content, `<a href="`), `</a>`)
contentParts := strings.SplitN(content, `">`, 2)
if len(contentParts) == 2 {
c.RefURL = contentParts[0]
c.RefMessage = contentParts[1]
c.RefExists = true
}
} else {
// this is a new style commit ref
contentParts := strings.SplitN(c.Content, " ", 2)
if len(contentParts) == 2 {
var repoID int64
n, err := fmt.Sscanf(contentParts[0], "%d", &repoID)
if err != nil {
return err
}
if n == 1 {
refRepo, err := GetRepositoryByID(repoID)
if err != nil {
return err
}
gitRepo, err := git.OpenRepository(refRepo.RepoPath())
if err != nil {
return err
}
refCommit, err := gitRepo.GetCommit(contentParts[1][:40])
if err != nil {
return err
}
c.RefURL = fmt.Sprintf("%s/commit/%s", refRepo.Link(), refCommit.ID.String())
c.RefMessage = refCommit.CommitMessage
c.RefExists = true
}
}
}
} else if c.Type == CommentTypeCommentRef {
var commentID int64
n, err := fmt.Sscanf(c.Content, "%d", &commentID)
if err != nil {
return err
}
if n == 1 {
refComment, err := GetCommentByID(commentID)
if err != nil {
return err
}
refIssue, err := GetIssueByID(refComment.IssueID)
if err != nil {
return err
}
pullOrIssue := "issues"
if refIssue.IsPull {
pullOrIssue = "pulls"
}
c.RefIssue = refIssue
c.RefComment = refComment
c.RefURL = fmt.Sprintf("%s/%s/%d#%s", refIssue.Repo.Link(), pullOrIssue, refIssue.Index, refComment.HashTag())
c.RefExists = true
}
}
return nil
}
// LoadLabel if comment.Type is CommentTypeLabel, then load Label
func (c *Comment) LoadLabel() error {
var label Label
@ -589,6 +695,10 @@ func CreateComment(opts *CreateCommentOptions) (comment *Comment, err error) {
if opts.Type == CommentTypeComment {
UpdateIssueIndexer(opts.Issue.ID)
if err = CreateOrUpdateCommentAction(comment.Poster, opts.Repo, opts.Issue, comment); err != nil {
return nil, err
}
}
return comment, nil
}
@ -622,17 +732,17 @@ func CreateIssueComment(doer *User, repo *Repository, issue *Issue, content stri
return comment, nil
}
// CreateRefComment creates a commit reference comment to issue.
func CreateRefComment(doer *User, repo *Repository, issue *Issue, content, commitSHA string) error {
if len(commitSHA) == 0 {
return fmt.Errorf("cannot create reference with empty commit SHA")
// createRefComment creates a commit, comment, issue, or pull request reference comment to issue.
func createRefComment(doer *User, repo *Repository, issue *Issue, content, refSHA string, commentType CommentType) error {
if len(refSHA) == 0 {
return fmt.Errorf("cannot create reference with empty SHA")
}
// Check if same reference from same commit has already existed.
// Check if same reference from same issue and comment has already existed.
has, err := x.Get(&Comment{
Type: CommentTypeCommitRef,
Type: commentType,
IssueID: issue.ID,
CommitSHA: commitSHA,
CommitSHA: refSHA,
})
if err != nil {
return fmt.Errorf("check reference comment: %v", err)
@ -641,16 +751,36 @@ func CreateRefComment(doer *User, repo *Repository, issue *Issue, content, commi
}
_, err = CreateComment(&CreateCommentOptions{
Type: CommentTypeCommitRef,
Type: commentType,
Doer: doer,
Repo: repo,
Issue: issue,
CommitSHA: commitSHA,
CommitSHA: refSHA,
Content: content,
})
return err
}
// CreateCommitRefComment creates a commit reference comment to issue.
func CreateCommitRefComment(doer *User, repo *Repository, issue *Issue, content, commitSHA string) error {
return createRefComment(doer, repo, issue, content, commitSHA, CommentTypeCommitRef)
}
// CreateCommentRefComment creates a comment reference comment to issue.
func CreateCommentRefComment(doer *User, repo *Repository, issue *Issue, content, refSHA string) error {
return createRefComment(doer, repo, issue, content, refSHA, CommentTypeCommentRef)
}
// CreateIssueRefComment creates a comment reference comment to issue.
func CreateIssueRefComment(doer *User, repo *Repository, issue *Issue, content, refSHA string) error {
return createRefComment(doer, repo, issue, content, refSHA, CommentTypeIssueRef)
}
// CreatePullRefComment creates a comment reference comment to issue.
func CreatePullRefComment(doer *User, repo *Repository, issue *Issue, content, refSHA string) error {
return createRefComment(doer, repo, issue, content, refSHA, CommentTypePullRef)
}
// GetCommentByID returns the comment by given ID.
func GetCommentByID(id int64) (*Comment, error) {
c := new(Comment)
@ -737,6 +867,15 @@ func UpdateComment(doer *User, c *Comment, oldContent string) error {
return err
} else if c.Type == CommentTypeComment {
UpdateIssueIndexer(c.IssueID)
issue, err := GetIssueByID(c.IssueID)
if err != nil {
return err
}
if err = CreateOrUpdateCommentAction(c.Poster, issue.Repo, issue, c); err != nil {
return err
}
}
if err := c.LoadIssue(); err != nil {

View File

@ -14,9 +14,27 @@ import (
func TestCreateComment(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())
issue := AssertExistsAndLoadBean(t, &Issue{}).(*Issue)
repo := AssertExistsAndLoadBean(t, &Repository{ID: issue.RepoID}).(*Repository)
doer := AssertExistsAndLoadBean(t, &User{ID: repo.OwnerID}).(*User)
doer := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
repo := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
repo.Owner = doer
issue := AssertExistsAndLoadBean(t, &Issue{RepoID: repo.ID, Index: 1}).(*Issue)
refIssue := AssertExistsAndLoadBean(t, &Issue{RepoID: repo.ID, Index: 2}).(*Issue)
commentBean := []*Comment{
{
Type: CommentTypeCommentRef,
PosterID: doer.ID,
IssueID: issue.ID,
},
{
Type: CommentTypeCommentRef,
PosterID: doer.ID,
IssueID: refIssue.ID,
},
}
AssertNotExistsBean(t, commentBean[0])
AssertNotExistsBean(t, commentBean[1])
now := time.Now().Unix()
comment, err := CreateComment(&CreateCommentOptions{
@ -24,18 +42,26 @@ func TestCreateComment(t *testing.T) {
Doer: doer,
Repo: repo,
Issue: issue,
Content: "Hello",
Content: "Hello, this comment references issue #2",
})
assert.NoError(t, err)
then := time.Now().Unix()
assert.EqualValues(t, CommentTypeComment, comment.Type)
assert.EqualValues(t, "Hello", comment.Content)
assert.EqualValues(t, "Hello, this comment references issue #2", comment.Content)
assert.EqualValues(t, issue.ID, comment.IssueID)
assert.EqualValues(t, doer.ID, comment.PosterID)
AssertInt64InRange(t, now, then, int64(comment.CreatedUnix))
AssertExistsAndLoadBean(t, comment) // assert actually added to DB
AssertNotExistsBean(t, commentBean[0])
AssertExistsAndLoadBean(t, commentBean[1])
updatedIssue := AssertExistsAndLoadBean(t, &Issue{ID: issue.ID}).(*Issue)
AssertInt64InRange(t, now, then, int64(updatedIssue.UpdatedUnix))
err = commentBean[1].LoadReference()
assert.NoError(t, err)
if assert.NotNil(t, commentBean[1].RefIssue) {
assert.EqualValues(t, issue.ID, commentBean[1].RefIssue.ID)
}
}

View File

@ -445,10 +445,6 @@ func (pr *PullRequest) Merge(doer *User, baseGitRepo *git.Repository, mergeStyle
log.Error(4, "setMerged [%d]: %v", pr.ID, err)
}
if err = MergePullRequestAction(doer, pr.Issue.Repo, pr.Issue); err != nil {
log.Error(4, "MergePullRequestAction [%d]: %v", pr.ID, err)
}
// Reset cached commit count
cache.Remove(pr.Issue.Repo.GetCommitsCountCacheKey(pr.BaseBranch, true))
@ -488,13 +484,18 @@ func (pr *PullRequest) Merge(doer *User, baseGitRepo *git.Repository, mergeStyle
if mergeStyle == MergeStyleMerge {
l.PushFront(mergeCommit)
}
commits := ListToPushCommits(l)
if err = MergePullRequestAction(doer, pr.Issue.Repo, pr.Issue, commits); err != nil {
log.Error(4, "MergePullRequestAction [%d]: %v", pr.ID, err)
}
p := &api.PushPayload{
Ref: git.BranchPrefix + pr.BaseBranch,
Before: pr.MergeBase,
After: mergeCommit.ID.String(),
CompareURL: setting.AppURL + pr.BaseRepo.ComposeCompareURL(pr.MergeBase, pr.MergedCommitID),
Commits: ListToPushCommits(l).ToAPIPayloadCommits(pr.BaseRepo.HTMLURL()),
Commits: commits.ToAPIPayloadCommits(pr.BaseRepo.HTMLURL()),
Repo: pr.BaseRepo.APIFormat(mode),
Pusher: pr.HeadRepo.MustOwner().APIFormat(),
Sender: doer.APIFormat(),
@ -580,6 +581,11 @@ func (pr *PullRequest) manuallyMerged() bool {
return false
}
log.Info("manuallyMerged[%d]: Marked as manually merged into %s/%s by commit id: %s", pr.ID, pr.BaseRepo.Name, pr.BaseBranch, commit.ID.String())
if err = MergePullRequestAction(pr.Merger, pr.Issue.Repo, pr.Issue, nil); err != nil {
log.Error(4, "MergePullRequestAction [%d]: %v", pr.ID, err)
}
return true
}
return false
@ -771,23 +777,45 @@ func NewPullRequest(repo *Repository, pull *Issue, labelIDs []int64, uuids []str
UpdateIssueIndexer(pull.ID)
if err = NotifyWatchers(&Action{
ActUserID: pull.Poster.ID,
ActUser: pull.Poster,
OpType: ActionCreatePullRequest,
Content: fmt.Sprintf("%d|%s", pull.Index, pull.Title),
RepoID: repo.ID,
Repo: repo,
IsPrivate: repo.IsPrivate,
}); err != nil {
log.Error(4, "NotifyWatchers: %v", err)
} else if err = pull.MailParticipants(); err != nil {
log.Error(4, "MailParticipants: %v", err)
}
pr.Issue = pull
pull.PullRequest = pr
mode, _ := AccessLevel(pull.Poster.ID, repo)
var (
baseBranch *Branch
headBranch *Branch
baseCommit *git.Commit
headCommit *git.Commit
baseGitRepo *git.Repository
)
if baseBranch, err = pr.BaseRepo.GetBranch(pr.BaseBranch); err != nil {
return nil
}
if baseCommit, err = baseBranch.GetCommit(); err != nil {
return nil
}
if headBranch, err = pr.HeadRepo.GetBranch(pr.HeadBranch); err != nil {
return nil
}
if headCommit, err = headBranch.GetCommit(); err != nil {
return nil
}
if baseGitRepo, err = git.OpenRepository(pr.BaseRepo.RepoPath()); err != nil {
log.Error(4, "OpenRepository", err)
return nil
}
l, err := baseGitRepo.CommitsBetweenIDs(headCommit.ID.String(), baseCommit.ID.String())
if err != nil {
log.Error(4, "CommitsBetweenIDs: %v", err)
return nil
}
commits := ListToPushCommits(l)
if err = NewPullRequestAction(pull.Poster, repo, pull, commits); err != nil {
log.Error(4, "NewPullRequestAction [%d]: %v", pr.ID, err)
}
if err = PrepareWebhooks(repo, HookEventPullRequest, &api.PullRequestPayload{
Action: api.HookIssueOpened,
Index: pull.Index,

View File

@ -67,7 +67,7 @@ type PushUpdateOptions struct {
// PushUpdate must be called for any push actions in order to
// generates necessary push action history feeds.
func PushUpdate(branch string, opt PushUpdateOptions) error {
repo, err := pushUpdate(opt)
repo, err := pushUpdate(branch, opt)
if err != nil {
return err
}
@ -183,7 +183,7 @@ func pushUpdateAddTag(repo *Repository, gitRepo *git.Repository, tagName string)
return nil
}
func pushUpdate(opts PushUpdateOptions) (repo *Repository, err error) {
func pushUpdate(branch string, opts PushUpdateOptions) (repo *Repository, err error) {
isNewRef := opts.OldCommitID == git.EmptySHA
isDelRef := opts.NewCommitID == git.EmptySHA
if isNewRef && isDelRef {
@ -277,5 +277,69 @@ func pushUpdate(opts PushUpdateOptions) (repo *Repository, err error) {
}); err != nil {
return nil, fmt.Errorf("CommitRepoAction: %v", err)
}
// create actions that update pull requests tracking the branch that was pushed to
prs, err := GetUnmergedPullRequestsByHeadInfo(repo.ID, branch)
if err != nil {
log.Error(4, "Find pull requests [head_repo_id: %d, head_branch: %s]: %v", repo.ID, branch, err)
} else {
pusher, err := GetUserByID(opts.PusherID)
if err != nil {
return nil, fmt.Errorf("GetUserByID: %v", err)
}
for _, pr := range prs {
if err = pr.GetHeadRepo(); err != nil {
log.Error(4, "GetHeadRepo: %v", err)
continue
} else if err = pr.GetBaseRepo(); err != nil {
log.Error(4, "GetBaseRepo: %v", err)
continue
}
var (
baseBranch *Branch
headBranch *Branch
baseCommit *git.Commit
headCommit *git.Commit
headGitRepo *git.Repository
)
if baseBranch, err = pr.BaseRepo.GetBranch(pr.BaseBranch); err != nil {
log.Error(4, "BaseRepo.GetBranch: %v", err)
continue
}
if baseCommit, err = baseBranch.GetCommit(); err != nil {
log.Error(4, "baseBranch.GetCommit: %v", err)
continue
}
if headBranch, err = pr.HeadRepo.GetBranch(pr.HeadBranch); err != nil {
log.Error(4, "HeadRepo.GetBranch: %v", err)
continue
}
if headCommit, err = headBranch.GetCommit(); err != nil {
log.Error(4, "headRepo.GetCommit: %v", err)
continue
}
// NOTICE: this is using pr.HeadRepo rather than pr.BaseRepo to get commits since they are going to be pushed in a go routine
if headGitRepo, err = git.OpenRepository(pr.HeadRepo.RepoPath()); err != nil {
log.Error(4, "OpenRepository", err)
continue
}
l, err := headGitRepo.CommitsBetweenIDs(headCommit.ID.String(), baseCommit.ID.String())
if err != nil {
log.Error(4, "CommitsBetweenIDs: %v", err)
continue
}
commits := ListToPushCommits(l)
if err = CommitPullRequestAction(pusher, pr.BaseRepo, commits); err != nil {
log.Error(4, "CommitPullRequestAction [%d]: %v", pr.ID, err)
continue
}
}
}
return repo, nil
}

View File

@ -721,7 +721,10 @@ issues.reopen_comment_issue = Comment and Reopen
issues.create_comment = Comment
issues.closed_at = `closed <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.reopened_at = `reopened <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.issue_ref_at = `referenced this issue from an issue <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.commit_ref_at = `referenced this issue from a commit <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.comment_ref_at = `referenced this issue from a comment <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.pull_ref_at = `referenced this issue from a pull request <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.poster = Poster
issues.collaborator = Collaborator
issues.owner = Owner

View File

@ -696,6 +696,10 @@ func ViewIssue(ctx *context.Context) {
if !isAdded && !issue.IsPoster(comment.Poster.ID) {
participants = append(participants, comment.Poster)
}
} else if comment.Type == models.CommentTypeIssueRef || comment.Type == models.CommentTypeCommitRef || comment.Type == models.CommentTypeCommentRef || comment.Type == models.CommentTypePullRef {
if err = comment.LoadReference(); err != nil {
continue
}
} else if comment.Type == models.CommentTypeLabel {
if err = comment.LoadLabel(); err != nil {
ctx.ServerError("LoadLabel", err)

View File

@ -82,7 +82,20 @@
</a>
<span class="text grey"><a href="{{.Poster.HomeLink}}">{{.Poster.Name}}</a> {{$.i18n.Tr "repo.issues.closed_at" .EventTag $createdStr | Safe}}</span>
</div>
{{else if eq .Type 4}}
{{else if and (eq .Type 3) .RefExists}}
<div class="event">
<span class="octicon octicon-bookmark"></span>
<a class="ui avatar image" href="{{.Poster.HomeLink}}">
<img src="{{.Poster.RelAvatarLink}}">
</a>
<span class="text grey"><a href="{{.Poster.HomeLink}}">{{.Poster.Name}}</a> {{$.i18n.Tr "repo.issues.issue_ref_at" .EventTag $createdStr | Safe}}</span>
<div class="detail">
<span class="octicon octicon-issue-opened"></span>
<span class="text grey"><a href="{{.RefURL}}"><span class="title has-emoji">{{.RefIssue.Title | Escape}}</span> (#{{.RefIssue.Index}})</a></span>
</div>
</div>
{{else if and (eq .Type 4) .RefExists}}
<div class="event">
<span class="octicon octicon-bookmark"></span>
<a class="ui avatar image" href="{{.Poster.HomeLink}}">
@ -92,7 +105,33 @@
<div class="detail">
<span class="octicon octicon-git-commit"></span>
<span class="text grey">{{.Content | Str2html}}</span>
<span class="text grey"><a href="{{.RefURL}}"><span class="title has-emoji">{{.RefMessage | Escape}}</span> ({{ShortSha .CommitSHA}})</a></span>
</div>
</div>
{{else if and (eq .Type 5) .RefExists}}
<div class="event">
<span class="octicon octicon-bookmark"></span>
<a class="ui avatar image" href="{{.Poster.HomeLink}}">
<img src="{{.Poster.RelAvatarLink}}">
</a>
<span class="text grey"><a href="{{.Poster.HomeLink}}">{{.Poster.Name}}</a> {{$.i18n.Tr "repo.issues.comment_ref_at" .EventTag $createdStr | Safe}}</span>
<div class="detail">
<span class="octicon octicon-comment-discussion"></span>
<span class="text grey"><a href="{{.RefURL}}"><span class="title has-emoji">{{.RefIssue.Title | Escape}}</span> (#{{.RefIssue.Index}})</a></span>
</div>
</div>
{{else if and (eq .Type 6) .RefExists}}
<div class="event">
<span class="octicon octicon-bookmark"></span>
<a class="ui avatar image" href="{{.Poster.HomeLink}}">
<img src="{{.Poster.RelAvatarLink}}">
</a>
<span class="text grey"><a href="{{.Poster.HomeLink}}">{{.Poster.Name}}</a> {{$.i18n.Tr "repo.issues.pull_ref_at" .EventTag $createdStr | Safe}}</span>
<div class="detail">
<span class="octicon octicon-git-pull-request"></span>
<span class="text grey"><a href="{{.RefURL}}"><span class="title has-emoji">{{.RefIssue.Title | Escape}}</span> (#{{.RefIssue.Index}})</a></span>
</div>
</div>
{{else if eq .Type 7}}