Skip to content

Commit 242b331

Browse files
authored
Prevent re-review and dismiss review actions on closed and merged PRs (#30065)
Resolves #29965. --- Manually tested this by: - Following the [installation](https://docs.gitea.com/next/installation/install-with-docker#basics) guide (but built a local Docker image instead) - Creating 2 users, one who is the `Owner` of a newly-created repository and the other a `Collaborator` - Had the `Collaborator` create a PR that the `Owner` reviews - `Collaborator` resolves conversation and `Owner` merges PR And with this change we see that we can no longer see re-request review button for the `Owner`: <img width="1351" alt="Screenshot 2024-03-25 at 12 39 18 AM" src="https://github.com/go-gitea/gitea/assets/60799661/bcd9c579-3cf7-474f-a51e-b436fe1a39a4">
1 parent e40fc75 commit 242b331

File tree

9 files changed

+170
-11
lines changed

9 files changed

+170
-11
lines changed

models/issues/review.go

+35-3
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,23 @@ func (err ErrNotValidReviewRequest) Unwrap() error {
6666
return util.ErrInvalidArgument
6767
}
6868

69+
// ErrReviewRequestOnClosedPR represents an error when an user tries to request a re-review on a closed or merged PR.
70+
type ErrReviewRequestOnClosedPR struct{}
71+
72+
// IsErrReviewRequestOnClosedPR checks if an error is an ErrReviewRequestOnClosedPR.
73+
func IsErrReviewRequestOnClosedPR(err error) bool {
74+
_, ok := err.(ErrReviewRequestOnClosedPR)
75+
return ok
76+
}
77+
78+
func (err ErrReviewRequestOnClosedPR) Error() string {
79+
return "cannot request a re-review on a closed or merged PR"
80+
}
81+
82+
func (err ErrReviewRequestOnClosedPR) Unwrap() error {
83+
return util.ErrPermissionDenied
84+
}
85+
6986
// ReviewType defines the sort of feedback a review gives
7087
type ReviewType int
7188

@@ -618,9 +635,24 @@ func AddReviewRequest(ctx context.Context, issue *Issue, reviewer, doer *user_mo
618635
return nil, err
619636
}
620637

621-
// skip it when reviewer hase been request to review
622-
if review != nil && review.Type == ReviewTypeRequest {
623-
return nil, committer.Commit() // still commit the transaction, or committer.Close() will rollback it, even if it's a reused transaction.
638+
if review != nil {
639+
// skip it when reviewer hase been request to review
640+
if review.Type == ReviewTypeRequest {
641+
return nil, committer.Commit() // still commit the transaction, or committer.Close() will rollback it, even if it's a reused transaction.
642+
}
643+
644+
if issue.IsClosed {
645+
return nil, ErrReviewRequestOnClosedPR{}
646+
}
647+
648+
if issue.IsPull {
649+
if err := issue.LoadPullRequest(ctx); err != nil {
650+
return nil, err
651+
}
652+
if issue.PullRequest.HasMerged {
653+
return nil, ErrReviewRequestOnClosedPR{}
654+
}
655+
}
624656
}
625657

626658
// if the reviewer is an official reviewer,

models/issues/review_test.go

+30
Original file line numberDiff line numberDiff line change
@@ -288,3 +288,33 @@ func TestDeleteDismissedReview(t *testing.T) {
288288
assert.NoError(t, issues_model.DeleteReview(db.DefaultContext, review))
289289
unittest.AssertNotExistsBean(t, &issues_model.Comment{ID: comment.ID})
290290
}
291+
292+
func TestAddReviewRequest(t *testing.T) {
293+
assert.NoError(t, unittest.PrepareTestDatabase())
294+
295+
pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1})
296+
assert.NoError(t, pull.LoadIssue(db.DefaultContext))
297+
issue := pull.Issue
298+
assert.NoError(t, issue.LoadRepo(db.DefaultContext))
299+
reviewer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
300+
_, err := issues_model.CreateReview(db.DefaultContext, issues_model.CreateReviewOptions{
301+
Issue: issue,
302+
Reviewer: reviewer,
303+
Type: issues_model.ReviewTypeReject,
304+
})
305+
306+
assert.NoError(t, err)
307+
pull.HasMerged = false
308+
assert.NoError(t, pull.UpdateCols(db.DefaultContext, "has_merged"))
309+
issue.IsClosed = true
310+
_, err = issues_model.AddReviewRequest(db.DefaultContext, issue, reviewer, &user_model.User{})
311+
assert.Error(t, err)
312+
assert.True(t, issues_model.IsErrReviewRequestOnClosedPR(err))
313+
314+
pull.HasMerged = true
315+
assert.NoError(t, pull.UpdateCols(db.DefaultContext, "has_merged"))
316+
issue.IsClosed = false
317+
_, err = issues_model.AddReviewRequest(db.DefaultContext, issue, reviewer, &user_model.User{})
318+
assert.Error(t, err)
319+
assert.True(t, issues_model.IsErrReviewRequestOnClosedPR(err))
320+
}

routers/api/v1/repo/pull_review.go

+11-6
Original file line numberDiff line numberDiff line change
@@ -640,6 +640,8 @@ func DeleteReviewRequests(ctx *context.APIContext) {
640640
// "$ref": "#/responses/empty"
641641
// "422":
642642
// "$ref": "#/responses/validationError"
643+
// "403":
644+
// "$ref": "#/responses/forbidden"
643645
// "404":
644646
// "$ref": "#/responses/notFound"
645647
opts := web.GetForm(ctx).(*api.PullReviewRequestOptions)
@@ -708,6 +710,10 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions
708710
for _, reviewer := range reviewers {
709711
comment, err := issue_service.ReviewRequest(ctx, pr.Issue, ctx.Doer, reviewer, isAdd)
710712
if err != nil {
713+
if issues_model.IsErrReviewRequestOnClosedPR(err) {
714+
ctx.Error(http.StatusForbidden, "", err)
715+
return
716+
}
711717
ctx.Error(http.StatusInternalServerError, "ReviewRequest", err)
712718
return
713719
}
@@ -874,7 +880,7 @@ func dismissReview(ctx *context.APIContext, msg string, isDismiss, dismissPriors
874880
ctx.Error(http.StatusForbidden, "", "Must be repo admin")
875881
return
876882
}
877-
review, pr, isWrong := prepareSingleReview(ctx)
883+
review, _, isWrong := prepareSingleReview(ctx)
878884
if isWrong {
879885
return
880886
}
@@ -884,13 +890,12 @@ func dismissReview(ctx *context.APIContext, msg string, isDismiss, dismissPriors
884890
return
885891
}
886892

887-
if pr.Issue.IsClosed {
888-
ctx.Error(http.StatusForbidden, "", "not need to dismiss this review because this pr is closed")
889-
return
890-
}
891-
892893
_, err := pull_service.DismissReview(ctx, review.ID, ctx.Repo.Repository.ID, msg, ctx.Doer, isDismiss, dismissPriors)
893894
if err != nil {
895+
if pull_service.IsErrDismissRequestOnClosedPR(err) {
896+
ctx.Error(http.StatusForbidden, "", err)
897+
return
898+
}
894899
ctx.Error(http.StatusInternalServerError, "pull_service.DismissReview", err)
895900
return
896901
}

routers/web/repo/issue.go

+4
Original file line numberDiff line numberDiff line change
@@ -2498,6 +2498,10 @@ func UpdatePullReviewRequest(ctx *context.Context) {
24982498

24992499
_, err = issue_service.ReviewRequest(ctx, issue, ctx.Doer, reviewer, action == "attach")
25002500
if err != nil {
2501+
if issues_model.IsErrReviewRequestOnClosedPR(err) {
2502+
ctx.Status(http.StatusForbidden)
2503+
return
2504+
}
25012505
ctx.ServerError("ReviewRequest", err)
25022506
return
25032507
}

routers/web/repo/pull_review.go

+4
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,10 @@ func DismissReview(ctx *context.Context) {
277277
form := web.GetForm(ctx).(*forms.DismissReviewForm)
278278
comm, err := pull_service.DismissReview(ctx, form.ReviewID, ctx.Repo.Repository.ID, form.Message, ctx.Doer, true, true)
279279
if err != nil {
280+
if pull_service.IsErrDismissRequestOnClosedPR(err) {
281+
ctx.Status(http.StatusForbidden)
282+
return
283+
}
280284
ctx.ServerError("pull_service.DismissReview", err)
281285
return
282286
}

services/pull/review.go

+33
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,29 @@ import (
2020
"code.gitea.io/gitea/modules/log"
2121
"code.gitea.io/gitea/modules/optional"
2222
"code.gitea.io/gitea/modules/setting"
23+
"code.gitea.io/gitea/modules/util"
2324
notify_service "code.gitea.io/gitea/services/notify"
2425
)
2526

2627
var notEnoughLines = regexp.MustCompile(`fatal: file .* has only \d+ lines?`)
2728

29+
// ErrDismissRequestOnClosedPR represents an error when an user tries to dismiss a review associated to a closed or merged PR.
30+
type ErrDismissRequestOnClosedPR struct{}
31+
32+
// IsErrDismissRequestOnClosedPR checks if an error is an ErrDismissRequestOnClosedPR.
33+
func IsErrDismissRequestOnClosedPR(err error) bool {
34+
_, ok := err.(ErrDismissRequestOnClosedPR)
35+
return ok
36+
}
37+
38+
func (err ErrDismissRequestOnClosedPR) Error() string {
39+
return "can't dismiss a review associated to a closed or merged PR"
40+
}
41+
42+
func (err ErrDismissRequestOnClosedPR) Unwrap() error {
43+
return util.ErrPermissionDenied
44+
}
45+
2846
// checkInvalidation checks if the line of code comment got changed by another commit.
2947
// If the line got changed the comment is going to be invalidated.
3048
func checkInvalidation(ctx context.Context, c *issues_model.Comment, doer *user_model.User, repo *git.Repository, branch string) error {
@@ -382,6 +400,21 @@ func DismissReview(ctx context.Context, reviewID, repoID int64, message string,
382400
return nil, fmt.Errorf("reviews's repository is not the same as the one we expect")
383401
}
384402

403+
issue := review.Issue
404+
405+
if issue.IsClosed {
406+
return nil, ErrDismissRequestOnClosedPR{}
407+
}
408+
409+
if issue.IsPull {
410+
if err := issue.LoadPullRequest(ctx); err != nil {
411+
return nil, err
412+
}
413+
if issue.PullRequest.HasMerged {
414+
return nil, ErrDismissRequestOnClosedPR{}
415+
}
416+
}
417+
385418
if err := issues_model.DismissReview(ctx, review, isDismiss); err != nil {
386419
return nil, err
387420
}

services/pull/review_test.go

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package pull_test
5+
6+
import (
7+
"testing"
8+
9+
"code.gitea.io/gitea/models/db"
10+
issues_model "code.gitea.io/gitea/models/issues"
11+
"code.gitea.io/gitea/models/unittest"
12+
user_model "code.gitea.io/gitea/models/user"
13+
pull_service "code.gitea.io/gitea/services/pull"
14+
15+
"github.com/stretchr/testify/assert"
16+
)
17+
18+
func TestDismissReview(t *testing.T) {
19+
assert.NoError(t, unittest.PrepareTestDatabase())
20+
21+
pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{})
22+
assert.NoError(t, pull.LoadIssue(db.DefaultContext))
23+
issue := pull.Issue
24+
assert.NoError(t, issue.LoadRepo(db.DefaultContext))
25+
reviewer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
26+
review, err := issues_model.CreateReview(db.DefaultContext, issues_model.CreateReviewOptions{
27+
Issue: issue,
28+
Reviewer: reviewer,
29+
Type: issues_model.ReviewTypeReject,
30+
})
31+
32+
assert.NoError(t, err)
33+
issue.IsClosed = true
34+
pull.HasMerged = false
35+
assert.NoError(t, issues_model.UpdateIssueCols(db.DefaultContext, issue, "is_closed"))
36+
assert.NoError(t, pull.UpdateCols(db.DefaultContext, "has_merged"))
37+
_, err = pull_service.DismissReview(db.DefaultContext, review.ID, issue.RepoID, "", &user_model.User{}, false, false)
38+
assert.Error(t, err)
39+
assert.True(t, pull_service.IsErrDismissRequestOnClosedPR(err))
40+
41+
pull.HasMerged = true
42+
pull.Issue.IsClosed = false
43+
assert.NoError(t, issues_model.UpdateIssueCols(db.DefaultContext, issue, "is_closed"))
44+
assert.NoError(t, pull.UpdateCols(db.DefaultContext, "has_merged"))
45+
_, err = pull_service.DismissReview(db.DefaultContext, review.ID, issue.RepoID, "", &user_model.User{}, false, false)
46+
assert.Error(t, err)
47+
assert.True(t, pull_service.IsErrDismissRequestOnClosedPR(err))
48+
}

templates/repo/issue/view_content/sidebar.tmpl

+2-2
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@
5959
{{end}}
6060
</div>
6161
<div class="tw-flex tw-items-center tw-gap-2">
62-
{{if (and $.Permission.IsAdmin (or (eq .Review.Type 1) (eq .Review.Type 3)) (not $.Issue.IsClosed))}}
62+
{{if (and $.Permission.IsAdmin (or (eq .Review.Type 1) (eq .Review.Type 3)) (not $.Issue.IsClosed) (not $.Issue.PullRequest.HasMerged))}}
6363
<a href="#" class="ui muted icon tw-flex tw-items-center show-modal" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.dismiss_review"}}" data-modal="#dismiss-review-modal-{{.Review.ID}}">
6464
{{svg "octicon-x" 20}}
6565
</a>
@@ -91,7 +91,7 @@
9191
{{svg "octicon-hourglass" 16}}
9292
</span>
9393
{{end}}
94-
{{if .CanChange}}
94+
{{if and .CanChange (or .Checked (and (not $.Issue.IsClosed) (not $.Issue.PullRequest.HasMerged)))}}
9595
<a href="#" class="ui muted icon re-request-review{{if .Checked}} checked{{end}}" data-tooltip-content="{{if .Checked}}{{ctx.Locale.Tr "repo.issues.remove_request_review"}}{{else}}{{ctx.Locale.Tr "repo.issues.re_request_review"}}{{end}}" data-issue-id="{{$.Issue.ID}}" data-id="{{.ItemID}}" data-update-url="{{$.RepoLink}}/issues/request_review">{{if .Checked}}{{svg "octicon-trash"}}{{else}}{{svg "octicon-sync"}}{{end}}</a>
9696
{{end}}
9797
{{svg (printf "octicon-%s" .Review.Type.Icon) 16 (printf "text %s" (.Review.HTMLTypeColorName))}}

templates/swagger/v1_json.tmpl

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)