mirror of https://github.com/harness/drone.git
310 lines
9.6 KiB
Go
310 lines
9.6 KiB
Go
// Copyright 2023 Harness, Inc.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package pullreq
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/harness/gitness/app/api/usererror"
|
|
"github.com/harness/gitness/app/auth"
|
|
"github.com/harness/gitness/gitrpc"
|
|
"github.com/harness/gitness/store"
|
|
"github.com/harness/gitness/types"
|
|
"github.com/harness/gitness/types/enum"
|
|
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
type CommentCreateInput struct {
|
|
// ParentID is set only for replies
|
|
ParentID int64 `json:"parent_id"`
|
|
// Text is comment text
|
|
Text string `json:"text"`
|
|
// Used only for code comments
|
|
TargetCommitSHA string `json:"target_commit_sha"`
|
|
SourceCommitSHA string `json:"source_commit_sha"`
|
|
Path string `json:"path"`
|
|
LineStart int `json:"line_start"`
|
|
LineStartNew bool `json:"line_start_new"`
|
|
LineEnd int `json:"line_end"`
|
|
LineEndNew bool `json:"line_end_new"`
|
|
}
|
|
|
|
func (in *CommentCreateInput) IsReply() bool {
|
|
return in.ParentID != 0
|
|
}
|
|
|
|
func (in *CommentCreateInput) IsCodeComment() bool {
|
|
return in.SourceCommitSHA != ""
|
|
}
|
|
|
|
func (in *CommentCreateInput) Validate() error {
|
|
// TODO: Validate Text size.
|
|
|
|
if in.SourceCommitSHA == "" && in.TargetCommitSHA == "" {
|
|
return nil // not a code comment
|
|
}
|
|
|
|
if in.SourceCommitSHA == "" || in.TargetCommitSHA == "" {
|
|
return usererror.BadRequest("for code comments source commit SHA and target commit SHA must be provided")
|
|
}
|
|
|
|
if in.ParentID != 0 {
|
|
return usererror.BadRequest("can't create a reply that is a code comment")
|
|
}
|
|
|
|
if in.Path == "" {
|
|
return usererror.BadRequest("code comment requires file path")
|
|
}
|
|
|
|
if in.LineStart <= 0 || in.LineEnd <= 0 {
|
|
return usererror.BadRequest("code comments require line numbers")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// CommentCreate creates a new pull request comment (pull request activity, type=comment/code-comment).
|
|
//
|
|
//nolint:gocognit,funlen // refactor if needed
|
|
func (c *Controller) CommentCreate(
|
|
ctx context.Context,
|
|
session *auth.Session,
|
|
repoRef string,
|
|
prNum int64,
|
|
in *CommentCreateInput,
|
|
) (*types.PullReqActivity, error) {
|
|
repo, err := c.getRepoCheckAccess(ctx, session, repoRef, enum.PermissionRepoView)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to acquire access to repo: %w", err)
|
|
}
|
|
|
|
pr, err := c.pullreqStore.FindByNumber(ctx, repo.ID, prNum)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to find pull request by number: %w", err)
|
|
}
|
|
|
|
if errValidate := in.Validate(); errValidate != nil {
|
|
return nil, errValidate
|
|
}
|
|
|
|
act := getCommentActivity(session, pr, in)
|
|
|
|
switch {
|
|
case in.IsCodeComment():
|
|
var cut gitrpc.DiffCutOutput
|
|
|
|
cut, err = c.gitRPCClient.DiffCut(ctx, &gitrpc.DiffCutParams{
|
|
ReadParams: gitrpc.ReadParams{RepoUID: repo.GitUID},
|
|
SourceCommitSHA: in.SourceCommitSHA,
|
|
SourceBranch: pr.SourceBranch,
|
|
TargetCommitSHA: in.TargetCommitSHA,
|
|
TargetBranch: pr.TargetBranch,
|
|
Path: in.Path,
|
|
LineStart: in.LineStart,
|
|
LineStartNew: in.LineStartNew,
|
|
LineEnd: in.LineEnd,
|
|
LineEndNew: in.LineEndNew,
|
|
})
|
|
if gitrpc.ErrorStatus(err) == gitrpc.StatusNotFound || gitrpc.ErrorStatus(err) == gitrpc.StatusPathNotFound {
|
|
return nil, usererror.BadRequest(gitrpc.ErrorMessage(err))
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
setAsCodeComment(act, cut, in.Path, in.SourceCommitSHA)
|
|
_ = act.SetPayload(&types.PullRequestActivityPayloadCodeComment{
|
|
Title: cut.LinesHeader,
|
|
Lines: cut.Lines,
|
|
LineStartNew: in.LineStartNew,
|
|
LineEndNew: in.LineEndNew,
|
|
})
|
|
|
|
err = c.writeActivity(ctx, pr, act)
|
|
|
|
// Migrate the comment if necessary... Note: we still need to return the code comment as is.
|
|
needsNewLineMigrate := in.SourceCommitSHA != cut.LatestSourceSHA
|
|
needsOldLineMigrate := pr.MergeBaseSHA != cut.MergeBaseSHA
|
|
if err == nil && (needsNewLineMigrate || needsOldLineMigrate) {
|
|
comments := []*types.CodeComment{act.AsCodeComment()}
|
|
|
|
if needsNewLineMigrate {
|
|
c.codeCommentMigrator.MigrateNew(ctx, repo.GitUID, cut.LatestSourceSHA, comments)
|
|
}
|
|
if needsOldLineMigrate {
|
|
c.codeCommentMigrator.MigrateOld(ctx, repo.GitUID, cut.MergeBaseSHA, comments)
|
|
}
|
|
|
|
if errMigrateUpdate := c.codeCommentView.UpdateAll(ctx, comments); errMigrateUpdate != nil {
|
|
// non-critical error
|
|
log.Ctx(ctx).Err(errMigrateUpdate).
|
|
Msgf("failed to migrate code comment to the latest source/merge-base commit SHA")
|
|
}
|
|
}
|
|
case in.ParentID != 0:
|
|
var parentAct *types.PullReqActivity
|
|
parentAct, err = c.checkIsReplyable(ctx, pr, in.ParentID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
act.ParentID = &parentAct.ID
|
|
act.Kind = parentAct.Kind
|
|
_ = act.SetPayload(types.PullRequestActivityPayloadComment{})
|
|
|
|
err = c.writeReplyActivity(ctx, parentAct, act)
|
|
default:
|
|
_ = act.SetPayload(types.PullRequestActivityPayloadComment{})
|
|
err = c.writeActivity(ctx, pr, act)
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create comment: %w", err)
|
|
}
|
|
|
|
pr, err = c.pullreqStore.UpdateOptLock(ctx, pr, func(pr *types.PullReq) error {
|
|
pr.CommentCount++
|
|
if act.IsBlocking() {
|
|
pr.UnresolvedCount++
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
// non-critical error
|
|
log.Ctx(ctx).Err(err).Msgf("failed to increment pull request comment counters")
|
|
}
|
|
|
|
if err = c.sseStreamer.Publish(ctx, repo.ParentID, enum.SSETypePullrequesUpdated, pr); err != nil {
|
|
log.Ctx(ctx).Warn().Msg("failed to publish PR changed event")
|
|
}
|
|
|
|
return act, nil
|
|
}
|
|
|
|
func (c *Controller) checkIsReplyable(ctx context.Context,
|
|
pr *types.PullReq, parentID int64) (*types.PullReqActivity, error) {
|
|
// make sure the parent comment exists, belongs to the same PR and isn't itself a reply
|
|
parentAct, err := c.activityStore.Find(ctx, parentID)
|
|
if errors.Is(err, store.ErrResourceNotFound) || parentAct == nil {
|
|
return nil, usererror.BadRequest("Parent pull request activity not found.")
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to find parent pull request activity: %w", err)
|
|
}
|
|
|
|
if parentAct.PullReqID != pr.ID || parentAct.RepoID != pr.TargetRepoID {
|
|
return nil, usererror.BadRequest("Parent pull request activity doesn't belong to the same pull request.")
|
|
}
|
|
|
|
if !parentAct.IsReplyable() {
|
|
return nil, usererror.BadRequest("Can't create a reply to the specified entry.")
|
|
}
|
|
|
|
return parentAct, nil
|
|
}
|
|
|
|
// writeActivity updates the PR's activity sequence number (using the optimistic locking mechanism),
|
|
// sets the correct Order value and writes the activity to the database.
|
|
// Even if the writing fails, the updating of the sequence number can succeed.
|
|
func (c *Controller) writeActivity(ctx context.Context, pr *types.PullReq, act *types.PullReqActivity) error {
|
|
prUpd, err := c.pullreqStore.UpdateActivitySeq(ctx, pr)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get pull request activity number: %w", err)
|
|
}
|
|
|
|
*pr = *prUpd // update the pull request object
|
|
|
|
act.Order = prUpd.ActivitySeq
|
|
|
|
err = c.activityStore.Create(ctx, act)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create pull request activity: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// writeReplyActivity updates the parent activity's reply sequence number (using the optimistic locking mechanism),
|
|
// sets the correct Order and SubOrder values and writes the activity to the database.
|
|
// Even if the writing fails, the updating of the sequence number can succeed.
|
|
func (c *Controller) writeReplyActivity(ctx context.Context, parent, act *types.PullReqActivity) error {
|
|
parentUpd, err := c.activityStore.UpdateOptLock(ctx, parent, func(act *types.PullReqActivity) error {
|
|
act.ReplySeq++
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get pull request activity number: %w", err)
|
|
}
|
|
|
|
*parent = *parentUpd // update the parent pull request activity object
|
|
|
|
act.Order = parentUpd.Order
|
|
act.SubOrder = parentUpd.ReplySeq
|
|
|
|
err = c.activityStore.Create(ctx, act)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create pull request activity: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func getCommentActivity(session *auth.Session, pr *types.PullReq, in *CommentCreateInput) *types.PullReqActivity {
|
|
now := time.Now().UnixMilli()
|
|
act := &types.PullReqActivity{
|
|
ID: 0, // Will be populated in the data layer
|
|
Version: 0,
|
|
CreatedBy: session.Principal.ID,
|
|
Created: now,
|
|
Updated: now,
|
|
Edited: now,
|
|
Deleted: nil,
|
|
ParentID: nil, // Will be filled in CommentCreate
|
|
RepoID: pr.TargetRepoID,
|
|
PullReqID: pr.ID,
|
|
Order: 0, // Will be filled in writeActivity/writeReplyActivity
|
|
SubOrder: 0, // Will be filled in writeReplyActivity
|
|
ReplySeq: 0,
|
|
Type: enum.PullReqActivityTypeComment,
|
|
Kind: enum.PullReqActivityKindComment,
|
|
Text: in.Text,
|
|
Metadata: nil,
|
|
ResolvedBy: nil,
|
|
Resolved: nil,
|
|
Author: *session.Principal.ToPrincipalInfo(),
|
|
}
|
|
|
|
return act
|
|
}
|
|
|
|
func setAsCodeComment(a *types.PullReqActivity, cut gitrpc.DiffCutOutput, path, sourceCommitSHA string) {
|
|
var falseBool bool
|
|
a.Type = enum.PullReqActivityTypeCodeComment
|
|
a.Kind = enum.PullReqActivityKindChangeComment
|
|
a.CodeComment = &types.CodeCommentFields{
|
|
Outdated: falseBool,
|
|
MergeBaseSHA: cut.MergeBaseSHA,
|
|
SourceSHA: sourceCommitSHA,
|
|
Path: path,
|
|
LineNew: cut.Header.NewLine,
|
|
SpanNew: cut.Header.NewSpan,
|
|
LineOld: cut.Header.OldLine,
|
|
SpanOld: cut.Header.OldSpan,
|
|
}
|
|
}
|