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 4 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
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
19 changes: 19 additions & 0 deletions models/migrations/v117.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// 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"`
}

// Old reviews will have commit ID set to "" and not stale
return x.Sync2(new(Review))
}
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 @@ -165,6 +167,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 @@ -196,6 +200,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 @@ -254,7 +260,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, commitSHA string, stale bool) (*Review, *Comment, error) {
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
Expand Down Expand Up @@ -291,6 +297,8 @@ func SubmitReview(doer *User, issue *Issue, reviewType ReviewType, content strin
Reviewer: doer,
Content: content,
Official: official,
CommitID: commitSHA,
Stale: stale,
})
if err != nil {
return nil, nil, err
Expand Down Expand Up @@ -318,8 +326,10 @@ func SubmitReview(doer *User, issue *Issue, reviewType ReviewType, content strin
review.Issue = issue
review.Content = content
review.Type = reviewType
review.CommitID = commitSHA
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 @@ -370,3 +380,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
}
5 changes: 3 additions & 2 deletions modules/auth/repo_form.go
Original file line number Diff line number Diff line change
Expand Up @@ -470,8 +470,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: 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
3 changes: 2 additions & 1 deletion routers/repo/pull_review.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ func CreateCodeComment(ctx *context.Context, form auth.CodeCommentForm) {

comment, err := pull_service.CreateCodeComment(
ctx.User,
ctx.Repo.GitRepo,
issue,
signedLine,
form.Content,
Expand Down Expand Up @@ -95,7 +96,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
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
93 changes: 92 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,75 @@ 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
return false, fmt.Errorf("GetDiffFromMergeBase: %v", err)
}
if err := headGitRepo.GetDiffFromMergeBase(base, newCommitID, diffAfter); err != nil {
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
30 changes: 25 additions & 5 deletions services/pull/review.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
)

// CreateCodeComment creates a comment on the code line
func CreateCodeComment(doer *models.User, issue *models.Issue, line int64, content string, treePath string, isReview bool, replyReviewID int64) (*models.Comment, error) {
func CreateCodeComment(doer *models.User, gitRepo *git.Repository, issue *models.Issue, line int64, content string, treePath string, isReview bool, replyReviewID int64) (*models.Comment, error) {

var (
existsReview bool
Expand Down Expand Up @@ -94,7 +94,7 @@ func CreateCodeComment(doer *models.User, issue *models.Issue, line int64, conte

if !isReview && !existsReview {
// Submit the review we've just created so the comment shows up in the issue view
if _, _, err = SubmitReview(doer, issue, models.ReviewTypeComment, ""); err != nil {
if _, _, err = SubmitReview(doer, gitRepo, issue, models.ReviewTypeComment, "", ""); err != nil {
return nil, err
}
}
Expand Down Expand Up @@ -159,16 +159,36 @@ func createCodeComment(doer *models.User, repo *models.Repository, issue *models
}

// SubmitReview creates a review out of the existing pending review or creates a new one if no pending review exist
func SubmitReview(doer *models.User, issue *models.Issue, reviewType models.ReviewType, content string) (*models.Review, *models.Comment, error) {
review, comm, err := models.SubmitReview(doer, issue, reviewType, content)
func SubmitReview(doer *models.User, gitRepo *git.Repository, issue *models.Issue, reviewType models.ReviewType, content, commitSHA string) (*models.Review, *models.Comment, error) {
pr, err := issue.GetPullRequest()
if err != nil {
return nil, nil, err
}

pr, err := issue.GetPullRequest()
var stale bool
if reviewType != models.ReviewTypeApprove && reviewType != models.ReviewTypeReject {
stale = false
} else {
headCommitID, err := gitRepo.GetRefCommitID(pr.GetGitRefName())
if err != nil {
return nil, nil, err
}

if headCommitID == commitSHA {
stale = false
} else {
stale, err = checkIfPRContentChanged(pr, commitSHA, headCommitID)
if err != nil {
return nil, nil, err
}
}
}

review, comm, err := models.SubmitReview(doer, issue, reviewType, content, commitSHA, stale)
if err != nil {
return nil, nil, err
}

notification.NotifyPullRequestReview(pr, review, comm)

return review, comm, nil
Expand Down
1 change: 1 addition & 0 deletions templates/repo/diff/new_review.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<div class="ui clearing segment">
<form class="ui form" action="{{.Link}}/reviews/submit" method="post">
{{.CsrfTokenHtml}}
<input type="hidden" name="commit_id" value="{{.AfterCommitID}}"/>
<i class="ui right floated link icon close"></i>
<div class="header">
{{$.i18n.Tr "repo.diff.review.header"}}
Expand Down
5 changes: 5 additions & 0 deletions templates/repo/issue/view_content/pull.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@
{{else}}grey{{end}}">
<span class="octicon octicon-{{.Type.Icon}}"></span>
</span>
{{if .Stale}}
<span class="type-icon text grey">
<i class="octicon icon fa-hourglass-end"></i>
</span>
{{end}}
<a class="ui avatar image" href="{{.Reviewer.HomeLink}}">
<img src="{{.Reviewer.RelAvatarLink}}">
</a>
Expand Down