From 3f7f4852efaaa56a0dada832dc652a1fc8869ae7 Mon Sep 17 00:00:00 2001 From: Unknwon Date: Sun, 14 Aug 2016 03:32:24 -0700 Subject: #2246 fully support of webhooks for pull request --- models/issue.go | 362 ++++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 300 insertions(+), 62 deletions(-) (limited to 'models/issue.go') diff --git a/models/issue.go b/models/issue.go index 3d7e862c..9d9d3db0 100644 --- a/models/issue.go +++ b/models/issue.go @@ -16,6 +16,7 @@ import ( "github.com/Unknwon/com" "github.com/go-xorm/xorm" + api "github.com/gogits/go-gogs-client" gouuid "github.com/satori/go.uuid" "github.com/gogits/gogs/modules/base" @@ -31,10 +32,10 @@ var ( // Issue represents an issue or pull request of repository. type Issue struct { - ID int64 `xorm:"pk autoincr"` - RepoID int64 `xorm:"INDEX UNIQUE(repo_index)"` - Index int64 `xorm:"UNIQUE(repo_index)"` // Index in one repository. - Name string + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"INDEX UNIQUE(repo_index)"` + Index int64 `xorm:"UNIQUE(repo_index)"` // Index in one repository. + Title string `xorm:"name"` Repo *Repository `xorm:"-"` PosterID int64 Poster *User `xorm:"-"` @@ -73,19 +74,6 @@ func (i *Issue) BeforeUpdate() { i.DeadlineUnix = i.Deadline.Unix() } -func (issue *Issue) loadAttributes(e Engine) (err error) { - issue.Repo, err = getRepositoryByID(e, issue.RepoID) - if err != nil { - return fmt.Errorf("getRepositoryByID: %v", err) - } - - return nil -} - -func (issue *Issue) LoadAttributes() error { - return issue.loadAttributes(x) -} - func (i *Issue) AfterSet(colName string, _ xorm.Cell) { var err error switch colName { @@ -110,7 +98,7 @@ func (i *Issue) AfterSet(colName string, _ xorm.Cell) { if err != nil { if IsErrUserNotExist(err) { i.PosterID = -1 - i.Poster = NewFakeUser() + i.Poster = NewGhostUser() } else { log.Error(3, "GetUserByID[%d]: %v", i.ID, err) } @@ -146,17 +134,80 @@ func (i *Issue) AfterSet(colName string, _ xorm.Cell) { } } -// HashTag returns unique hash tag for issue. -func (i *Issue) HashTag() string { - return "issue-" + com.ToStr(i.ID) +func (issue *Issue) loadAttributes(e Engine) (err error) { + if issue.Repo == nil { + issue.Repo, err = getRepositoryByID(e, issue.RepoID) + if err != nil { + return fmt.Errorf("getRepositoryByID [%d]: %v", issue.RepoID, err) + } + } + + if issue.IsPull && issue.PullRequest == nil { + // It is possible pull request is not yet created. + issue.PullRequest, err = getPullRequestByIssueID(e, issue.ID) + if err != nil && !IsErrPullRequestNotExist(err) { + return fmt.Errorf("getPullRequestByIssueID [%d]: %v", issue.ID, err) + } + } + + return nil +} + +func (issue *Issue) LoadAttributes() error { + return issue.loadAttributes(x) } // State returns string representation of issue status. -func (i *Issue) State() string { +func (i *Issue) State() api.StateType { if i.IsClosed { - return "closed" + return api.STATE_CLOSED } - return "open" + return api.STATE_OPEN +} + +// This method assumes some fields assigned with values: +// Required - Poster, Labels, +// Optional - Milestone, Assignee, PullRequest +func (issue *Issue) APIFormat() *api.Issue { + apiLabels := make([]*api.Label, len(issue.Labels)) + for i := range issue.Labels { + apiLabels[i] = issue.Labels[i].APIFormat() + } + + apiIssue := &api.Issue{ + ID: issue.ID, + Index: issue.Index, + State: issue.State(), + Title: issue.Title, + Body: issue.Content, + User: issue.Poster.APIFormat(), + Labels: apiLabels, + Comments: issue.NumComments, + Created: issue.Created, + Updated: issue.Updated, + } + + if issue.Milestone != nil { + apiIssue.Milestone = issue.Milestone.APIFormat() + } + if issue.Assignee != nil { + apiIssue.Assignee = issue.Assignee.APIFormat() + } + if issue.IsPull { + apiIssue.PullRequest = &api.PullRequestMeta{ + HasMerged: issue.PullRequest.HasMerged, + } + if issue.PullRequest.HasMerged { + apiIssue.PullRequest.Merged = &issue.PullRequest.Merged + } + } + + return apiIssue +} + +// HashTag returns unique hash tag for issue. +func (i *Issue) HashTag() string { + return "issue-" + com.ToStr(i.ID) } func (issue *Issue) FullLink() string { @@ -183,23 +234,37 @@ func (i *Issue) HasLabel(labelID int64) bool { return i.hasLabel(x, labelID) } +func (issue *Issue) sendLabelUpdatedWebhook(doer *User) { + var err error + if issue.IsPull { + issue.PullRequest.Issue = issue + err = PrepareWebhooks(issue.Repo, HOOK_EVENT_PULL_REQUEST, &api.PullRequestPayload{ + Action: api.HOOK_ISSUE_LABEL_UPDATED, + Index: issue.Index, + PullRequest: issue.PullRequest.APIFormat(), + Repository: issue.Repo.APIFormat(nil), + Sender: doer.APIFormat(), + }) + } + if err != nil { + log.Error(4, "PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err) + } else { + go HookQueue.Add(issue.RepoID) + } +} + func (i *Issue) addLabel(e *xorm.Session, label *Label) error { return newIssueLabel(e, i, label) } // AddLabel adds a new label to the issue. -func (i *Issue) AddLabel(label *Label) (err error) { - sess := x.NewSession() - defer sessionRelease(sess) - if err = sess.Begin(); err != nil { +func (issue *Issue) AddLabel(doer *User, label *Label) error { + if err := NewIssueLabel(issue, label); err != nil { return err } - if err = i.addLabel(sess, label); err != nil { - return err - } - - return sess.Commit() + issue.sendLabelUpdatedWebhook(doer) + return nil } func (issue *Issue) addLabels(e *xorm.Session, labels []*Label) error { @@ -207,8 +272,13 @@ func (issue *Issue) addLabels(e *xorm.Session, labels []*Label) error { } // AddLabels adds a list of new labels to the issue. -func (issue *Issue) AddLabels(labels []*Label) error { - return NewIssueLabels(issue, labels) +func (issue *Issue) AddLabels(doer *User, labels []*Label) error { + if err := NewIssueLabels(issue, labels); err != nil { + return err + } + + issue.sendLabelUpdatedWebhook(doer) + return nil } func (issue *Issue) getLabels(e Engine) (err error) { @@ -228,8 +298,13 @@ func (issue *Issue) removeLabel(e *xorm.Session, label *Label) error { } // RemoveLabel removes a label from issue by given ID. -func (issue *Issue) RemoveLabel(label *Label) (err error) { - return DeleteIssueLabel(issue, label) +func (issue *Issue) RemoveLabel(doer *User, label *Label) error { + if err := DeleteIssueLabel(issue, label); err != nil { + return err + } + + issue.sendLabelUpdatedWebhook(doer) + return nil } func (issue *Issue) clearLabels(e *xorm.Session) (err error) { @@ -246,7 +321,7 @@ func (issue *Issue) clearLabels(e *xorm.Session) (err error) { return nil } -func (issue *Issue) ClearLabels() (err error) { +func (issue *Issue) ClearLabels(doer *User) (err error) { sess := x.NewSession() defer sessionRelease(sess) if err = sess.Begin(); err != nil { @@ -257,7 +332,27 @@ func (issue *Issue) ClearLabels() (err error) { return err } - return sess.Commit() + if err = sess.Commit(); err != nil { + return fmt.Errorf("Commit: %v", err) + } + + if issue.IsPull { + issue.PullRequest.Issue = issue + err = PrepareWebhooks(issue.Repo, HOOK_EVENT_PULL_REQUEST, &api.PullRequestPayload{ + Action: api.HOOK_ISSUE_LABEL_CLEARED, + Index: issue.Index, + PullRequest: issue.PullRequest.APIFormat(), + Repository: issue.Repo.APIFormat(nil), + Sender: doer.APIFormat(), + }) + } + if err != nil { + log.Error(4, "PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err) + } else { + go HookQueue.Add(issue.RepoID) + } + + return nil } // ReplaceLabels removes all current labels and add new labels to the issue. @@ -294,6 +389,16 @@ func (i *Issue) ReadBy(uid int64) error { return UpdateIssueUserByRead(uid, i.ID) } +func updateIssueCols(e Engine, issue *Issue, cols ...string) error { + _, err := e.Id(issue.ID).Cols(cols...).Update(issue) + return err +} + +// UpdateIssueCols only updates values of specific columns for given issue. +func UpdateIssueCols(issue *Issue, cols ...string) error { + return updateIssueCols(x, issue, cols...) +} + func (i *Issue) changeStatus(e *xorm.Session, doer *User, repo *Repository, isClosed bool) (err error) { // Nothing should be performed if current status is same as target status if i.IsClosed == isClosed { @@ -336,32 +441,149 @@ func (i *Issue) changeStatus(e *xorm.Session, doer *User, repo *Repository, isCl } // ChangeStatus changes issue status to open or closed. -func (i *Issue) ChangeStatus(doer *User, repo *Repository, isClosed bool) (err error) { +func (issue *Issue) ChangeStatus(doer *User, repo *Repository, isClosed bool) (err error) { sess := x.NewSession() defer sessionRelease(sess) if err = sess.Begin(); err != nil { return err } - if err = i.changeStatus(sess, doer, repo, isClosed); err != nil { + if err = issue.changeStatus(sess, doer, repo, isClosed); err != nil { return err } - return sess.Commit() + if err = sess.Commit(); err != nil { + return fmt.Errorf("Commit: %v", err) + } + + if issue.IsPull { + // Merge pull request calls issue.changeStatus so we need to handle separately. + issue.PullRequest.Issue = issue + apiPullRequest := &api.PullRequestPayload{ + Index: issue.Index, + PullRequest: issue.PullRequest.APIFormat(), + Repository: repo.APIFormat(nil), + Sender: doer.APIFormat(), + } + if isClosed { + apiPullRequest.Action = api.HOOK_ISSUE_CLOSED + } else { + apiPullRequest.Action = api.HOOK_ISSUE_REOPENED + } + err = PrepareWebhooks(repo, HOOK_EVENT_PULL_REQUEST, apiPullRequest) + } + if err != nil { + log.Error(4, "PrepareWebhooks [is_pull: %v, is_closed: %v]: %v", issue.IsPull, isClosed, err) + } else { + go HookQueue.Add(repo.ID) + } + + return nil +} + +func (issue *Issue) ChangeTitle(doer *User, title string) (err error) { + oldTitle := issue.Title + issue.Title = title + if err = UpdateIssueCols(issue, "name"); err != nil { + return fmt.Errorf("UpdateIssueCols: %v", err) + } + + if issue.IsPull { + issue.PullRequest.Issue = issue + err = PrepareWebhooks(issue.Repo, HOOK_EVENT_PULL_REQUEST, &api.PullRequestPayload{ + Action: api.HOOK_ISSUE_EDITED, + Index: issue.Index, + Changes: &api.ChangesPayload{ + Title: &api.ChangesFromPayload{ + From: oldTitle, + }, + }, + PullRequest: issue.PullRequest.APIFormat(), + Repository: issue.Repo.APIFormat(nil), + Sender: doer.APIFormat(), + }) + } + if err != nil { + log.Error(4, "PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err) + } else { + go HookQueue.Add(issue.RepoID) + } + + return nil } -func (i *Issue) GetPullRequest() (err error) { - if i.PullRequest != nil { +func (issue *Issue) ChangeContent(doer *User, content string) (err error) { + oldContent := issue.Content + issue.Content = content + if err = UpdateIssueCols(issue, "content"); err != nil { + return fmt.Errorf("UpdateIssueCols: %v", err) + } + + if issue.IsPull { + issue.PullRequest.Issue = issue + err = PrepareWebhooks(issue.Repo, HOOK_EVENT_PULL_REQUEST, &api.PullRequestPayload{ + Action: api.HOOK_ISSUE_EDITED, + Index: issue.Index, + Changes: &api.ChangesPayload{ + Body: &api.ChangesFromPayload{ + From: oldContent, + }, + }, + PullRequest: issue.PullRequest.APIFormat(), + Repository: issue.Repo.APIFormat(nil), + Sender: doer.APIFormat(), + }) + } + if err != nil { + log.Error(4, "PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err) + } else { + go HookQueue.Add(issue.RepoID) + } + + return nil +} + +func (issue *Issue) ChangeAssignee(doer *User, assigneeID int64) (err error) { + issue.AssigneeID = assigneeID + if err = UpdateIssueUserByAssignee(issue); err != nil { + return fmt.Errorf("UpdateIssueUserByAssignee: %v", err) + } + + issue.Assignee, err = GetUserByID(issue.AssigneeID) + if err != nil && !IsErrUserNotExist(err) { + log.Error(4, "GetUserByID [assignee_id: %v]: %v", issue.AssigneeID, err) return nil } - i.PullRequest, err = GetPullRequestByIssueID(i.ID) - return err + // Error not nil here means user does not exist, which is remove assignee. + isRemoveAssignee := err != nil + if issue.IsPull { + issue.PullRequest.Issue = issue + apiPullRequest := &api.PullRequestPayload{ + Index: issue.Index, + PullRequest: issue.PullRequest.APIFormat(), + Repository: issue.Repo.APIFormat(nil), + Sender: doer.APIFormat(), + } + if isRemoveAssignee { + apiPullRequest.Action = api.HOOK_ISSUE_UNASSIGNED + } else { + apiPullRequest.Action = api.HOOK_ISSUE_ASSIGNED + } + err = PrepareWebhooks(issue.Repo, HOOK_EVENT_PULL_REQUEST, apiPullRequest) + } + if err != nil { + log.Error(4, "PrepareWebhooks [is_pull: %v, remove_assignee: %v]: %v", issue.IsPull, isRemoveAssignee, err) + } else { + go HookQueue.Add(issue.RepoID) + } + + return nil } // It's caller's responsibility to create action. func newIssue(e *xorm.Session, repo *Repository, issue *Issue, labelIDs []int64, uuids []string, isPull bool) (err error) { - issue.Name = strings.TrimSpace(issue.Name) + issue.Title = strings.TrimSpace(issue.Title) issue.Index = repo.NextIssueIndex() if issue.AssigneeID > 0 { @@ -462,7 +684,7 @@ func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, uuids []string) ActUserName: issue.Poster.Name, ActEmail: issue.Poster.Email, OpType: ACTION_CREATE_ISSUE, - Content: fmt.Sprintf("%d|%s", issue.Index, issue.Name), + Content: fmt.Sprintf("%d|%s", issue.Index, issue.Title), RepoID: repo.ID, RepoUserName: repo.Owner.Name, RepoName: repo.Name, @@ -518,10 +740,9 @@ func GetIssueByIndex(repoID, index int64) (*Issue, error) { return issue, issue.LoadAttributes() } -// GetIssueByID returns an issue by given ID. -func GetIssueByID(id int64) (*Issue, error) { +func getIssueByID(e Engine, id int64) (*Issue, error) { issue := new(Issue) - has, err := x.Id(id).Get(issue) + has, err := e.Id(id).Get(issue) if err != nil { return nil, err } else if !has { @@ -530,6 +751,11 @@ func GetIssueByID(id int64) (*Issue, error) { return issue, issue.LoadAttributes() } +// GetIssueByID returns an issue by given ID. +func GetIssueByID(id int64) (*Issue, error) { + return getIssueByID(x, id) +} + type IssuesOptions struct { UserID int64 AssigneeID int64 @@ -970,12 +1196,6 @@ func UpdateIssue(issue *Issue) error { return updateIssue(x, issue) } -// updateIssueCols only updates values of specific columns for given issue. -func updateIssueCols(e Engine, issue *Issue, cols ...string) error { - _, err := e.Id(issue.ID).Cols(cols...).Update(issue) - return err -} - func updateIssueUsersByStatus(e Engine, issueID int64, isClosed bool) error { _, err := e.Exec("UPDATE `issue_user` SET is_closed=? WHERE issue_id=?", isClosed, issueID) return err @@ -987,13 +1207,13 @@ func UpdateIssueUsersByStatus(issueID int64, isClosed bool) error { } func updateIssueUserByAssignee(e *xorm.Session, issue *Issue) (err error) { - if _, err = e.Exec("UPDATE `issue_user` SET is_assigned=? WHERE issue_id=?", false, issue.ID); err != nil { + if _, err = e.Exec("UPDATE `issue_user` SET is_assigned = ? WHERE issue_id = ?", false, issue.ID); err != nil { return err } // Assignee ID equals to 0 means clear assignee. if issue.AssigneeID > 0 { - if _, err = e.Exec("UPDATE `issue_user` SET is_assigned=? WHERE uid=? AND issue_id=?", true, issue.AssigneeID, issue.ID); err != nil { + if _, err = e.Exec("UPDATE `issue_user` SET is_assigned = ? WHERE uid = ? AND issue_id = ?", true, issue.AssigneeID, issue.ID); err != nil { return err } } @@ -1112,11 +1332,29 @@ func (m *Milestone) AfterSet(colName string, _ xorm.Cell) { } // State returns string representation of milestone status. -func (m *Milestone) State() string { +func (m *Milestone) State() api.StateType { if m.IsClosed { - return "closed" + return api.STATE_CLOSED + } + return api.STATE_OPEN +} + +func (m *Milestone) APIFormat() *api.Milestone { + apiMilestone := &api.Milestone{ + ID: m.ID, + State: m.State(), + Title: m.Name, + Description: m.Content, + OpenIssues: m.NumOpenIssues, + ClosedIssues: m.NumClosedIssues, + } + if m.IsClosed { + apiMilestone.Closed = &m.ClosedDate + } + if m.Deadline.Year() < 9999 { + apiMilestone.Deadline = &m.Deadline } - return "open" + return apiMilestone } // NewMilestone creates new milestone of repository. -- cgit v1.2.3