mirror of
https://github.com/harness/drone.git
synced 2025-05-18 18:09:56 +08:00
comment status API
This commit is contained in:
parent
f3eed8ba00
commit
59b7cf5e69
@ -169,11 +169,14 @@ func (c *Controller) CommentCreate(
|
|||||||
|
|
||||||
_, err = c.pullreqStore.UpdateOptLock(ctx, pr, func(pr *types.PullReq) error {
|
_, err = c.pullreqStore.UpdateOptLock(ctx, pr, func(pr *types.PullReq) error {
|
||||||
pr.CommentCount++
|
pr.CommentCount++
|
||||||
|
if act.IsBlocking() {
|
||||||
|
pr.UnresolvedCount++
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// non-critical error
|
// non-critical error
|
||||||
log.Ctx(ctx).Err(err).Msgf("failed to increment pull request comment counter")
|
log.Ctx(ctx).Err(err).Msgf("failed to increment pull request comment counters")
|
||||||
}
|
}
|
||||||
|
|
||||||
return act, nil
|
return act, nil
|
||||||
@ -226,7 +229,10 @@ func (c *Controller) writeActivity(ctx context.Context, pr *types.PullReq, act *
|
|||||||
// sets the correct Order and SubOrder values and writes the activity to the database.
|
// 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.
|
// 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 {
|
func (c *Controller) writeReplyActivity(ctx context.Context, parent, act *types.PullReqActivity) error {
|
||||||
parentUpd, err := c.activityStore.UpdateReplySeq(ctx, parent)
|
parentUpd, err := c.activityStore.UpdateOptLock(ctx, parent, func(act *types.PullReqActivity) error {
|
||||||
|
act.ReplySeq++
|
||||||
|
return nil
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get pull request activity number: %w", err)
|
return fmt.Errorf("failed to get pull request activity number: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,10 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/harness/gitness/internal/auth"
|
"github.com/harness/gitness/internal/auth"
|
||||||
|
"github.com/harness/gitness/types"
|
||||||
"github.com/harness/gitness/types/enum"
|
"github.com/harness/gitness/types/enum"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CommentDelete deletes a pull request comment.
|
// CommentDelete deletes a pull request comment.
|
||||||
@ -40,13 +43,28 @@ func (c *Controller) CommentDelete(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now().UnixMilli()
|
isBlocking := act.IsBlocking()
|
||||||
act.Deleted = &now
|
|
||||||
|
|
||||||
err = c.activityStore.Update(ctx, act)
|
_, err = c.activityStore.UpdateOptLock(ctx, act, func(act *types.PullReqActivity) error {
|
||||||
|
now := time.Now().UnixMilli()
|
||||||
|
act.Deleted = &now
|
||||||
|
return nil
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to mark comment as deleted: %w", err)
|
return fmt.Errorf("failed to mark comment as deleted: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, err = c.pullreqStore.UpdateOptLock(ctx, pr, func(pr *types.PullReq) error {
|
||||||
|
pr.CommentCount--
|
||||||
|
if isBlocking {
|
||||||
|
pr.UnresolvedCount--
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
// non-critical error
|
||||||
|
log.Ctx(ctx).Err(err).Msgf("failed to decrement pull request comment counters")
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
117
internal/api/controller/pullreq/comment_status.go
Normal file
117
internal/api/controller/pullreq/comment_status.go
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
// Copyright 2022 Harness Inc. All rights reserved.
|
||||||
|
// Use of this source code is governed by the Polyform Free Trial License
|
||||||
|
// that can be found in the LICENSE.md file for this repository.
|
||||||
|
|
||||||
|
package pullreq
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/harness/gitness/internal/api/controller"
|
||||||
|
"github.com/harness/gitness/internal/api/usererror"
|
||||||
|
"github.com/harness/gitness/internal/auth"
|
||||||
|
"github.com/harness/gitness/types"
|
||||||
|
"github.com/harness/gitness/types/enum"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CommentStatusInput struct {
|
||||||
|
Status enum.PullReqCommentStatus `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (in *CommentStatusInput) Validate() error {
|
||||||
|
_, ok := in.Status.Sanitize()
|
||||||
|
if !ok {
|
||||||
|
return usererror.BadRequest("Invalid value provided for comment status")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (in *CommentStatusInput) hasChanges(act *types.PullReqActivity, userID int64) bool {
|
||||||
|
// clearing resolved
|
||||||
|
if in.Status == enum.PullReqCommentStatusActive {
|
||||||
|
return act.Resolved != nil
|
||||||
|
}
|
||||||
|
// setting resolved
|
||||||
|
return act.Resolved == nil || act.ResolvedBy == nil || *act.ResolvedBy != userID
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommentStatus updates a pull request comment status.
|
||||||
|
func (c *Controller) CommentStatus(
|
||||||
|
ctx context.Context,
|
||||||
|
session *auth.Session,
|
||||||
|
repoRef string,
|
||||||
|
prNum int64,
|
||||||
|
commentID int64,
|
||||||
|
in *CommentStatusInput,
|
||||||
|
) (*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)
|
||||||
|
}
|
||||||
|
|
||||||
|
var act *types.PullReqActivity
|
||||||
|
|
||||||
|
err = controller.TxOptLock(ctx, c.db, func(ctx context.Context) error {
|
||||||
|
pr, err := c.pullreqStore.FindByNumber(ctx, repo.ID, prNum)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to find pull request by number: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if errValidate := in.Validate(); errValidate != nil {
|
||||||
|
return errValidate
|
||||||
|
}
|
||||||
|
|
||||||
|
act, err = c.getCommentCheckChangeStatusAccess(ctx, session, pr, commentID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get comment: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !in.hasChanges(act, session.Principal.ID) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now().UnixMilli()
|
||||||
|
act.Edited = now
|
||||||
|
|
||||||
|
act.Resolved = nil
|
||||||
|
act.ResolvedBy = nil
|
||||||
|
|
||||||
|
if in.Status != enum.PullReqCommentStatusActive {
|
||||||
|
// In the future if we add more comment resolved statuses
|
||||||
|
// we'll add the ResolvedReason field and put the reason there.
|
||||||
|
// For now, the nullable timestamp field/db-column "Resolved" tells the status (active/resolved).
|
||||||
|
act.Resolved = &now
|
||||||
|
act.ResolvedBy = &session.Principal.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.activityStore.Update(ctx, act)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to update status of pull request activity: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Here we deliberately use the transaction and counting the unresolved comments,
|
||||||
|
// rather than optimistic locking and incrementing/decrementing the counter.
|
||||||
|
// The idea is that if the counter ever goes out of sync, this would be the place where we get it back in sync.
|
||||||
|
unresolvedCount, err := c.activityStore.CountUnresolved(ctx, pr.ID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to count unresolved comments: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pr.UnresolvedCount = unresolvedCount
|
||||||
|
|
||||||
|
err = c.pullreqStore.Update(ctx, pr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to update pull request's unresolved comment count: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return act, nil
|
||||||
|
}
|
@ -18,6 +18,11 @@ type CommentUpdateInput struct {
|
|||||||
Text string `json:"text"`
|
Text string `json:"text"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (in *CommentUpdateInput) Validate() error {
|
||||||
|
// TODO: Check Text length
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (in *CommentUpdateInput) hasChanges(act *types.PullReqActivity) bool {
|
func (in *CommentUpdateInput) hasChanges(act *types.PullReqActivity) bool {
|
||||||
return in.Text != act.Text
|
return in.Text != act.Text
|
||||||
}
|
}
|
||||||
@ -41,6 +46,10 @@ func (c *Controller) CommentUpdate(
|
|||||||
return nil, fmt.Errorf("failed to find pull request by number: %w", err)
|
return nil, fmt.Errorf("failed to find pull request by number: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if errValidate := in.Validate(); errValidate != nil {
|
||||||
|
return nil, errValidate
|
||||||
|
}
|
||||||
|
|
||||||
act, err := c.getCommentCheckEditAccess(ctx, session, pr, commentID)
|
act, err := c.getCommentCheckEditAccess(ctx, session, pr, commentID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get comment: %w", err)
|
return nil, fmt.Errorf("failed to get comment: %w", err)
|
||||||
@ -50,12 +59,12 @@ func (c *Controller) CommentUpdate(
|
|||||||
return act, nil
|
return act, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now().UnixMilli()
|
act, err = c.activityStore.UpdateOptLock(ctx, act, func(act *types.PullReqActivity) error {
|
||||||
act.Edited = now
|
now := time.Now().UnixMilli()
|
||||||
|
act.Edited = now
|
||||||
act.Text = in.Text
|
act.Text = in.Text
|
||||||
|
return nil
|
||||||
err = c.activityStore.Update(ctx, act)
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to update comment: %w", err)
|
return nil, fmt.Errorf("failed to update comment: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -121,7 +121,7 @@ func (c *Controller) getRepoCheckAccess(ctx context.Context,
|
|||||||
return repo, nil
|
return repo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) getCommentCheckEditAccess(ctx context.Context,
|
func (c *Controller) getCommentCheckModifyAccess(ctx context.Context,
|
||||||
session *auth.Session, pr *types.PullReq, commentID int64,
|
session *auth.Session, pr *types.PullReq, commentID int64,
|
||||||
) (*types.PullReqActivity, error) {
|
) (*types.PullReqActivity, error) {
|
||||||
if commentID <= 0 {
|
if commentID <= 0 {
|
||||||
@ -133,7 +133,7 @@ func (c *Controller) getCommentCheckEditAccess(ctx context.Context,
|
|||||||
return nil, fmt.Errorf("failed to find comment by ID: %w", err)
|
return nil, fmt.Errorf("failed to find comment by ID: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if comment == nil || comment.Type != enum.PullReqActivityTypeComment {
|
if comment == nil {
|
||||||
return nil, usererror.ErrNotFound
|
return nil, usererror.ErrNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -145,6 +145,21 @@ func (c *Controller) getCommentCheckEditAccess(ctx context.Context,
|
|||||||
return nil, usererror.BadRequest("Can't update a comment created by the system.")
|
return nil, usererror.BadRequest("Can't update a comment created by the system.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if comment.Type != enum.PullReqActivityTypeComment && comment.Type != enum.PullReqActivityTypeCodeComment {
|
||||||
|
return nil, usererror.BadRequest("Only comments and code comments can be edited.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return comment, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) getCommentCheckEditAccess(ctx context.Context,
|
||||||
|
session *auth.Session, pr *types.PullReq, commentID int64,
|
||||||
|
) (*types.PullReqActivity, error) {
|
||||||
|
comment, err := c.getCommentCheckModifyAccess(ctx, session, pr, commentID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
if comment.CreatedBy != session.Principal.ID {
|
if comment.CreatedBy != session.Principal.ID {
|
||||||
return nil, usererror.BadRequest("Only own comments may be updated.")
|
return nil, usererror.BadRequest("Only own comments may be updated.")
|
||||||
}
|
}
|
||||||
@ -152,6 +167,21 @@ func (c *Controller) getCommentCheckEditAccess(ctx context.Context,
|
|||||||
return comment, nil
|
return comment, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Controller) getCommentCheckChangeStatusAccess(ctx context.Context,
|
||||||
|
session *auth.Session, pr *types.PullReq, commentID int64,
|
||||||
|
) (*types.PullReqActivity, error) {
|
||||||
|
comment, err := c.getCommentCheckModifyAccess(ctx, session, pr, commentID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if comment.SubOrder != 0 {
|
||||||
|
return nil, usererror.BadRequest("Can't change status of replies.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return comment, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Controller) checkIfAlreadyExists(ctx context.Context,
|
func (c *Controller) checkIfAlreadyExists(ctx context.Context,
|
||||||
targetRepoID, sourceRepoID int64, targetBranch, sourceBranch string,
|
targetRepoID, sourceRepoID int64, targetBranch, sourceBranch string,
|
||||||
) error {
|
) error {
|
||||||
|
36
internal/api/controller/tx.go
Normal file
36
internal/api/controller/tx.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
// Copyright 2022 Harness Inc. All rights reserved.
|
||||||
|
// Use of this source code is governed by the Polyform Free Trial License
|
||||||
|
// that can be found in the LICENSE.md file for this repository.
|
||||||
|
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"github.com/harness/gitness/internal/store"
|
||||||
|
"github.com/harness/gitness/internal/store/database/dbtx"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TxOptionRetryCount int
|
||||||
|
|
||||||
|
// TxOptLock runs the provided function inside a database transaction. If optimistic lock error occurs
|
||||||
|
// during the operation, the function will retry the whole transaction again (to the maximum of 5 times,
|
||||||
|
// but this can be overridden by providing an additional TxOptionRetryCount option)
|
||||||
|
func TxOptLock(ctx context.Context, db *sqlx.DB, txFn func(ctx context.Context) error, opts ...interface{}) (err error) {
|
||||||
|
tries := 5
|
||||||
|
for _, opt := range opts {
|
||||||
|
if n, ok := opt.(TxOptionRetryCount); ok {
|
||||||
|
tries = int(n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for try := 0; try < tries; try++ {
|
||||||
|
err = dbtx.New(db).WithTx(ctx, txFn, opts...)
|
||||||
|
if !errors.Is(err, store.ErrVersionConflict) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
55
internal/api/handler/pullreq/comment_status.go
Normal file
55
internal/api/handler/pullreq/comment_status.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
// Copyright 2022 Harness Inc. All rights reserved.
|
||||||
|
// Use of this source code is governed by the Polyform Free Trial License
|
||||||
|
// that can be found in the LICENSE.md file for this repository.
|
||||||
|
|
||||||
|
package pullreq
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/harness/gitness/internal/api/controller/pullreq"
|
||||||
|
"github.com/harness/gitness/internal/api/render"
|
||||||
|
"github.com/harness/gitness/internal/api/request"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HandleCommentStatus is an HTTP handler for updating a pull request comment status.
|
||||||
|
func HandleCommentStatus(pullreqCtrl *pullreq.Controller) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
session, _ := request.AuthSessionFrom(ctx)
|
||||||
|
|
||||||
|
repoRef, err := request.GetRepoRefFromPath(r)
|
||||||
|
if err != nil {
|
||||||
|
render.TranslatedUserError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pullreqNumber, err := request.GetPullReqNumberFromPath(r)
|
||||||
|
if err != nil {
|
||||||
|
render.TranslatedUserError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
commentID, err := request.GetPullReqCommentIDPath(r)
|
||||||
|
if err != nil {
|
||||||
|
render.TranslatedUserError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
in := new(pullreq.CommentStatusInput)
|
||||||
|
err = json.NewDecoder(r.Body).Decode(in)
|
||||||
|
if err != nil {
|
||||||
|
render.BadRequestf(w, "Invalid Request Body: %s.", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
comment, err := pullreqCtrl.CommentStatus(ctx, session, repoRef, pullreqNumber, commentID, in)
|
||||||
|
if err != nil {
|
||||||
|
render.TranslatedUserError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
render.JSON(w, http.StatusOK, comment)
|
||||||
|
}
|
||||||
|
}
|
@ -71,7 +71,11 @@ type commentUpdatePullReqRequest struct {
|
|||||||
|
|
||||||
type commentDeletePullReqRequest struct {
|
type commentDeletePullReqRequest struct {
|
||||||
pullReqCommentRequest
|
pullReqCommentRequest
|
||||||
pullreq.CommentUpdateInput
|
}
|
||||||
|
|
||||||
|
type commentStatusPullReqRequest struct {
|
||||||
|
pullReqCommentRequest
|
||||||
|
pullreq.CommentStatusInput
|
||||||
}
|
}
|
||||||
|
|
||||||
type reviewerListPullReqRequest struct {
|
type reviewerListPullReqRequest struct {
|
||||||
@ -364,6 +368,18 @@ func pullReqOperations(reflector *openapi3.Reflector) {
|
|||||||
_ = reflector.Spec.AddOperation(http.MethodDelete,
|
_ = reflector.Spec.AddOperation(http.MethodDelete,
|
||||||
"/repos/{repo_ref}/pullreq/{pullreq_number}/comments/{pullreq_comment_id}", commentDeletePullReq)
|
"/repos/{repo_ref}/pullreq/{pullreq_number}/comments/{pullreq_comment_id}", commentDeletePullReq)
|
||||||
|
|
||||||
|
commentStatusPullReq := openapi3.Operation{}
|
||||||
|
commentStatusPullReq.WithTags("pullreq")
|
||||||
|
commentStatusPullReq.WithMapOfAnything(map[string]interface{}{"operationId": "commentStatusPullReq"})
|
||||||
|
_ = reflector.SetRequest(&commentStatusPullReq, new(commentStatusPullReqRequest), http.MethodPut)
|
||||||
|
_ = reflector.SetJSONResponse(&commentStatusPullReq, new(types.PullReqActivity), http.StatusOK)
|
||||||
|
_ = reflector.SetJSONResponse(&commentStatusPullReq, new(usererror.Error), http.StatusBadRequest)
|
||||||
|
_ = reflector.SetJSONResponse(&commentStatusPullReq, new(usererror.Error), http.StatusInternalServerError)
|
||||||
|
_ = reflector.SetJSONResponse(&commentStatusPullReq, new(usererror.Error), http.StatusUnauthorized)
|
||||||
|
_ = reflector.SetJSONResponse(&commentStatusPullReq, new(usererror.Error), http.StatusForbidden)
|
||||||
|
_ = reflector.Spec.AddOperation(http.MethodPut,
|
||||||
|
"/repos/{repo_ref}/pullreq/{pullreq_number}/comments/{pullreq_comment_id}/status", commentStatusPullReq)
|
||||||
|
|
||||||
reviewerAdd := openapi3.Operation{}
|
reviewerAdd := openapi3.Operation{}
|
||||||
reviewerAdd.WithTags("pullreq")
|
reviewerAdd.WithTags("pullreq")
|
||||||
reviewerAdd.WithMapOfAnything(map[string]interface{}{"operationId": "reviewerAddPullReq"})
|
reviewerAdd.WithMapOfAnything(map[string]interface{}{"operationId": "reviewerAddPullReq"})
|
||||||
|
@ -266,6 +266,7 @@ func SetupPullReq(r chi.Router, pullreqCtrl *pullreq.Controller) {
|
|||||||
r.Route(fmt.Sprintf("/{%s}", request.PathParamPullReqCommentID), func(r chi.Router) {
|
r.Route(fmt.Sprintf("/{%s}", request.PathParamPullReqCommentID), func(r chi.Router) {
|
||||||
r.Patch("/", handlerpullreq.HandleCommentUpdate(pullreqCtrl))
|
r.Patch("/", handlerpullreq.HandleCommentUpdate(pullreqCtrl))
|
||||||
r.Delete("/", handlerpullreq.HandleCommentDelete(pullreqCtrl))
|
r.Delete("/", handlerpullreq.HandleCommentDelete(pullreqCtrl))
|
||||||
|
r.Put("/status", handlerpullreq.HandleCommentStatus(pullreqCtrl))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
r.Route("/reviewers", func(r chi.Router) {
|
r.Route("/reviewers", func(r chi.Router) {
|
||||||
|
@ -256,7 +256,7 @@ type (
|
|||||||
Create(ctx context.Context, pullreq *types.PullReq) error
|
Create(ctx context.Context, pullreq *types.PullReq) error
|
||||||
|
|
||||||
// Update the pull request. It will set new values to the Version and Updated fields.
|
// Update the pull request. It will set new values to the Version and Updated fields.
|
||||||
Update(ctx context.Context, repo *types.PullReq) error
|
Update(ctx context.Context, pr *types.PullReq) error
|
||||||
|
|
||||||
// UpdateOptLock the pull request details using the optimistic locking mechanism.
|
// UpdateOptLock the pull request details using the optimistic locking mechanism.
|
||||||
UpdateOptLock(ctx context.Context, pr *types.PullReq,
|
UpdateOptLock(ctx context.Context, pr *types.PullReq,
|
||||||
@ -281,7 +281,7 @@ type (
|
|||||||
Find(ctx context.Context, id int64) (*types.PullReqActivity, error)
|
Find(ctx context.Context, id int64) (*types.PullReqActivity, error)
|
||||||
|
|
||||||
// Create a new pull request activity. Value of the Order field should be fetched with UpdateActivitySeq.
|
// Create a new pull request activity. Value of the Order field should be fetched with UpdateActivitySeq.
|
||||||
// Value of the SubOrder field (for replies) should be fetched with UpdateReplySeq (non-replies have 0).
|
// Value of the SubOrder field (for replies) should be the incremented ReplySeq field (non-replies have 0).
|
||||||
Create(ctx context.Context, act *types.PullReqActivity) error
|
Create(ctx context.Context, act *types.PullReqActivity) error
|
||||||
|
|
||||||
// CreateWithPayload create a new system activity from the provided payload.
|
// CreateWithPayload create a new system activity from the provided payload.
|
||||||
@ -291,13 +291,18 @@ type (
|
|||||||
// Update the pull request activity. It will set new values to the Version and Updated fields.
|
// Update the pull request activity. It will set new values to the Version and Updated fields.
|
||||||
Update(ctx context.Context, act *types.PullReqActivity) error
|
Update(ctx context.Context, act *types.PullReqActivity) error
|
||||||
|
|
||||||
// UpdateReplySeq the pull request activity's reply sequence number.
|
// UpdateOptLock updates the pull request activity using the optimistic locking mechanism.
|
||||||
// It will set new values to the ReplySeq, Version and Updated fields.
|
UpdateOptLock(ctx context.Context,
|
||||||
UpdateReplySeq(ctx context.Context, act *types.PullReqActivity) (*types.PullReqActivity, error)
|
act *types.PullReqActivity,
|
||||||
|
mutateFn func(act *types.PullReqActivity) error,
|
||||||
|
) (*types.PullReqActivity, error)
|
||||||
|
|
||||||
// Count returns number of pull request activities in a pull request.
|
// Count returns number of pull request activities in a pull request.
|
||||||
Count(ctx context.Context, prID int64, opts *types.PullReqActivityFilter) (int64, error)
|
Count(ctx context.Context, prID int64, opts *types.PullReqActivityFilter) (int64, error)
|
||||||
|
|
||||||
|
// CountUnresolved returns number of unresolved comments.
|
||||||
|
CountUnresolved(ctx context.Context, prID int64) (int, error)
|
||||||
|
|
||||||
// List returns a list of pull request activities in a pull request (a timeline).
|
// List returns a list of pull request activities in a pull request (a timeline).
|
||||||
List(ctx context.Context, prID int64, opts *types.PullReqActivityFilter) ([]*types.PullReqActivity, error)
|
List(ctx context.Context, prID int64, opts *types.PullReqActivityFilter) ([]*types.PullReqActivity, error)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE pullreqs DROP COLUMN pullreq_unresolved_count;
|
@ -0,0 +1,18 @@
|
|||||||
|
ALTER TABLE pullreqs ADD COLUMN pullreq_unresolved_count INTEGER NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
WITH unresolved_counts AS (
|
||||||
|
SELECT
|
||||||
|
pullreq_activity_pullreq_id AS "unresolved_pullreq_id",
|
||||||
|
COUNT(*) AS "unresolved_count"
|
||||||
|
FROM pullreq_activities
|
||||||
|
WHERE
|
||||||
|
pullreq_activity_sub_order = 0 AND
|
||||||
|
pullreq_activity_resolved IS NULL AND
|
||||||
|
pullreq_activity_deleted IS NULL AND
|
||||||
|
pullreq_activity_kind <> 'system'
|
||||||
|
GROUP BY pullreq_activity_pullreq_id
|
||||||
|
)
|
||||||
|
UPDATE pullreqs
|
||||||
|
SET pullreq_unresolved_count = unresolved_counts.unresolved_count
|
||||||
|
FROM unresolved_counts
|
||||||
|
WHERE pullreq_id = unresolved_pullreq_id;
|
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE pullreqs DROP COLUMN pullreq_unresolved_count;
|
@ -0,0 +1,18 @@
|
|||||||
|
ALTER TABLE pullreqs ADD COLUMN pullreq_unresolved_count INTEGER NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
WITH unresolved_counts AS (
|
||||||
|
SELECT
|
||||||
|
pullreq_activity_pullreq_id AS "unresolved_pullreq_id",
|
||||||
|
COUNT(*) AS "unresolved_count"
|
||||||
|
FROM pullreq_activities
|
||||||
|
WHERE
|
||||||
|
pullreq_activity_sub_order = 0 AND
|
||||||
|
pullreq_activity_resolved IS NULL AND
|
||||||
|
pullreq_activity_deleted IS NULL AND
|
||||||
|
pullreq_activity_kind <> 'system'
|
||||||
|
GROUP BY pullreq_activity_pullreq_id
|
||||||
|
)
|
||||||
|
UPDATE pullreqs
|
||||||
|
SET pullreq_unresolved_count = unresolved_counts.unresolved_count
|
||||||
|
FROM unresolved_counts
|
||||||
|
WHERE pullreq_id = unresolved_pullreq_id;
|
@ -54,7 +54,8 @@ type pullReq struct {
|
|||||||
State enum.PullReqState `db:"pullreq_state"`
|
State enum.PullReqState `db:"pullreq_state"`
|
||||||
IsDraft bool `db:"pullreq_is_draft"`
|
IsDraft bool `db:"pullreq_is_draft"`
|
||||||
|
|
||||||
CommentCount int `db:"pullreq_comment_count"`
|
CommentCount int `db:"pullreq_comment_count"`
|
||||||
|
UnresolvedCount int `db:"pullreq_unresolved_count"`
|
||||||
|
|
||||||
Title string `db:"pullreq_title"`
|
Title string `db:"pullreq_title"`
|
||||||
Description string `db:"pullreq_description"`
|
Description string `db:"pullreq_description"`
|
||||||
@ -90,6 +91,7 @@ const (
|
|||||||
,pullreq_state
|
,pullreq_state
|
||||||
,pullreq_is_draft
|
,pullreq_is_draft
|
||||||
,pullreq_comment_count
|
,pullreq_comment_count
|
||||||
|
,pullreq_unresolved_count
|
||||||
,pullreq_title
|
,pullreq_title
|
||||||
,pullreq_description
|
,pullreq_description
|
||||||
,pullreq_source_repo_id
|
,pullreq_source_repo_id
|
||||||
@ -178,6 +180,7 @@ func (s *PullReqStore) Create(ctx context.Context, pr *types.PullReq) error {
|
|||||||
,pullreq_state
|
,pullreq_state
|
||||||
,pullreq_is_draft
|
,pullreq_is_draft
|
||||||
,pullreq_comment_count
|
,pullreq_comment_count
|
||||||
|
,pullreq_unresolved_count
|
||||||
,pullreq_title
|
,pullreq_title
|
||||||
,pullreq_description
|
,pullreq_description
|
||||||
,pullreq_source_repo_id
|
,pullreq_source_repo_id
|
||||||
@ -204,6 +207,7 @@ func (s *PullReqStore) Create(ctx context.Context, pr *types.PullReq) error {
|
|||||||
,:pullreq_state
|
,:pullreq_state
|
||||||
,:pullreq_is_draft
|
,:pullreq_is_draft
|
||||||
,:pullreq_comment_count
|
,:pullreq_comment_count
|
||||||
|
,:pullreq_unresolved_count
|
||||||
,:pullreq_title
|
,:pullreq_title
|
||||||
,:pullreq_description
|
,:pullreq_description
|
||||||
,:pullreq_source_repo_id
|
,:pullreq_source_repo_id
|
||||||
@ -247,6 +251,7 @@ func (s *PullReqStore) Update(ctx context.Context, pr *types.PullReq) error {
|
|||||||
,pullreq_state = :pullreq_state
|
,pullreq_state = :pullreq_state
|
||||||
,pullreq_is_draft = :pullreq_is_draft
|
,pullreq_is_draft = :pullreq_is_draft
|
||||||
,pullreq_comment_count = :pullreq_comment_count
|
,pullreq_comment_count = :pullreq_comment_count
|
||||||
|
,pullreq_unresolved_count = :pullreq_unresolved_count
|
||||||
,pullreq_title = :pullreq_title
|
,pullreq_title = :pullreq_title
|
||||||
,pullreq_description = :pullreq_description
|
,pullreq_description = :pullreq_description
|
||||||
,pullreq_activity_seq = :pullreq_activity_seq
|
,pullreq_activity_seq = :pullreq_activity_seq
|
||||||
@ -288,8 +293,7 @@ func (s *PullReqStore) Update(ctx context.Context, pr *types.PullReq) error {
|
|||||||
return store.ErrVersionConflict
|
return store.ErrVersionConflict
|
||||||
}
|
}
|
||||||
|
|
||||||
pr.Version = dbPR.Version
|
*pr = *s.mapPullReq(ctx, dbPR)
|
||||||
pr.Updated = dbPR.Updated
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -468,6 +472,7 @@ func mapPullReq(pr *pullReq) *types.PullReq {
|
|||||||
State: pr.State,
|
State: pr.State,
|
||||||
IsDraft: pr.IsDraft,
|
IsDraft: pr.IsDraft,
|
||||||
CommentCount: pr.CommentCount,
|
CommentCount: pr.CommentCount,
|
||||||
|
UnresolvedCount: pr.UnresolvedCount,
|
||||||
Title: pr.Title,
|
Title: pr.Title,
|
||||||
Description: pr.Description,
|
Description: pr.Description,
|
||||||
SourceRepoID: pr.SourceRepoID,
|
SourceRepoID: pr.SourceRepoID,
|
||||||
@ -487,7 +492,8 @@ func mapPullReq(pr *pullReq) *types.PullReq {
|
|||||||
Author: types.PrincipalInfo{},
|
Author: types.PrincipalInfo{},
|
||||||
Merger: nil,
|
Merger: nil,
|
||||||
Stats: types.PullReqStats{
|
Stats: types.PullReqStats{
|
||||||
Conversations: pr.CommentCount,
|
Conversations: pr.CommentCount,
|
||||||
|
UnresolvedCount: pr.UnresolvedCount,
|
||||||
DiffStats: types.DiffStats{
|
DiffStats: types.DiffStats{
|
||||||
Commits: 0,
|
Commits: 0,
|
||||||
FilesChanged: 0,
|
FilesChanged: 0,
|
||||||
@ -508,6 +514,7 @@ func mapInternalPullReq(pr *types.PullReq) *pullReq {
|
|||||||
State: pr.State,
|
State: pr.State,
|
||||||
IsDraft: pr.IsDraft,
|
IsDraft: pr.IsDraft,
|
||||||
CommentCount: pr.CommentCount,
|
CommentCount: pr.CommentCount,
|
||||||
|
UnresolvedCount: pr.UnresolvedCount,
|
||||||
Title: pr.Title,
|
Title: pr.Title,
|
||||||
Description: pr.Description,
|
Description: pr.Description,
|
||||||
SourceRepoID: pr.SourceRepoID,
|
SourceRepoID: pr.SourceRepoID,
|
||||||
|
@ -287,20 +287,25 @@ func (s *PullReqActivityStore) Update(ctx context.Context, act *types.PullReqAct
|
|||||||
return store.ErrVersionConflict
|
return store.ErrVersionConflict
|
||||||
}
|
}
|
||||||
|
|
||||||
act.Version = dbAct.Version
|
*act = *s.mapPullReqActivity(ctx, dbAct)
|
||||||
act.Updated = dbAct.Updated
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateReplySeq updates the pull request activity's reply sequence.
|
// UpdateOptLock updates the pull request using the optimistic locking mechanism.
|
||||||
func (s *PullReqActivityStore) UpdateReplySeq(ctx context.Context,
|
func (s *PullReqActivityStore) UpdateOptLock(ctx context.Context,
|
||||||
act *types.PullReqActivity) (*types.PullReqActivity, error) {
|
act *types.PullReqActivity,
|
||||||
|
mutateFn func(act *types.PullReqActivity) error,
|
||||||
|
) (*types.PullReqActivity, error) {
|
||||||
for {
|
for {
|
||||||
dup := *act
|
dup := *act
|
||||||
|
|
||||||
dup.ReplySeq++
|
err := mutateFn(&dup)
|
||||||
err := s.Update(ctx, &dup)
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.Update(ctx, &dup)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return &dup, nil
|
return &dup, nil
|
||||||
}
|
}
|
||||||
@ -414,6 +419,32 @@ func (s *PullReqActivityStore) List(ctx context.Context, prID int64,
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *PullReqActivityStore) CountUnresolved(ctx context.Context, prID int64) (int, error) {
|
||||||
|
stmt := builder.
|
||||||
|
Select("count(*)").
|
||||||
|
From("pullreq_activities").
|
||||||
|
Where("pullreq_activity_pullreq_id = ?", prID).
|
||||||
|
Where("pullreq_activity_sub_order = 0").
|
||||||
|
Where("pullreq_activity_resolved IS NULL").
|
||||||
|
Where("pullreq_activity_deleted IS NULL").
|
||||||
|
Where("pullreq_activity_kind <> ?", enum.PullReqActivityKindSystem)
|
||||||
|
|
||||||
|
sql, args, err := stmt.ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return 0, errors.Wrap(err, "Failed to convert query to sql")
|
||||||
|
}
|
||||||
|
|
||||||
|
db := dbtx.GetAccessor(ctx, s.db)
|
||||||
|
|
||||||
|
var count int
|
||||||
|
err = db.QueryRowContext(ctx, sql, args...).Scan(&count)
|
||||||
|
if err != nil {
|
||||||
|
return 0, processSQLErrorf(err, "Failed executing count unresolved query")
|
||||||
|
}
|
||||||
|
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
func mapPullReqActivity(act *pullReqActivity) *types.PullReqActivity {
|
func mapPullReqActivity(act *pullReqActivity) *types.PullReqActivity {
|
||||||
m := &types.PullReqActivity{
|
m := &types.PullReqActivity{
|
||||||
ID: act.ID,
|
ID: act.ID,
|
||||||
|
@ -111,6 +111,30 @@ var pullReqActivityKinds = sortEnum([]PullReqActivityKind{
|
|||||||
PullReqActivityKindChangeComment,
|
PullReqActivityKindChangeComment,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// PullReqCommentStatus defines status of a pull request comment.
|
||||||
|
type PullReqCommentStatus string
|
||||||
|
|
||||||
|
func (PullReqCommentStatus) Enum() []interface{} { return toInterfaceSlice(pullReqCommentStatuses) }
|
||||||
|
|
||||||
|
func (s PullReqCommentStatus) Sanitize() (PullReqCommentStatus, bool) {
|
||||||
|
return Sanitize(s, GetAllPullReqCommentStatuses)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAllPullReqCommentStatuses() ([]PullReqCommentStatus, PullReqCommentStatus) {
|
||||||
|
return pullReqCommentStatuses, "" // No default value
|
||||||
|
}
|
||||||
|
|
||||||
|
// PullReqCommentStatus enumeration.
|
||||||
|
const (
|
||||||
|
PullReqCommentStatusActive PullReqCommentStatus = "active"
|
||||||
|
PullReqCommentStatusResolved PullReqCommentStatus = "resolved"
|
||||||
|
)
|
||||||
|
|
||||||
|
var pullReqCommentStatuses = sortEnum([]PullReqCommentStatus{
|
||||||
|
PullReqCommentStatusActive,
|
||||||
|
PullReqCommentStatusResolved,
|
||||||
|
})
|
||||||
|
|
||||||
// PullReqReviewDecision defines state of a pull request review.
|
// PullReqReviewDecision defines state of a pull request review.
|
||||||
type PullReqReviewDecision string
|
type PullReqReviewDecision string
|
||||||
|
|
||||||
|
@ -22,7 +22,8 @@ type PullReq struct {
|
|||||||
State enum.PullReqState `json:"state"`
|
State enum.PullReqState `json:"state"`
|
||||||
IsDraft bool `json:"is_draft"`
|
IsDraft bool `json:"is_draft"`
|
||||||
|
|
||||||
CommentCount int `json:"-"` // returned as "conversations" in the Stats
|
CommentCount int `json:"-"` // returned as "conversations" in the Stats
|
||||||
|
UnresolvedCount int `json:"-"` // returned as "unresolved_count" in the Stats
|
||||||
|
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
@ -52,14 +53,15 @@ type PullReq struct {
|
|||||||
|
|
||||||
// DiffStats shows total number of commits and modified files.
|
// DiffStats shows total number of commits and modified files.
|
||||||
type DiffStats struct {
|
type DiffStats struct {
|
||||||
Commits int `json:"commits"`
|
Commits int `json:"commits,omitempty"`
|
||||||
FilesChanged int `json:"files_changed"`
|
FilesChanged int `json:"files_changed,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PullReqStats shows Diff statistics and number of conversations.
|
// PullReqStats shows Diff statistics and number of conversations.
|
||||||
type PullReqStats struct {
|
type PullReqStats struct {
|
||||||
DiffStats
|
DiffStats
|
||||||
Conversations int `json:"conversations,omitempty"`
|
Conversations int `json:"conversations,omitempty"`
|
||||||
|
UnresolvedCount int `json:"unresolved_count,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PullReqFilter stores pull request query parameters.
|
// PullReqFilter stores pull request query parameters.
|
||||||
|
@ -92,6 +92,11 @@ func (a *PullReqActivity) IsReply() bool {
|
|||||||
return a.SubOrder > 0
|
return a.SubOrder > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsBlocking returns true if the pull request activity (comment/code-comment) is blocking the pull request merge.
|
||||||
|
func (a *PullReqActivity) IsBlocking() bool {
|
||||||
|
return a.SubOrder == 0 && a.Resolved == nil && a.Deleted == nil && a.Kind != enum.PullReqActivityKindSystem
|
||||||
|
}
|
||||||
|
|
||||||
// SetPayload sets the payload and verifies it's of correct type for the activity.
|
// SetPayload sets the payload and verifies it's of correct type for the activity.
|
||||||
func (a *PullReqActivity) SetPayload(payload PullReqActivityPayload) error {
|
func (a *PullReqActivity) SetPayload(payload PullReqActivityPayload) error {
|
||||||
if payload == nil {
|
if payload == nil {
|
||||||
|
Loading…
Reference in New Issue
Block a user