From c8b5a1ddf70d4bf02a7607e293b4c59eab97f921 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Mon, 12 Jan 2026 13:47:06 -0800 Subject: [PATCH] Fix cancel auto merge bug (#36341) --- routers/api/v1/repo/pull.go | 2 +- routers/web/repo/pull.go | 22 ++++++++++++++++++++++ tests/integration/git_general_test.go | 27 +++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index b5bacd9669..f33867f4ed 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -1322,7 +1322,7 @@ func CancelScheduledAutoMerge(ctx *context.APIContext) { } if ctx.Doer.ID != autoMerge.DoerID { - allowed, err := access_model.IsUserRepoAdmin(ctx, ctx.Repo.Repository, ctx.Doer) + allowed, err := pull_service.IsUserAllowedToMerge(ctx, pull, ctx.Repo.Permission, ctx.Doer) if err != nil { ctx.APIErrorInternal(err) return diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index ecc1bb0644..60f0ee6db8 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -1265,6 +1265,28 @@ func CancelAutoMergePullRequest(ctx *context.Context) { return } + exist, autoMerge, err := pull_model.GetScheduledMergeByPullID(ctx, issue.PullRequest.ID) + if err != nil { + ctx.ServerError("GetScheduledMergeByPullID", err) + return + } + if !exist { + ctx.NotFound(nil) + return + } + + if ctx.Doer.ID != autoMerge.DoerID { + allowed, err := pull_service.IsUserAllowedToMerge(ctx, issue.PullRequest, ctx.Repo.Permission, ctx.Doer) + if err != nil { + ctx.ServerError("IsUserAllowedToMerge", err) + return + } + if !allowed { + ctx.HTTPError(http.StatusForbidden, "user has no permission to cancel the scheduled auto merge") + return + } + } + if err := automerge.RemoveScheduledAutoMerge(ctx, ctx.Doer, issue.PullRequest); err != nil { if db.IsErrNotExist(err) { ctx.Flash.Error(ctx.Tr("repo.pulls.auto_merge_not_scheduled")) diff --git a/tests/integration/git_general_test.go b/tests/integration/git_general_test.go index b9fa200dd0..f789ae3747 100644 --- a/tests/integration/git_general_test.go +++ b/tests/integration/git_general_test.go @@ -701,6 +701,11 @@ func doAutoPRMerge(baseCtx *APITestContext, dstPath string) func(t *testing.T) { defer tests.PrintCurrentTest(t)() ctx := NewAPITestContext(t, baseCtx.Username, baseCtx.Reponame, auth_model.AccessTokenScopeWriteRepository) + collaboratorCtx := NewAPITestContext(t, "user5", baseCtx.Reponame, auth_model.AccessTokenScopeWriteRepository) + readOnlyCtx := NewAPITestContext(t, "user4", baseCtx.Reponame, auth_model.AccessTokenScopeWriteRepository) + + t.Run("AddAutoMergeCollaborator", doAPIAddCollaborator(*baseCtx, collaboratorCtx.Username, perm.AccessModeWrite)) + t.Run("AddReadOnlyAutoMergeCollaborator", doAPIAddCollaborator(*baseCtx, readOnlyCtx.Username, perm.AccessModeRead)) // automerge will merge immediately if the PR is mergeable and there is no "status check" because no status check also means "all checks passed" // so we must set up a status check to test the auto merge feature @@ -747,10 +752,32 @@ func doAutoPRMerge(baseCtx *APITestContext, dstPath string) func(t *testing.T) { ctx.ExpectedCode = http.StatusConflict t.Run("AutoMergePRTwice", doAPIAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index)) + // Read-only collaborator still cannot cancel + readOnlyCtx.ExpectedCode = http.StatusForbidden + t.Run("CancelAutoMergePRByReadOnlyCollaboratorForbidden", doAPICancelAutoMergePullRequest(readOnlyCtx, baseCtx.Username, baseCtx.Reponame, pr.Index)) + readOnlyCtx.ExpectedCode = 0 + + // Collaborators with merge permissions can cancel a schedule made by someone else + collaboratorCtx.ExpectedCode = http.StatusNoContent + t.Run("CancelAutoMergePRByCollaborator", doAPICancelAutoMergePullRequest(collaboratorCtx, baseCtx.Username, baseCtx.Reponame, pr.Index)) + collaboratorCtx.ExpectedCode = 0 + + // Re-add auto merge request so the repo owner can cancel it as well + ctx.ExpectedCode = http.StatusCreated + t.Run("AutoMergePRAfterCollaboratorCancel", doAPIAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index)) + // Cancel auto merge request ctx.ExpectedCode = http.StatusNoContent t.Run("CancelAutoMergePR", doAPICancelAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index)) + // Collaborator can schedule but admins should still be able to cancel their schedule + collaboratorCtx.ExpectedCode = http.StatusCreated + t.Run("AutoMergePRByCollaborator", doAPIAutoMergePullRequest(collaboratorCtx, baseCtx.Username, baseCtx.Reponame, pr.Index)) + collaboratorCtx.ExpectedCode = 0 + + ctx.ExpectedCode = http.StatusNoContent + t.Run("CancelAutoMergePRByAdmin", doAPICancelAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index)) + // Add auto merge request ctx.ExpectedCode = http.StatusCreated t.Run("AutoMergePR", doAPIAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index))