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

[RFC] Update issues via pull requests #3695

Closed
wants to merge 32 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
d6ff201
Update issues when a pull request is merged
krrutkow Mar 19, 2018
224f0fd
Factor out detection of reference, close, reopen keywords into a more…
krrutkow Mar 21, 2018
d418d92
Only change issue status when commits have been merged
krrutkow Mar 23, 2018
06fcda9
Add issue updating from commit messages in new pull request
krrutkow Mar 23, 2018
c1dac9c
Clean up {Merge,New}PullRequestAction and add UpdatePullRequestAction
krrutkow Mar 27, 2018
32c9209
Add issue updating from commits being tracked by pull requests
krrutkow Mar 27, 2018
bb65b7f
Fix code formatting
krrutkow Mar 27, 2018
cd17446
Rename {Update -> Commit}PullRequestAction to be more accurately named
krrutkow Apr 2, 2018
d9ea596
Factor out creation of reference comments
krrutkow Apr 2, 2018
29dacbd
Add the UpdateIssuesComment function
krrutkow Apr 2, 2018
ff00d31
Hook up the UpdateIssuesComment function to various actions
krrutkow Apr 2, 2018
83d08c2
Add the LoadReference function
krrutkow Apr 2, 2018
ce0333f
Hook up frontend templates to render reference comments
krrutkow Apr 2, 2018
c6a7ce5
Change commit ref comments to use a more parse-able form of content
krrutkow Apr 2, 2018
3d72103
Applied suggested code changes
krrutkow Apr 7, 2018
cf174ea
Improve code coverage of TestUpdateIssuesCommit
krrutkow Apr 7, 2018
943be3d
Add test for referencing issues in comments
krrutkow Apr 7, 2018
9f5b9ea
Add test for updating issues from issues
krrutkow Apr 7, 2018
98e12ce
Update TestCreateComment to check referenced issue
krrutkow Apr 7, 2018
af7c75b
Correct error message
krrutkow May 23, 2018
8454950
Trigger MergePullRequestAction when PR is manually merged
krrutkow May 25, 2018
5e48595
Change variable names from KeywordsFound* to Keyword*
krrutkow Jul 6, 2018
7c8204a
Move common status changing logic into a function
krrutkow Jul 6, 2018
1768f33
Change "x := int64(0)" statements to "var x int64"
krrutkow Jul 6, 2018
a398702
Refactor reference comment related code
krrutkow Aug 14, 2018
65efac5
Update issue referencing comment tests
krrutkow Aug 15, 2018
0da2736
Push pull request commits to base before trying to use them
krrutkow Aug 16, 2018
05c4691
Don't exit early in NewPullRequest
krrutkow Aug 17, 2018
18e63c1
Complete addition of code comments and review issue referencing comments
krrutkow Aug 20, 2018
ee360eb
Fix to only get commits along head branch
krrutkow Aug 20, 2018
7e2fbdb
Code cleanup
krrutkow Aug 20, 2018
ac30d94
Add reformatAndAddReferenceComments migration v75
krrutkow Aug 20, 2018
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
266 changes: 207 additions & 59 deletions models/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,43 @@ const (
ActionMirrorSyncDelete // 20
)

// 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{
jonasfranz marked this conversation as resolved.
Show resolved Hide resolved
{
Pattern: regexp.MustCompile(issueRefRegexpStr),
KeywordMask: KeywordReference,
},
{
Pattern: regexp.MustCompile(assembleKeywordsPattern(issueReopenKeywords)),
KeywordMask: KeywordReopen,
},
{
Pattern: regexp.MustCompile(assembleKeywordsPattern(issueCloseKeywords)),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a regex... shouldn't this do the trick?

regexp.MustCompile("(close[sd]?|fix|fixe[sd]?|resolve[sd]?)")

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the addition of the issue reference portion, yes, it could be reduced to a regex. I didn't change the process of how the regex patterns were built since it seemed that the existing way could easily support things like language translations or user-provided keywords.

KeywordMask: KeywordClose,
},
}
)

const issueRefRegexpStr = `(?:\S+/\S=)?#\d+`
Expand All @@ -68,12 +97,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.
Expand Down Expand Up @@ -438,74 +461,129 @@ func getIssueFromRef(repo *Repository, ref string) (*Issue, error) {
return issue, nil
}

// UpdateIssuesCommit checks if issues are manipulated by commit message.
func UpdateIssuesCommit(doer *User, repo *Repository, commits []*PushCommit) 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) {
// 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 err
return nil, err
}

if issue == nil || refMarked[issue.ID] {
continue
if issue != nil {
refs[issue.ID] |= kwToFind.KeywordMask
}
refMarked[issue.ID] = true
}
}
return refs, nil
}

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
// 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, issue *Issue) error {
// take no action if both KeywordClose and KeywordOpen are set
switch mask {
case KeywordClose:
if err := issue.ChangeStatus(doer, issue.Repo, true); err != nil {
// Don't return the error when dependencies are still open as this would cause a push, merge, etc. to fail
if IsErrDependenciesLeft(err) {
return nil
}
return err
}
case KeywordReopen:
if err := issue.ChangeStatus(doer, issue.Repo, false); err != nil {
return err
}
}
return nil
}

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)
// UpdateIssuesComment checks if issues are manipulated by a regular comment, code comment, or review comment (also issue/pull request title and description)
func UpdateIssuesComment(doer *User, repo *Repository, commentIssue *Issue, comment *Comment, canOpenClose bool) error {
var refString string
if comment != nil {
if comment.Type != CommentTypeComment && comment.Type != CommentTypeCode && comment.Type != CommentTypeReview {
return nil
}

refString = comment.Content
} else {
refString = commentIssue.Title + ": " + commentIssue.Content
}

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 && comment.Type == CommentTypeComment {
err = CreateCommentRefComment(doer, issue, commentIssue.ID, comment.ID)
} else if comment != nil && comment.Type == CommentTypeCode {
err = CreateCodeRefComment(doer, issue, commentIssue.ID, comment.ID)
} else if comment != nil && comment.Type == CommentTypeReview {
err = CreateReviewRefComment(doer, issue, commentIssue.ID, comment.ID)
} else if commentIssue.IsPull {
err = CreatePullRefComment(doer, issue, commentIssue.ID)
} else {
err = CreateIssueRefComment(doer, issue, commentIssue.ID)
}
if err != nil {
return err
}
}

if issue == nil || refMarked[issue.ID] {
continue
// only change issue status if this is a pull request in the issue's repo
if canOpenClose && comment == nil && commentIssue.IsPull && repo.ID == issue.RepoID {
if err = changeIssueStatus(mask&(KeywordReopen|KeywordClose), doer, issue); err != nil {
return err
}
refMarked[issue.ID] = true
}
}
return nil
}

if issue.RepoID != repo.ID || issue.IsClosed {
continue
}
// UpdateIssuesCommit checks if issues are manipulated by commit message.
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]

if err = issue.ChangeStatus(doer, repo, true); err != nil {
// Don't return an error when dependencies are open as this would let the push fail
if IsErrDependenciesLeft(err) {
return nil
}
return err
}
refs, err := findIssueReferencesInString(c.Message, repo)
if 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)
for id, mask := range refs {
issue, err := GetIssueByID(id)
if err != nil {
return err
}

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

if issue.RepoID != repo.ID || !issue.IsClosed {
continue
if (mask & KeywordReference) == KeywordReference {
if err = CreateCommitRefComment(doer, issue, repo.ID, c.Sha1); err != nil {
return err
}
}

if err = issue.ChangeStatus(doer, repo, false); err != nil {
return err
// only change issue status if the commit is merged to the issue's repo
if commitsAreMerged && repo.ID == issue.RepoID {
if err = changeIssueStatus(mask&(KeywordReopen|KeywordClose), doer, issue); err != nil {
return err
}
}
}
}
Expand Down Expand Up @@ -569,8 +647,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)
}
}

Expand Down Expand Up @@ -724,21 +802,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
}

func mirrorSyncAction(e Engine, opType ActionType, repo *Repository, refName string, data []byte) error {
Expand Down
Loading