diff options
author | kolaente <konrad@kola-entertainments.de> | 2018-05-09 18:29:04 +0200 |
---|---|---|
committer | Lauris BH <lauris@nix.lv> | 2018-05-09 19:29:04 +0300 |
commit | 95f2e2b57beedcdeb2b9623dc86e26f252fdd7bd (patch) | |
tree | ebfeb5b898f72fc57ac1fe9680374fdb4bc91aeb /models | |
parent | 238a997ec0b5d3e3bed0e55edcacebb44f86d373 (diff) | |
download | gitea-95f2e2b57beedcdeb2b9623dc86e26f252fdd7bd.tar.gz gitea-95f2e2b57beedcdeb2b9623dc86e26f252fdd7bd.zip |
Multiple assignees (#3705)
Diffstat (limited to 'models')
-rw-r--r-- | models/error.go | 16 | ||||
-rw-r--r-- | models/fixtures/issue.yml | 2 | ||||
-rw-r--r-- | models/fixtures/issue_assignees.yml | 8 | ||||
-rw-r--r-- | models/fixtures/issue_user.yml | 3 | ||||
-rw-r--r-- | models/issue.go | 159 | ||||
-rw-r--r-- | models/issue_assignees.go | 263 | ||||
-rw-r--r-- | models/issue_assignees_test.go | 71 | ||||
-rw-r--r-- | models/issue_comment.go | 112 | ||||
-rw-r--r-- | models/issue_list.go | 46 | ||||
-rw-r--r-- | models/issue_mail.go | 13 | ||||
-rw-r--r-- | models/issue_user.go | 52 | ||||
-rw-r--r-- | models/issue_user_test.go | 17 | ||||
-rw-r--r-- | models/migrations/migrations.go | 13 | ||||
-rw-r--r-- | models/migrations/v56.go | 12 | ||||
-rw-r--r-- | models/migrations/v64.go | 129 | ||||
-rw-r--r-- | models/models.go | 1 | ||||
-rw-r--r-- | models/pull.go | 7 | ||||
-rw-r--r-- | models/repo.go | 6 | ||||
-rw-r--r-- | models/user.go | 6 | ||||
-rw-r--r-- | models/webhook_dingtalk.go | 6 | ||||
-rw-r--r-- | models/webhook_discord.go | 6 | ||||
-rw-r--r-- | models/webhook_slack.go | 6 |
22 files changed, 699 insertions, 255 deletions
diff --git a/models/error.go b/models/error.go index d24da2642e..cdb18d23ce 100644 --- a/models/error.go +++ b/models/error.go @@ -733,6 +733,22 @@ func (err ErrRepoFileAlreadyExist) Error() string { return fmt.Sprintf("repository file already exists [file_name: %s]", err.FileName) } +// ErrUserDoesNotHaveAccessToRepo represets an error where the user doesn't has access to a given repo +type ErrUserDoesNotHaveAccessToRepo struct { + UserID int64 + RepoName string +} + +// IsErrUserDoesNotHaveAccessToRepo checks if an error is a ErrRepoFileAlreadyExist. +func IsErrUserDoesNotHaveAccessToRepo(err error) bool { + _, ok := err.(ErrUserDoesNotHaveAccessToRepo) + return ok +} + +func (err ErrUserDoesNotHaveAccessToRepo) Error() string { + return fmt.Sprintf("user doesn't have acces to repo [user_id: %d, repo_name: %s]", err.UserID, err.RepoName) +} + // __________ .__ // \______ \____________ ____ ____ | |__ // | | _/\_ __ \__ \ / \_/ ___\| | \ diff --git a/models/fixtures/issue.yml b/models/fixtures/issue.yml index ff514e706c..4de8c4fa7e 100644 --- a/models/fixtures/issue.yml +++ b/models/fixtures/issue.yml @@ -3,7 +3,6 @@ repo_id: 1 index: 1 poster_id: 1 - assignee_id: 1 name: issue1 content: content for the first issue is_closed: false @@ -67,7 +66,6 @@ repo_id: 3 index: 1 poster_id: 1 - assignee_id: 1 name: issue6 content: content6 is_closed: false diff --git a/models/fixtures/issue_assignees.yml b/models/fixtures/issue_assignees.yml new file mode 100644 index 0000000000..1be07df7d4 --- /dev/null +++ b/models/fixtures/issue_assignees.yml @@ -0,0 +1,8 @@ +- + id: 1 + assignee_id: 1 + issue_id: 1 +- + id: 2 + assignee_id: 1 + issue_id: 6 diff --git a/models/fixtures/issue_user.yml b/models/fixtures/issue_user.yml index b3f98a71d3..8039b1e40f 100644 --- a/models/fixtures/issue_user.yml +++ b/models/fixtures/issue_user.yml @@ -3,7 +3,6 @@ uid: 1 issue_id: 1 is_read: true - is_assigned: true is_mentioned: false - @@ -11,7 +10,6 @@ uid: 2 issue_id: 1 is_read: true - is_assigned: false is_mentioned: false - @@ -19,5 +17,4 @@ uid: 4 issue_id: 1 is_read: false - is_assigned: false is_mentioned: false diff --git a/models/issue.go b/models/issue.go index 7f83d59842..ad354cc34e 100644 --- a/models/issue.go +++ b/models/issue.go @@ -37,7 +37,7 @@ type Issue struct { MilestoneID int64 `xorm:"INDEX"` Milestone *Milestone `xorm:"-"` Priority int - AssigneeID int64 `xorm:"INDEX"` + AssigneeID int64 `xorm:"-"` Assignee *User `xorm:"-"` IsClosed bool `xorm:"INDEX"` IsRead bool `xorm:"-"` @@ -56,6 +56,7 @@ type Issue struct { Comments []*Comment `xorm:"-"` Reactions ReactionList `xorm:"-"` TotalTrackedTime int64 `xorm:"-"` + Assignees []*User `xorm:"-"` } var ( @@ -140,22 +141,6 @@ func (issue *Issue) loadPoster(e Engine) (err error) { return } -func (issue *Issue) loadAssignee(e Engine) (err error) { - if issue.Assignee == nil && issue.AssigneeID > 0 { - issue.Assignee, err = getUserByID(e, issue.AssigneeID) - if err != nil { - issue.AssigneeID = -1 - issue.Assignee = NewGhostUser() - if !IsErrUserNotExist(err) { - return fmt.Errorf("getUserByID.(assignee) [%d]: %v", issue.AssigneeID, err) - } - err = nil - return - } - } - return -} - func (issue *Issue) loadPullRequest(e Engine) (err error) { if issue.IsPull && issue.PullRequest == nil { issue.PullRequest, err = getPullRequestByIssueID(e, issue.ID) @@ -231,7 +216,7 @@ func (issue *Issue) loadAttributes(e Engine) (err error) { } } - if err = issue.loadAssignee(e); err != nil { + if err = issue.loadAssignees(e); err != nil { return } @@ -343,8 +328,11 @@ func (issue *Issue) APIFormat() *api.Issue { if issue.Milestone != nil { apiIssue.Milestone = issue.Milestone.APIFormat() } - if issue.Assignee != nil { - apiIssue.Assignee = issue.Assignee.APIFormat() + if len(issue.Assignees) > 0 { + for _, assignee := range issue.Assignees { + apiIssue.Assignees = append(apiIssue.Assignees, assignee.APIFormat()) + } + apiIssue.Assignee = issue.Assignees[0].APIFormat() // For compatibility, we're keeping the first assignee as `apiIssue.Assignee` } if issue.IsPull { apiIssue.PullRequest = &api.PullRequestMeta{ @@ -605,19 +593,6 @@ func (issue *Issue) ReplaceLabels(labels []*Label, doer *User) (err error) { return sess.Commit() } -// GetAssignee sets the Assignee attribute of this issue. -func (issue *Issue) GetAssignee() (err error) { - if issue.AssigneeID == 0 || issue.Assignee != nil { - return nil - } - - issue.Assignee, err = GetUserByID(issue.AssigneeID) - if IsErrUserNotExist(err) { - return nil - } - return err -} - // ReadBy sets issue to be read by given user. func (issue *Issue) ReadBy(userID int64) error { if err := UpdateIssueUserByRead(userID, issue.ID); err != nil { @@ -823,55 +798,6 @@ func (issue *Issue) ChangeContent(doer *User, content string) (err error) { return nil } -// ChangeAssignee changes the Assignee field of this issue. -func (issue *Issue) ChangeAssignee(doer *User, assigneeID int64) (err error) { - var oldAssigneeID = issue.AssigneeID - issue.AssigneeID = assigneeID - if err = UpdateIssueUserByAssignee(issue); err != nil { - return fmt.Errorf("UpdateIssueUserByAssignee: %v", err) - } - - sess := x.NewSession() - defer sess.Close() - - if err = issue.loadRepo(sess); err != nil { - return fmt.Errorf("loadRepo: %v", err) - } - - if _, err = createAssigneeComment(sess, doer, issue.Repo, issue, oldAssigneeID, assigneeID); err != nil { - return fmt.Errorf("createAssigneeComment: %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 - } - - // 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(AccessModeNone), - Sender: doer.APIFormat(), - } - if isRemoveAssignee { - apiPullRequest.Action = api.HookIssueUnassigned - } else { - apiPullRequest.Action = api.HookIssueAssigned - } - if err := PrepareWebhooks(issue.Repo, HookEventPullRequest, apiPullRequest); err != nil { - log.Error(4, "PrepareWebhooks [is_pull: %v, remove_assignee: %v]: %v", issue.IsPull, isRemoveAssignee, err) - return nil - } - } - go HookQueue.Add(issue.RepoID) - return nil -} - // GetTasks returns the amount of tasks in the issues content func (issue *Issue) GetTasks() int { return len(issueTasksPat.FindAllStringIndex(issue.Content, -1)) @@ -887,6 +813,7 @@ type NewIssueOptions struct { Repo *Repository Issue *Issue LabelIDs []int64 + AssigneeIDs []int64 Attachments []string // In UUID format. IsPull bool } @@ -909,14 +836,32 @@ func newIssue(e *xorm.Session, doer *User, opts NewIssueOptions) (err error) { } } - if assigneeID := opts.Issue.AssigneeID; assigneeID > 0 { - valid, err := hasAccess(e, assigneeID, opts.Repo, AccessModeWrite) - if err != nil { - return fmt.Errorf("hasAccess [user_id: %d, repo_id: %d]: %v", assigneeID, opts.Repo.ID, err) + // Keep the old assignee id thingy for compatibility reasons + if opts.Issue.AssigneeID > 0 { + isAdded := false + // Check if the user has already been passed to issue.AssigneeIDs, if not, add it + for _, aID := range opts.AssigneeIDs { + if aID == opts.Issue.AssigneeID { + isAdded = true + break + } } - if !valid { - opts.Issue.AssigneeID = 0 - opts.Issue.Assignee = nil + + if !isAdded { + opts.AssigneeIDs = append(opts.AssigneeIDs, opts.Issue.AssigneeID) + } + } + + // Check for and validate assignees + if len(opts.AssigneeIDs) > 0 { + for _, assigneeID := range opts.AssigneeIDs { + valid, err := hasAccess(e, assigneeID, opts.Repo, AccessModeWrite) + if err != nil { + return fmt.Errorf("hasAccess [user_id: %d, repo_id: %d]: %v", assigneeID, opts.Repo.ID, err) + } + if !valid { + return ErrUserDoesNotHaveAccessToRepo{UserID: assigneeID, RepoName: opts.Repo.Name} + } } } @@ -931,11 +876,10 @@ func newIssue(e *xorm.Session, doer *User, opts NewIssueOptions) (err error) { } } - if opts.Issue.AssigneeID > 0 { - if err = opts.Issue.loadRepo(e); err != nil { - return err - } - if _, err = createAssigneeComment(e, doer, opts.Issue.Repo, opts.Issue, -1, opts.Issue.AssigneeID); err != nil { + // Insert the assignees + for _, assigneeID := range opts.AssigneeIDs { + err = opts.Issue.changeAssignee(e, doer, assigneeID) + if err != nil { return err } } @@ -995,7 +939,7 @@ func newIssue(e *xorm.Session, doer *User, opts NewIssueOptions) (err error) { } // NewIssue creates new issue with labels for repository. -func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, uuids []string) (err error) { +func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, assigneeIDs []int64, uuids []string) (err error) { sess := x.NewSession() defer sess.Close() if err = sess.Begin(); err != nil { @@ -1007,7 +951,11 @@ func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, uuids []string) Issue: issue, LabelIDs: labelIDs, Attachments: uuids, + AssigneeIDs: assigneeIDs, }); err != nil { + if IsErrUserDoesNotHaveAccessToRepo(err) { + return err + } return fmt.Errorf("newIssue: %v", err) } @@ -1150,7 +1098,8 @@ func (opts *IssuesOptions) setupSession(sess *xorm.Session) error { } if opts.AssigneeID > 0 { - sess.And("issue.assignee_id=?", opts.AssigneeID) + sess.Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id"). + And("issue_assignees.assignee_id = ?", opts.AssigneeID) } if opts.PosterID > 0 { @@ -1372,7 +1321,8 @@ func GetIssueStats(opts *IssueStatsOptions) (*IssueStats, error) { } if opts.AssigneeID > 0 { - sess.And("issue.assignee_id = ?", opts.AssigneeID) + sess.Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id"). + And("issue_assignees.assignee_id = ?", opts.AssigneeID) } if opts.PosterID > 0 { @@ -1438,13 +1388,15 @@ func GetUserIssueStats(opts UserIssueStatsOptions) (*IssueStats, error) { } case FilterModeAssign: stats.OpenCount, err = x.Where(cond).And("is_closed = ?", false). - And("assignee_id = ?", opts.UserID). + Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id"). + And("issue_assignees.assignee_id = ?", opts.UserID). Count(new(Issue)) if err != nil { return nil, err } stats.ClosedCount, err = x.Where(cond).And("is_closed = ?", true). - And("assignee_id = ?", opts.UserID). + Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id"). + And("issue_assignees.assignee_id = ?", opts.UserID). Count(new(Issue)) if err != nil { return nil, err @@ -1466,7 +1418,8 @@ func GetUserIssueStats(opts UserIssueStatsOptions) (*IssueStats, error) { cond = cond.And(builder.Eq{"issue.is_closed": opts.IsClosed}) stats.AssignCount, err = x.Where(cond). - And("assignee_id = ?", opts.UserID). + Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id"). + And("issue_assignees.assignee_id = ?", opts.UserID). Count(new(Issue)) if err != nil { return nil, err @@ -1505,8 +1458,10 @@ func GetRepoIssueStats(repoID, uid int64, filterMode int, isPull bool) (numOpen switch filterMode { case FilterModeAssign: - openCountSession.And("assignee_id = ?", uid) - closedCountSession.And("assignee_id = ?", uid) + openCountSession.Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id"). + And("issue_assignees.assignee_id = ?", uid) + closedCountSession.Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id"). + And("issue_assignees.assignee_id = ?", uid) case FilterModeCreate: openCountSession.And("poster_id = ?", uid) closedCountSession.And("poster_id = ?", uid) diff --git a/models/issue_assignees.go b/models/issue_assignees.go new file mode 100644 index 0000000000..3e68126a6c --- /dev/null +++ b/models/issue_assignees.go @@ -0,0 +1,263 @@ +// Copyright 2018 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package models + +import ( + "fmt" + + "code.gitea.io/gitea/modules/log" + + api "code.gitea.io/sdk/gitea" + "github.com/go-xorm/xorm" +) + +// IssueAssignees saves all issue assignees +type IssueAssignees struct { + ID int64 `xorm:"pk autoincr"` + AssigneeID int64 `xorm:"INDEX"` + IssueID int64 `xorm:"INDEX"` +} + +// This loads all assignees of an issue +func (issue *Issue) loadAssignees(e Engine) (err error) { + // Reset maybe preexisting assignees + issue.Assignees = []*User{} + + err = e.Table("`user`"). + Join("INNER", "issue_assignees", "assignee_id = `user`.id"). + Where("issue_assignees.issue_id = ?", issue.ID). + Find(&issue.Assignees) + + if err != nil { + return err + } + + // Check if we have at least one assignee and if yes put it in as `Assignee` + if len(issue.Assignees) > 0 { + issue.Assignee = issue.Assignees[0] + } + + return +} + +// GetAssigneesByIssue returns everyone assigned to that issue +func GetAssigneesByIssue(issue *Issue) (assignees []*User, err error) { + err = issue.loadAssignees(x) + if err != nil { + return assignees, err + } + + return issue.Assignees, nil +} + +// IsUserAssignedToIssue returns true when the user is assigned to the issue +func IsUserAssignedToIssue(issue *Issue, user *User) (isAssigned bool, err error) { + isAssigned, err = x.Exist(&IssueAssignees{IssueID: issue.ID, AssigneeID: user.ID}) + return +} + +// DeleteNotPassedAssignee deletes all assignees who aren't passed via the "assignees" array +func DeleteNotPassedAssignee(issue *Issue, doer *User, assignees []*User) (err error) { + var found bool + + for _, assignee := range issue.Assignees { + + found = false + for _, alreadyAssignee := range assignees { + if assignee.ID == alreadyAssignee.ID { + found = true + break + } + } + + if !found { + // This function also does comments and hooks, which is why we call it seperatly instead of directly removing the assignees here + if err := UpdateAssignee(issue, doer, assignee.ID); err != nil { + return err + } + } + } + + return nil +} + +// MakeAssigneeList concats a string with all names of the assignees. Useful for logs. +func MakeAssigneeList(issue *Issue) (assigneeList string, err error) { + err = issue.loadAssignees(x) + if err != nil { + return "", err + } + + for in, assignee := range issue.Assignees { + assigneeList += assignee.Name + + if len(issue.Assignees) > (in + 1) { + assigneeList += ", " + } + } + return +} + +// ClearAssigneeByUserID deletes all assignments of an user +func clearAssigneeByUserID(sess *xorm.Session, userID int64) (err error) { + _, err = sess.Delete(&IssueAssignees{AssigneeID: userID}) + return +} + +// AddAssigneeIfNotAssigned adds an assignee only if he isn't aleady assigned to the issue +func AddAssigneeIfNotAssigned(issue *Issue, doer *User, assigneeID int64) (err error) { + // Check if the user is already assigned + isAssigned, err := IsUserAssignedToIssue(issue, &User{ID: assigneeID}) + if err != nil { + return err + } + + if !isAssigned { + return issue.ChangeAssignee(doer, assigneeID) + } + return nil +} + +// UpdateAssignee deletes or adds an assignee to an issue +func UpdateAssignee(issue *Issue, doer *User, assigneeID int64) (err error) { + return issue.ChangeAssignee(doer, assigneeID) +} + +// ChangeAssignee changes the Assignee of this issue. +func (issue *Issue) ChangeAssignee(doer *User, assigneeID int64) (err error) { + sess := x.NewSession() + defer sess.Close() + + if err := sess.Begin(); err != nil { + return err + } + + if err := issue.changeAssignee(sess, doer, assigneeID); err != nil { + return err + } + + return sess.Commit() +} + +func (issue *Issue) changeAssignee(sess *xorm.Session, doer *User, assigneeID int64) (err error) { + + // Update the assignee + removed, err := updateIssueAssignee(sess, issue, assigneeID) + if err != nil { + return fmt.Errorf("UpdateIssueUserByAssignee: %v", err) + } + + // Repo infos + if err = issue.loadRepo(sess); err != nil { + return fmt.Errorf("loadRepo: %v", err) + } + + // Comment + if _, err = createAssigneeComment(sess, doer, issue.Repo, issue, assigneeID, removed); err != nil { + return fmt.Errorf("createAssigneeComment: %v", err) + } + + if issue.IsPull { + issue.PullRequest = &PullRequest{Issue: issue} + apiPullRequest := &api.PullRequestPayload{ + Index: issue.Index, + PullRequest: issue.PullRequest.APIFormat(), + Repository: issue.Repo.APIFormat(AccessModeNone), + Sender: doer.APIFormat(), + } + if removed { + apiPullRequest.Action = api.HookIssueUnassigned + } else { + apiPullRequest.Action = api.HookIssueAssigned + } + if err := PrepareWebhooks(issue.Repo, HookEventPullRequest, apiPullRequest); err != nil { + log.Error(4, "PrepareWebhooks [is_pull: %v, remove_assignee: %v]: %v", issue.IsPull, removed, err) + return nil + } + } + go HookQueue.Add(issue.RepoID) + return nil +} + +// UpdateAPIAssignee is a helper function to add or delete one or multiple issue assignee(s) +// Deleting is done the Github way (quote from their api documentation): +// https://developer.github.com/v3/issues/#edit-an-issue +// "assignees" (array): Logins for Users to assign to this issue. +// Pass one or more user logins to replace the set of assignees on this Issue. +// Send an empty array ([]) to clear all assignees from the Issue. +func UpdateAPIAssignee(issue *Issue, oneAssignee string, multipleAssignees []string, doer *User) (err error) { + var allNewAssignees []*User + + // Keep the old assignee thingy for compatibility reasons + if oneAssignee != "" { + // Prevent double adding assignees + var isDouble bool + for _, assignee := range multipleAssignees { + if assignee == oneAssignee { + isDouble = true + break + } + } + + if !isDouble { + multipleAssignees = append(multipleAssignees, oneAssignee) + } + } + + // Loop through all assignees to add them + for _, assigneeName := range multipleAssignees { + assignee, err := GetUserByName(assigneeName) + if err != nil { + return err + } + + allNewAssignees = append(allNewAssignees, assignee) + } + + // Delete all old assignees not passed + if err = DeleteNotPassedAssignee(issue, doer, allNewAssignees); err != nil { + return err + } + + // Add all new assignees + // Update the assignee. The function will check if the user exists, is already + // assigned (which he shouldn't as we deleted all assignees before) and + // has access to the repo. + for _, assignee := range allNewAssignees { + // Extra method to prevent double adding (which would result in removing) + err = AddAssigneeIfNotAssigned(issue, doer, assignee.ID) + if err != nil { + return err + } + } + + return +} + +// MakeIDsFromAPIAssigneesToAdd returns an array with all assignee IDs +func MakeIDsFromAPIAssigneesToAdd(oneAssignee string, multipleAssignees []string) (assigneeIDs []int64, err error) { + + // Keeping the old assigning method for compatibility reasons + if oneAssignee != "" { + + // Prevent double adding assignees + var isDouble bool + for _, assignee := range multipleAssignees { + if assignee == oneAssignee { + isDouble = true + break + } + } + + if !isDouble { + multipleAssignees = append(multipleAssignees, oneAssignee) + } + } + + // Get the IDs of all assignees + assigneeIDs = GetUserIDsByNames(multipleAssignees) + + return +} diff --git a/models/issue_assignees_test.go b/models/issue_assignees_test.go new file mode 100644 index 0000000000..3247812198 --- /dev/null +++ b/models/issue_assignees_test.go @@ -0,0 +1,71 @@ +// Copyright 2018 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package models + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestUpdateAssignee(t *testing.T) { + assert.NoError(t, PrepareTestDatabase()) + + // Fake issue with assignees + issue, err := GetIssueByID(1) + assert.NoError(t, err) + + // Assign multiple users + user2, err := GetUserByID(2) + assert.NoError(t, err) + err = UpdateAssignee(issue, &User{ID: 1}, user2.ID) + assert.NoError(t, err) + + user3, err := GetUserByID(3) + assert.NoError(t, err) + err = UpdateAssignee(issue, &User{ID: 1}, user3.ID) + assert.NoError(t, err) + + user1, err := GetUserByID(1) // This user is already assigned (see the definition in fixtures), so running UpdateAssignee should unassign him + assert.NoError(t, err) + err = UpdateAssignee(issue, &User{ID: 1}, user1.ID) + assert.NoError(t, err) + + // Check if he got removed + isAssigned, err := IsUserAssignedToIssue(issue, user1) + assert.NoError(t, err) + assert.False(t, isAssigned) + + // Check if they're all there + assignees, err := GetAssigneesByIssue(issue) + assert.NoError(t, err) + + var expectedAssignees []*User + expectedAssignees = append(expectedAssignees, user2) + expectedAssignees = append(expectedAssignees, user3) + + for in, assignee := range assignees { + assert.Equal(t, assignee.ID, expectedAssignees[in].ID) + } + + // Check if the user is assigned + isAssigned, err = IsUserAssignedToIssue(issue, user2) + assert.NoError(t, err) + assert.True(t, isAssigned) + + // This user should not be assigned + isAssigned, err = IsUserAssignedToIssue(issue, &User{ID: 4}) + assert.NoError(t, err) + assert.False(t, isAssigned) + + // Clean everyone + err = DeleteNotPassedAssignee(issue, user1, []*User{}) + assert.NoError(t, err) + + // Check they're gone + assignees, err = GetAssigneesByIssue(issue) + assert.NoError(t, err) + assert.Equal(t, 0, len(assignees)) +} diff --git a/models/issue_comment.go b/models/issue_comment.go index aedb124863..2c5875c29c 100644 --- a/models/issue_comment.go +++ b/models/issue_comment.go @@ -81,23 +81,22 @@ const ( // Comment represents a comment in commit and issue page. type Comment struct { - ID int64 `xorm:"pk autoincr"` - Type CommentType - PosterID int64 `xorm:"INDEX"` - Poster *User `xorm:"-"` - IssueID int64 `xorm:"INDEX"` - LabelID int64 - Label *Label `xorm:"-"` - OldMilestoneID int64 - MilestoneID int64 - OldMilestone *Milestone `xorm:"-"` - Milestone *Milestone `xorm:"-"` - OldAssigneeID int64 - AssigneeID int64 - Assignee *User `xorm:"-"` - OldAssignee *User `xorm:"-"` - OldTitle string - NewTitle string + ID int64 `xorm:"pk autoincr"` + Type CommentType + PosterID int64 `xorm:"INDEX"` + Poster *User `xorm:"-"` + IssueID int64 `xorm:"INDEX"` + LabelID int64 + Label *Label `xorm:"-"` + OldMilestoneID int64 + MilestoneID int64 + OldMilestone *Milestone `xorm:"-"` + Milestone *Milestone `xorm:"-"` + AssigneeID int64 + RemovedAssignee bool + Assignee *User `xorm:"-"` + OldTitle string + NewTitle string CommitID int64 Line int64 @@ -247,18 +246,9 @@ func (c *Comment) LoadMilestone() error { return nil } -// LoadAssignees if comment.Type is CommentTypeAssignees, then load assignees -func (c *Comment) LoadAssignees() error { +// LoadAssigneeUser if comment.Type is CommentTypeAssignees, then load assignees +func (c *Comment) LoadAssigneeUser() error { var err error - if c.OldAssigneeID > 0 { - c.OldAssignee, err = getUserByID(x, c.OldAssigneeID) - if err != nil { - if !IsErrUserNotExist(err) { - return err - } - c.OldAssignee = NewGhostUser() - } - } if c.AssigneeID > 0 { c.Assignee, err = getUserByID(x, c.AssigneeID) @@ -324,21 +314,21 @@ func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err LabelID = opts.Label.ID } comment := &Comment{ - Type: opts.Type, - PosterID: opts.Doer.ID, - Poster: opts.Doer, - IssueID: opts.Issue.ID, - LabelID: LabelID, - OldMilestoneID: opts.OldMilestoneID, - MilestoneID: opts.MilestoneID, - OldAssigneeID: opts.OldAssigneeID, - AssigneeID: opts.AssigneeID, - CommitID: opts.CommitID, - CommitSHA: opts.CommitSHA, - Line: opts.LineNum, - Content: opts.Content, - OldTitle: opts.OldTitle, - NewTitle: opts.NewTitle, + Type: opts.Type, + PosterID: opts.Doer.ID, + Poster: opts.Doer, + IssueID: opts.Issue.ID, + LabelID: LabelID, + OldMilestoneID: opts.OldMilestoneID, + MilestoneID: opts.MilestoneID, + RemovedAssignee: opts.RemovedAssignee, + AssigneeID: opts.AssigneeID, + CommitID: opts.CommitID, + CommitSHA: opts.CommitSHA, + Line: opts.LineNum, + Content: opts.Content, + OldTitle: opts.OldTitle, + NewTitle: opts.NewTitle, } if _, err = e.Insert(comment); err != nil { return nil, err @@ -480,14 +470,14 @@ func createMilestoneComment(e *xorm.Session, doer *User, repo *Repository, issue }) } -func createAssigneeComment(e *xorm.Session, doer *User, repo *Repository, issue *Issue, oldAssigneeID, assigneeID int64) (*Comment, error) { +func createAssigneeComment(e *xorm.Session, doer *User, repo *Repository, issue *Issue, assigneeID int64, removedAssignee bool) (*Comment, error) { return createComment(e, &CreateCommentOptions{ - Type: CommentTypeAssignees, - Doer: doer, - Repo: repo, - Issue: issue, - OldAssigneeID: oldAssigneeID, - AssigneeID: assigneeID, + Type: CommentTypeAssignees, + Doer: doer, + Repo: repo, + Issue: issue, + RemovedAssignee: removedAssignee, + AssigneeID: assigneeID, }) } @@ -548,17 +538,17 @@ type CreateCommentOptions struct { Issue *Issue Label *Label - OldMilestoneID int64 - MilestoneID int64 - OldAssigneeID int64 - AssigneeID int64 - OldTitle string - NewTitle string - CommitID int64 - CommitSHA string - LineNum int64 - Content string - Attachments []string // UUIDs of attachments + OldMilestoneID int64 + MilestoneID int64 + AssigneeID int64 + RemovedAssignee bool + OldTitle string + NewTitle string + CommitID int64 + CommitSHA string + LineNum int64 + Content string + Attachments []string // UUIDs of attachments } // CreateComment creates comment of issue or commit. diff --git a/models/issue_list.go b/models/issue_list.go index 01a1a15f44..05130a6eef 100644 --- a/models/issue_list.go +++ b/models/issue_list.go @@ -154,38 +154,38 @@ func (issues IssueList) loadMilestones(e Engine) error { return nil } -func (issues IssueList) getAssigneeIDs() []int64 { - var ids = make(map[int64]struct{}, len(issues)) - for _, issue := range issues { - if _, ok := ids[issue.AssigneeID]; !ok { - ids[issue.AssigneeID] = struct{}{} - } - } - return keysInt64(ids) -} - func (issues IssueList) loadAssignees(e Engine) error { - assigneeIDs := issues.getAssigneeIDs() - if len(assigneeIDs) == 0 { + if len(issues) == 0 { return nil } - assigneeMaps := make(map[int64]*User, len(assigneeIDs)) - err := e. - In("id", assigneeIDs). - Find(&assigneeMaps) + type AssigneeIssue struct { + IssueAssignee *IssueAssignees `xorm:"extends"` + Assignee *User `xorm:"extends"` + } + + var assignees = make(map[int64][]*User, len(issues)) + rows, err := e.Table("issue_assignees"). + Join("INNER", "user", "`user`.id = `issue_assignees`.assignee_id"). + In("`issue_assignees`.issue_id", issues.getIssueIDs()). + Rows(new(AssigneeIssue)) if err != nil { return err } + defer rows.Close() - for _, issue := range issues { - if issue.AssigneeID <= 0 { - continue - } - var ok bool - if issue.Assignee, ok = assigneeMaps[issue.AssigneeID]; !ok { - issue.Assignee = NewGhostUser() + for rows.Next() { + var assigneeIssue AssigneeIssue + err = rows.Scan(&assigneeIssue) + if err != nil { + return err } + + assignees[assigneeIssue.IssueAssignee.IssueID] = append(assignees[assigneeIssue.IssueAssignee.IssueID], assigneeIssue.Assignee) + } + + for _, issue := range issues { + issue.Assignees = assignees[issue.ID] } return nil } diff --git a/models/issue_mail.go b/models/issue_mail.go index 08e4eed584..179bb6527b 100644 --- a/models/issue_mail.go +++ b/models/issue_mail.go @@ -46,9 +46,16 @@ func mailIssueCommentToParticipants(e Engine, issue *Issue, doer *User, content participants = append(participants, issue.Poster) } - // Assignee must receive any communications - if issue.Assignee != nil && issue.AssigneeID > 0 && issue.AssigneeID != doer.ID { - participants = append(participants, issue.Assignee) + // Assignees must receive any communications + assignees, err := GetAssigneesByIssue(issue) + if err != nil { + return err + } + + for _, assignee := range assignees { + if assignee.ID != doer.ID { + participants = append(participants, assignee) + } } tos := make([]string, 0, len(watchers)) // List of email addresses. diff --git a/models/issue_user.go b/models/issue_user.go index 1aded0b440..de5a185aec 100644 --- a/models/issue_user.go +++ b/models/issue_user.go @@ -6,6 +6,8 @@ package models import ( "fmt" + + "github.com/go-xorm/xorm" ) // IssueUser represents an issue-user relation. @@ -14,7 +16,6 @@ type IssueUser struct { UID int64 `xorm:"INDEX"` // User ID. IssueID int64 IsRead bool - IsAssigned bool IsMentioned bool } @@ -32,9 +33,8 @@ func newIssueUsers(e Engine, repo *Repository, issue *Issue) error { issueUsers := make([]*IssueUser, 0, len(assignees)+1) for _, assignee := range assignees { issueUsers = append(issueUsers, &IssueUser{ - IssueID: issue.ID, - UID: assignee.ID, - IsAssigned: assignee.ID == issue.AssigneeID, + IssueID: issue.ID, + UID: assignee.ID, }) isPosterAssignee = isPosterAssignee || assignee.ID == issue.PosterID } @@ -51,34 +51,38 @@ func newIssueUsers(e Engine, repo *Repository, issue *Issue) error { return nil } -func updateIssueUserByAssignee(e Engine, issue *Issue) (err error) { - if _, err = e.Exec("UPDATE `issue_user` SET is_assigned = ? WHERE issue_id = ?", false, issue.ID); err != nil { - return err +func updateIssueAssignee(e *xorm.Session, issue *Issue, assigneeID int64) (removed bool, err error) { + + // Check if the user exists + _, err = GetUserByID(assigneeID) + if err != nil { + return false, 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 { - return err + // Check if the submitted user is already assigne, if yes delete him otherwise add him + var toBeDeleted bool + for _, assignee := range issue.Assignees { + if assignee.ID == assigneeID { + toBeDeleted = true + break } } - return updateIssue(e, issue) -} + assigneeIn := IssueAssignees{AssigneeID: assigneeID, IssueID: issue.ID} -// UpdateIssueUserByAssignee updates issue-user relation for assignee. -func UpdateIssueUserByAssignee(issue *Issue) (err error) { - sess := x.NewSession() - defer sess.Close() - if err = sess.Begin(); err != nil { - return err - } - - if err = updateIssueUserByAssignee(sess, issue); err != nil { - return err + if toBeDeleted { + _, err = e.Delete(assigneeIn) + if err != nil { + return toBeDeleted, err + } + } else { + _, err = e.Insert(assigneeIn) + if err != nil { + return toBeDeleted, err + } } - return sess.Commit() + return toBeDeleted, nil } // UpdateIssueUserByRead updates issue-user relation for reading. diff --git a/models/issue_user_test.go b/models/issue_user_test.go index e3ebfa8fa2..a333bc8619 100644 --- a/models/issue_user_test.go +++ b/models/issue_user_test.go @@ -32,23 +32,6 @@ func Test_newIssueUsers(t *testing.T) { AssertExistsAndLoadBean(t, &IssueUser{IssueID: newIssue.ID, UID: repo.OwnerID}) } -func TestUpdateIssueUserByAssignee(t *testing.T) { - assert.NoError(t, PrepareTestDatabase()) - issue := AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue) - - // artificially change assignee in issue_user table - AssertSuccessfulInsert(t, &IssueUser{IssueID: issue.ID, UID: 5, IsAssigned: true}) - _, err := x.Cols("is_assigned"). - Update(&IssueUser{IsAssigned: false}, &IssueUser{IssueID: issue.ID, UID: issue.AssigneeID}) - assert.NoError(t, err) - - assert.NoError(t, UpdateIssueUserByAssignee(issue)) - - // issue_user table should now be correct again - AssertExistsAndLoadBean(t, &IssueUser{IssueID: issue.ID, UID: issue.AssigneeID}, "is_assigned=1") - AssertExistsAndLoadBean(t, &IssueUser{IssueID: issue.ID, UID: 5}, "is_assigned=0") -} - func TestUpdateIssueUserByRead(t *testing.T) { assert.NoError(t, PrepareTestDatabase()) issue := AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue) diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index aa9dd13107..e85da8de79 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -180,6 +180,8 @@ var migrations = []Migration{ NewMigration("add last used passcode column for TOTP", addLastUsedPasscodeTOTP), // v63 -> v64 NewMigration("add language column for user setting", addLanguageSetting), + // v64 -> v65 + NewMigration("add multiple assignees", addMultipleAssignees), } // Migrate database to current version @@ -229,7 +231,7 @@ Please try to upgrade to a lower version (>= v0.6.0) first, then upgrade to curr return nil } -func dropTableColumns(x *xorm.Engine, tableName string, columnNames ...string) (err error) { +func dropTableColumns(sess *xorm.Session, tableName string, columnNames ...string) (err error) { if tableName == "" || len(columnNames) == 0 { return nil } @@ -245,17 +247,10 @@ func dropTableColumns(x *xorm.Engine, tableName string, columnNames ...string) ( } cols += "DROP COLUMN `" + col + "`" } - if _, err := x.Exec(fmt.Sprintf("ALTER TABLE `%s` %s", tableName, cols)); err != nil { + if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE `%s` %s", tableName, cols)); err != nil { return fmt.Errorf("Drop table `%s` columns %v: %v", tableName, columnNames, err) } case setting.UseMSSQL: - sess := x.NewSession() - defer sess.Close() - - if err = sess.Begin(); err != nil { - return err - } - cols := "" for _, col := range columnNames { if cols != "" { diff --git a/models/migrations/v56.go b/models/migrations/v56.go index 1f96cc543e..79f8ce0ba5 100644 --- a/models/migrations/v56.go +++ b/models/migrations/v56.go @@ -9,5 +9,15 @@ import ( ) func removeIsOwnerColumnFromOrgUser(x *xorm.Engine) (err error) { - return dropTableColumns(x, "org_user", "is_owner", "num_teams") + sess := x.NewSession() + defer sess.Close() + + if err = sess.Begin(); err != nil { + return err + } + + if err := dropTableColumns(sess, "org_user", "is_owner", "num_teams"); err != nil { + return err + } + return sess.Commit() } diff --git a/models/migrations/v64.go b/models/migrations/v64.go new file mode 100644 index 0000000000..a281ac67e4 --- /dev/null +++ b/models/migrations/v64.go @@ -0,0 +1,129 @@ +// Copyright 2018 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package migrations + +import ( + "code.gitea.io/gitea/modules/util" + + "github.com/go-xorm/xorm" +) + +func addMultipleAssignees(x *xorm.Engine) error { + + // Redeclare issue struct + 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. + PosterID int64 `xorm:"INDEX"` + Title string `xorm:"name"` + Content string `xorm:"TEXT"` + MilestoneID int64 `xorm:"INDEX"` + Priority int + AssigneeID int64 `xorm:"INDEX"` + IsClosed bool `xorm:"INDEX"` + IsPull bool `xorm:"INDEX"` // Indicates whether is a pull request or not. + NumComments int + Ref string + + DeadlineUnix util.TimeStamp `xorm:"INDEX"` + CreatedUnix util.TimeStamp `xorm:"INDEX created"` + UpdatedUnix util.TimeStamp `xorm:"INDEX updated"` + ClosedUnix util.TimeStamp `xorm:"INDEX"` + } + + allIssues := []Issue{} + err := x.Find(&allIssues) + if err != nil { + return err + } + + // Create the table + type IssueAssignees struct { + ID int64 `xorm:"pk autoincr"` + AssigneeID int64 `xorm:"INDEX"` + IssueID int64 `xorm:"INDEX"` + } + err = x.Sync2(IssueAssignees{}) + if err != nil { + return err + } + + // Range over all issues and insert a new entry for each issue/assignee + sess := x.NewSession() + defer sess.Close() + + err = sess.Begin() + if err != nil { + return err + } + + for _, issue := range allIssues { + if issue.AssigneeID != 0 { + _, err := sess.Insert(IssueAssignees{IssueID: issue.ID, AssigneeID: issue.AssigneeID}) + if err != nil { + sess.Rollback() + return err + } + } + } + + // Updated the comment table + type Comment struct { + ID int64 `xorm:"pk autoincr"` + Type int + PosterID int64 `xorm:"INDEX"` + IssueID int64 `xorm:"INDEX"` + LabelID int64 + OldMilestoneID int64 + MilestoneID int64 + OldAssigneeID int64 + AssigneeID int64 + RemovedAssignee bool + OldTitle string + NewTitle string + + CommitID int64 + Line int64 + Content string `xorm:"TEXT"` + RenderedContent string `xorm:"-"` + + CreatedUnix util.TimeStamp `xorm:"INDEX created"` + UpdatedUnix util.TimeStamp `xorm:"INDEX updated"` + + // Reference issue in commit message + CommitSHA string `xorm:"VARCHAR(40)"` + } + if err := x.Sync2(Comment{}); err != nil { + return err + } + + // Migrate comments + // First update everything to not have nulls in db + if _, err := sess.Where("type = ?", 9).Cols("removed_assignee").Update(Comment{RemovedAssignee: false}); err != nil { + return err + } + + allAssignementComments := []Comment{} + if err := sess.Where("type = ?", 9).Find(&allAssignementComments); err != nil { + return err + } + + for _, comment := range allAssignementComments { + // Everytime where OldAssigneeID is > 0, the assignement was removed. + if comment.OldAssigneeID > 0 { + _, err = sess.ID(comment.ID).Update(Comment{RemovedAssignee: true}) + } + } + + if err := dropTableColumns(sess, "issue", "assignee_id"); err != nil { + return err + } + + if err := dropTableColumns(sess, "issue_user", "is_assigned"); err != nil { + return err + } + return sess.Commit() +} diff --git a/models/models.go b/models/models.go index 549d2eadc9..9213cd3b79 100644 --- a/models/models.go +++ b/models/models.go @@ -119,6 +119,7 @@ func init() { new(RepoIndexerStatus), new(LFSLock), new(Reaction), + new(IssueAssignees), ) gonicNames := []string{"SSL", "UID"} diff --git a/models/pull.go b/models/pull.go index 5f4a6e2054..d23d885b6c 100644 --- a/models/pull.go +++ b/models/pull.go @@ -198,6 +198,7 @@ func (pr *PullRequest) APIFormat() *api.PullRequest { Labels: apiIssue.Labels, Milestone: apiIssue.Milestone, Assignee: apiIssue.Assignee, + Assignees: apiIssue.Assignees, State: apiIssue.State, Comments: apiIssue.Comments, HTMLURL: pr.Issue.HTMLURL(), @@ -719,7 +720,7 @@ func (pr *PullRequest) testPatch() (err error) { } // NewPullRequest creates new pull request with labels for repository. -func NewPullRequest(repo *Repository, pull *Issue, labelIDs []int64, uuids []string, pr *PullRequest, patch []byte) (err error) { +func NewPullRequest(repo *Repository, pull *Issue, labelIDs []int64, uuids []string, pr *PullRequest, patch []byte, assigneeIDs []int64) (err error) { sess := x.NewSession() defer sess.Close() if err = sess.Begin(); err != nil { @@ -732,7 +733,11 @@ func NewPullRequest(repo *Repository, pull *Issue, labelIDs []int64, uuids []str LabelIDs: labelIDs, Attachments: uuids, IsPull: true, + AssigneeIDs: assigneeIDs, }); err != nil { + if IsErrUserDoesNotHaveAccessToRepo(err) { + return err + } return fmt.Errorf("newIssue: %v", err) } diff --git a/models/repo.go b/models/repo.go index 43fb1e0c73..4a7eb859c4 100644 --- a/models/repo.go +++ b/models/repo.go @@ -600,9 +600,9 @@ func (repo *Repository) GetAssignees() (_ []*User, err error) { return repo.getAssignees(x) } -// GetAssigneeByID returns the user that has write access of repository by given ID. -func (repo *Repository) GetAssigneeByID(userID int64) (*User, error) { - return GetAssigneeByID(repo, userID) +// GetUserIfHasWriteAccess returns the user that has write access of repository by given ID. +func (repo *Repository) GetUserIfHasWriteAccess(userID int64) (*User, error) { + return GetUserIfHasWriteAccess(repo, userID) } // GetMilestoneByID returns the milestone belongs to repository by given ID. diff --git a/models/user.go b/models/user.go index 106d79ffcc..8f5ee6e5a7 100644 --- a/models/user.go +++ b/models/user.go @@ -993,7 +993,7 @@ func deleteUser(e *xorm.Session, u *User) error { // ***** END: PublicKey ***** // Clear assignee. - if _, err = e.Exec("UPDATE `issue` SET assignee_id=0 WHERE assignee_id=?", u.ID); err != nil { + if err = clearAssigneeByUserID(e, u.ID); err != nil { return fmt.Errorf("clear assignee: %v", err) } @@ -1110,8 +1110,8 @@ func GetUserByID(id int64) (*User, error) { return getUserByID(x, id) } -// GetAssigneeByID returns the user with write access of repository by given ID. -func GetAssigneeByID(repo *Repository, userID int64) (*User, error) { +// GetUserIfHasWriteAccess returns the user with write access of repository by given ID. +func GetUserIfHasWriteAccess(repo *Repository, userID int64) (*User, error) { has, err := HasAccess(userID, repo, AccessModeWrite) if err != nil { return nil, err diff --git a/models/webhook_dingtalk.go b/models/webhook_dingtalk.go index e25e989084..719ffcae73 100644 --- a/models/webhook_dingtalk.go +++ b/models/webhook_dingtalk.go @@ -118,8 +118,12 @@ func getDingtalkPullRequestPayload(p *api.PullRequestPayload) (*DingtalkPayload, title = fmt.Sprintf("[%s] Pull request edited: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) text = p.PullRequest.Body case api.HookIssueAssigned: + list, err := MakeAssigneeList(&Issue{ID: p.PullRequest.ID}) + if err != nil { + return &DingtalkPayload{}, err + } title = fmt.Sprintf("[%s] Pull request assigned to %s: #%d %s", p.Repository.FullName, - p.PullRequest.Assignee.UserName, p.Index, p.PullRequest.Title) + list, p.Index, p.PullRequest.Title) text = p.PullRequest.Body case api.HookIssueUnassigned: title = fmt.Sprintf("[%s] Pull request unassigned: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) diff --git a/models/webhook_discord.go b/models/webhook_discord.go index 6d39d8b993..40d9d58992 100644 --- a/models/webhook_discord.go +++ b/models/webhook_discord.go @@ -191,8 +191,12 @@ func getDiscordPullRequestPayload(p *api.PullRequestPayload, meta *DiscordMeta) text = p.PullRequest.Body color = warnColor case api.HookIssueAssigned: + list, err := MakeAssigneeList(&Issue{ID: p.PullRequest.ID}) + if err != nil { + return &DiscordPayload{}, err + } title = fmt.Sprintf("[%s] Pull request assigned to %s: #%d %s", p.Repository.FullName, - p.PullRequest.Assignee.UserName, p.Index, p.PullRequest.Title) + list, p.Index, p.PullRequest.Title) text = p.PullRequest.Body color = successColor case api.HookIssueUnassigned: diff --git a/models/webhook_slack.go b/models/webhook_slack.go index dd25a8d7df..256819adc5 100644 --- a/models/webhook_slack.go +++ b/models/webhook_slack.go @@ -172,8 +172,12 @@ func getSlackPullRequestPayload(p *api.PullRequestPayload, slack *SlackMeta) (*S text = fmt.Sprintf("[%s] Pull request edited: %s by %s", p.Repository.FullName, titleLink, senderLink) attachmentText = SlackTextFormatter(p.PullRequest.Body) case api.HookIssueAssigned: + list, err := MakeAssigneeList(&Issue{ID: p.PullRequest.ID}) + if err != nil { + return &SlackPayload{}, err + } text = fmt.Sprintf("[%s] Pull request assigned to %s: %s by %s", p.Repository.FullName, - SlackLinkFormatter(setting.AppURL+p.PullRequest.Assignee.UserName, p.PullRequest.Assignee.UserName), + SlackLinkFormatter(setting.AppURL+list, list), titleLink, senderLink) case api.HookIssueUnassigned: text = fmt.Sprintf("[%s] Pull request unassigned: %s by %s", p.Repository.FullName, titleLink, senderLink) |