Skip to content

Commit 95f2e2b

Browse files
kolaentelafriks
authored andcommitted
Multiple assignees (#3705)
1 parent 238a997 commit 95f2e2b

36 files changed

+1012
-451
lines changed

models/error.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -733,6 +733,22 @@ func (err ErrRepoFileAlreadyExist) Error() string {
733733
return fmt.Sprintf("repository file already exists [file_name: %s]", err.FileName)
734734
}
735735

736+
// ErrUserDoesNotHaveAccessToRepo represets an error where the user doesn't has access to a given repo
737+
type ErrUserDoesNotHaveAccessToRepo struct {
738+
UserID int64
739+
RepoName string
740+
}
741+
742+
// IsErrUserDoesNotHaveAccessToRepo checks if an error is a ErrRepoFileAlreadyExist.
743+
func IsErrUserDoesNotHaveAccessToRepo(err error) bool {
744+
_, ok := err.(ErrUserDoesNotHaveAccessToRepo)
745+
return ok
746+
}
747+
748+
func (err ErrUserDoesNotHaveAccessToRepo) Error() string {
749+
return fmt.Sprintf("user doesn't have acces to repo [user_id: %d, repo_name: %s]", err.UserID, err.RepoName)
750+
}
751+
736752
// __________ .__
737753
// \______ \____________ ____ ____ | |__
738754
// | | _/\_ __ \__ \ / \_/ ___\| | \

models/fixtures/issue.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
repo_id: 1
44
index: 1
55
poster_id: 1
6-
assignee_id: 1
76
name: issue1
87
content: content for the first issue
98
is_closed: false
@@ -67,7 +66,6 @@
6766
repo_id: 3
6867
index: 1
6968
poster_id: 1
70-
assignee_id: 1
7169
name: issue6
7270
content: content6
7371
is_closed: false

models/fixtures/issue_assignees.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
-
2+
id: 1
3+
assignee_id: 1
4+
issue_id: 1
5+
-
6+
id: 2
7+
assignee_id: 1
8+
issue_id: 6

models/fixtures/issue_user.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,18 @@
33
uid: 1
44
issue_id: 1
55
is_read: true
6-
is_assigned: true
76
is_mentioned: false
87

98
-
109
id: 2
1110
uid: 2
1211
issue_id: 1
1312
is_read: true
14-
is_assigned: false
1513
is_mentioned: false
1614

1715
-
1816
id: 3
1917
uid: 4
2018
issue_id: 1
2119
is_read: false
22-
is_assigned: false
2320
is_mentioned: false

models/issue.go

Lines changed: 57 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ type Issue struct {
3737
MilestoneID int64 `xorm:"INDEX"`
3838
Milestone *Milestone `xorm:"-"`
3939
Priority int
40-
AssigneeID int64 `xorm:"INDEX"`
40+
AssigneeID int64 `xorm:"-"`
4141
Assignee *User `xorm:"-"`
4242
IsClosed bool `xorm:"INDEX"`
4343
IsRead bool `xorm:"-"`
@@ -56,6 +56,7 @@ type Issue struct {
5656
Comments []*Comment `xorm:"-"`
5757
Reactions ReactionList `xorm:"-"`
5858
TotalTrackedTime int64 `xorm:"-"`
59+
Assignees []*User `xorm:"-"`
5960
}
6061

6162
var (
@@ -140,22 +141,6 @@ func (issue *Issue) loadPoster(e Engine) (err error) {
140141
return
141142
}
142143

143-
func (issue *Issue) loadAssignee(e Engine) (err error) {
144-
if issue.Assignee == nil && issue.AssigneeID > 0 {
145-
issue.Assignee, err = getUserByID(e, issue.AssigneeID)
146-
if err != nil {
147-
issue.AssigneeID = -1
148-
issue.Assignee = NewGhostUser()
149-
if !IsErrUserNotExist(err) {
150-
return fmt.Errorf("getUserByID.(assignee) [%d]: %v", issue.AssigneeID, err)
151-
}
152-
err = nil
153-
return
154-
}
155-
}
156-
return
157-
}
158-
159144
func (issue *Issue) loadPullRequest(e Engine) (err error) {
160145
if issue.IsPull && issue.PullRequest == nil {
161146
issue.PullRequest, err = getPullRequestByIssueID(e, issue.ID)
@@ -231,7 +216,7 @@ func (issue *Issue) loadAttributes(e Engine) (err error) {
231216
}
232217
}
233218

234-
if err = issue.loadAssignee(e); err != nil {
219+
if err = issue.loadAssignees(e); err != nil {
235220
return
236221
}
237222

@@ -343,8 +328,11 @@ func (issue *Issue) APIFormat() *api.Issue {
343328
if issue.Milestone != nil {
344329
apiIssue.Milestone = issue.Milestone.APIFormat()
345330
}
346-
if issue.Assignee != nil {
347-
apiIssue.Assignee = issue.Assignee.APIFormat()
331+
if len(issue.Assignees) > 0 {
332+
for _, assignee := range issue.Assignees {
333+
apiIssue.Assignees = append(apiIssue.Assignees, assignee.APIFormat())
334+
}
335+
apiIssue.Assignee = issue.Assignees[0].APIFormat() // For compatibility, we're keeping the first assignee as `apiIssue.Assignee`
348336
}
349337
if issue.IsPull {
350338
apiIssue.PullRequest = &api.PullRequestMeta{
@@ -605,19 +593,6 @@ func (issue *Issue) ReplaceLabels(labels []*Label, doer *User) (err error) {
605593
return sess.Commit()
606594
}
607595

608-
// GetAssignee sets the Assignee attribute of this issue.
609-
func (issue *Issue) GetAssignee() (err error) {
610-
if issue.AssigneeID == 0 || issue.Assignee != nil {
611-
return nil
612-
}
613-
614-
issue.Assignee, err = GetUserByID(issue.AssigneeID)
615-
if IsErrUserNotExist(err) {
616-
return nil
617-
}
618-
return err
619-
}
620-
621596
// ReadBy sets issue to be read by given user.
622597
func (issue *Issue) ReadBy(userID int64) error {
623598
if err := UpdateIssueUserByRead(userID, issue.ID); err != nil {
@@ -823,55 +798,6 @@ func (issue *Issue) ChangeContent(doer *User, content string) (err error) {
823798
return nil
824799
}
825800

826-
// ChangeAssignee changes the Assignee field of this issue.
827-
func (issue *Issue) ChangeAssignee(doer *User, assigneeID int64) (err error) {
828-
var oldAssigneeID = issue.AssigneeID
829-
issue.AssigneeID = assigneeID
830-
if err = UpdateIssueUserByAssignee(issue); err != nil {
831-
return fmt.Errorf("UpdateIssueUserByAssignee: %v", err)
832-
}
833-
834-
sess := x.NewSession()
835-
defer sess.Close()
836-
837-
if err = issue.loadRepo(sess); err != nil {
838-
return fmt.Errorf("loadRepo: %v", err)
839-
}
840-
841-
if _, err = createAssigneeComment(sess, doer, issue.Repo, issue, oldAssigneeID, assigneeID); err != nil {
842-
return fmt.Errorf("createAssigneeComment: %v", err)
843-
}
844-
845-
issue.Assignee, err = GetUserByID(issue.AssigneeID)
846-
if err != nil && !IsErrUserNotExist(err) {
847-
log.Error(4, "GetUserByID [assignee_id: %v]: %v", issue.AssigneeID, err)
848-
return nil
849-
}
850-
851-
// Error not nil here means user does not exist, which is remove assignee.
852-
isRemoveAssignee := err != nil
853-
if issue.IsPull {
854-
issue.PullRequest.Issue = issue
855-
apiPullRequest := &api.PullRequestPayload{
856-
Index: issue.Index,
857-
PullRequest: issue.PullRequest.APIFormat(),
858-
Repository: issue.Repo.APIFormat(AccessModeNone),
859-
Sender: doer.APIFormat(),
860-
}
861-
if isRemoveAssignee {
862-
apiPullRequest.Action = api.HookIssueUnassigned
863-
} else {
864-
apiPullRequest.Action = api.HookIssueAssigned
865-
}
866-
if err := PrepareWebhooks(issue.Repo, HookEventPullRequest, apiPullRequest); err != nil {
867-
log.Error(4, "PrepareWebhooks [is_pull: %v, remove_assignee: %v]: %v", issue.IsPull, isRemoveAssignee, err)
868-
return nil
869-
}
870-
}
871-
go HookQueue.Add(issue.RepoID)
872-
return nil
873-
}
874-
875801
// GetTasks returns the amount of tasks in the issues content
876802
func (issue *Issue) GetTasks() int {
877803
return len(issueTasksPat.FindAllStringIndex(issue.Content, -1))
@@ -887,6 +813,7 @@ type NewIssueOptions struct {
887813
Repo *Repository
888814
Issue *Issue
889815
LabelIDs []int64
816+
AssigneeIDs []int64
890817
Attachments []string // In UUID format.
891818
IsPull bool
892819
}
@@ -909,14 +836,32 @@ func newIssue(e *xorm.Session, doer *User, opts NewIssueOptions) (err error) {
909836
}
910837
}
911838

912-
if assigneeID := opts.Issue.AssigneeID; assigneeID > 0 {
913-
valid, err := hasAccess(e, assigneeID, opts.Repo, AccessModeWrite)
914-
if err != nil {
915-
return fmt.Errorf("hasAccess [user_id: %d, repo_id: %d]: %v", assigneeID, opts.Repo.ID, err)
839+
// Keep the old assignee id thingy for compatibility reasons
840+
if opts.Issue.AssigneeID > 0 {
841+
isAdded := false
842+
// Check if the user has already been passed to issue.AssigneeIDs, if not, add it
843+
for _, aID := range opts.AssigneeIDs {
844+
if aID == opts.Issue.AssigneeID {
845+
isAdded = true
846+
break
847+
}
916848
}
917-
if !valid {
918-
opts.Issue.AssigneeID = 0
919-
opts.Issue.Assignee = nil
849+
850+
if !isAdded {
851+
opts.AssigneeIDs = append(opts.AssigneeIDs, opts.Issue.AssigneeID)
852+
}
853+
}
854+
855+
// Check for and validate assignees
856+
if len(opts.AssigneeIDs) > 0 {
857+
for _, assigneeID := range opts.AssigneeIDs {
858+
valid, err := hasAccess(e, assigneeID, opts.Repo, AccessModeWrite)
859+
if err != nil {
860+
return fmt.Errorf("hasAccess [user_id: %d, repo_id: %d]: %v", assigneeID, opts.Repo.ID, err)
861+
}
862+
if !valid {
863+
return ErrUserDoesNotHaveAccessToRepo{UserID: assigneeID, RepoName: opts.Repo.Name}
864+
}
920865
}
921866
}
922867

@@ -931,11 +876,10 @@ func newIssue(e *xorm.Session, doer *User, opts NewIssueOptions) (err error) {
931876
}
932877
}
933878

934-
if opts.Issue.AssigneeID > 0 {
935-
if err = opts.Issue.loadRepo(e); err != nil {
936-
return err
937-
}
938-
if _, err = createAssigneeComment(e, doer, opts.Issue.Repo, opts.Issue, -1, opts.Issue.AssigneeID); err != nil {
879+
// Insert the assignees
880+
for _, assigneeID := range opts.AssigneeIDs {
881+
err = opts.Issue.changeAssignee(e, doer, assigneeID)
882+
if err != nil {
939883
return err
940884
}
941885
}
@@ -995,7 +939,7 @@ func newIssue(e *xorm.Session, doer *User, opts NewIssueOptions) (err error) {
995939
}
996940

997941
// NewIssue creates new issue with labels for repository.
998-
func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, uuids []string) (err error) {
942+
func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, assigneeIDs []int64, uuids []string) (err error) {
999943
sess := x.NewSession()
1000944
defer sess.Close()
1001945
if err = sess.Begin(); err != nil {
@@ -1007,7 +951,11 @@ func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, uuids []string)
1007951
Issue: issue,
1008952
LabelIDs: labelIDs,
1009953
Attachments: uuids,
954+
AssigneeIDs: assigneeIDs,
1010955
}); err != nil {
956+
if IsErrUserDoesNotHaveAccessToRepo(err) {
957+
return err
958+
}
1011959
return fmt.Errorf("newIssue: %v", err)
1012960
}
1013961

@@ -1150,7 +1098,8 @@ func (opts *IssuesOptions) setupSession(sess *xorm.Session) error {
11501098
}
11511099

11521100
if opts.AssigneeID > 0 {
1153-
sess.And("issue.assignee_id=?", opts.AssigneeID)
1101+
sess.Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id").
1102+
And("issue_assignees.assignee_id = ?", opts.AssigneeID)
11541103
}
11551104

11561105
if opts.PosterID > 0 {
@@ -1372,7 +1321,8 @@ func GetIssueStats(opts *IssueStatsOptions) (*IssueStats, error) {
13721321
}
13731322

13741323
if opts.AssigneeID > 0 {
1375-
sess.And("issue.assignee_id = ?", opts.AssigneeID)
1324+
sess.Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id").
1325+
And("issue_assignees.assignee_id = ?", opts.AssigneeID)
13761326
}
13771327

13781328
if opts.PosterID > 0 {
@@ -1438,13 +1388,15 @@ func GetUserIssueStats(opts UserIssueStatsOptions) (*IssueStats, error) {
14381388
}
14391389
case FilterModeAssign:
14401390
stats.OpenCount, err = x.Where(cond).And("is_closed = ?", false).
1441-
And("assignee_id = ?", opts.UserID).
1391+
Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id").
1392+
And("issue_assignees.assignee_id = ?", opts.UserID).
14421393
Count(new(Issue))
14431394
if err != nil {
14441395
return nil, err
14451396
}
14461397
stats.ClosedCount, err = x.Where(cond).And("is_closed = ?", true).
1447-
And("assignee_id = ?", opts.UserID).
1398+
Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id").
1399+
And("issue_assignees.assignee_id = ?", opts.UserID).
14481400
Count(new(Issue))
14491401
if err != nil {
14501402
return nil, err
@@ -1466,7 +1418,8 @@ func GetUserIssueStats(opts UserIssueStatsOptions) (*IssueStats, error) {
14661418

14671419
cond = cond.And(builder.Eq{"issue.is_closed": opts.IsClosed})
14681420
stats.AssignCount, err = x.Where(cond).
1469-
And("assignee_id = ?", opts.UserID).
1421+
Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id").
1422+
And("issue_assignees.assignee_id = ?", opts.UserID).
14701423
Count(new(Issue))
14711424
if err != nil {
14721425
return nil, err
@@ -1505,8 +1458,10 @@ func GetRepoIssueStats(repoID, uid int64, filterMode int, isPull bool) (numOpen
15051458

15061459
switch filterMode {
15071460
case FilterModeAssign:
1508-
openCountSession.And("assignee_id = ?", uid)
1509-
closedCountSession.And("assignee_id = ?", uid)
1461+
openCountSession.Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id").
1462+
And("issue_assignees.assignee_id = ?", uid)
1463+
closedCountSession.Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id").
1464+
And("issue_assignees.assignee_id = ?", uid)
15101465
case FilterModeCreate:
15111466
openCountSession.And("poster_id = ?", uid)
15121467
closedCountSession.And("poster_id = ?", uid)

0 commit comments

Comments
 (0)