Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mark PR reviews as stale at push and allow to dismiss stale approvals #9532

Merged
merged 20 commits into from
Jan 9, 2020
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
1e723de
Check if push changes branch and mark reviews as stale
davidsvantesson Dec 28, 2019
92b55e7
Show old (stale) reviews with sandglass icon.
davidsvantesson Dec 28, 2019
fce82c4
Save review commit id and mark as not stale if force pushed back.
davidsvantesson Dec 29, 2019
1f4ccc9
Add migration step
davidsvantesson Dec 29, 2019
c65ba50
Merge branch 'master' into push-is-pr-changed
davidsvantesson Dec 29, 2019
74cd5a7
Assume changed if old commit not found
davidsvantesson Dec 29, 2019
bd32f94
Save commit id for complete review also when making code comments.
davidsvantesson Dec 30, 2019
edaa16e
Merge branch 'master' into push-is-pr-changed
davidsvantesson Jan 2, 2020
72d99aa
Add option to dismiss stale approvals
davidsvantesson Jan 2, 2020
0fc543f
Merge branch 'master' into push-is-pr-changed
davidsvantesson Jan 3, 2020
c076040
Fix review comments.
davidsvantesson Jan 5, 2020
69fec0c
Merge branch 'master' into push-is-pr-changed
davidsvantesson Jan 5, 2020
1c8a8b5
Merge branch 'master' into push-is-pr-changed
zeripath Jan 6, 2020
43b8b42
Merge branch 'master' into push-is-pr-changed
davidsvantesson Jan 7, 2020
97d2104
Merge branch 'master' into push-is-pr-changed
techknowlogick Jan 8, 2020
ffc6fcd
Merge branch 'master' into push-is-pr-changed
techknowlogick Jan 8, 2020
907f2db
Merge branch 'master' into push-is-pr-changed
lunny Jan 8, 2020
776904f
Merge branch 'master' into push-is-pr-changed
zeripath Jan 8, 2020
9994ac5
Merge branch 'master' into push-is-pr-changed
sapk Jan 8, 2020
3d11d3a
Merge branch 'master' into push-is-pr-changed
lafriks Jan 9, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions models/branches.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ type ProtectedBranch struct {
ApprovalsWhitelistUserIDs []int64 `xorm:"JSON TEXT"`
ApprovalsWhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
RequiredApprovals int64 `xorm:"NOT NULL DEFAULT 0"`
DismissStaleApprovals bool `xorm:"NOT NULL DEFAULT false"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}
Expand Down Expand Up @@ -154,10 +155,13 @@ func (protectBranch *ProtectedBranch) HasEnoughApprovals(pr *PullRequest) bool {

// GetGrantedApprovalsCount returns the number of granted approvals for pr. A granted approval must be authored by a user in an approval whitelist.
func (protectBranch *ProtectedBranch) GetGrantedApprovalsCount(pr *PullRequest) int64 {
approvals, err := x.Where("issue_id = ?", pr.IssueID).
sess := x.Where("issue_id = ?", pr.IssueID).
And("type = ?", ReviewTypeApprove).
And("official = ?", true).
Count(new(Review))
And("official = ?", true)
if protectBranch.DismissStaleApprovals {
sess = sess.And("stale = ?", false)
}
approvals, err := sess.Count(new(Review))
if err != nil {
log.Error("GetGrantedApprovalsCount: %v", err)
return 0
Expand Down
2 changes: 2 additions & 0 deletions models/migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,8 @@ var migrations = []Migration{
NewMigration("add user_id prefix to existing user avatar name", renameExistingUserAvatarName),
// v116 -> v117
NewMigration("Extend TrackedTimes", extendTrackedTimes),
// v117 -> v118
NewMigration("Add commit id and stale to reviews", addReviewCommitAndStale),
}

// Migrate database to current version
Expand Down
26 changes: 26 additions & 0 deletions models/migrations/v117.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package migrations

import (
"xorm.io/xorm"
)

func addReviewCommitAndStale(x *xorm.Engine) error {
type Review struct {
CommitID string `xorm:"VARCHAR(40)"`
Stale bool `xorm:"NOT NULL DEFAULT false"`
}

type ProtectedBranch struct {
DismissStaleApprovals bool `xorm:"NOT NULL DEFAULT false"`
}

// Old reviews will have commit ID set to "" and not stale
if err := x.Sync2(new(Review)); err != nil {
return err
}
return x.Sync2(new(ProtectedBranch))
}
30 changes: 27 additions & 3 deletions models/review.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ type Review struct {
IssueID int64 `xorm:"index"`
Content string `xorm:"TEXT"`
// Official is a review made by an assigned approver (counts towards approval)
Official bool `xorm:"NOT NULL DEFAULT false"`
Official bool `xorm:"NOT NULL DEFAULT false"`
CommitID string `xorm:"VARCHAR(40)"`
lafriks marked this conversation as resolved.
Show resolved Hide resolved
Stale bool `xorm:"NOT NULL DEFAULT false"`

CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
Expand Down Expand Up @@ -169,6 +171,8 @@ type CreateReviewOptions struct {
Issue *Issue
Reviewer *User
Official bool
CommitID string
Stale bool
}

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

// SubmitReview creates a review out of the existing pending review or creates a new one if no pending review exist
func SubmitReview(doer *User, issue *Issue, reviewType ReviewType, content string) (*Review, *Comment, error) {
func SubmitReview(doer *User, issue *Issue, reviewType ReviewType, content, commitID string, stale bool) (*Review, *Comment, error) {
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
Expand Down Expand Up @@ -295,6 +301,8 @@ func SubmitReview(doer *User, issue *Issue, reviewType ReviewType, content strin
Reviewer: doer,
Content: content,
Official: official,
CommitID: commitID,
Stale: stale,
})
if err != nil {
return nil, nil, err
Expand Down Expand Up @@ -322,8 +330,10 @@ func SubmitReview(doer *User, issue *Issue, reviewType ReviewType, content strin
review.Issue = issue
review.Content = content
review.Type = reviewType
review.CommitID = commitID
review.Stale = stale

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

return reviews, nil
}

// MarkReviewsAsStale marks existing reviews as stale
func MarkReviewsAsStale(issueID int64) (err error) {
_, err = x.Exec("UPDATE `review` SET stale=? WHERE issue_id=?", true, issueID)

return
}

// MarkReviewsAsNotStale marks existing reviews as not stale for a giving commit SHA
func MarkReviewsAsNotStale(issueID int64, commitID string) (err error) {
_, err = x.Exec("UPDATE `review` SET stale=? WHERE issue_id=? AND commit_id=?", false, issueID, commitID)

return
}
19 changes: 11 additions & 8 deletions modules/auth/repo_form.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ type ProtectBranchForm struct {
EnableApprovalsWhitelist bool
ApprovalsWhitelistUsers string
ApprovalsWhitelistTeams string
DismissStaleApprovals bool
}

// Validate validates the fields
Expand Down Expand Up @@ -455,12 +456,13 @@ func (f *MergePullRequestForm) Validate(ctx *macaron.Context, errs binding.Error

// CodeCommentForm form for adding code comments for PRs
type CodeCommentForm struct {
Content string `binding:"Required"`
Side string `binding:"Required;In(previous,proposed)"`
Line int64
TreePath string `form:"path" binding:"Required"`
IsReview bool `form:"is_review"`
Reply int64 `form:"reply"`
Content string `binding:"Required"`
Side string `binding:"Required;In(previous,proposed)"`
Line int64
TreePath string `form:"path" binding:"Required"`
IsReview bool `form:"is_review"`
Reply int64 `form:"reply"`
LatestCommitID string
}

// Validate validates the fields
Expand All @@ -470,8 +472,9 @@ func (f *CodeCommentForm) Validate(ctx *macaron.Context, errs binding.Errors) bi

// SubmitReviewForm for submitting a finished code review
type SubmitReviewForm struct {
Content string
Type string `binding:"Required;In(approve,comment,reject)"`
Content string
Type string `binding:"Required;In(approve,comment,reject)"`
CommitID string
}

// Validate validates the fields
Expand Down
6 changes: 6 additions & 0 deletions modules/git/repo_compare.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,9 @@ func (repo *Repository) GetPatch(base, head string, w io.Writer) error {
return NewCommand("format-patch", "--binary", "--stdout", base+"..."+head).
RunInDirPipeline(repo.Path, w, nil)
}

// GetDiffFromMergeBase generates and return patch data from merge base to head
func (repo *Repository) GetDiffFromMergeBase(base, head string, w io.Writer) error {
return NewCommand("diff", "-p", "--binary", base+"..."+head).
RunInDirPipeline(repo.Path, w, nil)
}
4 changes: 2 additions & 2 deletions modules/repofiles/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,7 @@ func PushUpdate(repo *models.Repository, branch string, opts PushUpdateOptions)

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

go pull_service.AddTestPullRequestTask(pusher, repo.ID, branch, true)
go pull_service.AddTestPullRequestTask(pusher, repo.ID, branch, true, opts.OldCommitID, opts.NewCommitID)

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

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

go pull_service.AddTestPullRequestTask(pusher, repo.ID, opts.Branch, true)
go pull_service.AddTestPullRequestTask(pusher, repo.ID, opts.Branch, true, opts.OldCommitID, opts.NewCommitID)

if err = models.WatchIfAuto(opts.PusherID, repo.ID, true); err != nil {
log.Warn("Fail to perform auto watch on user %v for repo %v: %v", opts.PusherID, repo.ID, err)
Expand Down
2 changes: 2 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -1411,6 +1411,8 @@ settings.protect_approvals_whitelist_enabled = Restrict approvals to whitelisted
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.
settings.protect_approvals_whitelist_users = Whitelisted reviewers:
settings.protect_approvals_whitelist_teams = Whitelisted teams for reviews:
settings.dismiss_stale_approvals = Dismiss stale approvals
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.
settings.add_protected_branch = Enable protection
settings.delete_protected_branch = Disable protection
settings.update_protect_branch_success = Branch protection for branch '%s' has been updated.
Expand Down
2 changes: 1 addition & 1 deletion routers/repo/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -822,7 +822,7 @@ func TriggerTask(ctx *context.Context) {

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

go pull_service.AddTestPullRequestTask(pusher, repo.ID, branch, true)
go pull_service.AddTestPullRequestTask(pusher, repo.ID, branch, true, "", "")
ctx.Status(202)
}

Expand Down
4 changes: 3 additions & 1 deletion routers/repo/pull_review.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,14 @@ func CreateCodeComment(ctx *context.Context, form auth.CodeCommentForm) {

comment, err := pull_service.CreateCodeComment(
ctx.User,
ctx.Repo.GitRepo,
issue,
signedLine,
form.Content,
form.TreePath,
form.IsReview,
form.Reply,
form.LatestCommitID,
)
if err != nil {
ctx.ServerError("CreateCodeComment", err)
Expand Down Expand Up @@ -95,7 +97,7 @@ func SubmitReview(ctx *context.Context, form auth.SubmitReviewForm) {
}
}

_, comm, err := pull_service.SubmitReview(ctx.User, issue, reviewType, form.Content)
_, comm, err := pull_service.SubmitReview(ctx.User, ctx.Repo.GitRepo, issue, reviewType, form.Content, form.CommitID)
if err != nil {
if models.IsContentEmptyErr(err) {
ctx.Flash.Error(ctx.Tr("repo.issues.review.content.empty"))
Expand Down
1 change: 1 addition & 0 deletions routers/repo/setting_protected_branch.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ func SettingsProtectedBranchPost(ctx *context.Context, f auth.ProtectBranchForm)
approvalsWhitelistTeams, _ = base.StringsToInt64s(strings.Split(f.ApprovalsWhitelistTeams, ","))
}
}
protectBranch.DismissStaleApprovals = f.DismissStaleApprovals
techknowlogick marked this conversation as resolved.
Show resolved Hide resolved

err = models.UpdateProtectBranch(ctx.Repo.Repository, protectBranch, models.WhitelistOptions{
UserIDs: whitelistUsers,
Expand Down
2 changes: 1 addition & 1 deletion services/pull/merge.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repositor
}

defer func() {
go AddTestPullRequestTask(doer, pr.BaseRepo.ID, pr.BaseBranch, false)
go AddTestPullRequestTask(doer, pr.BaseRepo.ID, pr.BaseBranch, false, "", "")
}()

// Clone base repo.
Expand Down
96 changes: 95 additions & 1 deletion services/pull/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,23 @@
package pull

import (
"bufio"
"bytes"
"context"
"fmt"
"os"
"path"
"strings"
"time"

"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/notification"
issue_service "code.gitea.io/gitea/services/issue"

"github.com/unknwon/com"
)

// NewPullRequest creates new pull request with labels for repository.
Expand Down Expand Up @@ -168,7 +174,7 @@ func addHeadRepoTasks(prs []*models.PullRequest) {

// AddTestPullRequestTask adds new test tasks by given head/base repository and head/base branch,
// and generate new patch for testing as needed.
func AddTestPullRequestTask(doer *models.User, repoID int64, branch string, isSync bool) {
func AddTestPullRequestTask(doer *models.User, repoID int64, branch string, isSync bool, oldCommitID, newCommitID string) {
log.Trace("AddTestPullRequestTask [head_repo_id: %d, head_branch: %s]: finding pull requests", repoID, branch)
graceful.GetManager().RunWithShutdownContext(func(ctx context.Context) {
// There is no sensible way to shut this down ":-("
Expand All @@ -191,6 +197,22 @@ func AddTestPullRequestTask(doer *models.User, repoID int64, branch string, isSy
}
if err == nil {
for _, pr := range prs {
if newCommitID != "" && newCommitID != git.EmptySHA {
changed, err := checkIfPRContentChanged(pr, oldCommitID, newCommitID)
if err != nil {
log.Error("checkIfPRContentChanged: %v", err)
}
if changed {
// Mark old reviews as stale if diff to mergebase has changed
if err := models.MarkReviewsAsStale(pr.IssueID); err != nil {
log.Error("MarkReviewsAsStale: %v", err)
}
}
if err := models.MarkReviewsAsNotStale(pr.IssueID, newCommitID); err != nil {
log.Error("MarkReviewsAsNotStale: %v", err)
}
}

pr.Issue.PullRequest = pr
notification.NotifyPullRequestSynchronized(doer, pr)
}
Expand All @@ -211,6 +233,78 @@ func AddTestPullRequestTask(doer *models.User, repoID int64, branch string, isSy
})
}

// checkIfPRContentChanged checks if diff to target branch has changed by push
// A commit can be considered to leave the PR untouched if the patch/diff with its merge base is unchanged
func checkIfPRContentChanged(pr *models.PullRequest, oldCommitID, newCommitID string) (hasChanged bool, err error) {

if err = pr.GetHeadRepo(); err != nil {
return false, fmt.Errorf("GetHeadRepo: %v", err)
} else if pr.HeadRepo == nil {
// corrupt data assumed changed
return true, nil
}

if err = pr.GetBaseRepo(); err != nil {
return false, fmt.Errorf("GetBaseRepo: %v", err)
}

headGitRepo, err := git.OpenRepository(pr.HeadRepo.RepoPath())
if err != nil {
return false, fmt.Errorf("OpenRepository: %v", err)
}
defer headGitRepo.Close()

// Add a temporary remote.
tmpRemote := com.ToStr(time.Now().UnixNano())
davidsvantesson marked this conversation as resolved.
Show resolved Hide resolved
if err = headGitRepo.AddRemote(tmpRemote, models.RepoPath(pr.BaseRepo.MustOwner().Name, pr.BaseRepo.Name), true); err != nil {
return false, fmt.Errorf("AddRemote: %v", err)
}
defer func() {
if err := headGitRepo.RemoveRemote(tmpRemote); err != nil {
log.Error("checkIfPRContentChanged: RemoveRemote: %s", err)
davidsvantesson marked this conversation as resolved.
Show resolved Hide resolved
}
}()
// To synchronize repo and get a base ref
_, base, err := headGitRepo.GetMergeBase(tmpRemote, pr.BaseBranch, pr.HeadBranch)
if err != nil {
return false, fmt.Errorf("GetMergeBase: %v", err)
}

diffBefore := &bytes.Buffer{}
diffAfter := &bytes.Buffer{}
if err := headGitRepo.GetDiffFromMergeBase(base, oldCommitID, diffBefore); err != nil {
lafriks marked this conversation as resolved.
Show resolved Hide resolved
// If old commit not found, assume changed.
log.Debug("GetDiffFromMergeBase: %v", err)
return true, nil
}
if err := headGitRepo.GetDiffFromMergeBase(base, newCommitID, diffAfter); err != nil {
// New commit should be found
return false, fmt.Errorf("GetDiffFromMergeBase: %v", err)
}

diffBeforeLines := bufio.NewScanner(diffBefore)
diffAfterLines := bufio.NewScanner(diffAfter)

for diffBeforeLines.Scan() && diffAfterLines.Scan() {
if strings.HasPrefix(diffBeforeLines.Text(), "index") && strings.HasPrefix(diffAfterLines.Text(), "index") {
// file hashes can change without the diff changing
continue
} else if strings.HasPrefix(diffBeforeLines.Text(), "@@") && strings.HasPrefix(diffAfterLines.Text(), "@@") {
// the location of the difference may change
continue
} else if !bytes.Equal(diffBeforeLines.Bytes(), diffAfterLines.Bytes()) {
return true, nil
}
}

if diffBeforeLines.Scan() || diffAfterLines.Scan() {
// Diffs not of equal length
return true, nil
}

return false, nil
}

// PushToBaseRepo pushes commits from branches of head repository to
// corresponding branches of base repository.
// FIXME: Only push branches that are actually updates?
Expand Down
Loading