1// Copyright 2018 The Gitea Authors. All rights reserved.
2// Use of this source code is governed by a MIT-style
3// license that can be found in the LICENSE file.
4
5package models
6
7import (
8	"context"
9	"fmt"
10	"strings"
11
12	"code.gitea.io/gitea/models/db"
13	"code.gitea.io/gitea/models/perm"
14	"code.gitea.io/gitea/models/unit"
15	user_model "code.gitea.io/gitea/models/user"
16	"code.gitea.io/gitea/modules/base"
17	"code.gitea.io/gitea/modules/timeutil"
18
19	"xorm.io/builder"
20)
21
22// ReviewType defines the sort of feedback a review gives
23type ReviewType int
24
25// ReviewTypeUnknown unknown review type
26const ReviewTypeUnknown ReviewType = -1
27
28const (
29	// ReviewTypePending is a review which is not published yet
30	ReviewTypePending ReviewType = iota
31	// ReviewTypeApprove approves changes
32	ReviewTypeApprove
33	// ReviewTypeComment gives general feedback
34	ReviewTypeComment
35	// ReviewTypeReject gives feedback blocking merge
36	ReviewTypeReject
37	// ReviewTypeRequest request review from others
38	ReviewTypeRequest
39)
40
41// Icon returns the corresponding icon for the review type
42func (rt ReviewType) Icon() string {
43	switch rt {
44	case ReviewTypeApprove:
45		return "check"
46	case ReviewTypeReject:
47		return "diff"
48	case ReviewTypeComment:
49		return "comment"
50	case ReviewTypeRequest:
51		return "dot-fill"
52	default:
53		return "comment"
54	}
55}
56
57// Review represents collection of code comments giving feedback for a PR
58type Review struct {
59	ID               int64 `xorm:"pk autoincr"`
60	Type             ReviewType
61	Reviewer         *user_model.User `xorm:"-"`
62	ReviewerID       int64            `xorm:"index"`
63	ReviewerTeamID   int64            `xorm:"NOT NULL DEFAULT 0"`
64	ReviewerTeam     *Team            `xorm:"-"`
65	OriginalAuthor   string
66	OriginalAuthorID int64
67	Issue            *Issue `xorm:"-"`
68	IssueID          int64  `xorm:"index"`
69	Content          string `xorm:"TEXT"`
70	// Official is a review made by an assigned approver (counts towards approval)
71	Official  bool   `xorm:"NOT NULL DEFAULT false"`
72	CommitID  string `xorm:"VARCHAR(40)"`
73	Stale     bool   `xorm:"NOT NULL DEFAULT false"`
74	Dismissed bool   `xorm:"NOT NULL DEFAULT false"`
75
76	CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
77	UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
78
79	// CodeComments are the initial code comments of the review
80	CodeComments CodeComments `xorm:"-"`
81
82	Comments []*Comment `xorm:"-"`
83}
84
85func init() {
86	db.RegisterModel(new(Review))
87}
88
89func (r *Review) loadCodeComments(ctx context.Context) (err error) {
90	if r.CodeComments != nil {
91		return
92	}
93	if err = r.loadIssue(db.GetEngine(ctx)); err != nil {
94		return
95	}
96	r.CodeComments, err = fetchCodeCommentsByReview(ctx, r.Issue, nil, r)
97	return
98}
99
100// LoadCodeComments loads CodeComments
101func (r *Review) LoadCodeComments() error {
102	return r.loadCodeComments(db.DefaultContext)
103}
104
105func (r *Review) loadIssue(e db.Engine) (err error) {
106	if r.Issue != nil {
107		return
108	}
109	r.Issue, err = getIssueByID(e, r.IssueID)
110	return
111}
112
113func (r *Review) loadReviewer(e db.Engine) (err error) {
114	if r.ReviewerID == 0 || r.Reviewer != nil {
115		return
116	}
117	r.Reviewer, err = user_model.GetUserByIDEngine(e, r.ReviewerID)
118	return
119}
120
121func (r *Review) loadReviewerTeam(e db.Engine) (err error) {
122	if r.ReviewerTeamID == 0 || r.ReviewerTeam != nil {
123		return
124	}
125
126	r.ReviewerTeam, err = getTeamByID(e, r.ReviewerTeamID)
127	return
128}
129
130// LoadReviewer loads reviewer
131func (r *Review) LoadReviewer() error {
132	return r.loadReviewer(db.GetEngine(db.DefaultContext))
133}
134
135// LoadReviewerTeam loads reviewer team
136func (r *Review) LoadReviewerTeam() error {
137	return r.loadReviewerTeam(db.GetEngine(db.DefaultContext))
138}
139
140func (r *Review) loadAttributes(ctx context.Context) (err error) {
141	e := db.GetEngine(ctx)
142	if err = r.loadIssue(e); err != nil {
143		return
144	}
145	if err = r.loadCodeComments(ctx); err != nil {
146		return
147	}
148	if err = r.loadReviewer(e); err != nil {
149		return
150	}
151	if err = r.loadReviewerTeam(e); err != nil {
152		return
153	}
154	return
155}
156
157// LoadAttributes loads all attributes except CodeComments
158func (r *Review) LoadAttributes() error {
159	return r.loadAttributes(db.DefaultContext)
160}
161
162func getReviewByID(e db.Engine, id int64) (*Review, error) {
163	review := new(Review)
164	if has, err := e.ID(id).Get(review); err != nil {
165		return nil, err
166	} else if !has {
167		return nil, ErrReviewNotExist{ID: id}
168	} else {
169		return review, nil
170	}
171}
172
173// GetReviewByID returns the review by the given ID
174func GetReviewByID(id int64) (*Review, error) {
175	return getReviewByID(db.GetEngine(db.DefaultContext), id)
176}
177
178// FindReviewOptions represent possible filters to find reviews
179type FindReviewOptions struct {
180	db.ListOptions
181	Type         ReviewType
182	IssueID      int64
183	ReviewerID   int64
184	OfficialOnly bool
185}
186
187func (opts *FindReviewOptions) toCond() builder.Cond {
188	cond := builder.NewCond()
189	if opts.IssueID > 0 {
190		cond = cond.And(builder.Eq{"issue_id": opts.IssueID})
191	}
192	if opts.ReviewerID > 0 {
193		cond = cond.And(builder.Eq{"reviewer_id": opts.ReviewerID})
194	}
195	if opts.Type != ReviewTypeUnknown {
196		cond = cond.And(builder.Eq{"type": opts.Type})
197	}
198	if opts.OfficialOnly {
199		cond = cond.And(builder.Eq{"official": true})
200	}
201	return cond
202}
203
204func findReviews(e db.Engine, opts FindReviewOptions) ([]*Review, error) {
205	reviews := make([]*Review, 0, 10)
206	sess := e.Where(opts.toCond())
207	if opts.Page > 0 {
208		sess = db.SetSessionPagination(sess, &opts)
209	}
210	return reviews, sess.
211		Asc("created_unix").
212		Asc("id").
213		Find(&reviews)
214}
215
216// FindReviews returns reviews passing FindReviewOptions
217func FindReviews(opts FindReviewOptions) ([]*Review, error) {
218	return findReviews(db.GetEngine(db.DefaultContext), opts)
219}
220
221// CountReviews returns count of reviews passing FindReviewOptions
222func CountReviews(opts FindReviewOptions) (int64, error) {
223	return db.GetEngine(db.DefaultContext).Where(opts.toCond()).Count(&Review{})
224}
225
226// CreateReviewOptions represent the options to create a review. Type, Issue and Reviewer are required.
227type CreateReviewOptions struct {
228	Content      string
229	Type         ReviewType
230	Issue        *Issue
231	Reviewer     *user_model.User
232	ReviewerTeam *Team
233	Official     bool
234	CommitID     string
235	Stale        bool
236}
237
238// IsOfficialReviewer check if at least one of the provided reviewers can make official reviews in issue (counts towards required approvals)
239func IsOfficialReviewer(issue *Issue, reviewers ...*user_model.User) (bool, error) {
240	return isOfficialReviewer(db.DefaultContext, issue, reviewers...)
241}
242
243func isOfficialReviewer(ctx context.Context, issue *Issue, reviewers ...*user_model.User) (bool, error) {
244	pr, err := getPullRequestByIssueID(db.GetEngine(ctx), issue.ID)
245	if err != nil {
246		return false, err
247	}
248	if err = pr.loadProtectedBranch(ctx); err != nil {
249		return false, err
250	}
251	if pr.ProtectedBranch == nil {
252		return false, nil
253	}
254
255	for _, reviewer := range reviewers {
256		official, err := isUserOfficialReviewer(ctx, pr.ProtectedBranch, reviewer)
257		if official || err != nil {
258			return official, err
259		}
260	}
261
262	return false, nil
263}
264
265// IsOfficialReviewerTeam check if reviewer in this team can make official reviews in issue (counts towards required approvals)
266func IsOfficialReviewerTeam(issue *Issue, team *Team) (bool, error) {
267	return isOfficialReviewerTeam(db.DefaultContext, issue, team)
268}
269
270func isOfficialReviewerTeam(ctx context.Context, issue *Issue, team *Team) (bool, error) {
271	pr, err := getPullRequestByIssueID(db.GetEngine(ctx), issue.ID)
272	if err != nil {
273		return false, err
274	}
275	if err = pr.loadProtectedBranch(ctx); err != nil {
276		return false, err
277	}
278	if pr.ProtectedBranch == nil {
279		return false, nil
280	}
281
282	if !pr.ProtectedBranch.EnableApprovalsWhitelist {
283		return team.UnitAccessMode(unit.TypeCode) >= perm.AccessModeWrite, nil
284	}
285
286	return base.Int64sContains(pr.ProtectedBranch.ApprovalsWhitelistTeamIDs, team.ID), nil
287}
288
289func createReview(e db.Engine, opts CreateReviewOptions) (*Review, error) {
290	review := &Review{
291		Type:         opts.Type,
292		Issue:        opts.Issue,
293		IssueID:      opts.Issue.ID,
294		Reviewer:     opts.Reviewer,
295		ReviewerTeam: opts.ReviewerTeam,
296		Content:      opts.Content,
297		Official:     opts.Official,
298		CommitID:     opts.CommitID,
299		Stale:        opts.Stale,
300	}
301	if opts.Reviewer != nil {
302		review.ReviewerID = opts.Reviewer.ID
303	} else {
304		if review.Type != ReviewTypeRequest {
305			review.Type = ReviewTypeRequest
306		}
307		review.ReviewerTeamID = opts.ReviewerTeam.ID
308	}
309	if _, err := e.Insert(review); err != nil {
310		return nil, err
311	}
312
313	return review, nil
314}
315
316// CreateReview creates a new review based on opts
317func CreateReview(opts CreateReviewOptions) (*Review, error) {
318	return createReview(db.GetEngine(db.DefaultContext), opts)
319}
320
321func getCurrentReview(e db.Engine, reviewer *user_model.User, issue *Issue) (*Review, error) {
322	if reviewer == nil {
323		return nil, nil
324	}
325	reviews, err := findReviews(e, FindReviewOptions{
326		Type:       ReviewTypePending,
327		IssueID:    issue.ID,
328		ReviewerID: reviewer.ID,
329	})
330	if err != nil {
331		return nil, err
332	}
333	if len(reviews) == 0 {
334		return nil, ErrReviewNotExist{}
335	}
336	reviews[0].Reviewer = reviewer
337	reviews[0].Issue = issue
338	return reviews[0], nil
339}
340
341// ReviewExists returns whether a review exists for a particular line of code in the PR
342func ReviewExists(issue *Issue, treePath string, line int64) (bool, error) {
343	return db.GetEngine(db.DefaultContext).Cols("id").Exist(&Comment{IssueID: issue.ID, TreePath: treePath, Line: line, Type: CommentTypeCode})
344}
345
346// GetCurrentReview returns the current pending review of reviewer for given issue
347func GetCurrentReview(reviewer *user_model.User, issue *Issue) (*Review, error) {
348	return getCurrentReview(db.GetEngine(db.DefaultContext), reviewer, issue)
349}
350
351// ContentEmptyErr represents an content empty error
352type ContentEmptyErr struct{}
353
354func (ContentEmptyErr) Error() string {
355	return "Review content is empty"
356}
357
358// IsContentEmptyErr returns true if err is a ContentEmptyErr
359func IsContentEmptyErr(err error) bool {
360	_, ok := err.(ContentEmptyErr)
361	return ok
362}
363
364// SubmitReview creates a review out of the existing pending review or creates a new one if no pending review exist
365func SubmitReview(doer *user_model.User, issue *Issue, reviewType ReviewType, content, commitID string, stale bool, attachmentUUIDs []string) (*Review, *Comment, error) {
366	ctx, committer, err := db.TxContext()
367	if err != nil {
368		return nil, nil, err
369	}
370	defer committer.Close()
371	sess := db.GetEngine(ctx)
372
373	official := false
374
375	review, err := getCurrentReview(sess, doer, issue)
376	if err != nil {
377		if !IsErrReviewNotExist(err) {
378			return nil, nil, err
379		}
380
381		if reviewType != ReviewTypeApprove && len(strings.TrimSpace(content)) == 0 {
382			return nil, nil, ContentEmptyErr{}
383		}
384
385		if reviewType == ReviewTypeApprove || reviewType == ReviewTypeReject {
386			// Only reviewers latest review of type approve and reject shall count as "official", so existing reviews needs to be cleared
387			if _, err := sess.Exec("UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_id=?", false, issue.ID, doer.ID); err != nil {
388				return nil, nil, err
389			}
390			if official, err = isOfficialReviewer(ctx, issue, doer); err != nil {
391				return nil, nil, err
392			}
393		}
394
395		// No current review. Create a new one!
396		if review, err = createReview(sess, CreateReviewOptions{
397			Type:     reviewType,
398			Issue:    issue,
399			Reviewer: doer,
400			Content:  content,
401			Official: official,
402			CommitID: commitID,
403			Stale:    stale,
404		}); err != nil {
405			return nil, nil, err
406		}
407	} else {
408		if err := review.loadCodeComments(ctx); err != nil {
409			return nil, nil, err
410		}
411		if reviewType != ReviewTypeApprove && len(review.CodeComments) == 0 && len(strings.TrimSpace(content)) == 0 {
412			return nil, nil, ContentEmptyErr{}
413		}
414
415		if reviewType == ReviewTypeApprove || reviewType == ReviewTypeReject {
416			// Only reviewers latest review of type approve and reject shall count as "official", so existing reviews needs to be cleared
417			if _, err := sess.Exec("UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_id=?", false, issue.ID, doer.ID); err != nil {
418				return nil, nil, err
419			}
420			if official, err = isOfficialReviewer(ctx, issue, doer); err != nil {
421				return nil, nil, err
422			}
423		}
424
425		review.Official = official
426		review.Issue = issue
427		review.Content = content
428		review.Type = reviewType
429		review.CommitID = commitID
430		review.Stale = stale
431
432		if _, err := sess.ID(review.ID).Cols("content, type, official, commit_id, stale").Update(review); err != nil {
433			return nil, nil, err
434		}
435	}
436
437	comm, err := createComment(ctx, &CreateCommentOptions{
438		Type:        CommentTypeReview,
439		Doer:        doer,
440		Content:     review.Content,
441		Issue:       issue,
442		Repo:        issue.Repo,
443		ReviewID:    review.ID,
444		Attachments: attachmentUUIDs,
445	})
446	if err != nil || comm == nil {
447		return nil, nil, err
448	}
449
450	// try to remove team review request if need
451	if issue.Repo.Owner.IsOrganization() && (reviewType == ReviewTypeApprove || reviewType == ReviewTypeReject) {
452		teamReviewRequests := make([]*Review, 0, 10)
453		if err := sess.SQL("SELECT * FROM review WHERE issue_id = ? AND reviewer_team_id > 0 AND type = ?", issue.ID, ReviewTypeRequest).Find(&teamReviewRequests); err != nil {
454			return nil, nil, err
455		}
456
457		for _, teamReviewRequest := range teamReviewRequests {
458			ok, err := isTeamMember(sess, issue.Repo.OwnerID, teamReviewRequest.ReviewerTeamID, doer.ID)
459			if err != nil {
460				return nil, nil, err
461			} else if !ok {
462				continue
463			}
464
465			if _, err := sess.Delete(teamReviewRequest); err != nil {
466				return nil, nil, err
467			}
468		}
469	}
470
471	comm.Review = review
472	return review, comm, committer.Commit()
473}
474
475// GetReviewersByIssueID gets the latest review of each reviewer for a pull request
476func GetReviewersByIssueID(issueID int64) ([]*Review, error) {
477	reviews := make([]*Review, 0, 10)
478
479	sess := db.GetEngine(db.DefaultContext)
480
481	// Get latest review of each reviewer, sorted in order they were made
482	if err := sess.SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id = ? AND reviewer_team_id = 0 AND type in (?, ?, ?) AND dismissed = ? AND original_author_id = 0 GROUP BY issue_id, reviewer_id) ORDER BY review.updated_unix ASC",
483		issueID, ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest, false).
484		Find(&reviews); err != nil {
485		return nil, err
486	}
487
488	teamReviewRequests := make([]*Review, 0, 5)
489	if err := sess.SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id = ? AND reviewer_team_id <> 0 AND original_author_id = 0 GROUP BY issue_id, reviewer_team_id) ORDER BY review.updated_unix ASC",
490		issueID).
491		Find(&teamReviewRequests); err != nil {
492		return nil, err
493	}
494
495	if len(teamReviewRequests) > 0 {
496		reviews = append(reviews, teamReviewRequests...)
497	}
498
499	return reviews, nil
500}
501
502// GetReviewersFromOriginalAuthorsByIssueID gets the latest review of each original authors for a pull request
503func GetReviewersFromOriginalAuthorsByIssueID(issueID int64) ([]*Review, error) {
504	reviews := make([]*Review, 0, 10)
505
506	// Get latest review of each reviewer, sorted in order they were made
507	if err := db.GetEngine(db.DefaultContext).SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id = ? AND reviewer_team_id = 0 AND type in (?, ?, ?) AND original_author_id <> 0 GROUP BY issue_id, original_author_id) ORDER BY review.updated_unix ASC",
508		issueID, ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest).
509		Find(&reviews); err != nil {
510		return nil, err
511	}
512
513	return reviews, nil
514}
515
516// GetReviewByIssueIDAndUserID get the latest review of reviewer for a pull request
517func GetReviewByIssueIDAndUserID(issueID, userID int64) (*Review, error) {
518	return getReviewByIssueIDAndUserID(db.GetEngine(db.DefaultContext), issueID, userID)
519}
520
521func getReviewByIssueIDAndUserID(e db.Engine, issueID, userID int64) (*Review, error) {
522	review := new(Review)
523
524	has, err := e.SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id = ? AND reviewer_id = ? AND original_author_id = 0 AND type in (?, ?, ?))",
525		issueID, userID, ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest).
526		Get(review)
527	if err != nil {
528		return nil, err
529	}
530
531	if !has {
532		return nil, ErrReviewNotExist{}
533	}
534
535	return review, nil
536}
537
538// GetTeamReviewerByIssueIDAndTeamID get the latest review request of reviewer team for a pull request
539func GetTeamReviewerByIssueIDAndTeamID(issueID, teamID int64) (review *Review, err error) {
540	return getTeamReviewerByIssueIDAndTeamID(db.GetEngine(db.DefaultContext), issueID, teamID)
541}
542
543func getTeamReviewerByIssueIDAndTeamID(e db.Engine, issueID, teamID int64) (review *Review, err error) {
544	review = new(Review)
545
546	has := false
547	if has, err = e.SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id = ? AND reviewer_team_id = ?)",
548		issueID, teamID).
549		Get(review); err != nil {
550		return nil, err
551	}
552
553	if !has {
554		return nil, ErrReviewNotExist{0}
555	}
556
557	return
558}
559
560// MarkReviewsAsStale marks existing reviews as stale
561func MarkReviewsAsStale(issueID int64) (err error) {
562	_, err = db.GetEngine(db.DefaultContext).Exec("UPDATE `review` SET stale=? WHERE issue_id=?", true, issueID)
563
564	return
565}
566
567// MarkReviewsAsNotStale marks existing reviews as not stale for a giving commit SHA
568func MarkReviewsAsNotStale(issueID int64, commitID string) (err error) {
569	_, err = db.GetEngine(db.DefaultContext).Exec("UPDATE `review` SET stale=? WHERE issue_id=? AND commit_id=?", false, issueID, commitID)
570
571	return
572}
573
574// DismissReview change the dismiss status of a review
575func DismissReview(review *Review, isDismiss bool) (err error) {
576	if review.Dismissed == isDismiss || (review.Type != ReviewTypeApprove && review.Type != ReviewTypeReject) {
577		return nil
578	}
579
580	review.Dismissed = isDismiss
581
582	if review.ID == 0 {
583		return ErrReviewNotExist{}
584	}
585
586	_, err = db.GetEngine(db.DefaultContext).ID(review.ID).Cols("dismissed").Update(review)
587
588	return
589}
590
591// InsertReviews inserts review and review comments
592func InsertReviews(reviews []*Review) error {
593	ctx, committer, err := db.TxContext()
594	if err != nil {
595		return err
596	}
597	defer committer.Close()
598	sess := db.GetEngine(ctx)
599
600	for _, review := range reviews {
601		if _, err := sess.NoAutoTime().Insert(review); err != nil {
602			return err
603		}
604
605		if _, err := sess.NoAutoTime().Insert(&Comment{
606			Type:             CommentTypeReview,
607			Content:          review.Content,
608			PosterID:         review.ReviewerID,
609			OriginalAuthor:   review.OriginalAuthor,
610			OriginalAuthorID: review.OriginalAuthorID,
611			IssueID:          review.IssueID,
612			ReviewID:         review.ID,
613			CreatedUnix:      review.CreatedUnix,
614			UpdatedUnix:      review.UpdatedUnix,
615		}); err != nil {
616			return err
617		}
618
619		for _, c := range review.Comments {
620			c.ReviewID = review.ID
621		}
622
623		if len(review.Comments) > 0 {
624			if _, err := sess.NoAutoTime().Insert(review.Comments); err != nil {
625				return err
626			}
627		}
628	}
629
630	return committer.Commit()
631}
632
633// AddReviewRequest add a review request from one reviewer
634func AddReviewRequest(issue *Issue, reviewer, doer *user_model.User) (*Comment, error) {
635	ctx, committer, err := db.TxContext()
636	if err != nil {
637		return nil, err
638	}
639	defer committer.Close()
640	sess := db.GetEngine(ctx)
641
642	review, err := getReviewByIssueIDAndUserID(sess, issue.ID, reviewer.ID)
643	if err != nil && !IsErrReviewNotExist(err) {
644		return nil, err
645	}
646
647	// skip it when reviewer hase been request to review
648	if review != nil && review.Type == ReviewTypeRequest {
649		return nil, nil
650	}
651
652	official, err := isOfficialReviewer(ctx, issue, reviewer, doer)
653	if err != nil {
654		return nil, err
655	} else if official {
656		if _, err := sess.Exec("UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_id=?", false, issue.ID, reviewer.ID); err != nil {
657			return nil, err
658		}
659	}
660
661	review, err = createReview(sess, CreateReviewOptions{
662		Type:     ReviewTypeRequest,
663		Issue:    issue,
664		Reviewer: reviewer,
665		Official: official,
666		Stale:    false,
667	})
668	if err != nil {
669		return nil, err
670	}
671
672	comment, err := createComment(ctx, &CreateCommentOptions{
673		Type:            CommentTypeReviewRequest,
674		Doer:            doer,
675		Repo:            issue.Repo,
676		Issue:           issue,
677		RemovedAssignee: false,       // Use RemovedAssignee as !isRequest
678		AssigneeID:      reviewer.ID, // Use AssigneeID as reviewer ID
679		ReviewID:        review.ID,
680	})
681	if err != nil {
682		return nil, err
683	}
684
685	return comment, committer.Commit()
686}
687
688// RemoveReviewRequest remove a review request from one reviewer
689func RemoveReviewRequest(issue *Issue, reviewer, doer *user_model.User) (*Comment, error) {
690	ctx, committer, err := db.TxContext()
691	if err != nil {
692		return nil, err
693	}
694	defer committer.Close()
695	sess := db.GetEngine(ctx)
696
697	review, err := getReviewByIssueIDAndUserID(sess, issue.ID, reviewer.ID)
698	if err != nil && !IsErrReviewNotExist(err) {
699		return nil, err
700	}
701
702	if review == nil || review.Type != ReviewTypeRequest {
703		return nil, nil
704	}
705
706	if _, err = sess.Delete(review); err != nil {
707		return nil, err
708	}
709
710	official, err := isOfficialReviewer(ctx, issue, reviewer)
711	if err != nil {
712		return nil, err
713	} else if official {
714		// recalculate the latest official review for reviewer
715		review, err := getReviewByIssueIDAndUserID(sess, issue.ID, reviewer.ID)
716		if err != nil && !IsErrReviewNotExist(err) {
717			return nil, err
718		}
719
720		if review != nil {
721			if _, err := sess.Exec("UPDATE `review` SET official=? WHERE id=?", true, review.ID); err != nil {
722				return nil, err
723			}
724		}
725	}
726
727	comment, err := createComment(ctx, &CreateCommentOptions{
728		Type:            CommentTypeReviewRequest,
729		Doer:            doer,
730		Repo:            issue.Repo,
731		Issue:           issue,
732		RemovedAssignee: true,        // Use RemovedAssignee as !isRequest
733		AssigneeID:      reviewer.ID, // Use AssigneeID as reviewer ID
734	})
735	if err != nil {
736		return nil, err
737	}
738
739	return comment, committer.Commit()
740}
741
742// AddTeamReviewRequest add a review request from one team
743func AddTeamReviewRequest(issue *Issue, reviewer *Team, doer *user_model.User) (*Comment, error) {
744	ctx, committer, err := db.TxContext()
745	if err != nil {
746		return nil, err
747	}
748	defer committer.Close()
749	sess := db.GetEngine(ctx)
750
751	review, err := getTeamReviewerByIssueIDAndTeamID(sess, issue.ID, reviewer.ID)
752	if err != nil && !IsErrReviewNotExist(err) {
753		return nil, err
754	}
755
756	// This team already has been requested to review - therefore skip this.
757	if review != nil {
758		return nil, nil
759	}
760
761	official, err := isOfficialReviewerTeam(ctx, issue, reviewer)
762	if err != nil {
763		return nil, fmt.Errorf("isOfficialReviewerTeam(): %v", err)
764	} else if !official {
765		if official, err = isOfficialReviewer(ctx, issue, doer); err != nil {
766			return nil, fmt.Errorf("isOfficialReviewer(): %v", err)
767		}
768	}
769
770	if review, err = createReview(sess, CreateReviewOptions{
771		Type:         ReviewTypeRequest,
772		Issue:        issue,
773		ReviewerTeam: reviewer,
774		Official:     official,
775		Stale:        false,
776	}); err != nil {
777		return nil, err
778	}
779
780	if official {
781		if _, err := sess.Exec("UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_team_id=?", false, issue.ID, reviewer.ID); err != nil {
782			return nil, err
783		}
784	}
785
786	comment, err := createComment(ctx, &CreateCommentOptions{
787		Type:            CommentTypeReviewRequest,
788		Doer:            doer,
789		Repo:            issue.Repo,
790		Issue:           issue,
791		RemovedAssignee: false,       // Use RemovedAssignee as !isRequest
792		AssigneeTeamID:  reviewer.ID, // Use AssigneeTeamID as reviewer team ID
793		ReviewID:        review.ID,
794	})
795	if err != nil {
796		return nil, fmt.Errorf("createComment(): %v", err)
797	}
798
799	return comment, committer.Commit()
800}
801
802// RemoveTeamReviewRequest remove a review request from one team
803func RemoveTeamReviewRequest(issue *Issue, reviewer *Team, doer *user_model.User) (*Comment, error) {
804	ctx, committer, err := db.TxContext()
805	if err != nil {
806		return nil, err
807	}
808	defer committer.Close()
809	sess := db.GetEngine(ctx)
810
811	review, err := getTeamReviewerByIssueIDAndTeamID(sess, issue.ID, reviewer.ID)
812	if err != nil && !IsErrReviewNotExist(err) {
813		return nil, err
814	}
815
816	if review == nil {
817		return nil, nil
818	}
819
820	if _, err = sess.Delete(review); err != nil {
821		return nil, err
822	}
823
824	official, err := isOfficialReviewerTeam(ctx, issue, reviewer)
825	if err != nil {
826		return nil, fmt.Errorf("isOfficialReviewerTeam(): %v", err)
827	}
828
829	if official {
830		// recalculate which is the latest official review from that team
831		review, err := getReviewByIssueIDAndUserID(sess, issue.ID, -reviewer.ID)
832		if err != nil && !IsErrReviewNotExist(err) {
833			return nil, err
834		}
835
836		if review != nil {
837			if _, err := sess.Exec("UPDATE `review` SET official=? WHERE id=?", true, review.ID); err != nil {
838				return nil, err
839			}
840		}
841	}
842
843	if doer == nil {
844		return nil, committer.Commit()
845	}
846
847	comment, err := createComment(ctx, &CreateCommentOptions{
848		Type:            CommentTypeReviewRequest,
849		Doer:            doer,
850		Repo:            issue.Repo,
851		Issue:           issue,
852		RemovedAssignee: true,        // Use RemovedAssignee as !isRequest
853		AssigneeTeamID:  reviewer.ID, // Use AssigneeTeamID as reviewer team ID
854	})
855	if err != nil {
856		return nil, fmt.Errorf("createComment(): %v", err)
857	}
858
859	return comment, committer.Commit()
860}
861
862// MarkConversation Add or remove Conversation mark for a code comment
863func MarkConversation(comment *Comment, doer *user_model.User, isResolve bool) (err error) {
864	if comment.Type != CommentTypeCode {
865		return nil
866	}
867
868	if isResolve {
869		if comment.ResolveDoerID != 0 {
870			return nil
871		}
872
873		if _, err = db.GetEngine(db.DefaultContext).Exec("UPDATE `comment` SET resolve_doer_id=? WHERE id=?", doer.ID, comment.ID); err != nil {
874			return err
875		}
876	} else {
877		if comment.ResolveDoerID == 0 {
878			return nil
879		}
880
881		if _, err = db.GetEngine(db.DefaultContext).Exec("UPDATE `comment` SET resolve_doer_id=? WHERE id=?", 0, comment.ID); err != nil {
882			return err
883		}
884	}
885
886	return nil
887}
888
889// CanMarkConversation  Add or remove Conversation mark for a code comment permission check
890// the PR writer , offfcial reviewer and poster can do it
891func CanMarkConversation(issue *Issue, doer *user_model.User) (permResult bool, err error) {
892	if doer == nil || issue == nil {
893		return false, fmt.Errorf("issue or doer is nil")
894	}
895
896	if doer.ID != issue.PosterID {
897		if err = issue.LoadRepo(); err != nil {
898			return false, err
899		}
900
901		p, err := GetUserRepoPermission(issue.Repo, doer)
902		if err != nil {
903			return false, err
904		}
905
906		permResult = p.CanAccess(perm.AccessModeWrite, unit.TypePullRequests)
907		if !permResult {
908			if permResult, err = IsOfficialReviewer(issue, doer); err != nil {
909				return false, err
910			}
911		}
912
913		if !permResult {
914			return false, nil
915		}
916	}
917
918	return true, nil
919}
920
921// DeleteReview delete a review and it's code comments
922func DeleteReview(r *Review) error {
923	ctx, committer, err := db.TxContext()
924	if err != nil {
925		return err
926	}
927	defer committer.Close()
928	sess := db.GetEngine(ctx)
929
930	if r.ID == 0 {
931		return fmt.Errorf("review is not allowed to be 0")
932	}
933
934	if r.Type == ReviewTypeRequest {
935		return fmt.Errorf("review request can not be deleted using this method")
936	}
937
938	opts := FindCommentsOptions{
939		Type:     CommentTypeCode,
940		IssueID:  r.IssueID,
941		ReviewID: r.ID,
942	}
943
944	if _, err := sess.Where(opts.toConds()).Delete(new(Comment)); err != nil {
945		return err
946	}
947
948	opts = FindCommentsOptions{
949		Type:     CommentTypeReview,
950		IssueID:  r.IssueID,
951		ReviewID: r.ID,
952	}
953
954	if _, err := sess.Where(opts.toConds()).Delete(new(Comment)); err != nil {
955		return err
956	}
957
958	if _, err := sess.ID(r.ID).Delete(new(Review)); err != nil {
959		return err
960	}
961
962	return committer.Commit()
963}
964
965// GetCodeCommentsCount return count of CodeComments a Review has
966func (r *Review) GetCodeCommentsCount() int {
967	opts := FindCommentsOptions{
968		Type:     CommentTypeCode,
969		IssueID:  r.IssueID,
970		ReviewID: r.ID,
971	}
972	conds := opts.toConds()
973	if r.ID == 0 {
974		conds = conds.And(builder.Eq{"invalidated": false})
975	}
976
977	count, err := db.GetEngine(db.DefaultContext).Where(conds).Count(new(Comment))
978	if err != nil {
979		return 0
980	}
981	return int(count)
982}
983
984// HTMLURL formats a URL-string to the related review issue-comment
985func (r *Review) HTMLURL() string {
986	opts := FindCommentsOptions{
987		Type:     CommentTypeReview,
988		IssueID:  r.IssueID,
989		ReviewID: r.ID,
990	}
991	comment := new(Comment)
992	has, err := db.GetEngine(db.DefaultContext).Where(opts.toConds()).Get(comment)
993	if err != nil || !has {
994		return ""
995	}
996	return comment.HTMLURL()
997}
998