diff --git a/models/fixtures/comment.yml b/models/fixtures/comment.yml index 8fde386e226d4..00f7f6e536653 100644 --- a/models/fixtures/comment.yml +++ b/models/fixtures/comment.yml @@ -102,3 +102,21 @@ review_id: 22 assignee_id: 5 created_unix: 946684817 + +- + id: 12 + type: 22 # review comment + poster_id: 1 + issue_id: 3 + content: "" + review_id: 5 + created_unix: 946684810 + +- + id: 13 + type: 21 # code comment + poster_id: 1 + issue_id: 3 + content: "Some codes need to be changed" + review_id: 5 + created_unix: 946684810 diff --git a/models/issues/comment.go b/models/issues/comment.go index d22f08fa87621..141ec0027c84a 100644 --- a/models/issues/comment.go +++ b/models/issues/comment.go @@ -1102,21 +1102,21 @@ func UpdateComment(ctx context.Context, c *Comment, contentVersion int, doer *us } // DeleteComment deletes the comment -func DeleteComment(ctx context.Context, comment *Comment) error { +func DeleteComment(ctx context.Context, comment *Comment) (*Comment, error) { e := db.GetEngine(ctx) if _, err := e.ID(comment.ID).NoAutoCondition().Delete(comment); err != nil { - return err + return nil, err } if _, err := db.DeleteByBean(ctx, &ContentHistory{ CommentID: comment.ID, }); err != nil { - return err + return nil, err } if comment.Type.CountedAsConversation() { if err := UpdateIssueNumComments(ctx, comment.IssueID); err != nil { - return err + return nil, err } } if _, err := e.Table("action"). @@ -1124,14 +1124,48 @@ func DeleteComment(ctx context.Context, comment *Comment) error { Update(map[string]any{ "is_deleted": true, }); err != nil { - return err + return nil, err + } + + var deletedReviewComment *Comment + // delete review & review comment if the code comment is the last comment of the review + if comment.Type == CommentTypeCode && comment.ReviewID > 0 { + if err := comment.LoadReview(ctx); err != nil { + return nil, err + } + if comment.Review != nil && comment.Review.Type == ReviewTypeComment { + res, err := db.GetEngine(ctx).ID(comment.ReviewID). + Where("NOT EXISTS (SELECT 1 FROM comment WHERE review_id = ? AND `type` = ?)", comment.ReviewID, CommentTypeCode). + Delete(new(Review)) + if err != nil { + return nil, err + } + if res > 0 { + var reviewComment Comment + has, err := db.GetEngine(ctx).Where("review_id = ?", comment.ReviewID). + And("`type` = ?", CommentTypeReview).Get(&reviewComment) + if err != nil { + return nil, err + } + if has && reviewComment.Content == "" { + if _, err := db.GetEngine(ctx).ID(reviewComment.ID).Delete(new(Comment)); err != nil { + return nil, err + } + deletedReviewComment = &reviewComment + } + comment.ReviewID = 0 // reset review ID to 0 for the notification + } + } } if err := comment.neuterCrossReferences(ctx); err != nil { - return err + return nil, err } - return DeleteReaction(ctx, &ReactionOptions{CommentID: comment.ID}) + if err := DeleteReaction(ctx, &ReactionOptions{CommentID: comment.ID}); err != nil { + return nil, err + } + return deletedReviewComment, nil } // UpdateCommentsMigrationsByType updates comments' migrations information via given git service type and original id and poster id diff --git a/models/issues/pull_list_test.go b/models/issues/pull_list_test.go index eb2de006d60a4..feb59df216045 100644 --- a/models/issues/pull_list_test.go +++ b/models/issues/pull_list_test.go @@ -39,9 +39,8 @@ func TestPullRequestList_LoadReviewCommentsCounts(t *testing.T) { reviewComments, err := prs.LoadReviewCommentsCounts(db.DefaultContext) assert.NoError(t, err) assert.Len(t, reviewComments, 2) - for _, pr := range prs { - assert.Equal(t, 1, reviewComments[pr.IssueID]) - } + assert.Equal(t, 1, reviewComments[prs[0].IssueID]) + assert.Equal(t, 2, reviewComments[prs[1].IssueID]) } func TestPullRequestList_LoadReviews(t *testing.T) { diff --git a/routers/api/v1/repo/issue_comment.go b/routers/api/v1/repo/issue_comment.go index cc342a9313c71..feb9f1da64cfe 100644 --- a/routers/api/v1/repo/issue_comment.go +++ b/routers/api/v1/repo/issue_comment.go @@ -721,12 +721,12 @@ func deleteIssueComment(ctx *context.APIContext) { if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) { ctx.Status(http.StatusForbidden) return - } else if comment.Type != issues_model.CommentTypeComment { + } else if comment.Type != issues_model.CommentTypeComment && comment.Type != issues_model.CommentTypeCode { ctx.Status(http.StatusNoContent) return } - if err = issue_service.DeleteComment(ctx, ctx.Doer, comment); err != nil { + if _, err = issue_service.DeleteComment(ctx, ctx.Doer, comment); err != nil { ctx.APIErrorInternal(err) return } diff --git a/routers/web/repo/issue_comment.go b/routers/web/repo/issue_comment.go index cb5b2d801952d..1ad6c588a7798 100644 --- a/routers/web/repo/issue_comment.go +++ b/routers/web/repo/issue_comment.go @@ -325,12 +325,18 @@ func DeleteComment(ctx *context.Context) { return } - if err = issue_service.DeleteComment(ctx, ctx.Doer, comment); err != nil { + deletedReviewComment, err := issue_service.DeleteComment(ctx, ctx.Doer, comment) + if err != nil { ctx.ServerError("DeleteComment", err) return } - ctx.Status(http.StatusOK) + res := map[string]any{} + if deletedReviewComment != nil { + res["deletedReviewCommentHashTag"] = deletedReviewComment.HashTag() + } + + ctx.JSON(http.StatusOK, res) } // ChangeCommentReaction create a reaction for comment diff --git a/services/issue/comments.go b/services/issue/comments.go index 10c81198d57e2..74159992f4da9 100644 --- a/services/issue/comments.go +++ b/services/issue/comments.go @@ -131,17 +131,17 @@ func UpdateComment(ctx context.Context, c *issues_model.Comment, contentVersion } // DeleteComment deletes the comment -func DeleteComment(ctx context.Context, doer *user_model.User, comment *issues_model.Comment) error { - err := db.WithTx(ctx, func(ctx context.Context) error { +func DeleteComment(ctx context.Context, doer *user_model.User, comment *issues_model.Comment) (*issues_model.Comment, error) { + deletedReviewComment, err := db.WithTx2(ctx, func(ctx context.Context) (*issues_model.Comment, error) { return issues_model.DeleteComment(ctx, comment) }) if err != nil { - return err + return nil, err } notify_service.DeleteComment(ctx, doer, comment) - return nil + return deletedReviewComment, nil } // LoadCommentPushCommits Load push commits diff --git a/services/issue/comments_test.go b/services/issue/comments_test.go new file mode 100644 index 0000000000000..771a070330d8d --- /dev/null +++ b/services/issue/comments_test.go @@ -0,0 +1,33 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package issue + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + + "github.com/stretchr/testify/assert" +) + +func Test_DeleteCommentWithReview(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 13}) + assert.Equal(t, int64(5), comment.ReviewID) + review := unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: comment.ReviewID}) + user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + + // since this is the last comment of the review, it should be deleted when the comment is deleted + deletedReviewComment, err := DeleteComment(db.DefaultContext, user1, comment) + assert.NoError(t, err) + assert.NotNil(t, deletedReviewComment) + + // the review should be deleted as well + unittest.AssertNotExistsBean(t, &issues_model.Review{ID: review.ID}) + unittest.AssertNotExistsBean(t, &issues_model.Comment{ID: deletedReviewComment.ID}) +} diff --git a/services/user/delete.go b/services/user/delete.go index 39c6ef052dca7..f05eb6464b43d 100644 --- a/services/user/delete.go +++ b/services/user/delete.go @@ -117,7 +117,7 @@ func deleteUser(ctx context.Context, u *user_model.User, purge bool) (err error) } for _, comment := range comments { - if err = issues_model.DeleteComment(ctx, comment); err != nil { + if _, err = issues_model.DeleteComment(ctx, comment); err != nil { return err } } diff --git a/web_src/js/features/repo-issue.ts b/web_src/js/features/repo-issue.ts index b330b4869ba8f..c7c32bd840942 100644 --- a/web_src/js/features/repo-issue.ts +++ b/web_src/js/features/repo-issue.ts @@ -150,6 +150,13 @@ export function initRepoIssueCommentDelete() { counter.textContent = String(num); } + const json: Record = await response.json(); + if (json.errorMessage) throw new Error(json.errorMessage); + + if (json.deletedReviewCommentHashTag) { + document.querySelector(`#${json.deletedReviewCommentHashTag}`)?.remove(); + } + document.querySelector(`#${deleteButton.getAttribute('data-comment-id')}`)?.remove(); if (conversationHolder && !conversationHolder.querySelector('.comment')) {