mirror of
1
Fork 0

Merge pull request 'feat: add branch deletion for scheduled PRs' (#5689) from Tom3201/forgejo:tn-fix-branch-deletion into forgejo

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/5689
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
This commit is contained in:
Earl Warren 2024-10-31 06:41:50 +00:00
commit 745b1e778e
12 changed files with 340 additions and 274 deletions

View File

@ -82,6 +82,8 @@ var migrations = []*Migration{
NewMigration("Add SSH keypair to `pull_mirror` table", AddSSHKeypairToPushMirror), NewMigration("Add SSH keypair to `pull_mirror` table", AddSSHKeypairToPushMirror),
// v22 -> v23 // v22 -> v23
NewMigration("Add `legacy` to `web_authn_credential` table", AddLegacyToWebAuthnCredential), NewMigration("Add `legacy` to `web_authn_credential` table", AddLegacyToWebAuthnCredential),
// v23 -> v24
NewMigration("Add `delete_branch_after_merge` to `auto_merge` table", AddDeleteBranchAfterMergeToAutoMerge),
} }
// GetCurrentDBVersion returns the current Forgejo database version. // GetCurrentDBVersion returns the current Forgejo database version.

View File

@ -0,0 +1,16 @@
// Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package forgejo_migrations //nolint:revive
import "xorm.io/xorm"
// AddDeleteBranchAfterMergeToAutoMerge: add DeleteBranchAfterMerge column, setting existing rows to false
func AddDeleteBranchAfterMergeToAutoMerge(x *xorm.Engine) error {
type AutoMerge struct {
ID int64 `xorm:"pk autoincr"`
DeleteBranchAfterMerge bool `xorm:"NOT NULL DEFAULT false"`
}
return x.Sync(&AutoMerge{})
}

View File

@ -15,13 +15,14 @@ import (
// AutoMerge represents a pull request scheduled for merging when checks succeed // AutoMerge represents a pull request scheduled for merging when checks succeed
type AutoMerge struct { type AutoMerge struct {
ID int64 `xorm:"pk autoincr"` ID int64 `xorm:"pk autoincr"`
PullID int64 `xorm:"UNIQUE"` PullID int64 `xorm:"UNIQUE"`
DoerID int64 `xorm:"INDEX NOT NULL"` DoerID int64 `xorm:"INDEX NOT NULL"`
Doer *user_model.User `xorm:"-"` Doer *user_model.User `xorm:"-"`
MergeStyle repo_model.MergeStyle `xorm:"varchar(30)"` MergeStyle repo_model.MergeStyle `xorm:"varchar(30)"`
Message string `xorm:"LONGTEXT"` Message string `xorm:"LONGTEXT"`
CreatedUnix timeutil.TimeStamp `xorm:"created"` DeleteBranchAfterMerge bool `xorm:"NOT NULL DEFAULT false"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
} }
// TableName return database table name for xorm // TableName return database table name for xorm
@ -49,7 +50,7 @@ func IsErrAlreadyScheduledToAutoMerge(err error) bool {
} }
// ScheduleAutoMerge schedules a pull request to be merged when all checks succeed // ScheduleAutoMerge schedules a pull request to be merged when all checks succeed
func ScheduleAutoMerge(ctx context.Context, doer *user_model.User, pullID int64, style repo_model.MergeStyle, message string) error { func ScheduleAutoMerge(ctx context.Context, doer *user_model.User, pullID int64, style repo_model.MergeStyle, message string, deleteBranch bool) error {
// Check if we already have a merge scheduled for that pull request // Check if we already have a merge scheduled for that pull request
if exists, _, err := GetScheduledMergeByPullID(ctx, pullID); err != nil { if exists, _, err := GetScheduledMergeByPullID(ctx, pullID); err != nil {
return err return err
@ -58,10 +59,11 @@ func ScheduleAutoMerge(ctx context.Context, doer *user_model.User, pullID int64,
} }
_, err := db.GetEngine(ctx).Insert(&AutoMerge{ _, err := db.GetEngine(ctx).Insert(&AutoMerge{
DoerID: doer.ID, DoerID: doer.ID,
PullID: pullID, PullID: pullID,
MergeStyle: style, MergeStyle: style,
Message: message, Message: message,
DeleteBranchAfterMerge: deleteBranch,
}) })
return err return err
} }

View File

@ -989,7 +989,7 @@ func MergePullRequest(ctx *context.APIContext) {
} }
if form.MergeWhenChecksSucceed { if form.MergeWhenChecksSucceed {
scheduled, err := automerge.ScheduleAutoMerge(ctx, ctx.Doer, pr, repo_model.MergeStyle(form.Do), message) scheduled, err := automerge.ScheduleAutoMerge(ctx, ctx.Doer, pr, repo_model.MergeStyle(form.Do), message, form.DeleteBranchAfterMerge)
if err != nil { if err != nil {
if pull_model.IsErrAlreadyScheduledToAutoMerge(err) { if pull_model.IsErrAlreadyScheduledToAutoMerge(err) {
ctx.Error(http.StatusConflict, "ScheduleAutoMerge", err) ctx.Error(http.StatusConflict, "ScheduleAutoMerge", err)

View File

@ -28,7 +28,7 @@ func TestHandlePullRequestMerging(t *testing.T) {
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
err = pull_model.ScheduleAutoMerge(db.DefaultContext, user1, pr.ID, repo_model.MergeStyleSquash, "squash merge a pr") err = pull_model.ScheduleAutoMerge(db.DefaultContext, user1, pr.ID, repo_model.MergeStyleSquash, "squash merge a pr", false)
require.NoError(t, err) require.NoError(t, err)
autoMerge := unittest.AssertExistsAndLoadBean(t, &pull_model.AutoMerge{PullID: pr.ID}) autoMerge := unittest.AssertExistsAndLoadBean(t, &pull_model.AutoMerge{PullID: pr.ID})

View File

@ -1302,7 +1302,7 @@ func MergePullRequest(ctx *context.Context) {
// delete all scheduled auto merges // delete all scheduled auto merges
_ = pull_model.DeleteScheduledAutoMerge(ctx, pr.ID) _ = pull_model.DeleteScheduledAutoMerge(ctx, pr.ID)
// schedule auto merge // schedule auto merge
scheduled, err := automerge.ScheduleAutoMerge(ctx, ctx.Doer, pr, repo_model.MergeStyle(form.Do), message) scheduled, err := automerge.ScheduleAutoMerge(ctx, ctx.Doer, pr, repo_model.MergeStyle(form.Do), message, form.DeleteBranchAfterMerge)
if err != nil { if err != nil {
ctx.ServerError("ScheduleAutoMerge", err) ctx.ServerError("ScheduleAutoMerge", err)
return return

View File

@ -7,8 +7,6 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"strconv"
"strings"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
@ -24,20 +22,19 @@ import (
"code.gitea.io/gitea/modules/queue" "code.gitea.io/gitea/modules/queue"
notify_service "code.gitea.io/gitea/services/notify" notify_service "code.gitea.io/gitea/services/notify"
pull_service "code.gitea.io/gitea/services/pull" pull_service "code.gitea.io/gitea/services/pull"
repo_service "code.gitea.io/gitea/services/repository"
shared_automerge "code.gitea.io/gitea/services/shared/automerge"
) )
// prAutoMergeQueue represents a queue to handle update pull request tests
var prAutoMergeQueue *queue.WorkerPoolQueue[string]
// Init runs the task queue to that handles auto merges // Init runs the task queue to that handles auto merges
func Init() error { func Init() error {
notify_service.RegisterNotifier(NewNotifier()) notify_service.RegisterNotifier(NewNotifier())
prAutoMergeQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "pr_auto_merge", handler) shared_automerge.PRAutoMergeQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "pr_auto_merge", handler)
if prAutoMergeQueue == nil { if shared_automerge.PRAutoMergeQueue == nil {
return fmt.Errorf("unable to create pr_auto_merge queue") return fmt.Errorf("unable to create pr_auto_merge queue")
} }
go graceful.GetManager().RunWithCancel(prAutoMergeQueue) go graceful.GetManager().RunWithCancel(shared_automerge.PRAutoMergeQueue)
return nil return nil
} }
@ -55,17 +52,10 @@ func handler(items ...string) []string {
return nil return nil
} }
func addToQueue(pr *issues_model.PullRequest, sha string) {
log.Trace("Adding pullID: %d to the pull requests patch checking queue with sha %s", pr.ID, sha)
if err := prAutoMergeQueue.Push(fmt.Sprintf("%d_%s", pr.ID, sha)); err != nil {
log.Error("Error adding pullID: %d to the pull requests patch checking queue %v", pr.ID, err)
}
}
// ScheduleAutoMerge if schedule is false and no error, pull can be merged directly // ScheduleAutoMerge if schedule is false and no error, pull can be merged directly
func ScheduleAutoMerge(ctx context.Context, doer *user_model.User, pull *issues_model.PullRequest, style repo_model.MergeStyle, message string) (scheduled bool, err error) { func ScheduleAutoMerge(ctx context.Context, doer *user_model.User, pull *issues_model.PullRequest, style repo_model.MergeStyle, message string, deleteBranch bool) (scheduled bool, err error) {
err = db.WithTx(ctx, func(ctx context.Context) error { err = db.WithTx(ctx, func(ctx context.Context) error {
if err := pull_model.ScheduleAutoMerge(ctx, doer, pull.ID, style, message); err != nil { if err := pull_model.ScheduleAutoMerge(ctx, doer, pull.ID, style, message, deleteBranch); err != nil {
return err return err
} }
scheduled = true scheduled = true
@ -90,94 +80,12 @@ func RemoveScheduledAutoMerge(ctx context.Context, doer *user_model.User, pull *
// StartPRCheckAndAutoMergeBySHA start an automerge check and auto merge task for all pull requests of repository and SHA // StartPRCheckAndAutoMergeBySHA start an automerge check and auto merge task for all pull requests of repository and SHA
func StartPRCheckAndAutoMergeBySHA(ctx context.Context, sha string, repo *repo_model.Repository) error { func StartPRCheckAndAutoMergeBySHA(ctx context.Context, sha string, repo *repo_model.Repository) error {
pulls, err := getPullRequestsByHeadSHA(ctx, sha, repo, func(pr *issues_model.PullRequest) bool { return shared_automerge.StartPRCheckAndAutoMergeBySHA(ctx, sha, repo)
return !pr.HasMerged && pr.CanAutoMerge()
})
if err != nil {
return err
}
for _, pr := range pulls {
addToQueue(pr, sha)
}
return nil
} }
// StartPRCheckAndAutoMerge start an automerge check and auto merge task for a pull request // StartPRCheckAndAutoMerge start an automerge check and auto merge task for a pull request
func StartPRCheckAndAutoMerge(ctx context.Context, pull *issues_model.PullRequest) { func StartPRCheckAndAutoMerge(ctx context.Context, pull *issues_model.PullRequest) {
if pull == nil || pull.HasMerged || !pull.CanAutoMerge() { shared_automerge.StartPRCheckAndAutoMerge(ctx, pull)
return
}
if err := pull.LoadBaseRepo(ctx); err != nil {
log.Error("LoadBaseRepo: %v", err)
return
}
gitRepo, err := gitrepo.OpenRepository(ctx, pull.BaseRepo)
if err != nil {
log.Error("OpenRepository: %v", err)
return
}
defer gitRepo.Close()
commitID, err := gitRepo.GetRefCommitID(pull.GetGitRefName())
if err != nil {
log.Error("GetRefCommitID: %v", err)
return
}
addToQueue(pull, commitID)
}
func getPullRequestsByHeadSHA(ctx context.Context, sha string, repo *repo_model.Repository, filter func(*issues_model.PullRequest) bool) (map[int64]*issues_model.PullRequest, error) {
gitRepo, err := gitrepo.OpenRepository(ctx, repo)
if err != nil {
return nil, err
}
defer gitRepo.Close()
refs, err := gitRepo.GetRefsBySha(sha, "")
if err != nil {
return nil, err
}
pulls := make(map[int64]*issues_model.PullRequest)
for _, ref := range refs {
// Each pull branch starts with refs/pull/ we then go from there to find the index of the pr and then
// use that to get the pr.
if strings.HasPrefix(ref, git.PullPrefix) {
parts := strings.Split(ref[len(git.PullPrefix):], "/")
// e.g. 'refs/pull/1/head' would be []string{"1", "head"}
if len(parts) != 2 {
log.Error("getPullRequestsByHeadSHA found broken pull ref [%s] on repo [%-v]", ref, repo)
continue
}
prIndex, err := strconv.ParseInt(parts[0], 10, 64)
if err != nil {
log.Error("getPullRequestsByHeadSHA found broken pull ref [%s] on repo [%-v]", ref, repo)
continue
}
p, err := issues_model.GetPullRequestByIndex(ctx, repo.ID, prIndex)
if err != nil {
// If there is no pull request for this branch, we don't try to merge it.
if issues_model.IsErrPullRequestNotExist(err) {
continue
}
return nil, err
}
if filter(p) {
pulls[p.ID] = p
}
}
}
return pulls, nil
} }
// handlePullRequestAutoMerge merge the pull request if all checks are successful // handlePullRequestAutoMerge merge the pull request if all checks are successful
@ -303,4 +211,11 @@ func handlePullRequestAutoMerge(pullID int64, sha string) {
// on the pull request page. But this should not be finished in a bug fix PR which will be backport to release branch. // on the pull request page. But this should not be finished in a bug fix PR which will be backport to release branch.
return return
} }
if scheduledPRM.DeleteBranchAfterMerge {
err := repo_service.DeleteBranchAfterMerge(ctx, doer, pr, headGitRepo)
if err != nil {
log.Error("%d repo_service.DeleteBranchIfUnused: %v", pr.ID, err)
}
}
} }

View File

@ -19,7 +19,7 @@ import (
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/services/automerge" shared_automerge "code.gitea.io/gitea/services/shared/automerge"
) )
func getCacheKey(repoID int64, brancheName string) string { func getCacheKey(repoID int64, brancheName string) string {
@ -117,7 +117,7 @@ func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, creato
} }
if status.State.IsSuccess() { if status.State.IsSuccess() {
if err := automerge.StartPRCheckAndAutoMergeBySHA(ctx, sha, repo); err != nil { if err := shared_automerge.StartPRCheckAndAutoMergeBySHA(ctx, sha, repo); err != nil {
return fmt.Errorf("MergeScheduledPullRequest[repo_id: %d, user_id: %d, sha: %s]: %w", repo.ID, creator.ID, sha, err) return fmt.Errorf("MergeScheduledPullRequest[repo_id: %d, user_id: %d, sha: %s]: %w", repo.ID, creator.ID, sha, err)
} }
} }

View File

@ -0,0 +1,120 @@
// Copyright 2021 Gitea. All rights reserved.
// SPDX-License-Identifier: MIT
package automerge
import (
"context"
"fmt"
"strconv"
"strings"
issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/queue"
)
// PRAutoMergeQueue represents a queue to handle update pull request tests
var PRAutoMergeQueue *queue.WorkerPoolQueue[string]
func addToQueue(pr *issues_model.PullRequest, sha string) {
log.Trace("Adding pullID: %d to the pull requests patch checking queue with sha %s", pr.ID, sha)
if err := PRAutoMergeQueue.Push(fmt.Sprintf("%d_%s", pr.ID, sha)); err != nil {
log.Error("Error adding pullID: %d to the pull requests patch checking queue %v", pr.ID, err)
}
}
// StartPRCheckAndAutoMergeBySHA start an automerge check and auto merge task for all pull requests of repository and SHA
func StartPRCheckAndAutoMergeBySHA(ctx context.Context, sha string, repo *repo_model.Repository) error {
pulls, err := getPullRequestsByHeadSHA(ctx, sha, repo, func(pr *issues_model.PullRequest) bool {
return !pr.HasMerged && pr.CanAutoMerge()
})
if err != nil {
return err
}
for _, pr := range pulls {
addToQueue(pr, sha)
}
return nil
}
// StartPRCheckAndAutoMerge start an automerge check and auto merge task for a pull request
func StartPRCheckAndAutoMerge(ctx context.Context, pull *issues_model.PullRequest) {
if pull == nil || pull.HasMerged || !pull.CanAutoMerge() {
return
}
if err := pull.LoadBaseRepo(ctx); err != nil {
log.Error("LoadBaseRepo: %v", err)
return
}
gitRepo, err := gitrepo.OpenRepository(ctx, pull.BaseRepo)
if err != nil {
log.Error("OpenRepository: %v", err)
return
}
defer gitRepo.Close()
commitID, err := gitRepo.GetRefCommitID(pull.GetGitRefName())
if err != nil {
log.Error("GetRefCommitID: %v", err)
return
}
addToQueue(pull, commitID)
}
func getPullRequestsByHeadSHA(ctx context.Context, sha string, repo *repo_model.Repository, filter func(*issues_model.PullRequest) bool) (map[int64]*issues_model.PullRequest, error) {
gitRepo, err := gitrepo.OpenRepository(ctx, repo)
if err != nil {
return nil, err
}
defer gitRepo.Close()
refs, err := gitRepo.GetRefsBySha(sha, "")
if err != nil {
return nil, err
}
pulls := make(map[int64]*issues_model.PullRequest)
for _, ref := range refs {
// Each pull branch starts with refs/pull/ we then go from there to find the index of the pr and then
// use that to get the pr.
if strings.HasPrefix(ref, git.PullPrefix) {
parts := strings.Split(ref[len(git.PullPrefix):], "/")
// e.g. 'refs/pull/1/head' would be []string{"1", "head"}
if len(parts) != 2 {
log.Error("getPullRequestsByHeadSHA found broken pull ref [%s] on repo [%-v]", ref, repo)
continue
}
prIndex, err := strconv.ParseInt(parts[0], 10, 64)
if err != nil {
log.Error("getPullRequestsByHeadSHA found broken pull ref [%s] on repo [%-v]", ref, repo)
continue
}
p, err := issues_model.GetPullRequestByIndex(ctx, repo.ID, prIndex)
if err != nil {
// If there is no pull request for this branch, we don't try to merge it.
if issues_model.IsErrPullRequestNotExist(err) {
continue
}
return nil, err
}
if filter(p) {
pulls[p.ID] = p
}
}
}
return pulls, nil
}

View File

@ -34,7 +34,7 @@ func TestActionsAutomerge(t *testing.T) {
assert.False(t, pr.HasMerged, "PR should not be merged") assert.False(t, pr.HasMerged, "PR should not be merged")
assert.Equal(t, issues_model.PullRequestStatusMergeable, pr.Status, "PR should be mergeable") assert.Equal(t, issues_model.PullRequestStatusMergeable, pr.Status, "PR should be mergeable")
scheduled, err := automerge.ScheduleAutoMerge(ctx, user, pr, repo_model.MergeStyleMerge, "Dummy") scheduled, err := automerge.ScheduleAutoMerge(ctx, user, pr, repo_model.MergeStyleMerge, "Dummy", false)
require.NoError(t, err, "PR should be scheduled for automerge") require.NoError(t, err, "PR should be scheduled for automerge")
assert.True(t, scheduled, "PR should be scheduled for automerge") assert.True(t, scheduled, "PR should be scheduled for automerge")

View File

@ -846,180 +846,191 @@ func TestPullMergeBranchProtect(t *testing.T) {
}) })
} }
func TestPullAutoMergeAfterCommitStatusSucceed(t *testing.T) { func testPullAutoMergeAfterCommitStatusSucceed(t *testing.T, ctx APITestContext, forkName string, approval, deleteBranch, withAPI bool) {
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { // prepare environment (fork repo, create user)
// create a pull request user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
session := loginUser(t, "user1") testRepoFork(t, ctx.Session, "user2", "repo1", "user1", forkName)
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) defer func() {
forkedName := "repo1-1" testDeleteRepository(t, ctx.Session, "user1", forkName)
testRepoFork(t, session, "user2", "repo1", "user1", forkedName) }()
defer func() {
testDeleteRepository(t, session, "user1", forkedName)
}()
testEditFile(t, session, "user1", forkedName, "master", "README.md", "Hello, World (Edited)\n")
testPullCreate(t, session, "user1", forkedName, false, "master", "master", "Indexer notifier test pull")
baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user2", Name: "repo1"}) // create a pull request with some changes
forkedRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user1", Name: forkedName}) branchName := "master"
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ if deleteBranch {
BaseRepoID: baseRepo.ID, branchName = "new_branch_1"
BaseBranch: "master", testEditFileToNewBranch(t, ctx.Session, "user1", forkName, "master", branchName, "README.md", "Hello, World (Edited)\n")
HeadRepoID: forkedRepo.ID, } else {
HeadBranch: "master", testEditFile(t, ctx.Session, "user1", forkName, "master", "README.md", "Hello, World (Edited)\n")
}) }
testPullCreate(t, ctx.Session, "user1", forkName, false, "master", branchName, "Indexer notifier test pull")
// add protected branch for commit status baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user2", Name: "repo1"})
csrf := GetCSRF(t, session, "/user2/repo1/settings/branches") forkedRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user1", Name: forkName})
// Change master branch to protected pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
req := NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/edit", map[string]string{ BaseRepoID: baseRepo.ID,
"_csrf": csrf, BaseBranch: "master",
"rule_name": "master", HeadRepoID: forkedRepo.ID,
"enable_push": "true", HeadBranch: branchName,
"enable_status_check": "true",
"status_check_contexts": "gitea/actions",
})
session.MakeRequest(t, req, http.StatusSeeOther)
// first time insert automerge record, return true
scheduled, err := automerge.ScheduleAutoMerge(db.DefaultContext, user1, pr, repo_model.MergeStyleMerge, "auto merge test")
require.NoError(t, err)
assert.True(t, scheduled)
// second time insert automerge record, return false because it does exist
scheduled, err = automerge.ScheduleAutoMerge(db.DefaultContext, user1, pr, repo_model.MergeStyleMerge, "auto merge test")
require.Error(t, err)
assert.False(t, scheduled)
// reload pr again
pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID})
assert.False(t, pr.HasMerged)
assert.Empty(t, pr.MergedCommitID)
// update commit status to success, then it should be merged automatically
baseGitRepo, err := gitrepo.OpenRepository(db.DefaultContext, baseRepo)
require.NoError(t, err)
sha, err := baseGitRepo.GetRefCommitID(pr.GetGitRefName())
require.NoError(t, err)
masterCommitID, err := baseGitRepo.GetBranchCommitID("master")
require.NoError(t, err)
branches, _, err := baseGitRepo.GetBranchNames(0, 100)
require.NoError(t, err)
assert.ElementsMatch(t, []string{"sub-home-md-img-check", "home-md-img-check", "pr-to-update", "branch2", "DefaultBranch", "develop", "feature/1", "master"}, branches)
baseGitRepo.Close()
defer func() {
testResetRepo(t, baseRepo.RepoPath(), "master", masterCommitID)
}()
err = commitstatus_service.CreateCommitStatus(db.DefaultContext, baseRepo, user1, sha, &git_model.CommitStatus{
State: api.CommitStatusSuccess,
TargetURL: "https://gitea.com",
Context: "gitea/actions",
})
require.NoError(t, err)
time.Sleep(2 * time.Second)
// realod pr again
pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID})
assert.True(t, pr.HasMerged)
assert.NotEmpty(t, pr.MergedCommitID)
unittest.AssertNotExistsBean(t, &pull_model.AutoMerge{PullID: pr.ID})
}) })
}
func TestPullAutoMergeAfterCommitStatusSucceedAndApproval(t *testing.T) { if deleteBranch {
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { // check if new branch exists
// create a pull request forkedGitRepo, err := gitrepo.OpenRepository(db.DefaultContext, forkedRepo)
session := loginUser(t, "user1")
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
forkedName := "repo1-2"
testRepoFork(t, session, "user2", "repo1", "user1", forkedName)
defer func() {
testDeleteRepository(t, session, "user1", forkedName)
}()
testEditFile(t, session, "user1", forkedName, "master", "README.md", "Hello, World (Edited)\n")
testPullCreate(t, session, "user1", forkedName, false, "master", "master", "Indexer notifier test pull")
baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user2", Name: "repo1"})
forkedRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user1", Name: forkedName})
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
BaseRepoID: baseRepo.ID,
BaseBranch: "master",
HeadRepoID: forkedRepo.ID,
HeadBranch: "master",
})
// add protected branch for commit status
csrf := GetCSRF(t, session, "/user2/repo1/settings/branches")
// Change master branch to protected
req := NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/edit", map[string]string{
"_csrf": csrf,
"rule_name": "master",
"enable_push": "true",
"enable_status_check": "true",
"status_check_contexts": "gitea/actions",
"required_approvals": "1",
})
session.MakeRequest(t, req, http.StatusSeeOther)
// first time insert automerge record, return true
scheduled, err := automerge.ScheduleAutoMerge(db.DefaultContext, user1, pr, repo_model.MergeStyleMerge, "auto merge test")
require.NoError(t, err) require.NoError(t, err)
assert.True(t, scheduled) newBranch, err := forkedGitRepo.GetBranch(branchName)
// second time insert automerge record, return false because it does exist
scheduled, err = automerge.ScheduleAutoMerge(db.DefaultContext, user1, pr, repo_model.MergeStyleMerge, "auto merge test")
require.Error(t, err)
assert.False(t, scheduled)
// reload pr again
pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID})
assert.False(t, pr.HasMerged)
assert.Empty(t, pr.MergedCommitID)
// update commit status to success, then it should be merged automatically
baseGitRepo, err := gitrepo.OpenRepository(db.DefaultContext, baseRepo)
require.NoError(t, err) require.NoError(t, err)
sha, err := baseGitRepo.GetRefCommitID(pr.GetGitRefName()) assert.NotNil(t, newBranch)
require.NoError(t, err) forkedGitRepo.Close()
masterCommitID, err := baseGitRepo.GetBranchCommitID("master") }
require.NoError(t, err)
baseGitRepo.Close()
defer func() {
testResetRepo(t, baseRepo.RepoPath(), "master", masterCommitID)
}()
err = commitstatus_service.CreateCommitStatus(db.DefaultContext, baseRepo, user1, sha, &git_model.CommitStatus{ // schedule pull request for automerge
State: api.CommitStatusSuccess, if withAPI {
TargetURL: "https://gitea.com", mergePullRequestForm := forms.MergePullRequestForm{
Context: "gitea/actions", MergeMessageField: "auto merge test",
}) Do: string(repo_model.MergeStyleMerge),
require.NoError(t, err) MergeWhenChecksSucceed: true,
DeleteBranchAfterMerge: deleteBranch,
}
time.Sleep(2 * time.Second) // first time scheduling an automerge pull request, should return a 201
ctx.ExpectedCode = http.StatusCreated
doAPIMergePullRequestForm(t, ctx, "user2", "repo1", pr.Index, &mergePullRequestForm)
// reload pr again // second time scheduling an automerge pull request, should return a 409
ctx.ExpectedCode = http.StatusConflict
doAPIMergePullRequestForm(t, ctx, "user2", "repo1", pr.Index, &mergePullRequestForm)
} else {
mergePullRequestForm := map[string]string{
"merge_message_field": "auto merge test",
"do": string(repo_model.MergeStyleMerge),
"merge_when_checks_succeed": "true",
"delete_branch_after_merge": strconv.FormatBool(deleteBranch),
}
// first time scheduling an automerge pull request, should return a 200
testPullMergeForm(t, ctx.Session, http.StatusOK, "user2", "repo1", strconv.FormatInt(pr.Index, 10), mergePullRequestForm)
// second time scheduling an automerge pull request, should delete the previous scheduled automerge and return a 200 again
testPullMergeForm(t, ctx.Session, http.StatusOK, "user2", "repo1", strconv.FormatInt(pr.Index, 10), mergePullRequestForm)
}
// reload PR again
pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID})
assert.False(t, pr.HasMerged)
assert.Empty(t, pr.MergedCommitID)
// update commit status to success, then it should be merged automatically
baseGitRepo, err := gitrepo.OpenRepository(db.DefaultContext, baseRepo)
require.NoError(t, err)
sha, err := baseGitRepo.GetRefCommitID(pr.GetGitRefName())
require.NoError(t, err)
masterCommitID, err := baseGitRepo.GetBranchCommitID("master")
require.NoError(t, err)
branches, _, err := baseGitRepo.GetBranchNames(0, 100)
require.NoError(t, err)
assert.ElementsMatch(t, []string{"sub-home-md-img-check", "home-md-img-check", "pr-to-update", "branch2", "DefaultBranch", "develop", "feature/1", "master"}, branches)
baseGitRepo.Close()
defer func() {
testResetRepo(t, baseRepo.RepoPath(), "master", masterCommitID)
}()
err = commitstatus_service.CreateCommitStatus(db.DefaultContext, baseRepo, user1, sha, &git_model.CommitStatus{
State: api.CommitStatusSuccess,
TargetURL: "https://gitea.com",
Context: "gitea/actions",
})
require.NoError(t, err)
time.Sleep(2 * time.Second)
// approve PR if necessary
if approval {
// reload PR again
pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID}) pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID})
assert.False(t, pr.HasMerged) assert.False(t, pr.HasMerged)
assert.Empty(t, pr.MergedCommitID) assert.Empty(t, pr.MergedCommitID)
// approve the PR from non-author // approve the PR from non-author
approveSession := loginUser(t, "user2") approveSession := loginUser(t, "user2")
req = NewRequest(t, "GET", fmt.Sprintf("/user2/repo1/pulls/%d", pr.Index)) req := NewRequest(t, "GET", fmt.Sprintf("/user2/repo1/pulls/%d", pr.Index))
resp := approveSession.MakeRequest(t, req, http.StatusOK) resp := approveSession.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body) htmlDoc := NewHTMLParser(t, resp.Body)
testSubmitReview(t, approveSession, htmlDoc.GetCSRF(), "user2", "repo1", strconv.Itoa(int(pr.Index)), sha, "approve", http.StatusOK) testSubmitReview(t, approveSession, htmlDoc.GetCSRF(), "user2", "repo1", strconv.Itoa(int(pr.Index)), sha, "approve", http.StatusOK)
time.Sleep(2 * time.Second) time.Sleep(2 * time.Second)
}
// realod pr again // reload PR again
pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID}) pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID})
assert.True(t, pr.HasMerged) assert.True(t, pr.HasMerged)
assert.NotEmpty(t, pr.MergedCommitID) assert.NotEmpty(t, pr.MergedCommitID)
unittest.AssertNotExistsBean(t, &pull_model.AutoMerge{PullID: pr.ID}) unittest.AssertNotExistsBean(t, &pull_model.AutoMerge{PullID: pr.ID})
if deleteBranch {
// check if new branch got removed
forkedGitRepo, err := gitrepo.OpenRepository(db.DefaultContext, forkedRepo)
require.NoError(t, err)
_, err = forkedGitRepo.GetBranch(branchName)
require.Error(t, err)
assert.True(t, git.IsErrBranchNotExist(err))
forkedGitRepo.Close()
}
}
func TestPullAutoMergeAfterCommitStatusSucceed(t *testing.T) {
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
for _, testCase := range []struct {
name string
forkName string
approval bool
deleteBranch bool
}{
{
name: "TestPullAutoMergeAfterCommitStatusSucceed",
forkName: "repo1-1",
approval: false,
deleteBranch: false,
},
{
name: "TestPullAutoMergeAfterCommitStatusSucceedWithBranchDeletion",
forkName: "repo1-2",
approval: false,
deleteBranch: true,
},
{
name: "TestPullAutoMergeAfterCommitStatusSucceedAndApproval",
forkName: "repo1-3",
approval: true,
deleteBranch: false,
},
{
name: "TestPullAutoMergeAfterCommitStatusSucceedAndApprovalWithBranchDeletion",
forkName: "repo1-4",
approval: true,
deleteBranch: true,
},
} {
// perform all tests with API and web routes
for _, withAPI := range []bool{false, true} {
t.Run(testCase.name, func(t *testing.T) {
protectedBranch := parameterProtectBranch{
"enable_push": "true",
"enable_status_check": "true",
"status_check_contexts": "gitea/actions",
}
if testCase.approval {
protectedBranch["required_approvals"] = "1"
}
ctx := NewAPITestContext(t, "user2", "repo1", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
doProtectBranch(ctx, "master", protectedBranch)(t)
ctx = NewAPITestContext(t, "user1", "repo1", auth_model.AccessTokenScopeWriteRepository)
testPullAutoMergeAfterCommitStatusSucceed(t, ctx, testCase.forkName, testCase.approval, testCase.deleteBranch, withAPI)
})
}
}
}) })
} }
@ -1094,12 +1105,12 @@ func TestPullAutoMergeAfterCommitStatusSucceedAndApprovalForAgitFlow(t *testing.
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
// first time insert automerge record, return true // first time insert automerge record, return true
scheduled, err := automerge.ScheduleAutoMerge(db.DefaultContext, user1, pr, repo_model.MergeStyleMerge, "auto merge test") scheduled, err := automerge.ScheduleAutoMerge(db.DefaultContext, user1, pr, repo_model.MergeStyleMerge, "auto merge test", false)
require.NoError(t, err) require.NoError(t, err)
assert.True(t, scheduled) assert.True(t, scheduled)
// second time insert automerge record, return false because it does exist // second time insert automerge record, return false because it does exist
scheduled, err = automerge.ScheduleAutoMerge(db.DefaultContext, user1, pr, repo_model.MergeStyleMerge, "auto merge test") scheduled, err = automerge.ScheduleAutoMerge(db.DefaultContext, user1, pr, repo_model.MergeStyleMerge, "auto merge test", false)
require.Error(t, err) require.Error(t, err)
assert.False(t, scheduled) assert.False(t, scheduled)

View File

@ -130,7 +130,7 @@ export default {
{{ mergeForm.textCancel }} {{ mergeForm.textCancel }}
</button> </button>
<div class="ui checkbox tw-ml-1" v-if="mergeForm.isPullBranchDeletable && !autoMergeWhenSucceed"> <div class="ui checkbox tw-ml-1" v-if="mergeForm.isPullBranchDeletable">
<input name="delete_branch_after_merge" type="checkbox" v-model="deleteBranchAfterMerge" id="delete-branch-after-merge"> <input name="delete_branch_after_merge" type="checkbox" v-model="deleteBranchAfterMerge" id="delete-branch-after-merge">
<label for="delete-branch-after-merge">{{ mergeForm.textDeleteBranch }}</label> <label for="delete-branch-after-merge">{{ mergeForm.textDeleteBranch }}</label>
</div> </div>