Skip to content

Commit 25531c7

Browse files
davidsvantessonzeripath
authored andcommitted
Mark PR reviews as stale at push and allow to dismiss stale approvals (#9532)
Fix #5997. If a push causes the patch/diff of a PR towards target branch to change, all existing reviews for the PR will be set and shown as stale. New branch protection option to dismiss stale approvals are added. To show that a review is not based on the latest PR changes, an hourglass is shown
1 parent 5b2d933 commit 25531c7

File tree

18 files changed

+241
-40
lines changed

18 files changed

+241
-40
lines changed

models/branches.go

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -32,21 +32,23 @@ type ProtectedBranch struct {
3232
BranchName string `xorm:"UNIQUE(s)"`
3333
CanPush bool `xorm:"NOT NULL DEFAULT false"`
3434
EnableWhitelist bool
35-
WhitelistUserIDs []int64 `xorm:"JSON TEXT"`
36-
WhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
37-
EnableMergeWhitelist bool `xorm:"NOT NULL DEFAULT false"`
38-
WhitelistDeployKeys bool `xorm:"NOT NULL DEFAULT false"`
39-
MergeWhitelistUserIDs []int64 `xorm:"JSON TEXT"`
40-
MergeWhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
41-
EnableStatusCheck bool `xorm:"NOT NULL DEFAULT false"`
42-
StatusCheckContexts []string `xorm:"JSON TEXT"`
43-
EnableApprovalsWhitelist bool `xorm:"NOT NULL DEFAULT false"`
44-
ApprovalsWhitelistUserIDs []int64 `xorm:"JSON TEXT"`
45-
ApprovalsWhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
46-
RequiredApprovals int64 `xorm:"NOT NULL DEFAULT 0"`
47-
BlockOnRejectedReviews bool `xorm:"NOT NULL DEFAULT false"`
48-
CreatedUnix timeutil.TimeStamp `xorm:"created"`
49-
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
35+
WhitelistUserIDs []int64 `xorm:"JSON TEXT"`
36+
WhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
37+
EnableMergeWhitelist bool `xorm:"NOT NULL DEFAULT false"`
38+
WhitelistDeployKeys bool `xorm:"NOT NULL DEFAULT false"`
39+
MergeWhitelistUserIDs []int64 `xorm:"JSON TEXT"`
40+
MergeWhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
41+
EnableStatusCheck bool `xorm:"NOT NULL DEFAULT false"`
42+
StatusCheckContexts []string `xorm:"JSON TEXT"`
43+
EnableApprovalsWhitelist bool `xorm:"NOT NULL DEFAULT false"`
44+
ApprovalsWhitelistUserIDs []int64 `xorm:"JSON TEXT"`
45+
ApprovalsWhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
46+
RequiredApprovals int64 `xorm:"NOT NULL DEFAULT 0"`
47+
BlockOnRejectedReviews bool `xorm:"NOT NULL DEFAULT false"`
48+
DismissStaleApprovals bool `xorm:"NOT NULL DEFAULT false"`
49+
50+
CreatedUnix timeutil.TimeStamp `xorm:"created"`
51+
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
5052
}
5153

5254
// IsProtected returns if the branch is protected
@@ -155,10 +157,13 @@ func (protectBranch *ProtectedBranch) HasEnoughApprovals(pr *PullRequest) bool {
155157

156158
// GetGrantedApprovalsCount returns the number of granted approvals for pr. A granted approval must be authored by a user in an approval whitelist.
157159
func (protectBranch *ProtectedBranch) GetGrantedApprovalsCount(pr *PullRequest) int64 {
158-
approvals, err := x.Where("issue_id = ?", pr.IssueID).
160+
sess := x.Where("issue_id = ?", pr.IssueID).
159161
And("type = ?", ReviewTypeApprove).
160-
And("official = ?", true).
161-
Count(new(Review))
162+
And("official = ?", true)
163+
if protectBranch.DismissStaleApprovals {
164+
sess = sess.And("stale = ?", false)
165+
}
166+
approvals, err := sess.Count(new(Review))
162167
if err != nil {
163168
log.Error("GetGrantedApprovalsCount: %v", err)
164169
return 0

models/migrations/migrations.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,8 @@ var migrations = []Migration{
290290
NewMigration("Extend TrackedTimes", extendTrackedTimes),
291291
// v117 -> v118
292292
NewMigration("Add block on rejected reviews branch protection", addBlockOnRejectedReviews),
293+
// v118 -> v119
294+
NewMigration("Add commit id and stale to reviews", addReviewCommitAndStale),
293295
}
294296

295297
// Migrate database to current version

models/migrations/v118.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright 2019 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+
5+
package migrations
6+
7+
import (
8+
"xorm.io/xorm"
9+
)
10+
11+
func addReviewCommitAndStale(x *xorm.Engine) error {
12+
type Review struct {
13+
CommitID string `xorm:"VARCHAR(40)"`
14+
Stale bool `xorm:"NOT NULL DEFAULT false"`
15+
}
16+
17+
type ProtectedBranch struct {
18+
DismissStaleApprovals bool `xorm:"NOT NULL DEFAULT false"`
19+
}
20+
21+
// Old reviews will have commit ID set to "" and not stale
22+
if err := x.Sync2(new(Review)); err != nil {
23+
return err
24+
}
25+
return x.Sync2(new(ProtectedBranch))
26+
}

models/review.go

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@ type Review struct {
5353
IssueID int64 `xorm:"index"`
5454
Content string `xorm:"TEXT"`
5555
// Official is a review made by an assigned approver (counts towards approval)
56-
Official bool `xorm:"NOT NULL DEFAULT false"`
56+
Official bool `xorm:"NOT NULL DEFAULT false"`
57+
CommitID string `xorm:"VARCHAR(40)"`
58+
Stale bool `xorm:"NOT NULL DEFAULT false"`
5759

5860
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
5961
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
@@ -169,6 +171,8 @@ type CreateReviewOptions struct {
169171
Issue *Issue
170172
Reviewer *User
171173
Official bool
174+
CommitID string
175+
Stale bool
172176
}
173177

174178
// IsOfficialReviewer check if reviewer can make official reviews in issue (counts towards required approvals)
@@ -200,6 +204,8 @@ func createReview(e Engine, opts CreateReviewOptions) (*Review, error) {
200204
ReviewerID: opts.Reviewer.ID,
201205
Content: opts.Content,
202206
Official: opts.Official,
207+
CommitID: opts.CommitID,
208+
Stale: opts.Stale,
203209
}
204210
if _, err := e.Insert(review); err != nil {
205211
return nil, err
@@ -258,7 +264,7 @@ func IsContentEmptyErr(err error) bool {
258264
}
259265

260266
// SubmitReview creates a review out of the existing pending review or creates a new one if no pending review exist
261-
func SubmitReview(doer *User, issue *Issue, reviewType ReviewType, content string) (*Review, *Comment, error) {
267+
func SubmitReview(doer *User, issue *Issue, reviewType ReviewType, content, commitID string, stale bool) (*Review, *Comment, error) {
262268
sess := x.NewSession()
263269
defer sess.Close()
264270
if err := sess.Begin(); err != nil {
@@ -295,6 +301,8 @@ func SubmitReview(doer *User, issue *Issue, reviewType ReviewType, content strin
295301
Reviewer: doer,
296302
Content: content,
297303
Official: official,
304+
CommitID: commitID,
305+
Stale: stale,
298306
})
299307
if err != nil {
300308
return nil, nil, err
@@ -322,8 +330,10 @@ func SubmitReview(doer *User, issue *Issue, reviewType ReviewType, content strin
322330
review.Issue = issue
323331
review.Content = content
324332
review.Type = reviewType
333+
review.CommitID = commitID
334+
review.Stale = stale
325335

326-
if _, err := sess.ID(review.ID).Cols("content, type, official").Update(review); err != nil {
336+
if _, err := sess.ID(review.ID).Cols("content, type, official, commit_id, stale").Update(review); err != nil {
327337
return nil, nil, err
328338
}
329339
}
@@ -374,3 +384,17 @@ func GetReviewersByIssueID(issueID int64) (reviews []*Review, err error) {
374384

375385
return reviews, nil
376386
}
387+
388+
// MarkReviewsAsStale marks existing reviews as stale
389+
func MarkReviewsAsStale(issueID int64) (err error) {
390+
_, err = x.Exec("UPDATE `review` SET stale=? WHERE issue_id=?", true, issueID)
391+
392+
return
393+
}
394+
395+
// MarkReviewsAsNotStale marks existing reviews as not stale for a giving commit SHA
396+
func MarkReviewsAsNotStale(issueID int64, commitID string) (err error) {
397+
_, err = x.Exec("UPDATE `review` SET stale=? WHERE issue_id=? AND commit_id=?", false, issueID, commitID)
398+
399+
return
400+
}

modules/auth/repo_form.go

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ type ProtectBranchForm struct {
172172
ApprovalsWhitelistUsers string
173173
ApprovalsWhitelistTeams string
174174
BlockOnRejectedReviews bool
175+
DismissStaleApprovals bool
175176
}
176177

177178
// Validate validates the fields
@@ -456,12 +457,13 @@ func (f *MergePullRequestForm) Validate(ctx *macaron.Context, errs binding.Error
456457

457458
// CodeCommentForm form for adding code comments for PRs
458459
type CodeCommentForm struct {
459-
Content string `binding:"Required"`
460-
Side string `binding:"Required;In(previous,proposed)"`
461-
Line int64
462-
TreePath string `form:"path" binding:"Required"`
463-
IsReview bool `form:"is_review"`
464-
Reply int64 `form:"reply"`
460+
Content string `binding:"Required"`
461+
Side string `binding:"Required;In(previous,proposed)"`
462+
Line int64
463+
TreePath string `form:"path" binding:"Required"`
464+
IsReview bool `form:"is_review"`
465+
Reply int64 `form:"reply"`
466+
LatestCommitID string
465467
}
466468

467469
// Validate validates the fields
@@ -471,8 +473,9 @@ func (f *CodeCommentForm) Validate(ctx *macaron.Context, errs binding.Errors) bi
471473

472474
// SubmitReviewForm for submitting a finished code review
473475
type SubmitReviewForm struct {
474-
Content string
475-
Type string `binding:"Required;In(approve,comment,reject)"`
476+
Content string
477+
Type string `binding:"Required;In(approve,comment,reject)"`
478+
CommitID string
476479
}
477480

478481
// Validate validates the fields

modules/git/repo_compare.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,3 +112,9 @@ func (repo *Repository) GetPatch(base, head string, w io.Writer) error {
112112
return NewCommand("format-patch", "--binary", "--stdout", base+"..."+head).
113113
RunInDirPipeline(repo.Path, w, nil)
114114
}
115+
116+
// GetDiffFromMergeBase generates and return patch data from merge base to head
117+
func (repo *Repository) GetDiffFromMergeBase(base, head string, w io.Writer) error {
118+
return NewCommand("diff", "-p", "--binary", base+"..."+head).
119+
RunInDirPipeline(repo.Path, w, nil)
120+
}

modules/repofiles/update.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,7 @@ func PushUpdate(repo *models.Repository, branch string, opts PushUpdateOptions)
477477

478478
log.Trace("TriggerTask '%s/%s' by %s", repo.Name, branch, pusher.Name)
479479

480-
go pull_service.AddTestPullRequestTask(pusher, repo.ID, branch, true)
480+
go pull_service.AddTestPullRequestTask(pusher, repo.ID, branch, true, opts.OldCommitID, opts.NewCommitID)
481481

482482
if err = models.WatchIfAuto(opts.PusherID, repo.ID, true); err != nil {
483483
log.Warn("Fail to perform auto watch on user %v for repo %v: %v", opts.PusherID, repo.ID, err)
@@ -528,7 +528,7 @@ func PushUpdates(repo *models.Repository, optsList []*PushUpdateOptions) error {
528528

529529
log.Trace("TriggerTask '%s/%s' by %s", repo.Name, opts.Branch, pusher.Name)
530530

531-
go pull_service.AddTestPullRequestTask(pusher, repo.ID, opts.Branch, true)
531+
go pull_service.AddTestPullRequestTask(pusher, repo.ID, opts.Branch, true, opts.OldCommitID, opts.NewCommitID)
532532

533533
if err = models.WatchIfAuto(opts.PusherID, repo.ID, true); err != nil {
534534
log.Warn("Fail to perform auto watch on user %v for repo %v: %v", opts.PusherID, repo.ID, err)

options/locale/locale_en-US.ini

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1413,6 +1413,8 @@ settings.protect_approvals_whitelist_enabled = Restrict approvals to whitelisted
14131413
settings.protect_approvals_whitelist_enabled_desc = Only reviews from whitelisted users or teams will count to the required approvals. Without approval whitelist, reviews from anyone with write access count to the required approvals.
14141414
settings.protect_approvals_whitelist_users = Whitelisted reviewers:
14151415
settings.protect_approvals_whitelist_teams = Whitelisted teams for reviews:
1416+
settings.dismiss_stale_approvals = Dismiss stale approvals
1417+
settings.dismiss_stale_approvals_desc = When new commits that change the content of the pull request are pushed to the branch, old approvals will be dismissed.
14161418
settings.add_protected_branch = Enable protection
14171419
settings.delete_protected_branch = Disable protection
14181420
settings.update_protect_branch_success = Branch protection for branch '%s' has been updated.

routers/repo/pull.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -841,7 +841,7 @@ func TriggerTask(ctx *context.Context) {
841841

842842
log.Trace("TriggerTask '%s/%s' by %s", repo.Name, branch, pusher.Name)
843843

844-
go pull_service.AddTestPullRequestTask(pusher, repo.ID, branch, true)
844+
go pull_service.AddTestPullRequestTask(pusher, repo.ID, branch, true, "", "")
845845
ctx.Status(202)
846846
}
847847

routers/repo/pull_review.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,14 @@ func CreateCodeComment(ctx *context.Context, form auth.CodeCommentForm) {
3737

3838
comment, err := pull_service.CreateCodeComment(
3939
ctx.User,
40+
ctx.Repo.GitRepo,
4041
issue,
4142
signedLine,
4243
form.Content,
4344
form.TreePath,
4445
form.IsReview,
4546
form.Reply,
47+
form.LatestCommitID,
4648
)
4749
if err != nil {
4850
ctx.ServerError("CreateCodeComment", err)
@@ -95,7 +97,7 @@ func SubmitReview(ctx *context.Context, form auth.SubmitReviewForm) {
9597
}
9698
}
9799

98-
_, comm, err := pull_service.SubmitReview(ctx.User, issue, reviewType, form.Content)
100+
_, comm, err := pull_service.SubmitReview(ctx.User, ctx.Repo.GitRepo, issue, reviewType, form.Content, form.CommitID)
99101
if err != nil {
100102
if models.IsContentEmptyErr(err) {
101103
ctx.Flash.Error(ctx.Tr("repo.issues.review.content.empty"))

routers/repo/setting_protected_branch.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,7 @@ func SettingsProtectedBranchPost(ctx *context.Context, f auth.ProtectBranchForm)
245245
}
246246
}
247247
protectBranch.BlockOnRejectedReviews = f.BlockOnRejectedReviews
248+
protectBranch.DismissStaleApprovals = f.DismissStaleApprovals
248249

249250
err = models.UpdateProtectBranch(ctx.Repo.Repository, protectBranch, models.WhitelistOptions{
250251
UserIDs: whitelistUsers,

services/pull/merge.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repositor
6464
}
6565

6666
defer func() {
67-
go AddTestPullRequestTask(doer, pr.BaseRepo.ID, pr.BaseBranch, false)
67+
go AddTestPullRequestTask(doer, pr.BaseRepo.ID, pr.BaseBranch, false, "", "")
6868
}()
6969

7070
// Clone base repo.

0 commit comments

Comments
 (0)