mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-21 21:23:30 +02:00
fix(pulls): tolerate invalid stored review commit_id (#39849)
Fixes #39848 --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
1 parent
edd0c0e2ce
commit
35fa0400e9
10 files changed
+196
-136
No files matched your search
@@ -428,9 +428,6 @@ func CreateReview(ctx context.Context, opts CreateReviewOptions) (*Review, error
|
||||
|
||||
// GetCurrentReview returns the current pending review of reviewer for given issue
|
||||
func GetCurrentReview(ctx context.Context, reviewer *user_model.User, issue *Issue) (*Review, error) {
|
||||
if reviewer == nil {
|
||||
return nil, nil //nolint:nilnil // return nil when reviewer is nil
|
||||
}
|
||||
reviews, err := FindReviews(ctx, FindReviewOptions{
|
||||
Types: []ReviewType{ReviewTypePending},
|
||||
IssueID: issue.ID,
|
||||
@@ -442,9 +439,10 @@ func GetCurrentReview(ctx context.Context, reviewer *user_model.User, issue *Iss
|
||||
if len(reviews) == 0 {
|
||||
return nil, ErrReviewNotExist{}
|
||||
}
|
||||
reviews[0].Reviewer = reviewer
|
||||
reviews[0].Issue = issue
|
||||
return reviews[0], nil
|
||||
pendingReview := reviews[0]
|
||||
pendingReview.Reviewer = reviewer
|
||||
pendingReview.Issue = issue
|
||||
return pendingReview, nil
|
||||
}
|
||||
|
||||
// ReviewExists returns whether a review exists for a particular line of code in the PR
|
||||
|
||||
+11
-2
@@ -7,19 +7,28 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/modules/git/gitcmd"
|
||||
"gitea.dev/modules/setting"
|
||||
)
|
||||
|
||||
func LineBlame(ctx context.Context, repo RepositoryFacade, revision, file string, line uint) (string, error) {
|
||||
func LineBlameCommitID(ctx context.Context, repo RepositoryFacade, revision, file string, line uint) (string, error) {
|
||||
stdout, _, err := gitcmd.NewCommand("blame").WithRepo(repo).
|
||||
AddOptionFormat("-L %d,%d", line, line).
|
||||
AddOptionValues("-p", revision).
|
||||
AddDashesAndList(file).
|
||||
RunStdString(ctx)
|
||||
return stdout, err
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
commitID, _, _ := strings.Cut(stdout, " ")
|
||||
if !IsStringValidObjectID(nil, commitID) {
|
||||
return "", fmt.Errorf("LineBlameCommitID: invalid commit ID %q", commitID)
|
||||
}
|
||||
return commitID, nil
|
||||
}
|
||||
|
||||
// BlamePart represents block of blame - continuous lines with one sha
|
||||
|
||||
@@ -11,6 +11,18 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLineBlameCommitID(t *testing.T) {
|
||||
storage := mockRepository("repo5_pulls")
|
||||
const revision = "f32b0a9dfd09a60f616f29158f772cedd89942d2"
|
||||
commitID, err := LineBlameCommitID(t.Context(), storage, revision, "README.md", 1)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "72866af952e98d02a73003501836074b286a78f6", commitID)
|
||||
|
||||
commitID, err = LineBlameCommitID(t.Context(), storage, revision, "missing.txt", 1)
|
||||
assert.Error(t, err)
|
||||
assert.Empty(t, commitID)
|
||||
}
|
||||
|
||||
func TestReadingBlameOutput(t *testing.T) {
|
||||
setting.AppDataPath = t.TempDir()
|
||||
ctx := t.Context()
|
||||
|
||||
@@ -93,6 +93,7 @@ const (
|
||||
StderrPathSpec StderrPrefix = "fatal: pathspec"
|
||||
StderrBadRevision StderrPrefix = "fatal: bad revision"
|
||||
StderrNoSuchPath StderrPrefix = "fatal: no such path"
|
||||
StderrBadObject StderrPrefix = "fatal: bad object "
|
||||
|
||||
StderrNoSuchRemote StderrPrefix = "error: no such remote"
|
||||
|
||||
|
||||
+18
-15
@@ -885,24 +885,27 @@ func viewPullFiles(ctx *context.Context, beforeCommitID, afterCommitID string) {
|
||||
}
|
||||
ctx.Data["Assignees"] = shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers)
|
||||
|
||||
currentReview, err := issues_model.GetCurrentReview(ctx, ctx.Doer, issue)
|
||||
if err != nil && !issues_model.IsErrReviewNotExist(err) {
|
||||
ctx.ServerError("GetCurrentReview", err)
|
||||
return
|
||||
}
|
||||
numPendingCodeComments := int64(0)
|
||||
if currentReview != nil {
|
||||
numPendingCodeComments, err = issues_model.CountComments(ctx, &issues_model.FindCommentsOptions{
|
||||
Type: issues_model.CommentTypeCode,
|
||||
ReviewID: currentReview.ID,
|
||||
IssueID: issue.ID,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("CountComments", err)
|
||||
var numPendingCodeComments int64
|
||||
var currentPendingReview *issues_model.Review
|
||||
if ctx.Doer != nil {
|
||||
currentPendingReview, err = issues_model.GetCurrentReview(ctx, ctx.Doer, issue)
|
||||
if err != nil && !issues_model.IsErrReviewNotExist(err) {
|
||||
ctx.ServerError("GetCurrentReview", err)
|
||||
return
|
||||
}
|
||||
if currentPendingReview != nil {
|
||||
numPendingCodeComments, err = issues_model.CountComments(ctx, &issues_model.FindCommentsOptions{
|
||||
Type: issues_model.CommentTypeCode,
|
||||
ReviewID: currentPendingReview.ID,
|
||||
IssueID: issue.ID,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("CountComments", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.Data["CurrentReview"] = currentReview
|
||||
ctx.Data["CurrentReview"] = currentPendingReview
|
||||
ctx.Data["PendingCodeCommentNumber"] = numPendingCodeComments
|
||||
|
||||
ctx.Data["IsIssuePoster"] = ctx.Doer != nil && issue.IsPoster(ctx.Doer.ID)
|
||||
|
||||
+1
-1
@@ -1662,7 +1662,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
m.Get("/new_comment", repo.RenderNewCodeCommentForm)
|
||||
m.Post("/comments", web.Bind[*forms.CodeCommentForm](), repo.SetShowOutdatedComments, repo.CreateCodeComment)
|
||||
m.Post("/submit", web.Bind[*forms.SubmitReviewForm](), repo.SubmitReview)
|
||||
}, context.RepoMustNotBeArchived())
|
||||
}, reqSignIn, context.RepoMustNotBeArchived())
|
||||
})
|
||||
})
|
||||
}, optSignIn, context.RepoAssignment, repo.MustAllowPulls, reqUnitPullsReader)
|
||||
|
||||
+2
-22
@@ -358,26 +358,6 @@ func ChangeTargetBranch(ctx context.Context, pr *issues_model.PullRequest, doer
|
||||
})
|
||||
}
|
||||
|
||||
func checkForInvalidation(ctx context.Context, requests issues_model.PullRequestList, repoID int64, doer *user_model.User, branch string) error {
|
||||
repo, err := repo_model.GetRepositoryByID(ctx, repoID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("GetRepositoryByIDCtx: %w", err)
|
||||
}
|
||||
gitRepo, err := git.OpenRepository(ctx, repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("gitrepo.OpenRepository: %w", err)
|
||||
}
|
||||
go func() {
|
||||
// FIXME: graceful: We need to tell the manager we're doing something...
|
||||
err := InvalidateCodeComments(ctx, requests, doer, repo, gitRepo, branch)
|
||||
if err != nil {
|
||||
log.Error("PullRequestList.InvalidateCodeComments: %v", err)
|
||||
}
|
||||
gitRepo.Close()
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
type TestPullRequestOptions struct {
|
||||
RepoID int64
|
||||
Doer *user_model.User
|
||||
@@ -436,8 +416,8 @@ func AddTestPullRequestTask(opts TestPullRequestOptions) {
|
||||
if err = headBranchPRs.LoadAttributes(ctx); err != nil {
|
||||
log.Error("PullRequestList.LoadAttributes: %v", err)
|
||||
}
|
||||
if invalidationErr := checkForInvalidation(ctx, headBranchPRs, opts.RepoID, opts.Doer, opts.Branch); invalidationErr != nil {
|
||||
log.Error("checkForInvalidation: %v", invalidationErr)
|
||||
if invalidationErr := InvalidateCodeComments(ctx, headBranchPRs, repo, opts.Branch); invalidationErr != nil {
|
||||
log.Error("InvalidateCodeComments: %v", invalidationErr)
|
||||
}
|
||||
if err == nil {
|
||||
for _, pr := range headBranchPRs {
|
||||
|
||||
+76
-84
@@ -23,8 +23,8 @@ import (
|
||||
notify_service "gitea.dev/services/notify"
|
||||
)
|
||||
|
||||
func isErrBlameNotFoundOrNotEnoughLines(err error) bool {
|
||||
return gitcmd.IsStderr(err, gitcmd.StderrNoSuchPath, gitcmd.StderrFileNoEnoughLines)
|
||||
func isErrBlameInvalidated(err error) bool {
|
||||
return gitcmd.IsStderr(err, gitcmd.StderrNoSuchPath, gitcmd.StderrFileNoEnoughLines, gitcmd.StderrBadObject)
|
||||
}
|
||||
|
||||
// ErrDismissRequestOnClosedPR represents an error when a user tries to dismiss a review associated to a closed or merged PR.
|
||||
@@ -47,41 +47,27 @@ func (err ErrDismissRequestOnClosedPR) Unwrap() error {
|
||||
// ErrSubmitReviewOnClosedPR represents an error when a user tries to submit an approve or reject review associated to a closed or merged PR.
|
||||
var ErrSubmitReviewOnClosedPR = errors.New("can't submit review for a closed or merged PR")
|
||||
|
||||
// LineBlame returns the latest commit at the given line
|
||||
func lineBlame(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, branch, file string, line uint) (*git.Commit, error) {
|
||||
sha, err := git.LineBlame(ctx, repo, branch, file, line)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(sha) < 40 {
|
||||
return nil, fmt.Errorf("invalid result of blame: %s", sha)
|
||||
}
|
||||
|
||||
objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName)
|
||||
return gitRepo.GetCommit(ctx, sha[:objectFormat.FullLength()])
|
||||
}
|
||||
|
||||
// checkInvalidation checks if the line of code comment got changed by another commit.
|
||||
// If the line got changed the comment is going to be invalidated.
|
||||
func checkInvalidation(ctx context.Context, c *issues_model.Comment, repo *repo_model.Repository, gitRepo *git.Repository, branch string) error {
|
||||
func checkInvalidation(ctx context.Context, c *issues_model.Comment, repo *repo_model.Repository, branch string) error {
|
||||
// FIXME differentiate between previous and proposed line
|
||||
commit, err := lineBlame(ctx, repo, gitRepo, branch, c.TreePath, uint(c.UnsignedLine()))
|
||||
if isErrBlameNotFoundOrNotEnoughLines(err) {
|
||||
commitID, err := git.LineBlameCommitID(ctx, repo, branch, c.TreePath, uint(c.UnsignedLine()))
|
||||
if isErrBlameInvalidated(err) {
|
||||
c.Invalidated = true
|
||||
return issues_model.UpdateCommentInvalidate(ctx, c)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if c.CommitSHA != "" && c.CommitSHA != commit.ID.String() {
|
||||
if c.CommitSHA != "" && c.CommitSHA != commitID {
|
||||
c.Invalidated = true
|
||||
return issues_model.UpdateCommentInvalidate(ctx, c)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// InvalidateCodeComments will lookup the prs for code comments which got invalidated by change
|
||||
func InvalidateCodeComments(ctx context.Context, prs issues_model.PullRequestList, doer *user_model.User, repo *repo_model.Repository, gitRepo *git.Repository, branch string) error {
|
||||
// InvalidateCodeComments will look up the prs for code comments which got invalidated by change
|
||||
func InvalidateCodeComments(ctx context.Context, prs issues_model.PullRequestList, repo *repo_model.Repository, branch string) error {
|
||||
if len(prs) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -97,35 +83,31 @@ func InvalidateCodeComments(ctx context.Context, prs issues_model.PullRequestLis
|
||||
return fmt.Errorf("find code comments: %v", err)
|
||||
}
|
||||
for _, comment := range codeComments {
|
||||
if err := checkInvalidation(ctx, comment, repo, gitRepo, branch); err != nil {
|
||||
if err := checkInvalidation(ctx, comment, repo, branch); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateCodeComment creates a comment on the code line
|
||||
func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git.Repository, issue *issues_model.Issue, line int64, content, treePath string, pendingReview bool, replyReviewID int64, latestCommitID string, attachments []string) (*issues_model.Comment, error) {
|
||||
var (
|
||||
existsReview bool
|
||||
err error
|
||||
)
|
||||
|
||||
// CreateCodeComment() is used for:
|
||||
// - Single comments
|
||||
// - Comments that are part of a review
|
||||
// - Comments that reply to an existing review
|
||||
|
||||
if !pendingReview && replyReviewID != 0 {
|
||||
// CreateCodeComment creates a code comment in a review on the code line:
|
||||
// When pendingReview=false:
|
||||
// * no replyReviewID: create a new review and add the comment to it, and submit the review
|
||||
// * replyReviewID is set: add the comment to the existing submitted review
|
||||
// When pendingReview=true: get current pending review or create a new one, and add the comment to it
|
||||
func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git.Repository, issue *issues_model.Issue, line int64, content, treePath string, pendingReview bool, replyReviewID int64, latestCommitID string, attachments []string) (_ *issues_model.Comment, err error) {
|
||||
replyToSubmittedReview := !pendingReview && replyReviewID != 0
|
||||
if replyToSubmittedReview {
|
||||
// It's not part of a review; maybe a reply to a review comment or a single comment.
|
||||
// Check if there are reviews for that line already; if there are, this is a reply
|
||||
if existsReview, err = issues_model.ReviewExists(ctx, issue, treePath, line); err != nil {
|
||||
if replyToSubmittedReview, err = issues_model.ReviewExists(ctx, issue, treePath, line); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Comments that are replies don't require a review header to show up in the issue view
|
||||
if !pendingReview && existsReview {
|
||||
// Add the code comment to existing submitted review
|
||||
if replyToSubmittedReview {
|
||||
if err = issue.LoadRepo(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -154,39 +136,43 @@ func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git.
|
||||
return comment, nil
|
||||
}
|
||||
|
||||
review, err := issues_model.GetCurrentReview(ctx, doer, issue)
|
||||
if err != nil {
|
||||
if !issues_model.IsErrReviewNotExist(err) {
|
||||
// If no existing submitted review, create a pending review and its comment in a single transaction
|
||||
// to avoid failed code comment creation leaving a pending review without any comments
|
||||
comment, err := db.WithTx2(ctx, func(ctx context.Context) (*issues_model.Comment, error) {
|
||||
curReview, err := issues_model.GetCurrentReview(ctx, doer, issue)
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
// create a new pending review for the code comment
|
||||
if curReview, err = issues_model.CreateReview(ctx, issues_model.CreateReviewOptions{
|
||||
Type: issues_model.ReviewTypePending,
|
||||
Reviewer: doer,
|
||||
Issue: issue,
|
||||
Official: false,
|
||||
CommitID: latestCommitID,
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if review, err = issues_model.CreateReview(ctx, issues_model.CreateReviewOptions{
|
||||
Type: issues_model.ReviewTypePending,
|
||||
Reviewer: doer,
|
||||
Issue: issue,
|
||||
Official: false,
|
||||
CommitID: latestCommitID,
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
comment, err := createCodeComment(ctx,
|
||||
doer,
|
||||
issue.Repo,
|
||||
issue,
|
||||
content,
|
||||
treePath,
|
||||
line,
|
||||
review.ID,
|
||||
attachments,
|
||||
)
|
||||
// add the code comment to the pending review
|
||||
return createCodeComment(ctx,
|
||||
doer,
|
||||
issue.Repo,
|
||||
issue,
|
||||
content,
|
||||
treePath,
|
||||
line,
|
||||
curReview.ID,
|
||||
attachments,
|
||||
)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !pendingReview && !existsReview {
|
||||
// Submit the review we've just created so the comment shows up in the issue view
|
||||
if !pendingReview {
|
||||
// Doer is creating a submitted review, so submit the pending review we've just created,
|
||||
// then the comment shows up in the issue view
|
||||
if _, _, err = SubmitReview(ctx, doer, gitRepo, issue, issues_model.ReviewTypeComment, "", latestCommitID, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -214,7 +200,7 @@ func createCodeComment(ctx context.Context, doer *user_model.User, repo *repo_mo
|
||||
defer closer.Close()
|
||||
|
||||
invalidated := false
|
||||
head := pr.GetGitHeadRefName()
|
||||
headRef := pr.GetGitHeadRefName()
|
||||
if line > 0 {
|
||||
if reviewID != 0 {
|
||||
first, err := issues_model.FindComments(ctx, &issues_model.FindCommentsOptions{
|
||||
@@ -227,42 +213,48 @@ func createCodeComment(ctx context.Context, doer *user_model.User, repo *repo_mo
|
||||
Page: 1,
|
||||
},
|
||||
})
|
||||
if err == nil && len(first) > 0 {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("find first comment for %d line %d path %s. Error: %w", reviewID, line, treePath, err)
|
||||
}
|
||||
|
||||
if len(first) > 0 {
|
||||
// if there are existing comments, use the first comment's information to create the new comment
|
||||
commitID = first[0].CommitSHA
|
||||
invalidated = first[0].Invalidated
|
||||
patch = first[0].Patch
|
||||
} else if err != nil && !issues_model.IsErrCommentNotExist(err) {
|
||||
return nil, fmt.Errorf("Find first comment for %d line %d path %s. Error: %w", reviewID, line, treePath, err)
|
||||
} else {
|
||||
// otherwise, try to get the commitID from the review itself
|
||||
review, err := issues_model.GetReviewByID(ctx, reviewID)
|
||||
if err == nil && len(review.CommitID) > 0 {
|
||||
head = review.CommitID
|
||||
} else if err != nil && !issues_model.IsErrReviewNotExist(err) {
|
||||
return nil, fmt.Errorf("GetReviewByID %d. Error: %w", reviewID, err)
|
||||
if err != nil && !errors.Is(err, util.ErrNotExist) {
|
||||
return nil, fmt.Errorf("createCodeComment: get review %d: %w", reviewID, err)
|
||||
}
|
||||
if review != nil && review.CommitID != "" {
|
||||
headRef = review.CommitID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(commitID) == 0 {
|
||||
// FIXME validate treePath
|
||||
// Get latest commit referencing the commented line
|
||||
// No need for get commit for base branch changes
|
||||
commit, err := lineBlame(ctx, pr.BaseRepo, gitRepo, head, treePath, uint(line))
|
||||
if err == nil {
|
||||
commitID = commit.ID.String()
|
||||
} else if !isErrBlameNotFoundOrNotEnoughLines(err) {
|
||||
return nil, fmt.Errorf("LineBlame[%s, %s, %s, %d]: %w", pr.GetGitHeadRefName(), gitRepo.LogString(), treePath, line, err)
|
||||
if commitID == "" {
|
||||
// FIXME validate treePath (TODO: unclear comment)
|
||||
// Get latest commit referencing the commented line, error can be ignored then the head commit will be used as fallback
|
||||
commitID, err = git.LineBlameCommitID(ctx, pr.BaseRepo, headRef, treePath, uint(line))
|
||||
// Preserve attribution to the reviewed revision, falling back to the PR head if blame fails.
|
||||
if err != nil && headRef != pr.GetGitHeadRefName() {
|
||||
commitID, err = git.LineBlameCommitID(ctx, pr.BaseRepo, pr.GetGitHeadRefName(), treePath, uint(line))
|
||||
}
|
||||
if err != nil {
|
||||
log.Debug("LineBlame[%s, %s, %s, %d]: %v", pr.GetGitHeadRefName(), gitRepo.LogString(), treePath, line, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only fetch diff if comment is review comment
|
||||
if len(patch) == 0 && reviewID != 0 {
|
||||
if patch == "" && reviewID != 0 {
|
||||
headCommitID, err := gitRepo.GetRefCommitID(ctx, pr.GetGitHeadRefName())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetRefCommitID[%s]: %w", pr.GetGitHeadRefName(), err)
|
||||
}
|
||||
if len(commitID) == 0 {
|
||||
if commitID == "" {
|
||||
commitID = headCommitID
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/util"
|
||||
pull_service "gitea.dev/services/pull"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -53,7 +54,6 @@ func TestDismissReview(t *testing.T) {
|
||||
func TestCreateCodeCommentContextOfUnchangedLine(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
ctx := t.Context()
|
||||
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 6})
|
||||
require.NoError(t, pr.LoadBaseRepo(ctx))
|
||||
|
||||
@@ -86,22 +86,76 @@ func TestCreateCodeCommentContextOfUnchangedLine(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
mergeBase, err := headCommit.ParentID(0)
|
||||
require.NoError(t, err)
|
||||
mergeBaseCommit, err := gitRepo.GetCommit(ctx, mergeBase.String())
|
||||
require.NoError(t, err)
|
||||
lineCommitID, err := mergeBaseCommit.ParentID(0)
|
||||
require.NoError(t, err)
|
||||
pr.MergeBase = mergeBase.String()
|
||||
require.NoError(t, pr.UpdateCols(ctx, "merge_base"))
|
||||
|
||||
for _, c := range []struct {
|
||||
name string
|
||||
line int64
|
||||
name string
|
||||
line int64
|
||||
doerID int64
|
||||
commitID string
|
||||
}{
|
||||
{"NewSide", 17},
|
||||
{"OldSide", -15},
|
||||
{"NewSide", 17, 2, ""},
|
||||
{"OldSide", -15, 2, ""},
|
||||
{"UnknownReviewCommit", 17, 4, strings.Repeat("a", 40)},
|
||||
} {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: c.doerID})
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: pr.IssueID})
|
||||
require.NoError(t, issue.LoadRepo(ctx))
|
||||
comment, err := pull_service.CreateCodeComment(ctx, doer, gitRepo, issue, c.line, "test", treePath, true, 0, headCommit.ID.String(), nil)
|
||||
comment, err := pull_service.CreateCodeComment(ctx, doer, gitRepo, issue, c.line, "test", treePath, true, 0, c.commitID, nil)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, strings.HasSuffix(comment.Patch, "\n old 10\n"), "patch: %q", comment.Patch)
|
||||
if c.line > 0 {
|
||||
assert.Equal(t, lineCommitID.String(), comment.CommitSHA)
|
||||
require.NoError(t, pull_service.InvalidateCodeComments(ctx, issues_model.PullRequestList{pr}, pr.BaseRepo, headRef))
|
||||
comment = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: comment.ID})
|
||||
assert.False(t, comment.Invalidated)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateCodeCommentUnknownCommitID(t *testing.T) {
|
||||
unittest.PrepareTestEnv(t)
|
||||
ctx := t.Context()
|
||||
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
|
||||
// org3/repo3.git: "test_branch" exists, but "refs/pull/2/head" (PR ID: 6) doesn't exist yet
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 6})
|
||||
require.NoError(t, pr.LoadBaseRepo(ctx))
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: pr.IssueID})
|
||||
require.NoError(t, issue.LoadRepo(ctx))
|
||||
|
||||
gitRepo, err := git.OpenRepository(ctx, pr.BaseRepo)
|
||||
require.NoError(t, err)
|
||||
defer gitRepo.Close()
|
||||
|
||||
const treePath = "file.txt"
|
||||
|
||||
t.Run("RollbackOnFailure", func(t *testing.T) {
|
||||
// the PR head ref doesn't exist yet, so creating the comment fails
|
||||
_, err := gitRepo.GetCommit(ctx, pr.GetGitHeadRefName())
|
||||
require.ErrorIs(t, err, util.ErrNotExist)
|
||||
_, err = pull_service.CreateCodeComment(ctx, doer, gitRepo, issue, 1, "test", treePath, true, 0, "", nil)
|
||||
require.Error(t, err)
|
||||
_, err = issues_model.GetCurrentReview(ctx, doer, issue)
|
||||
assert.True(t, issues_model.IsErrReviewNotExist(err), "got: %v", err)
|
||||
})
|
||||
|
||||
t.Run("FallbackToHead", func(t *testing.T) {
|
||||
headCommitID, err := gitRepo.GetBranchCommitID(ctx, pr.HeadBranch)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, git.UpdateRef(ctx, gitRepo, pr.GetGitHeadRefName(), headCommitID))
|
||||
// the first comment creates the pending review with the unknown commit, the second one reuses it
|
||||
for i, commitID := range []string{strings.Repeat("a", 40), headCommitID} {
|
||||
comment, err := pull_service.CreateCodeComment(ctx, doer, gitRepo, issue, int64(i+1), "test", treePath, true, 0, commitID, nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, headCommitID, comment.CommitSHA)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -6,6 +6,7 @@ package integration
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
auth_model "gitea.dev/models/auth"
|
||||
@@ -232,6 +233,16 @@ func testAPIPullReviewGeneral(t *testing.T) {
|
||||
json.Unmarshal(resp.Body.Bytes(), &errMap)
|
||||
assert.Equal(t, "review requires a body or a comment", errMap["message"])
|
||||
|
||||
unknownCommitID := strings.Repeat("a", 40)
|
||||
req = NewRequestWithJSON(t, http.MethodPost, reviewsURL, &api.CreatePullReviewOptions{
|
||||
Event: api.ReviewStateComment,
|
||||
CommitID: unknownCommitID,
|
||||
Comments: []api.CreatePullReviewComment{{Path: "README.md", Body: "inline", NewLineNum: 1}},
|
||||
}).AddTokenAuth(token)
|
||||
review = DecodeJSON(t, MakeRequest(t, req, http.StatusOK), &api.PullReview{})
|
||||
assert.Equal(t, unknownCommitID, review.CommitID)
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ReviewID: review.ID, Type: issues_model.CommentTypeCode, Content: "inline"})
|
||||
|
||||
// test get review requests
|
||||
// to make it simple, use same api with get review
|
||||
pullIssue12 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 12})
|
||||
|
||||
Reference in new issue
Block a user