mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-26 06:16:56 +09:00
With "Autodetect manual merge" enabled, an empty pull request (one with no commits of its own, for example opened to discuss a branch) was closed as manually merged as soon as it was checked. Its head commit is already an ancestor of the base branch, so the detection took the "fast-forwarded" path and recorded the head itself as the merge commit, even though nothing was merged. Detection now skips a PR whose head commit is its merge base, the same condition Gitea already uses to show a PR as "already included in the target branch". Real manual merges are still detected, including when the base branch and the PR branch are pushed together. The merge base is refreshed on recheck, so a fast-forward manual merge whose merge base was already refreshed to the head is not detected either. This only misses a detection and never closes a PR wrongly; telling the two apart would need a value stored at PR creation. Fixes https://github.com/go-gitea/gitea/issues/31947 --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
116 lines
4.8 KiB
Go
116 lines
4.8 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package integration
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
auth_model "gitea.dev/models/auth"
|
|
issues_model "gitea.dev/models/issues"
|
|
repo_model "gitea.dev/models/repo"
|
|
"gitea.dev/models/unittest"
|
|
user_model "gitea.dev/models/user"
|
|
"gitea.dev/modules/git/gitcmd"
|
|
"gitea.dev/modules/queue"
|
|
"gitea.dev/modules/setting"
|
|
api "gitea.dev/modules/structs"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestManualMergeAutodetect(t *testing.T) {
|
|
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
|
|
// user2 is the repo owner
|
|
// user1 is the pusher/merger
|
|
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
|
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
|
|
|
// Create a repo owned by user2
|
|
repoName := "manual-merge-autodetect"
|
|
defaultBranch := setting.Repository.DefaultBranch
|
|
user2Ctx := NewAPITestContext(t, user2.Name, repoName, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
|
doAPICreateRepository(user2Ctx, false)(t)
|
|
|
|
// Enable autodetect manual merge
|
|
doAPIEditRepository(user2Ctx, &api.EditRepoOption{
|
|
HasPullRequests: new(true),
|
|
AllowManualMerge: new(true),
|
|
AutodetectManualMerge: new(true),
|
|
})(t)
|
|
|
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: repoName})
|
|
user1Ctx := NewAPITestContext(t, user1.Name, repoName, auth_model.AccessTokenScopeWriteRepository)
|
|
|
|
// multiple PRs should be able to be closed together if a push contains their branch commits.
|
|
branchNames := []string{"fix-1", "fix-2"}
|
|
apiPulls := make([]api.PullRequest, len(branchNames))
|
|
for i, branchName := range branchNames {
|
|
_, err := createFileInBranch(user2, repo,
|
|
createFileInBranchOptions{OldBranch: defaultBranch, NewBranch: branchName},
|
|
map[string]string{"test-file-" + branchName: "dummy"},
|
|
)
|
|
require.NoError(t, err)
|
|
apiPulls[i], err = doAPICreatePullRequest(user1Ctx, user2.Name, repoName, defaultBranch, branchName)(t)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
testAPICreateBranch(t, user2Ctx.Session, user2.Name, repoName, defaultBranch, "empty-pr", http.StatusCreated)
|
|
emptyAPIPull, err := doAPICreatePullRequest(user1Ctx, user2.Name, repoName, defaultBranch, "empty-pr")(t)
|
|
require.NoError(t, err)
|
|
|
|
// user1 clones, then merges every branch sequentially, then pushes once.
|
|
// The first merge fast-forwards; the rest produce real merge commits,
|
|
// which generates multiple commits for "git rev-list --ancestry-path --merges ...".
|
|
dstPath := t.TempDir()
|
|
u, _ := url.Parse(giteaURL.String())
|
|
u.Path = fmt.Sprintf("%s/%s.git", user2.Name, repoName)
|
|
u.User = url.UserPassword(user1.Name, userPassword)
|
|
|
|
doGitClone(dstPath, u)(t)
|
|
|
|
// Capture each branch's expected merge commit hash from the local clone,
|
|
// so we can assert that Gitea recorded the correct merge commit per PR
|
|
// (and not just "some merge commit" — see the regression where every PR
|
|
// was attributed to the last merge in the push).
|
|
expectedMergeCommits := make([]string, len(branchNames))
|
|
for i, branchName := range branchNames {
|
|
doGitMerge(dstPath, "--no-ff", "-m", "merge "+branchName, "origin/"+branchName)(t)
|
|
head, _, cmdErr := gitcmd.NewCommand("rev-parse", "HEAD").WithDir(dstPath).RunStdString(t.Context())
|
|
require.NoError(t, cmdErr)
|
|
expectedMergeCommits[i] = strings.TrimSpace(head)
|
|
}
|
|
doGitPushTestRepository(dstPath, "origin", defaultBranch)(t)
|
|
|
|
// Every PR should be detected as manually merged by the background task, not just the last one.
|
|
require.Eventually(t, func() bool {
|
|
for _, apiPull := range apiPulls {
|
|
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: apiPull.ID})
|
|
if !pr.HasMerged {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}, 10*time.Second, 100*time.Millisecond)
|
|
|
|
for i, apiPull := range apiPulls {
|
|
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: apiPull.ID})
|
|
assert.Truef(t, pr.HasMerged, "PR %d (%s) should be marked as merged", i+1, branchNames[i])
|
|
assert.Equalf(t, issues_model.PullRequestStatusManuallyMerged, pr.Status, "PR %d (%s) should have ManuallyMerged status", i+1, branchNames[i])
|
|
assert.Equalf(t, user1.ID, pr.MergerID, "PR %d (%s) merger should be the pusher", i+1, branchNames[i])
|
|
assert.Equalf(t, expectedMergeCommits[i], pr.MergedCommitID, "PR %d (%s) should be attributed to its own merge commit, not another PR's", i+1, branchNames[i])
|
|
}
|
|
|
|
require.NoError(t, queue.GetManager().FlushAll(t.Context(), 5*time.Second))
|
|
emptyPR := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: emptyAPIPull.ID})
|
|
assert.False(t, emptyPR.HasMerged, "empty PR must not be detected as manually merged")
|
|
assert.Equal(t, issues_model.PullRequestStatusAncestor, emptyPR.Status)
|
|
})
|
|
}
|