From f4304547a458b969668f19b4b369db31f6e88831 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Mon, 16 Feb 2026 21:42:34 -0800 Subject: [PATCH] Fix force push comments of pull request --- services/pull/comment.go | 61 ++++++++++++++++------- services/pull/comment_test.go | 94 +++++++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+), 18 deletions(-) create mode 100644 services/pull/comment_test.go diff --git a/services/pull/comment.go b/services/pull/comment.go index 6c10bf2aa8..31ecd95019 100644 --- a/services/pull/comment.go +++ b/services/pull/comment.go @@ -6,6 +6,7 @@ package pull import ( "context" + "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" @@ -61,27 +62,51 @@ func CreatePushPullComment(ctx context.Context, pusher *user_model.User, pr *iss } var data issues_model.PushActionContent - if opts.IsForcePush { - data.CommitIDs = []string{oldCommitID, newCommitID} - data.IsForcePush = true - } else { - data.CommitIDs, err = getCommitIDsFromRepo(ctx, pr.BaseRepo, oldCommitID, newCommitID, pr.BaseBranch) - if err != nil { - return nil, err - } - // It maybe an empty pull request. Only non-empty pull request need to create push comment - if len(data.CommitIDs) == 0 { - return nil, nil //nolint:nilnil // return nil because no comment needs to be created - } - } - - dataJSON, err := json.Marshal(data) + data.CommitIDs, err = getCommitIDsFromRepo(ctx, pr.BaseRepo, oldCommitID, newCommitID, pr.BaseBranch) if err != nil { return nil, err } + // It maybe an empty pull request. Only non-empty pull request need to create push comment + if len(data.CommitIDs) == 0 && !isForcePush { + return nil, nil //nolint:nilnil // return nil because no comment needs to be created + } - opts.Content = string(dataJSON) - comment, err = issues_model.CreateComment(ctx, opts) + return db.WithTx2(ctx, func(ctx context.Context) (*issues_model.Comment, error) { + if isForcePush { + if _, err := db.GetEngine(ctx).Where("issue_id = ?", pr.IssueID). + And("type = ?", issues_model.CommentTypePullRequestPush). + NoAutoCondition(). + Delete(new(issues_model.Comment)); err != nil { + return nil, err + } + } - return comment, err + if len(data.CommitIDs) > 0 { + dataJSON, err := json.Marshal(data) + if err != nil { + return nil, err + } + opts.Content = string(dataJSON) + comment, err = issues_model.CreateComment(ctx, opts) + if err != nil { + return nil, err + } + } + + if opts.IsForcePush { // if it's a force push, we needs to add a force push comment + data.CommitIDs = []string{oldCommitID, newCommitID} + data.IsForcePush = true + dataJSON, err := json.Marshal(data) + if err != nil { + return nil, err + } + opts.Content = string(dataJSON) + comment, err = issues_model.CreateComment(ctx, opts) + if err != nil { + return nil, err + } + } + + return comment, err + }) } diff --git a/services/pull/comment_test.go b/services/pull/comment_test.go new file mode 100644 index 0000000000..a91bb6f00d --- /dev/null +++ b/services/pull/comment_test.go @@ -0,0 +1,94 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package pull + +import ( + "testing" + + issues_model "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/gitrepo" + "code.gitea.io/gitea/modules/json" + + "github.com/stretchr/testify/assert" +) + +func TestCreatePushPullCommentForcePushDeletesOldComments(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}) + assert.NoError(t, pr.LoadIssue(t.Context())) + assert.NoError(t, pr.LoadBaseRepo(t.Context())) + + pusher := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + + _, err := issues_model.CreateComment(t.Context(), &issues_model.CreateCommentOptions{ + Type: issues_model.CommentTypePullRequestPush, + Doer: pusher, + Repo: pr.BaseRepo, + Issue: pr.Issue, + Content: "{}", + }) + assert.NoError(t, err) + _, err = issues_model.CreateComment(t.Context(), &issues_model.CreateCommentOptions{ + Type: issues_model.CommentTypePullRequestPush, + Doer: pusher, + Repo: pr.BaseRepo, + Issue: pr.Issue, + Content: "{}", + }) + assert.NoError(t, err) + + comments, err := issues_model.FindComments(t.Context(), &issues_model.FindCommentsOptions{ + IssueID: pr.IssueID, + Type: issues_model.CommentTypePullRequestPush, + }) + assert.NoError(t, err) + assert.Len(t, comments, 2) + + gitRepo, err := gitrepo.OpenRepository(t.Context(), pr.BaseRepo) + assert.NoError(t, err) + defer gitRepo.Close() + + headCommit, err := gitRepo.GetBranchCommit(pr.BaseBranch) + assert.NoError(t, err) + oldCommit := headCommit + if headCommit.ParentCount() > 0 { + parentCommit, err := headCommit.Parent(0) + assert.NoError(t, err) + oldCommit = parentCommit + } + + comment, err := CreatePushPullComment(t.Context(), pusher, pr, oldCommit.ID.String(), headCommit.ID.String(), true) + assert.NoError(t, err) + assert.NotNil(t, comment) + var createdData issues_model.PushActionContent + assert.NoError(t, json.Unmarshal([]byte(comment.Content), &createdData)) + assert.True(t, createdData.IsForcePush) + + commits, err := gitRepo.CommitsBetweenNotBase(headCommit, oldCommit, pr.BaseBranch) + assert.NoError(t, err) + expectedCount := 1 + if len(commits) > 0 { + expectedCount = 2 + } + + comments, err = issues_model.FindComments(t.Context(), &issues_model.FindCommentsOptions{ + IssueID: pr.IssueID, + Type: issues_model.CommentTypePullRequestPush, + }) + assert.NoError(t, err) + assert.Len(t, comments, expectedCount) + + forcePushCount := 0 + for _, comment := range comments { + var pushData issues_model.PushActionContent + assert.NoError(t, json.Unmarshal([]byte(comment.Content), &pushData)) + if pushData.IsForcePush { + forcePushCount++ + } + } + assert.Equal(t, 1, forcePushCount) +}