Skip to content

Commit 217d71c

Browse files
authored
Workaround to clean up old reviews on creating a new one (#28554)
close #28542 blocks #28544 --- *Sponsored by Kithara Software GmbH*
1 parent 39a77d9 commit 217d71c

File tree

3 files changed

+165
-9
lines changed

3 files changed

+165
-9
lines changed

models/issues/review.go

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -292,8 +292,14 @@ func IsOfficialReviewerTeam(ctx context.Context, issue *Issue, team *organizatio
292292

293293
// CreateReview creates a new review based on opts
294294
func CreateReview(ctx context.Context, opts CreateReviewOptions) (*Review, error) {
295+
ctx, committer, err := db.TxContext(ctx)
296+
if err != nil {
297+
return nil, err
298+
}
299+
defer committer.Close()
300+
sess := db.GetEngine(ctx)
301+
295302
review := &Review{
296-
Type: opts.Type,
297303
Issue: opts.Issue,
298304
IssueID: opts.Issue.ID,
299305
Reviewer: opts.Reviewer,
@@ -303,15 +309,39 @@ func CreateReview(ctx context.Context, opts CreateReviewOptions) (*Review, error
303309
CommitID: opts.CommitID,
304310
Stale: opts.Stale,
305311
}
312+
306313
if opts.Reviewer != nil {
314+
review.Type = opts.Type
307315
review.ReviewerID = opts.Reviewer.ID
308-
} else {
309-
if review.Type != ReviewTypeRequest {
310-
review.Type = ReviewTypeRequest
316+
317+
reviewCond := builder.Eq{"reviewer_id": opts.Reviewer.ID, "issue_id": opts.Issue.ID}
318+
// make sure user review requests are cleared
319+
if opts.Type != ReviewTypePending {
320+
if _, err := sess.Where(reviewCond.And(builder.Eq{"type": ReviewTypeRequest})).Delete(new(Review)); err != nil {
321+
return nil, err
322+
}
311323
}
324+
// make sure if the created review gets dismissed no old review surface
325+
// other types can be ignored, as they don't affect branch protection
326+
if opts.Type == ReviewTypeApprove || opts.Type == ReviewTypeReject {
327+
if _, err := sess.Where(reviewCond.And(builder.In("type", ReviewTypeApprove, ReviewTypeReject))).
328+
Cols("dismissed").Update(&Review{Dismissed: true}); err != nil {
329+
return nil, err
330+
}
331+
}
332+
333+
} else if opts.ReviewerTeam != nil {
334+
review.Type = ReviewTypeRequest
312335
review.ReviewerTeamID = opts.ReviewerTeam.ID
336+
337+
} else {
338+
return nil, fmt.Errorf("provide either reviewer or reviewer team")
339+
}
340+
341+
if _, err := sess.Insert(review); err != nil {
342+
return nil, err
313343
}
314-
return review, db.Insert(ctx, review)
344+
return review, committer.Commit()
315345
}
316346

317347
// GetCurrentReview returns the current pending review of reviewer for given issue

models/unittest/unit_tests.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,8 @@ func AssertSuccessfulInsert(t assert.TestingT, beans ...any) {
131131
}
132132

133133
// AssertCount assert the count of a bean
134-
func AssertCount(t assert.TestingT, bean, expected any) {
135-
assert.EqualValues(t, expected, GetCount(t, bean))
134+
func AssertCount(t assert.TestingT, bean, expected any) bool {
135+
return assert.EqualValues(t, expected, GetCount(t, bean))
136136
}
137137

138138
// AssertInt64InRange assert value is in range [low, high]
@@ -150,7 +150,7 @@ func GetCountByCond(t assert.TestingT, tableName string, cond builder.Cond) int6
150150
}
151151

152152
// AssertCountByCond test the count of database entries matching bean
153-
func AssertCountByCond(t assert.TestingT, tableName string, cond builder.Cond, expected int) {
154-
assert.EqualValues(t, expected, GetCountByCond(t, tableName, cond),
153+
func AssertCountByCond(t assert.TestingT, tableName string, cond builder.Cond, expected int) bool {
154+
return assert.EqualValues(t, expected, GetCountByCond(t, tableName, cond),
155155
"Failed consistency test, the counted bean (of table %s) was %+v", tableName, cond)
156156
}

tests/integration/api_pull_review_test.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,14 @@ import (
1313
issues_model "code.gitea.io/gitea/models/issues"
1414
repo_model "code.gitea.io/gitea/models/repo"
1515
"code.gitea.io/gitea/models/unittest"
16+
user_model "code.gitea.io/gitea/models/user"
1617
"code.gitea.io/gitea/modules/json"
1718
api "code.gitea.io/gitea/modules/structs"
19+
issue_service "code.gitea.io/gitea/services/issue"
1820
"code.gitea.io/gitea/tests"
1921

2022
"github.com/stretchr/testify/assert"
23+
"xorm.io/builder"
2124
)
2225

2326
func TestAPIPullReview(t *testing.T) {
@@ -314,3 +317,126 @@ func TestAPIPullReviewRequest(t *testing.T) {
314317
AddTokenAuth(token)
315318
MakeRequest(t, req, http.StatusNoContent)
316319
}
320+
321+
func TestAPIPullReviewStayDismissed(t *testing.T) {
322+
// This test against issue https://github.com/go-gitea/gitea/issues/28542
323+
// where old reviews surface after a review request got dismissed.
324+
defer tests.PrepareTestEnv(t)()
325+
pullIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3})
326+
assert.NoError(t, pullIssue.LoadAttributes(db.DefaultContext))
327+
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pullIssue.RepoID})
328+
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
329+
session2 := loginUser(t, user2.LoginName)
330+
token2 := getTokenForLoggedInUser(t, session2, auth_model.AccessTokenScopeWriteRepository)
331+
user8 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 8})
332+
session8 := loginUser(t, user8.LoginName)
333+
token8 := getTokenForLoggedInUser(t, session8, auth_model.AccessTokenScopeWriteRepository)
334+
335+
// user2 request user8
336+
req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", repo.OwnerName, repo.Name, pullIssue.Index), &api.PullReviewRequestOptions{
337+
Reviewers: []string{user8.LoginName},
338+
}).AddTokenAuth(token2)
339+
MakeRequest(t, req, http.StatusCreated)
340+
341+
reviewsCountCheck(t,
342+
"check we have only one review request",
343+
pullIssue.ID, user8.ID, 0, 1, 1, false)
344+
345+
// user2 request user8 again, it is expected to be ignored
346+
req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", repo.OwnerName, repo.Name, pullIssue.Index), &api.PullReviewRequestOptions{
347+
Reviewers: []string{user8.LoginName},
348+
}).AddTokenAuth(token2)
349+
MakeRequest(t, req, http.StatusCreated)
350+
351+
reviewsCountCheck(t,
352+
"check we have only one review request, even after re-request it again",
353+
pullIssue.ID, user8.ID, 0, 1, 1, false)
354+
355+
// user8 reviews it as accept
356+
req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews", repo.OwnerName, repo.Name, pullIssue.Index), &api.CreatePullReviewOptions{
357+
Event: "APPROVED",
358+
Body: "lgtm",
359+
}).AddTokenAuth(token8)
360+
MakeRequest(t, req, http.StatusOK)
361+
362+
reviewsCountCheck(t,
363+
"check we have one valid approval",
364+
pullIssue.ID, user8.ID, 0, 0, 1, true)
365+
366+
// emulate of auto-dismiss lgtm on a protected branch that where a pull just got an update
367+
_, err := db.GetEngine(db.DefaultContext).Where("issue_id = ? AND reviewer_id = ?", pullIssue.ID, user8.ID).
368+
Cols("dismissed").Update(&issues_model.Review{Dismissed: true})
369+
assert.NoError(t, err)
370+
371+
// user2 request user8 again
372+
req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", repo.OwnerName, repo.Name, pullIssue.Index), &api.PullReviewRequestOptions{
373+
Reviewers: []string{user8.LoginName},
374+
}).AddTokenAuth(token2)
375+
MakeRequest(t, req, http.StatusCreated)
376+
377+
reviewsCountCheck(t,
378+
"check we have no valid approval and one review request",
379+
pullIssue.ID, user8.ID, 1, 1, 2, false)
380+
381+
// user8 dismiss review
382+
_, err = issue_service.ReviewRequest(db.DefaultContext, pullIssue, user8, user8, false)
383+
assert.NoError(t, err)
384+
385+
reviewsCountCheck(t,
386+
"check new review request is now dismissed",
387+
pullIssue.ID, user8.ID, 1, 0, 1, false)
388+
389+
// add a new valid approval
390+
req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews", repo.OwnerName, repo.Name, pullIssue.Index), &api.CreatePullReviewOptions{
391+
Event: "APPROVED",
392+
Body: "lgtm",
393+
}).AddTokenAuth(token8)
394+
MakeRequest(t, req, http.StatusOK)
395+
396+
reviewsCountCheck(t,
397+
"check that old reviews requests are deleted",
398+
pullIssue.ID, user8.ID, 1, 0, 2, true)
399+
400+
// now add a change request witch should dismiss the approval
401+
req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews", repo.OwnerName, repo.Name, pullIssue.Index), &api.CreatePullReviewOptions{
402+
Event: "REQUEST_CHANGES",
403+
Body: "please change XYZ",
404+
}).AddTokenAuth(token8)
405+
MakeRequest(t, req, http.StatusOK)
406+
407+
reviewsCountCheck(t,
408+
"check that old reviews are dismissed",
409+
pullIssue.ID, user8.ID, 2, 0, 3, false)
410+
}
411+
412+
func reviewsCountCheck(t *testing.T, name string, issueID, reviewerID int64, expectedDismissed, expectedRequested, expectedTotal int, expectApproval bool) {
413+
t.Run(name, func(t *testing.T) {
414+
unittest.AssertCountByCond(t, "review", builder.Eq{
415+
"issue_id": issueID,
416+
"reviewer_id": reviewerID,
417+
"dismissed": true,
418+
}, expectedDismissed)
419+
420+
unittest.AssertCountByCond(t, "review", builder.Eq{
421+
"issue_id": issueID,
422+
"reviewer_id": reviewerID,
423+
}, expectedTotal)
424+
425+
unittest.AssertCountByCond(t, "review", builder.Eq{
426+
"issue_id": issueID,
427+
"reviewer_id": reviewerID,
428+
"type": issues_model.ReviewTypeRequest,
429+
}, expectedRequested)
430+
431+
approvalCount := 0
432+
if expectApproval {
433+
approvalCount = 1
434+
}
435+
unittest.AssertCountByCond(t, "review", builder.Eq{
436+
"issue_id": issueID,
437+
"reviewer_id": reviewerID,
438+
"type": issues_model.ReviewTypeApprove,
439+
"dismissed": false,
440+
}, approvalCount)
441+
})
442+
}

0 commit comments

Comments
 (0)