From 2a5c34c3791e7a144476ded0f747cc33628930e3 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 1 Nov 2026 08:53:22 +0100 Subject: [PATCH] fix(pull): reject pull requests without common history (#39872) Pull requests between branches without a common history show no commits and can never be merged. The web form already rejects them, but the API and crafted `..` compare POSTs did not. The API compared with `..` since https://github.com/go-gitea/gitea/pull/36105, it now uses `...` again and returns 422 when there is no merge base. --- routers/api/v1/repo/pull.go | 6 +++++- routers/web/repo/pull.go | 2 +- tests/integration/pull_merge_test.go | 14 +++++++------- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index 8190ee4f1ff..eb522a28521 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -404,7 +404,7 @@ func CreatePullRequest(ctx *context.APIContext) { ) // Get repo/branch information - compareResult, closer := parseCompareInfo(ctx, form.Base+".."+form.Head) + compareResult, closer := parseCompareInfo(ctx, form.Base+"..."+form.Head) if ctx.Written() { return } @@ -414,6 +414,10 @@ func CreatePullRequest(ctx *context.APIContext) { ctx.APIError(http.StatusUnprocessableEntity, "Invalid PullRequest: base and head must be branches") return } + if compareResult.CompareBase == "" { + ctx.APIError(http.StatusUnprocessableEntity, "Invalid PullRequest: base and head have no common history") + return + } // Check if another PR exists with the same targets existingPr, err := issues_model.GetUnmergedPullRequest(ctx, compareResult.HeadRepo.ID, ctx.Repo.Repository.ID, diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index f6ae0148da7..7ee49f50ac3 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -1362,7 +1362,7 @@ func CompareAndPullRequestPost(ctx *context.Context) { return } ci := comparePageInfo.compareInfo - if ci.CompareBase == "" { + if ci.CompareBase == "" || ci.DirectComparison() { ctx.JSONError(ctx.Tr("repo.pulls.no_common_history")) return } diff --git a/tests/integration/pull_merge_test.go b/tests/integration/pull_merge_test.go index 4b4ee29105b..c969d078c66 100644 --- a/tests/integration/pull_merge_test.go +++ b/tests/integration/pull_merge_test.go @@ -451,13 +451,7 @@ func TestCantMergeUnrelated(t *testing.T) { assert.NoError(t, err) commitSha := strings.TrimSpace(stdout) - _, _, err = gitcmd.NewCommand("branch", "unrelated"). - AddDynamicArguments(commitSha). - WithRepo(repo1). - RunStdString(t.Context()) - assert.NoError(t, err) - - testEditFileToNewBranch(t, session, "user1", "repo1", "master", "conflict", "README.md", "Hello, World (Edited Once)\n") + testEditFileToNewBranch(t, session, "user1", "repo1", "master", "unrelated", "README.md", "Hello, World (Edited Once)\n") // Use API to create a conflicting pr token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) @@ -468,6 +462,12 @@ func TestCantMergeUnrelated(t *testing.T) { }).AddTokenAuth(token) session.MakeRequest(t, req, http.StatusCreated) + assert.NoError(t, git.UpdateRef(t.Context(), repo1, git.BranchPrefix+"unrelated", commitSha)) + session.MakeRequest(t, NewRequestWithJSON(t, http.MethodPost, "/api/v1/repos/user1/repo1/pulls", &api.CreatePullRequestOption{ + Head: "unrelated", Base: "base", Title: "create an unrelated pr", + }).AddTokenAuth(token), http.StatusUnprocessableEntity) + session.MakeRequest(t, NewRequestWithValues(t, "POST", "/user1/repo1/compare/master..unrelated", map[string]string{"title": "create an unrelated pr"}), http.StatusBadRequest) + // Now this PR could be marked conflict - or at least a race may occur - so drop down to pure code at this point... pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ HeadRepoID: repo1.ID,