Skip to content

Commit 75ab581

Browse files
a1012112796lafriks
authored and
Yohann Delafollye
committed
add request review from specific reviewers feature in pull request (go-gitea#10756)
* add request review feature in pull request add a way to notify specific reviewers to review like github , by add or delet a special type review . The acton is is similar to Assign , so many code reuse the function and items of Assignee, but the meaning and result is different. The Permission style is is similar to github, that only writer can add a review request from Reviewers, but the poster can recall and remove a review request after a reviwer has revied even if he don't have Write Premission. only manager , the poster and reviewer of a request review can remove it. The reviewers can be requested to review contain all readers for private repo , for public, contain all writers and watchers. The offical Review Request will block merge if Reject can block it. an other change: add ui otify for Assignees. Co-authored-by: guillep2k <[email protected]> Co-authored-by: Lauris BH <[email protected]> Signed-off-by: a1012112796 <[email protected]> * new change * add placeholder string * do some changes follow go-gitea#10238 to add review requests num on lists also change icon for review requests to eye Co-authored-by: Lauris BH <[email protected]>
1 parent a742aae commit 75ab581

File tree

24 files changed

+714
-67
lines changed

24 files changed

+714
-67
lines changed

models/branches.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,12 +177,13 @@ func (protectBranch *ProtectedBranch) GetGrantedApprovalsCount(pr *PullRequest)
177177
}
178178

179179
// MergeBlockedByRejectedReview returns true if merge is blocked by rejected reviews
180+
// An official ReviewRequest should also block Merge like Reject
180181
func (protectBranch *ProtectedBranch) MergeBlockedByRejectedReview(pr *PullRequest) bool {
181182
if !protectBranch.BlockOnRejectedReviews {
182183
return false
183184
}
184185
rejectExist, err := x.Where("issue_id = ?", pr.IssueID).
185-
And("type = ?", ReviewTypeReject).
186+
And("type in ( ?, ?)", ReviewTypeReject, ReviewTypeRequest).
186187
And("official = ?", true).
187188
Exist(new(Review))
188189
if err != nil {

models/issue_comment.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ const (
8686
CommentTypeChangeTargetBranch
8787
// Delete time manual for time tracking
8888
CommentTypeDeleteTimeManual
89+
// add or remove Request from one
90+
CommentTypeReviewRequest
8991
)
9092

9193
// CommentTag defines comment tag type

models/notification.go

Lines changed: 43 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -118,64 +118,73 @@ func GetNotifications(opts FindNotificationOptions) (NotificationList, error) {
118118

119119
// CreateOrUpdateIssueNotifications creates an issue notification
120120
// for each watcher, or updates it if already exists
121-
func CreateOrUpdateIssueNotifications(issueID, commentID int64, notificationAuthorID int64) error {
121+
// receiverID > 0 just send to reciver, else send to all watcher
122+
func CreateOrUpdateIssueNotifications(issueID, commentID, notificationAuthorID, receiverID int64) error {
122123
sess := x.NewSession()
123124
defer sess.Close()
124125
if err := sess.Begin(); err != nil {
125126
return err
126127
}
127128

128-
if err := createOrUpdateIssueNotifications(sess, issueID, commentID, notificationAuthorID); err != nil {
129+
if err := createOrUpdateIssueNotifications(sess, issueID, commentID, notificationAuthorID, receiverID); err != nil {
129130
return err
130131
}
131132

132133
return sess.Commit()
133134
}
134135

135-
func createOrUpdateIssueNotifications(e Engine, issueID, commentID int64, notificationAuthorID int64) error {
136+
func createOrUpdateIssueNotifications(e Engine, issueID, commentID, notificationAuthorID, receiverID int64) error {
136137
// init
137-
toNotify := make(map[int64]struct{}, 32)
138+
var toNotify map[int64]struct{}
138139
notifications, err := getNotificationsByIssueID(e, issueID)
140+
139141
if err != nil {
140142
return err
141143
}
144+
142145
issue, err := getIssueByID(e, issueID)
143146
if err != nil {
144147
return err
145148
}
146149

147-
issueWatches, err := getIssueWatchersIDs(e, issueID, true)
148-
if err != nil {
149-
return err
150-
}
151-
for _, id := range issueWatches {
152-
toNotify[id] = struct{}{}
153-
}
150+
if receiverID > 0 {
151+
toNotify = make(map[int64]struct{}, 1)
152+
toNotify[receiverID] = struct{}{}
153+
} else {
154+
toNotify = make(map[int64]struct{}, 32)
155+
issueWatches, err := getIssueWatchersIDs(e, issueID, true)
156+
if err != nil {
157+
return err
158+
}
159+
for _, id := range issueWatches {
160+
toNotify[id] = struct{}{}
161+
}
154162

155-
repoWatches, err := getRepoWatchersIDs(e, issue.RepoID)
156-
if err != nil {
157-
return err
158-
}
159-
for _, id := range repoWatches {
160-
toNotify[id] = struct{}{}
161-
}
162-
issueParticipants, err := issue.getParticipantIDsByIssue(e)
163-
if err != nil {
164-
return err
165-
}
166-
for _, id := range issueParticipants {
167-
toNotify[id] = struct{}{}
168-
}
163+
repoWatches, err := getRepoWatchersIDs(e, issue.RepoID)
164+
if err != nil {
165+
return err
166+
}
167+
for _, id := range repoWatches {
168+
toNotify[id] = struct{}{}
169+
}
170+
issueParticipants, err := issue.getParticipantIDsByIssue(e)
171+
if err != nil {
172+
return err
173+
}
174+
for _, id := range issueParticipants {
175+
toNotify[id] = struct{}{}
176+
}
169177

170-
// dont notify user who cause notification
171-
delete(toNotify, notificationAuthorID)
172-
// explicit unwatch on issue
173-
issueUnWatches, err := getIssueWatchersIDs(e, issueID, false)
174-
if err != nil {
175-
return err
176-
}
177-
for _, id := range issueUnWatches {
178-
delete(toNotify, id)
178+
// dont notify user who cause notification
179+
delete(toNotify, notificationAuthorID)
180+
// explicit unwatch on issue
181+
issueUnWatches, err := getIssueWatchersIDs(e, issueID, false)
182+
if err != nil {
183+
return err
184+
}
185+
for _, id := range issueUnWatches {
186+
delete(toNotify, id)
187+
}
179188
}
180189

181190
err = issue.loadRepo(e)

models/notification_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ func TestCreateOrUpdateIssueNotifications(t *testing.T) {
1414
assert.NoError(t, PrepareTestDatabase())
1515
issue := AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue)
1616

17-
assert.NoError(t, CreateOrUpdateIssueNotifications(issue.ID, 0, 2))
17+
assert.NoError(t, CreateOrUpdateIssueNotifications(issue.ID, 0, 2, 0))
1818

1919
// User 9 is inactive, thus notifications for user 1 and 4 are created
2020
notf := AssertExistsAndLoadBean(t, &Notification{UserID: 1, IssueID: issue.ID}).(*Notification)

models/repo.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,64 @@ func (repo *Repository) GetAssignees() (_ []*User, err error) {
622622
return repo.getAssignees(x)
623623
}
624624

625+
func (repo *Repository) getReviewersPrivate(e Engine, doerID, posterID int64) (users []*User, err error) {
626+
users = make([]*User, 0, 20)
627+
628+
if err = e.
629+
SQL("SELECT * FROM `user` WHERE id in (SELECT user_id FROM `access` WHERE repo_id = ? AND mode >= ? AND user_id NOT IN ( ?, ?)) ORDER BY name",
630+
repo.ID, AccessModeRead,
631+
doerID, posterID).
632+
Find(&users); err != nil {
633+
return nil, err
634+
}
635+
636+
return users, nil
637+
}
638+
639+
func (repo *Repository) getReviewersPublic(e Engine, doerID, posterID int64) (_ []*User, err error) {
640+
641+
users := make([]*User, 0)
642+
643+
const SQLCmd = "SELECT * FROM `user` WHERE id IN ( " +
644+
"SELECT user_id FROM `access` WHERE repo_id = ? AND mode >= ? AND user_id NOT IN ( ?, ?) " +
645+
"UNION " +
646+
"SELECT user_id FROM `watch` WHERE repo_id = ? AND user_id NOT IN ( ?, ?) AND mode IN (?, ?) " +
647+
") ORDER BY name"
648+
649+
if err = e.
650+
SQL(SQLCmd,
651+
repo.ID, AccessModeRead, doerID, posterID,
652+
repo.ID, doerID, posterID, RepoWatchModeNormal, RepoWatchModeAuto).
653+
Find(&users); err != nil {
654+
return nil, err
655+
}
656+
657+
return users, nil
658+
}
659+
660+
func (repo *Repository) getReviewers(e Engine, doerID, posterID int64) (users []*User, err error) {
661+
if err = repo.getOwner(e); err != nil {
662+
return nil, err
663+
}
664+
665+
if repo.IsPrivate ||
666+
(repo.Owner.IsOrganization() && repo.Owner.Visibility == api.VisibleTypePrivate) {
667+
users, err = repo.getReviewersPrivate(x, doerID, posterID)
668+
} else {
669+
users, err = repo.getReviewersPublic(x, doerID, posterID)
670+
}
671+
return
672+
}
673+
674+
// GetReviewers get all users can be requested to review
675+
// for private rpo , that return all users that have read access or higher to the repository.
676+
// but for public rpo, that return all users that have write access or higher to the repository,
677+
// and all repo watchers.
678+
// TODO: may be we should hava a busy choice for users to block review request to them.
679+
func (repo *Repository) GetReviewers(doerID, posterID int64) (_ []*User, err error) {
680+
return repo.getReviewers(x, doerID, posterID)
681+
}
682+
625683
// GetMilestoneByID returns the milestone belongs to repository by given ID.
626684
func (repo *Repository) GetMilestoneByID(milestoneID int64) (*Milestone, error) {
627685
return GetMilestoneByRepoID(repo.ID, milestoneID)

models/review.go

Lines changed: 151 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ const (
2727
ReviewTypeComment
2828
// ReviewTypeReject gives feedback blocking merge
2929
ReviewTypeReject
30+
// ReviewTypeRequest request review from others
31+
ReviewTypeRequest
3032
)
3133

3234
// Icon returns the corresponding icon for the review type
@@ -38,6 +40,8 @@ func (rt ReviewType) Icon() string {
3840
return "request-changes"
3941
case ReviewTypeComment:
4042
return "comment"
43+
case ReviewTypeRequest:
44+
return "primitive-dot"
4145
default:
4246
return "comment"
4347
}
@@ -369,15 +373,15 @@ func GetReviewersByIssueID(issueID int64) (reviews []*Review, err error) {
369373
}
370374

371375
// Get latest review of each reviwer, sorted in order they were made
372-
if err := sess.SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id = ? AND type in (?, ?) GROUP BY issue_id, reviewer_id) ORDER BY review.updated_unix ASC",
373-
issueID, ReviewTypeApprove, ReviewTypeReject).
376+
if err := sess.SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id = ? AND type in (?, ?, ?) GROUP BY issue_id, reviewer_id) ORDER BY review.updated_unix ASC",
377+
issueID, ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest).
374378
Find(&reviewsUnfiltered); err != nil {
375379
return nil, err
376380
}
377381

378382
// Load reviewer and skip if user is deleted
379383
for _, review := range reviewsUnfiltered {
380-
if err := review.loadReviewer(sess); err != nil {
384+
if err = review.loadReviewer(sess); err != nil {
381385
if !IsErrUserNotExist(err) {
382386
return nil, err
383387
}
@@ -389,6 +393,19 @@ func GetReviewersByIssueID(issueID int64) (reviews []*Review, err error) {
389393
return reviews, nil
390394
}
391395

396+
// GetReviewerByIssueIDAndUserID get the latest review of reviewer for a pull request
397+
func GetReviewerByIssueIDAndUserID(issueID, userID int64) (review *Review, err error) {
398+
review = new(Review)
399+
400+
if _, err := x.SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id = ? AND reviewer_id = ? AND type in (?, ?, ?))",
401+
issueID, userID, ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest).
402+
Get(review); err != nil {
403+
return nil, err
404+
}
405+
406+
return
407+
}
408+
392409
// MarkReviewsAsStale marks existing reviews as stale
393410
func MarkReviewsAsStale(issueID int64) (err error) {
394411
_, err = x.Exec("UPDATE `review` SET stale=? WHERE issue_id=?", true, issueID)
@@ -442,3 +459,134 @@ func InsertReviews(reviews []*Review) error {
442459

443460
return sess.Commit()
444461
}
462+
463+
// AddRewiewRequest add a review request from one reviewer
464+
func AddRewiewRequest(issue *Issue, reviewer *User, doer *User) (comment *Comment, err error) {
465+
review, err := GetReviewerByIssueIDAndUserID(issue.ID, reviewer.ID)
466+
if err != nil {
467+
return
468+
}
469+
470+
// skip it when reviewer hase been request to review
471+
if review != nil && review.Type == ReviewTypeRequest {
472+
return nil, nil
473+
}
474+
475+
sess := x.NewSession()
476+
defer sess.Close()
477+
if err := sess.Begin(); err != nil {
478+
return nil, err
479+
}
480+
481+
var official bool
482+
official, err = isOfficialReviewer(sess, issue, reviewer)
483+
484+
if err != nil {
485+
return nil, err
486+
}
487+
488+
if !official {
489+
official, err = isOfficialReviewer(sess, issue, doer)
490+
491+
if err != nil {
492+
return nil, err
493+
}
494+
}
495+
496+
if official {
497+
if _, err := sess.Exec("UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_id=?", false, issue.ID, reviewer.ID); err != nil {
498+
return nil, err
499+
}
500+
}
501+
502+
_, err = createReview(sess, CreateReviewOptions{
503+
Type: ReviewTypeRequest,
504+
Issue: issue,
505+
Reviewer: reviewer,
506+
Official: official,
507+
Stale: false,
508+
})
509+
510+
if err != nil {
511+
return
512+
}
513+
514+
comment, err = createComment(sess, &CreateCommentOptions{
515+
Type: CommentTypeReviewRequest,
516+
Doer: doer,
517+
Repo: issue.Repo,
518+
Issue: issue,
519+
RemovedAssignee: false, // Use RemovedAssignee as !isRequest
520+
AssigneeID: reviewer.ID, // Use AssigneeID as reviewer ID
521+
})
522+
523+
if err != nil {
524+
return nil, err
525+
}
526+
527+
return comment, sess.Commit()
528+
}
529+
530+
//RemoveRewiewRequest remove a review request from one reviewer
531+
func RemoveRewiewRequest(issue *Issue, reviewer *User, doer *User) (comment *Comment, err error) {
532+
review, err := GetReviewerByIssueIDAndUserID(issue.ID, reviewer.ID)
533+
if err != nil {
534+
return
535+
}
536+
537+
if review.Type != ReviewTypeRequest {
538+
return nil, nil
539+
}
540+
541+
sess := x.NewSession()
542+
defer sess.Close()
543+
if err := sess.Begin(); err != nil {
544+
return nil, err
545+
}
546+
547+
_, err = sess.Delete(review)
548+
if err != nil {
549+
return nil, err
550+
}
551+
552+
var official bool
553+
official, err = isOfficialReviewer(sess, issue, reviewer)
554+
if err != nil {
555+
return
556+
}
557+
558+
if official {
559+
// recalculate which is the latest official review from that user
560+
var review *Review
561+
562+
review, err = GetReviewerByIssueIDAndUserID(issue.ID, reviewer.ID)
563+
if err != nil {
564+
return nil, err
565+
}
566+
567+
if review != nil {
568+
if _, err := sess.Exec("UPDATE `review` SET official=? WHERE id=?", true, review.ID); err != nil {
569+
return nil, err
570+
}
571+
}
572+
}
573+
574+
if err != nil {
575+
return nil, err
576+
}
577+
578+
comment, err = CreateComment(&CreateCommentOptions{
579+
Type: CommentTypeReviewRequest,
580+
Doer: doer,
581+
Repo: issue.Repo,
582+
Issue: issue,
583+
RemovedAssignee: true, // Use RemovedAssignee as !isRequest
584+
AssigneeID: reviewer.ID, // Use AssigneeID as reviewer ID
585+
})
586+
587+
if err != nil {
588+
return nil, err
589+
}
590+
591+
return comment, sess.Commit()
592+
}

0 commit comments

Comments
 (0)