fix(pull): recover PRs whose recorded merge commit never reached the base repo (#39831)

When pushing the merge commit to the base repo fails (e.g.
non-fast-forward), the merge intent recorded by
https://github.com/go-gitea/gitea/pull/39442 is left behind.
`merged_commit_id` then points to a commit that does not exist in the
base repo, and `git branch --contains` fails with `no such commit`, so
the PR can never be checked or merged again.

- Clear the recorded merge intent if the push fails and the commit is
not on the base branch
- `IsCommitInBranch` now returns false for a missing commit, so
already-stuck PRs recover on the next check

Fixes https://github.com/go-gitea/gitea/issues/39824

_Written with Codet assistance._

---------

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Giteabot <teabot@gitea.io>
This commit is contained in:
authored and GitHub committed 2026-10-18 09:52:44 +00:00
1 parent 13ddad6e07
commit 52b18b4903
4 files changed
+77 -2

No files matched your search

+19
View File
@@ -279,6 +279,18 @@ func recordMergeIntent(ctx context.Context, pr *issues_model.PullRequest, doer *
})
}
// clearMergeIntent reverts recordMergeIntent after the merged commit failed to reach the base repo
func clearMergeIntent(ctx context.Context, pr *issues_model.PullRequest) error {
return db.WithTx(ctx, func(ctx context.Context) error {
pr.MergedUnix, pr.MergedCommitID, pr.MergerID = 0, "", 0
if _, err := db.GetEngine(ctx).NoAutoTime().ID(pr.ID).Cols("merged_unix", "merged_commit_id", "merger_id").Update(pr); err != nil {
return err
}
_, err := db.GetEngine(ctx).Where("pull_id=?", pr.ID).Cols("merged_commit_id").Update(&pull_model.AutoMerge{})
return err
})
}
func hasPullRequestCommitBeenMerged(ctx context.Context, pr *issues_model.PullRequest) (bool, error) {
if pr.MergedCommitID == "" {
return false, nil
@@ -286,6 +298,7 @@ func hasPullRequestCommitBeenMerged(ctx context.Context, pr *issues_model.PullRe
if err := pr.LoadBaseRepo(ctx); err != nil {
return false, err
}
// the recorded commit is missing from the base repo if the push never succeeded, then it is not merged
return git.IsCommitInBranch(ctx, pr.BaseRepo, pr.MergedCommitID, pr.BaseBranch)
}
@@ -340,6 +353,12 @@ func Merge(outerCtx context.Context, prID int64, doer *user_model.User, mergeSty
},
})
if err != nil {
// clear the intent only if the commit verifiably didn't reach the base branch
if merged, checkErr := hasPullRequestCommitBeenMerged(ctx, pr); checkErr == nil && !merged && pr.MergedCommitID != "" {
if clearErr := clearMergeIntent(ctx, pr); clearErr != nil {
log.Error("Unable to clear merge intent of %-v: %v", pr, clearErr)
}
}
return err
}
}
+20
View File
@@ -4,8 +4,12 @@
package pull
import (
"strings"
"testing"
issues_model "gitea.dev/models/issues"
"gitea.dev/models/unittest"
user_model "gitea.dev/models/user"
"gitea.dev/modules/git"
"github.com/stretchr/testify/assert"
@@ -137,3 +141,19 @@ func TestResolveMergeMessageTemplate(t *testing.T) {
assert.Equal(t, "rebase template", tmpl)
})
}
func TestMergeIntentWithMissingCommit(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
ctx := t.Context()
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
require.NoError(t, recordMergeIntent(ctx, pr, &user_model.User{ID: 2}, strings.Repeat("a", 40), false))
merged, err := hasPullRequestCommitBeenMerged(ctx, pr)
require.NoError(t, err)
assert.False(t, merged)
require.NoError(t, clearMergeIntent(ctx, pr))
pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
assert.Empty(t, pr.MergedCommitID)
assert.Zero(t, pr.MergedUnix)
}