diff --git a/models/issues/review.go b/models/issues/review.go index bb1541df2ed..7ac096f454d 100644 --- a/models/issues/review.go +++ b/models/issues/review.go @@ -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 diff --git a/modules/git/blame.go b/modules/git/blame.go index 68b43b75f87..3dd07d0b849 100644 --- a/modules/git/blame.go +++ b/modules/git/blame.go @@ -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 diff --git a/modules/git/blame_test.go b/modules/git/blame_test.go index 3c258d0dd76..7cf8d92e919 100644 --- a/modules/git/blame_test.go +++ b/modules/git/blame_test.go @@ -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() diff --git a/modules/git/gitcmd/error.go b/modules/git/gitcmd/error.go index a98a3db08f0..42c2e92f1c8 100644 --- a/modules/git/gitcmd/error.go +++ b/modules/git/gitcmd/error.go @@ -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" diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index bf67269f3fa..83517b9228a 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -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) diff --git a/routers/web/web.go b/routers/web/web.go index 17733003113..329d12fb6ff 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -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) diff --git a/services/pull/pull.go b/services/pull/pull.go index 7c74b855761..58a3e8ab25d 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -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 { diff --git a/services/pull/review.go b/services/pull/review.go index 3d423306375..5190230acc2 100644 --- a/services/pull/review.go +++ b/services/pull/review.go @@ -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 } diff --git a/services/pull/review_test.go b/services/pull/review_test.go index 2f9e022b3d3..d133dc76dcf 100644 --- a/services/pull/review_test.go +++ b/services/pull/review_test.go @@ -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) + } + }) +} diff --git a/tests/integration/api_pull_review_test.go b/tests/integration/api_pull_review_test.go index d5c8a4b41b3..fa82c909565 100644 --- a/tests/integration/api_pull_review_test.go +++ b/tests/integration/api_pull_review_test.go @@ -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})