Changes
47 changed files (+869/-26)
-
-
@@ -314,6 +314,37 @@ func doAPIManuallyMergePullRequest(ctx APITestContext, owner, repo, commitID str} } func doAPIAutoMergePullRequest(ctx APITestContext, owner, repo string, index int64) func(*testing.T) { return func(t *testing.T) { urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/merge?token=%s", owner, repo, index, ctx.Token) req := NewRequestWithJSON(t, http.MethodPost, urlStr, &forms.MergePullRequestForm{ MergeMessageField: "doAPIMergePullRequest Merge", Do: string(repo_model.MergeStyleMerge), MergeWhenChecksSucceed: true, }) if ctx.ExpectedCode != 0 { ctx.Session.MakeRequest(t, req, ctx.ExpectedCode) return } ctx.Session.MakeRequest(t, req, 200) } } func doAPICancelAutoMergePullRequest(ctx APITestContext, owner, repo string, index int64) func(*testing.T) { return func(t *testing.T) { urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/merge?token=%s", owner, repo, index, ctx.Token) req := NewRequest(t, http.MethodDelete, urlStr) if ctx.ExpectedCode != 0 { ctx.Session.MakeRequest(t, req, ctx.ExpectedCode) return } ctx.Session.MakeRequest(t, req, 204) } } func doAPIGetBranch(ctx APITestContext, branch string, callback ...func(*testing.T, api.Branch)) func(*testing.T) { return func(t *testing.T) { req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/branches/%s?token=%s", ctx.Username, ctx.Reponame, branch, ctx.Token)
-
-
-
@@ -82,6 +82,7 @@ func testGit(t *testing.T, u *url.URL) {t.Run("CreateAgitFlowPull", doCreateAgitFlowPull(dstPath, &httpContext, "master", "test/head")) t.Run("BranchProtectMerge", doBranchProtectPRMerge(&httpContext, dstPath)) t.Run("AutoMerge", doAutoPRMerge(&httpContext, dstPath)) t.Run("CreatePRAndSetManuallyMerged", doCreatePRAndSetManuallyMerged(httpContext, httpContext, dstPath, "master", "test-manually-merge")) t.Run("MergeFork", func(t *testing.T) { defer PrintCurrentTest(t)()
-
@@ -615,6 +616,88 @@ func doBranchDelete(ctx APITestContext, owner, repo, branch string) func(*testin} } func doAutoPRMerge(baseCtx *APITestContext, dstPath string) func(t *testing.T) { return func(t *testing.T) { defer PrintCurrentTest(t)() ctx := NewAPITestContext(t, baseCtx.Username, baseCtx.Reponame) t.Run("CheckoutProtected", doGitCheckoutBranch(dstPath, "protected")) t.Run("PullProtected", doGitPull(dstPath, "origin", "protected")) t.Run("GenerateCommit", func(t *testing.T) { _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-") assert.NoError(t, err) }) t.Run("PushToUnprotectedBranch", doGitPushTestRepository(dstPath, "origin", "protected:unprotected3")) var pr api.PullRequest var err error t.Run("CreatePullRequest", func(t *testing.T) { pr, err = doAPICreatePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, "protected", "unprotected3")(t) assert.NoError(t, err) }) // Request repository commits page req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d/commits", baseCtx.Username, baseCtx.Reponame, pr.Index)) resp := ctx.Session.MakeRequest(t, req, http.StatusOK) doc := NewHTMLParser(t, resp.Body) // Get first commit URL commitURL, exists := doc.doc.Find("#commits-table tbody tr td.sha a").Last().Attr("href") assert.True(t, exists) assert.NotEmpty(t, commitURL) commitID := path.Base(commitURL) // Call API to add Pending status for commit t.Run("CreateStatus", doAPICreateCommitStatus(ctx, commitID, api.CommitStatusPending)) // Cancel not existing auto merge ctx.ExpectedCode = http.StatusNotFound t.Run("CancelAutoMergePR", 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)) // Can not create schedule twice ctx.ExpectedCode = http.StatusConflict t.Run("AutoMergePRTwice", 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)) // Add auto merge request ctx.ExpectedCode = http.StatusCreated t.Run("AutoMergePR", doAPIAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index)) // Check pr status ctx.ExpectedCode = 0 pr, err = doAPIGetPullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index)(t) assert.NoError(t, err) assert.False(t, pr.HasMerged) // Call API to add Failure status for commit t.Run("CreateStatus", doAPICreateCommitStatus(ctx, commitID, api.CommitStatusFailure)) // Check pr status pr, err = doAPIGetPullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index)(t) assert.NoError(t, err) assert.False(t, pr.HasMerged) // Call API to add Success status for commit t.Run("CreateStatus", doAPICreateCommitStatus(ctx, commitID, api.CommitStatusSuccess)) // wait to let gitea merge stuff time.Sleep(time.Second) // test pr status pr, err = doAPIGetPullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index)(t) assert.NoError(t, err) assert.True(t, pr.HasMerged) } } func doCreateAgitFlowPull(dstPath string, ctx *APITestContext, baseBranch, headBranch string) func(t *testing.T) { return func(t *testing.T) { defer PrintCurrentTest(t)()
-
-
-
@@ -63,20 +63,13 @@ func TestPullCreate_CommitStatus(t *testing.T) {api.CommitStatusWarning: "warning sign icon yellow", } testCtx := NewAPITestContext(t, "user1", "repo1") // Update commit status, and check if icon is updated as well for _, status := range statusList { // Call API to add status for commit token := getTokenForLoggedInUser(t, session) req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/user1/repo1/statuses/%s?token=%s", commitID, token), api.CreateStatusOption{ State: status, TargetURL: "http://test.ci/", Description: "", Context: "testci", }, ) session.MakeRequest(t, req, http.StatusCreated) t.Run("CreateStatus", doAPICreateCommitStatus(testCtx, commitID, status)) req = NewRequestf(t, "GET", "/user1/repo1/pulls/1/commits") resp = session.MakeRequest(t, req, http.StatusOK)
-
@@ -94,6 +87,24 @@ func TestPullCreate_CommitStatus(t *testing.T) {}) } func doAPICreateCommitStatus(ctx APITestContext, commitID string, status api.CommitStatusState) func(*testing.T) { return func(t *testing.T) { req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/statuses/%s?token=%s", ctx.Username, ctx.Reponame, commitID, ctx.Token), api.CreateStatusOption{ State: status, TargetURL: "http://test.ci/", Description: "", Context: "testci", }, ) if ctx.ExpectedCode != 0 { ctx.Session.MakeRequest(t, req, ctx.ExpectedCode) return } ctx.Session.MakeRequest(t, req, http.StatusCreated) } } func TestPullCreate_EmptyChangesWithCommits(t *testing.T) { onGiteaRun(t, func(t *testing.T, u *url.URL) { session := loginUser(t, "user1")
-
-
-
@@ -36,7 +36,6 @@ func doTestRepoCommitWithStatus(t *testing.T, state string, classes ...string) {defer prepareTestEnv(t)() session := loginUser(t, "user2") token := getTokenForLoggedInUser(t, session) // Request repository commits page req := NewRequest(t, "GET", "/user2/repo1/commits/branch/master")
-
@@ -49,16 +48,7 @@ func doTestRepoCommitWithStatus(t *testing.T, state string, classes ...string) {assert.NotEmpty(t, commitURL) // Call API to add status for commit req = NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/statuses/"+path.Base(commitURL)+"?token="+token, api.CreateStatusOption{ State: api.CommitStatusState(state), TargetURL: "http://test.ci/", Description: "", Context: "testci", }, ) resp = session.MakeRequest(t, req, http.StatusCreated) t.Run("CreateStatus", doAPICreateCommitStatus(NewAPITestContext(t, "user2", "repo1"), path.Base(commitURL), api.CommitStatusState(state))) req = NewRequest(t, "GET", "/user2/repo1/commits/branch/master") resp = session.MakeRequest(t, req, http.StatusOK)
-
-
-
@@ -110,6 +110,10 @@ const (CommentTypeDismissReview // 33 Change issue ref CommentTypeChangeIssueRef // 34 pr was scheduled to auto merge when checks succeed CommentTypePRScheduledToAutoMerge // 35 pr was un scheduled to auto merge when checks succeed CommentTypePRUnScheduledToAutoMerge ) var commentStrings = []string{
-
@@ -147,6 +151,8 @@ var commentStrings = []string{"project_board", "dismiss_review", "change_issue_ref", "pull_scheduled_merge", "pull_cancel_scheduled_merge", } func (t CommentType) String() string {
-
-
-
@@ -383,6 +383,8 @@ var migrations = []Migration{NewMigration("Add package tables", addPackageTables), // v213 -> v214 NewMigration("Add allow edits from maintainers to PullRequest table", addAllowMaintainerEdit), // v214 -> v215 NewMigration("Add auto merge table", addAutoMergeTable), } // GetCurrentDBVersion returns the current db version
-
-
-
@@ -0,0 +1,23 @@// Copyright 2022 The Gitea Authors. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package migrations import ( "xorm.io/xorm" ) func addAutoMergeTable(x *xorm.Engine) error { type MergeStyle string type PullAutoMerge struct { ID int64 `xorm:"pk autoincr"` PullID int64 `xorm:"UNIQUE"` DoerID int64 `xorm:"NOT NULL"` MergeStyle MergeStyle `xorm:"varchar(30)"` Message string `xorm:"LONGTEXT"` CreatedUnix int64 `xorm:"created"` } return x.Sync2(&PullAutoMerge{}) }
-
-
-
@@ -20,6 +20,8 @@ import ("code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" "xorm.io/builder" ) // PullRequestType defines pull request type
-
@@ -675,6 +677,18 @@ func (pr *PullRequest) IsSameRepo() bool {return pr.BaseRepoID == pr.HeadRepoID } // GetPullRequestsByHeadBranch returns all prs by head branch // Since there could be multiple prs with the same head branch, this function returns a slice of prs func GetPullRequestsByHeadBranch(ctx context.Context, headBranch string, headRepoID int64) ([]*PullRequest, error) { log.Trace("GetPullRequestsByHeadBranch: headBranch: '%s', headRepoID: '%d'", headBranch, headRepoID) prs := make([]*PullRequest, 0, 2) if err := db.GetEngine(ctx).Where(builder.Eq{"head_branch": headBranch, "head_repo_id": headRepoID}). Find(&prs); err != nil { return nil, err } return prs, nil } // GetBaseBranchHTMLURL returns the HTML URL of the base branch func (pr *PullRequest) GetBaseBranchHTMLURL() string { if err := pr.LoadBaseRepo(); err != nil {
-
-
models/pull/automerge.go (new)
-
@@ -0,0 +1,143 @@// Copyright 2022 Gitea. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package pull import ( "context" "fmt" "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/timeutil" ) // AutoMerge represents a pull request scheduled for merging when checks succeed type AutoMerge struct { ID int64 `xorm:"pk autoincr"` PullID int64 `xorm:"UNIQUE"` DoerID int64 `xorm:"NOT NULL"` Doer *user_model.User `xorm:"-"` MergeStyle repo_model.MergeStyle `xorm:"varchar(30)"` Message string `xorm:"LONGTEXT"` CreatedUnix timeutil.TimeStamp `xorm:"created"` } // TableName return database table name for xorm func (AutoMerge) TableName() string { return "pull_auto_merge" } func init() { db.RegisterModel(new(AutoMerge)) } // ErrAlreadyScheduledToAutoMerge represents a "PullRequestHasMerged"-error type ErrAlreadyScheduledToAutoMerge struct { PullID int64 } func (err ErrAlreadyScheduledToAutoMerge) Error() string { return fmt.Sprintf("pull request is already scheduled to auto merge when checks succeed [pull_id: %d]", err.PullID) } // IsErrAlreadyScheduledToAutoMerge checks if an error is a ErrAlreadyScheduledToAutoMerge. func IsErrAlreadyScheduledToAutoMerge(err error) bool { _, ok := err.(ErrAlreadyScheduledToAutoMerge) return ok } // 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 { // Check if we already have a merge scheduled for that pull request if exists, _, err := GetScheduledMergeByPullID(ctx, pullID); err != nil { return err } else if exists { return ErrAlreadyScheduledToAutoMerge{PullID: pullID} } if _, err := db.GetEngine(ctx).Insert(&AutoMerge{ DoerID: doer.ID, PullID: pullID, MergeStyle: style, Message: message, }); err != nil { return err } pr, err := models.GetPullRequestByID(ctx, pullID) if err != nil { return err } _, err = createAutoMergeComment(ctx, models.CommentTypePRScheduledToAutoMerge, pr, doer) return err } // GetScheduledMergeByPullID gets a scheduled pull request merge by pull request id func GetScheduledMergeByPullID(ctx context.Context, pullID int64) (bool, *AutoMerge, error) { scheduledPRM := &AutoMerge{} exists, err := db.GetEngine(ctx).Where("pull_id = ?", pullID).Get(scheduledPRM) if err != nil || !exists { return false, nil, err } doer, err := user_model.GetUserByIDCtx(ctx, scheduledPRM.DoerID) if err != nil { return false, nil, err } scheduledPRM.Doer = doer return true, scheduledPRM, nil } // RemoveScheduledAutoMerge cancels a previously scheduled pull request func RemoveScheduledAutoMerge(ctx context.Context, doer *user_model.User, pullID int64, comment bool) error { return db.WithTx(func(ctx context.Context) error { exist, scheduledPRM, err := GetScheduledMergeByPullID(ctx, pullID) if err != nil { return err } else if !exist { return models.ErrNotExist{ID: pullID} } if _, err := db.GetEngine(ctx).ID(scheduledPRM.ID).Delete(&AutoMerge{}); err != nil { return err } // if pull got merged we don't need to add "auto-merge canceled comment" if !comment || doer == nil { return nil } pr, err := models.GetPullRequestByID(ctx, pullID) if err != nil { return err } _, err = createAutoMergeComment(ctx, models.CommentTypePRUnScheduledToAutoMerge, pr, doer) return err }, ctx) } // createAutoMergeComment is a internal function, only use it for CommentTypePRScheduledToAutoMerge and CommentTypePRUnScheduledToAutoMerge CommentTypes func createAutoMergeComment(ctx context.Context, typ models.CommentType, pr *models.PullRequest, doer *user_model.User) (comment *models.Comment, err error) { if err = pr.LoadIssueCtx(ctx); err != nil { return } if err = pr.LoadBaseRepoCtx(ctx); err != nil { return } comment, err = models.CreateCommentCtx(ctx, &models.CreateCommentOptions{ Type: typ, Doer: doer, Repo: pr.BaseRepo, Issue: pr.Issue, }) return }
-
-
-
@@ -144,3 +144,19 @@ func (repo *Repository) WalkReferences(arg ObjectType, skip, limit int, walkfn f}) return i, err } // GetRefsBySha returns all references filtered with prefix that belong to a sha commit hash func (repo *Repository) GetRefsBySha(sha, prefix string) ([]string, error) { var revList []string iter, err := repo.gogitRepo.References() if err != nil { return nil, err } err = iter.ForEach(func(ref *plumbing.Reference) error { if ref.Hash().String() == sha && strings.HasPrefix(string(ref.Name()), prefix) { revList = append(revList, string(ref.Name())) } return nil }) return revList, err }
-
-
-
@@ -190,3 +190,15 @@ func walkShowRef(ctx context.Context, repoPath, arg string, skip, limit int, wal} return i, nil } // GetRefsBySha returns all references filtered with prefix that belong to a sha commit hash func (repo *Repository) GetRefsBySha(sha, prefix string) ([]string, error) { var revList []string _, err := walkShowRef(repo.Ctx, repo.Path, "", 0, 0, func(walkSha, refname string) error { if walkSha == sha && strings.HasPrefix(refname, prefix) { revList = append(revList, refname) } return nil }) return revList, err }
-
-
-
@@ -54,3 +54,44 @@ func BenchmarkRepository_GetBranches(b *testing.B) {} } } func TestGetRefsBySha(t *testing.T) { bareRepo5Path := filepath.Join(testReposDir, "repo5_pulls") bareRepo5, err := OpenRepository(DefaultContext, bareRepo5Path) if err != nil { t.Fatal(err) } defer bareRepo5.Close() // do not exist branches, err := bareRepo5.GetRefsBySha("8006ff9adbf0cb94da7dad9e537e53817f9fa5c0", "") assert.NoError(t, err) assert.Len(t, branches, 0) // refs/pull/1/head branches, err = bareRepo5.GetRefsBySha("c83380d7056593c51a699d12b9c00627bd5743e9", PullPrefix) assert.NoError(t, err) assert.EqualValues(t, []string{"refs/pull/1/head"}, branches) branches, err = bareRepo5.GetRefsBySha("d8e0bbb45f200e67d9a784ce55bd90821af45ebd", BranchPrefix) assert.NoError(t, err) assert.EqualValues(t, []string{"refs/heads/master", "refs/heads/master-clone"}, branches) branches, err = bareRepo5.GetRefsBySha("58a4bcc53ac13e7ff76127e0fb518b5262bf09af", BranchPrefix) assert.NoError(t, err) assert.EqualValues(t, []string{"refs/heads/test-patch-1"}, branches) } func BenchmarkGetRefsBySha(b *testing.B) { bareRepo5Path := filepath.Join(testReposDir, "repo5_pulls") bareRepo5, err := OpenRepository(DefaultContext, bareRepo5Path) if err != nil { b.Fatal(err) } defer bareRepo5.Close() _, _ = bareRepo5.GetRefsBySha("8006ff9adbf0cb94da7dad9e537e53817f9fa5c0", "") _, _ = bareRepo5.GetRefsBySha("d8e0bbb45f200e67d9a784ce55bd90821af45ebd", "") _, _ = bareRepo5.GetRefsBySha("c83380d7056593c51a699d12b9c00627bd5743e9", "") _, _ = bareRepo5.GetRefsBySha("58a4bcc53ac13e7ff76127e0fb518b5262bf09af", "") }
-
-
-
@@ -0,0 +1,1 @@ref: refs/heads/master
-
-
-
@@ -0,0 +1,6 @@[core] repositoryformatversion = 0 filemode = true bare = true [receive] advertisePushOptions = true
-
-
-
@@ -0,0 +1,1 @@Unnamed repository; edit this file 'description' to name the repository.
-
-
-
@@ -0,0 +1,6 @@# git ls-files --others --exclude-from=.git/info/exclude # Lines that start with '#' are comments. # For a project mostly in C, the following would be a good set of # exclude patterns (uncomment them if you want to use them): # *.[oa] # *~
-
-
-
-
-
@@ -0,0 +1,1 @@x%n0;)0H1 P](F2Tk7|wu]{OқHp8$A1"\aRff4#ZL:J\-#fO2sN6ӯN;v# 3p5py^yL)xۼs_n1]ާa_)@X
-
-
-
-
-
@@ -0,0 +1,2 @@xMN0Yl' i%4ܟ <=}~2MccM"h֬z)q(CRIOtk27Ƚ1=GrL&]YBFt'&o?^/uѾ*Lݛů6,\ǵO 5ؤ#xj吇CA9VyBciޤ^Rs<mo>8.klyCi
-
-
-
@@ -0,0 +1,2 @@xAJAE])"VwWt EčzNU5$T9&$'1+y|f6=^XSNpE̅"R1v>W(gDJ@%WPKZ c2D2)rm`Yyfh:j\)۩=.">W~65w<|>>/| mp?X
-
-
-
-
@@ -0,0 +1,3 @@xAJAE])!VtM"YF=@uw5$D\yoh n?lxbMd,TC7f%uĔP3Jr;i:ԎJ,`5P)a̔1ƞ 9ym9U.nIgYOlG,:=qs$DMwa_S6o9X
-
-
-
-
@@ -0,0 +1,2 @@P pack-81423f591973f5d9dab89cc45afa1c544448133e.pack
-
-
modules/git/tests/repos/repo5_pulls/objects/pack/pack-81423f591973f5d9dab89cc45afa1c544448133e.idx (new)
-
modules/git/tests/repos/repo5_pulls/objects/pack/pack-81423f591973f5d9dab89cc45afa1c544448133e.pack (new)
-
-
@@ -0,0 +1,5 @@# pack-refs with: peeled fully-peeled sorted c83380d7056593c51a699d12b9c00627bd5743e9 refs/heads/test-patch-1 c83380d7056593c51a699d12b9c00627bd5743e9 refs/pull/1/head 111cac04bd7d20301964e27a93698aabb5781b80 refs/pull/1/merge 72866af952e98d02a73003501836074b286a78f6 refs/tags/v0.9.99
-
-
-
@@ -0,0 +1,1 @@d8e0bbb45f200e67d9a784ce55bd90821af45ebd
-
-
-
@@ -0,0 +1,1 @@d8e0bbb45f200e67d9a784ce55bd90821af45ebd
-
-
-
@@ -0,0 +1,1 @@58a4bcc53ac13e7ff76127e0fb518b5262bf09af
-
-
-
@@ -0,0 +1,1 @@58a4bcc53ac13e7ff76127e0fb518b5262bf09af
-
-
-
@@ -1560,6 +1560,14 @@ pulls.squash_merge_pull_request = Create squash commitpulls.merge_manually = Manually merged pulls.merge_commit_id = The merge commit ID pulls.require_signed_wont_sign = The branch requires signed commits but this merge will not be signed pulls.merge_pull_request_now = Merge Pull Request Now pulls.rebase_merge_pull_request_now = Rebase and Merge Now pulls.rebase_merge_commit_pull_request_now = Rebase and Merge Now (--no-ff) pulls.squash_merge_pull_request_now = Squash and Merge Now pulls.merge_pull_request_on_status_success = Merge Pull Request When All Checks Succeed pulls.rebase_merge_pull_request_on_status_success = Rebase and Merge When All Checks Succeed pulls.rebase_merge_commit_pull_request_on_status_success = Rebase and Merge (--no-ff) When All Checks Succeed pulls.squash_merge_pull_request_on_status_success = Squash and Merge When All Checks Succeed pulls.invalid_merge_option = You cannot use this merge option for this pull request. pulls.merge_conflict = Merge Failed: There was a conflict whilst merging. Hint: Try a different strategy pulls.merge_conflict_summary = Error Message
-
@@ -1588,9 +1596,16 @@ pulls.outdated_with_base_branch = This branch is out-of-date with the base brancpulls.closed_at = `closed this pull request <a id="%[1]s" href="#%[1]s">%[2]s</a>` pulls.reopened_at = `reopened this pull request <a id="%[1]s" href="#%[1]s">%[2]s</a>` pulls.merge_instruction_hint = `You can also view <a class="show-instruction">command line instructions</a>.` pulls.merge_instruction_step1_desc = From your project repository, check out a new branch and test the changes. pulls.merge_instruction_step2_desc = Merge the changes and update on Gitea. pulls.merge_on_status_success = The pull request was scheduled to merge when all checks succeed. pulls.merge_on_status_success_already_scheduled = This pull request is already scheduled to merge when all checks succeed. pulls.pr_has_pending_merge_on_success = %[1]s scheduled this pull request to auto merge when all checks succeed %[2]s. pulls.merge_pull_on_success_cancel = Cancel auto merge pulls.pull_request_not_scheduled = This pull request is not scheduled to auto merge. pulls.pull_request_schedule_canceled = The auto merge was canceled for this pull request. pulls.pull_request_scheduled_auto_merge = `scheduled this pull request to auto merge when all checks succeed %[1]s` pulls.pull_request_canceled_scheduled_auto_merge = `canceled auto merging this pull request when all checks succeed %[1]s` milestones.new = New Milestone milestones.open_tab = %d Open
-
-
-
@@ -984,7 +984,8 @@ func Routes() *web.Route {m.Post("/update", reqToken(), repo.UpdatePullRequest) m.Get("/commits", repo.GetPullRequestCommits) m.Combo("/merge").Get(repo.IsPullRequestMerged). Post(reqToken(), mustNotBeArchived, bind(forms.MergePullRequestForm{}), repo.MergePullRequest) Post(reqToken(), mustNotBeArchived, bind(forms.MergePullRequestForm{}), repo.MergePullRequest). Delete(reqToken(), mustNotBeArchived, repo.CancelScheduledAutoMerge) m.Group("/reviews", func() { m.Combo(""). Get(repo.ListPullReviews).
-
-
-
@@ -15,6 +15,7 @@ import ("code.gitea.io/gitea/models" issues_model "code.gitea.io/gitea/models/issues" pull_model "code.gitea.io/gitea/models/pull" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user"
-
@@ -28,6 +29,7 @@ import ("code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/api/v1/utils" asymkey_service "code.gitea.io/gitea/services/asymkey" "code.gitea.io/gitea/services/automerge" "code.gitea.io/gitea/services/forms" issue_service "code.gitea.io/gitea/services/issue" pull_service "code.gitea.io/gitea/services/pull"
-
@@ -805,6 +807,22 @@ func MergePullRequest(ctx *context.APIContext) {return } if form.MergeWhenChecksSucceed { scheduled, err := automerge.ScheduleAutoMerge(ctx, ctx.Doer, pr, repo_model.MergeStyle(form.Do), form.MergeTitleField) if err != nil { if pull_model.IsErrAlreadyScheduledToAutoMerge(err) { ctx.Error(http.StatusConflict, "ScheduleAutoMerge", err) return } ctx.Error(http.StatusInternalServerError, "ScheduleAutoMerge", err) return } else if scheduled { // nothing more to do ... ctx.Status(http.StatusCreated) return } } if err := pull_service.Merge(pr, ctx.Doer, ctx.Repo.GitRepo, repo_model.MergeStyle(form.Do), form.HeadCommitID, form.MergeTitleField); err != nil { if models.IsErrInvalidMergeStyle(err) { ctx.Error(http.StatusMethodNotAllowed, "Invalid merge style", fmt.Errorf("%s is not allowed an allowed merge style for this repository", repo_model.MergeStyle(form.Do)))
-
@@ -1113,6 +1131,78 @@ func UpdatePullRequest(ctx *context.APIContext) {ctx.Status(http.StatusOK) } // MergePullRequest cancel an auto merge scheduled for a given PullRequest by index func CancelScheduledAutoMerge(ctx *context.APIContext) { // swagger:operation DELETE /repos/{owner}/{repo}/pulls/{index}/merge repository repoCancelScheduledAutoMerge // --- // summary: Cancel the scheduled auto merge for the given pull request // produces: // - application/json // parameters: // - name: owner // in: path // description: owner of the repo // type: string // required: true // - name: repo // in: path // description: name of the repo // type: string // required: true // - name: index // in: path // description: index of the pull request to merge // type: integer // format: int64 // required: true // responses: // "204": // "$ref": "#/responses/empty" // "403": // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" pullIndex := ctx.ParamsInt64(":index") pull, err := models.GetPullRequestByIndex(ctx.Repo.Repository.ID, pullIndex) if err != nil { if models.IsErrPullRequestNotExist(err) { ctx.NotFound() return } ctx.InternalServerError(err) return } exist, autoMerge, err := pull_model.GetScheduledMergeByPullID(ctx, pull.ID) if err != nil { ctx.InternalServerError(err) return } if !exist { ctx.NotFound() return } if ctx.Doer.ID != autoMerge.DoerID { allowed, err := models.IsUserRepoAdminCtx(ctx, ctx.Repo.Repository, ctx.Doer) if err != nil { ctx.InternalServerError(err) return } if !allowed { ctx.Error(http.StatusForbidden, "No permission to cancel", "user has no permission to cancel the scheduled auto merge") return } } if err := pull_model.RemoveScheduledAutoMerge(ctx, ctx.Doer, pull.ID, true); err != nil { ctx.InternalServerError(err) } else { ctx.Status(http.StatusNoContent) } } // GetPullRequestCommits gets all commits associated with a given PR func GetPullRequestCommits(ctx *context.APIContext) { // swagger:operation GET /repos/{owner}/{repo}/pulls/{index}/commits repository repoGetPullRequestCommits
-
-
-
@@ -39,6 +39,7 @@ import (web_routers "code.gitea.io/gitea/routers/web" "code.gitea.io/gitea/services/auth" "code.gitea.io/gitea/services/auth/source/oauth2" "code.gitea.io/gitea/services/automerge" "code.gitea.io/gitea/services/cron" "code.gitea.io/gitea/services/mailer" repo_migrations "code.gitea.io/gitea/services/migrations"
-
@@ -147,6 +148,7 @@ func GlobalInitInstalled(ctx context.Context) {mirror_service.InitSyncMirrors() mustInit(webhook.Init) mustInit(pull_service.Init) mustInit(automerge.Init) mustInit(task.Init) mustInit(repo_migrations.Init) eventsource.GetManager().Init()
-
-
-
@@ -24,6 +24,7 @@ import (issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/organization" project_model "code.gitea.io/gitea/models/project" pull_model "code.gitea.io/gitea/models/pull" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user"
-
@@ -1662,6 +1663,13 @@ func ViewIssue(ctx *context.Context) {} ctx.Data["StillCanManualMerge"] = stillCanManualMerge() // Check if there is a pending pr merge ctx.Data["HasPendingPullRequestMerge"], ctx.Data["PendingPullRequestMerge"], err = pull_model.GetScheduledMergeByPullID(ctx, pull.ID) if err != nil { ctx.ServerError("GetScheduledMergeByPullID", err) return } } // Get Dependencies
-
-
-
@@ -0,0 +1,241 @@// Copyright 2021 Gitea. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package automerge import ( "context" "errors" "fmt" "strconv" "strings" "code.gitea.io/gitea/models" pull_model "code.gitea.io/gitea/models/pull" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/queue" pull_service "code.gitea.io/gitea/services/pull" ) // prAutoMergeQueue represents a queue to handle update pull request tests var prAutoMergeQueue queue.UniqueQueue // Init runs the task queue to that handles auto merges func Init() error { prAutoMergeQueue = queue.CreateUniqueQueue("pr_auto_merge", handle, "") if prAutoMergeQueue == nil { return fmt.Errorf("Unable to create pr_auto_merge Queue") } go graceful.GetManager().RunWithShutdownFns(prAutoMergeQueue.Run) return nil } // handle passed PR IDs and test the PRs func handle(data ...queue.Data) []queue.Data { for _, d := range data { var id int64 var sha string if _, err := fmt.Sscanf(d.(string), "%d_%s", &id, &sha); err != nil { log.Error("could not parse data from pr_auto_merge queue (%v): %v", d, err) continue } handlePull(id, sha) } return nil } func addToQueue(pr *models.PullRequest, sha string) { if err := prAutoMergeQueue.PushFunc(fmt.Sprintf("%d_%s", pr.ID, sha), func() error { log.Trace("Adding pullID: %d to the pull requests patch checking queue with sha %s", pr.ID, sha) return nil }); 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 func ScheduleAutoMerge(ctx context.Context, doer *user_model.User, pull *models.PullRequest, style repo_model.MergeStyle, message string) (scheduled bool, err error) { lastCommitStatus, err := pull_service.GetPullRequestCommitStatusState(ctx, pull) if err != nil { return false, err } // we don't need to schedule if lastCommitStatus.IsSuccess() { return false, nil } return true, pull_model.ScheduleAutoMerge(ctx, doer, pull.ID, style, message) } // MergeScheduledPullRequest merges a previously scheduled pull request when all checks succeeded func MergeScheduledPullRequest(ctx context.Context, sha string, repo *repo_model.Repository) error { pulls, err := getPullRequestsByHeadSHA(ctx, sha, repo, func(pr *models.PullRequest) bool { return !pr.HasMerged && pr.CanAutoMerge() }) if err != nil { return err } for _, pr := range pulls { addToQueue(pr, sha) } return nil } func getPullRequestsByHeadSHA(ctx context.Context, sha string, repo *repo_model.Repository, filter func(*models.PullRequest) bool) (map[int64]*models.PullRequest, error) { gitRepo, err := git.OpenRepository(ctx, repo.RepoPath()) if err != nil { return nil, err } defer gitRepo.Close() refs, err := gitRepo.GetRefsBySha(sha, "") if err != nil { return nil, err } pulls := make(map[int64]*models.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 := models.GetPullRequestByIndexCtx(ctx, repo.ID, prIndex) if err != nil { // If there is no pull request for this branch, we don't try to merge it. if models.IsErrPullRequestNotExist(err) { continue } return nil, err } if filter(p) { pulls[p.ID] = p } } } return pulls, nil } func handlePull(pullID int64, sha string) { ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("Handle AutoMerge of pull[%d] with sha[%s]", pullID, sha)) defer finished() pr, err := models.GetPullRequestByID(ctx, pullID) if err != nil { log.Error("GetPullRequestByID[%d]: %v", pullID, err) return } // Check if there is a scheduled pr in the db exists, scheduledPRM, err := pull_model.GetScheduledMergeByPullID(ctx, pr.ID) if err != nil { log.Error("pull[%d] GetScheduledMergeByPullID: %v", pr.ID, err) return } if !exists { return } // Get all checks for this pr // We get the latest sha commit hash again to handle the case where the check of a previous push // did not succeed or was not finished yet. if err = pr.LoadHeadRepoCtx(ctx); err != nil { log.Error("pull[%d] LoadHeadRepoCtx: %v", pr.ID, err) return } headGitRepo, err := git.OpenRepository(ctx, pr.HeadRepo.RepoPath()) if err != nil { log.Error("OpenRepository: %v", err) return } defer headGitRepo.Close() headBranchExist := headGitRepo.IsBranchExist(pr.HeadBranch) if pr.HeadRepo == nil || !headBranchExist { log.Warn("Head branch of auto merge pr does not exist [HeadRepoID: %d, Branch: %s, PR ID: %d]", pr.HeadRepoID, pr.HeadBranch, pr.ID) return } // Check if all checks succeeded pass, err := pull_service.IsPullCommitStatusPass(ctx, pr) if err != nil { log.Error("IsPullCommitStatusPass: %v", err) return } if !pass { log.Info("Scheduled auto merge pr has unsuccessful status checks [PullID: %d]", pr.ID) return } // Merge if all checks succeeded doer, err := user_model.GetUserByIDCtx(ctx, scheduledPRM.DoerID) if err != nil { log.Error("GetUserByIDCtx: %v", err) return } perm, err := models.GetUserRepoPermission(ctx, pr.HeadRepo, doer) if err != nil { log.Error("GetUserRepoPermission: %v", err) return } if err := pull_service.CheckPullMergable(ctx, doer, &perm, pr, false, false); err != nil { if errors.Is(pull_service.ErrUserNotAllowedToMerge, err) { log.Info("PR %d was scheduled to automerge by an unauthorized user", pr.ID) return } log.Error("pull[%d] CheckPullMergable: %v", pr.ID, err) return } var baseGitRepo *git.Repository if pr.BaseRepoID == pr.HeadRepoID { baseGitRepo = headGitRepo } else { if err = pr.LoadBaseRepoCtx(ctx); err != nil { log.Error("LoadBaseRepoCtx: %v", err) return } baseGitRepo, err = git.OpenRepository(ctx, pr.BaseRepo.RepoPath()) if err != nil { log.Error("OpenRepository: %v", err) return } defer baseGitRepo.Close() } if err := pull_service.Merge(pr, doer, baseGitRepo, scheduledPRM.MergeStyle, "", scheduledPRM.Message); err != nil { log.Error("pull_service.Merge: %v", err) return } }
-
-
-
@@ -592,6 +592,7 @@ type MergePullRequestForm struct {MergeCommitID string // only used for manually-merged HeadCommitID string `json:"head_commit_id,omitempty"` ForceMerge *bool `json:"force_merge,omitempty"` MergeWhenChecksSucceed bool `json:"merge_when_checks_succeed,omitempty"` DeleteBranchAfterMerge bool `json:"delete_branch_after_merge,omitempty"` }
-
-
-
@@ -137,5 +137,13 @@ func GetPullRequestCommitStatusState(ctx context.Context, pr *models.PullRequestreturn "", errors.Wrap(err, "GetLatestCommitStatus") } return MergeRequiredContextsCommitStatus(commitStatuses, pr.ProtectedBranch.StatusCheckContexts), nil if err := pr.LoadProtectedBranchCtx(ctx); err != nil { return "", errors.Wrap(err, "LoadProtectedBranch") } var requiredContexts []string if pr.ProtectedBranch != nil { requiredContexts = pr.ProtectedBranch.StatusCheckContexts } return MergeRequiredContextsCommitStatus(commitStatuses, requiredContexts), nil }
-
-
-
@@ -18,6 +18,7 @@ import ("code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" pull_model "code.gitea.io/gitea/models/pull" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user"
-
@@ -46,6 +47,11 @@ func Merge(pr *models.PullRequest, doer *user_model.User, baseGitRepo *git.RepospullWorkingPool.CheckIn(fmt.Sprint(pr.ID)) defer pullWorkingPool.CheckOut(fmt.Sprint(pr.ID)) // Removing an auto merge pull and ignore if not exist if err := pull_model.RemoveScheduledAutoMerge(db.DefaultContext, doer, pr.ID, false); err != nil && !models.IsErrNotExist(err) { return err } prUnit, err := pr.BaseRepo.GetUnit(unit.TypePullRequests) if err != nil { log.Error("pr.BaseRepo.GetUnit(unit.TypePullRequests): %v", err)
-
-
-
@@ -253,7 +253,7 @@ func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string,graceful.GetManager().RunWithShutdownContext(func(ctx context.Context) { // There is no sensible way to shut this down ":-(" // If you don't let it run all the way then you will lose data // FIXME: graceful: AddTestPullRequestTask needs to become a queue! // TODO: graceful: AddTestPullRequestTask needs to become a queue! prs, err := models.GetUnmergedPullRequestsByHeadInfo(repoID, branch) if err != nil {
-
-
-
@@ -14,6 +14,7 @@ import (user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/services/automerge" ) // CreateCommitStatus creates a new CommitStatus given a bunch of parameters
-
@@ -44,6 +45,12 @@ func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, creatoreturn fmt.Errorf("NewCommitStatus[repo_id: %d, user_id: %d, sha: %s]: %v", repo.ID, creator.ID, sha, err) } if status.State.IsSuccess() { if err := automerge.MergeScheduledPullRequest(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 nil }
-
-
-
@@ -10,7 +10,8 @@22 = REVIEW, 23 = ISSUE_LOCKED, 24 = ISSUE_UNLOCKED, 25 = TARGET_BRANCH_CHANGED, 26 = DELETE_TIME_MANUAL, 27 = REVIEW_REQUEST, 28 = MERGE_PULL_REQUEST, 29 = PULL_PUSH_EVENT, 30 = PROJECT_CHANGED, 31 = PROJECT_BOARD_CHANGED 32 = DISMISSED_REVIEW --> 32 = DISMISSED_REVIEW, 33 = COMMENT_TYPE_CHANGE_ISSUE_REF, 34 = PR_SCHEDULE_TO_AUTO_MERGE, 35 = CANCEL_SCHEDULED_AUTO_MERGE_PR --> {{if eq .Type 0}} <div class="timeline-item comment" id="{{.HashTag}}"> {{if .OriginalAuthor }}
-
@@ -837,6 +838,15 @@{{end}} </span> </div> {{else if or (eq .Type 34) (eq .Type 35)}} <div class="timeline-item event" id="{{.HashTag}}"> <span class="badge">{{svg "octicon-git-merge" 16}}</span> <span class="text grey"> <a class="author" href="{{.Poster.HomeLink}}">{{.Poster.GetDisplayName}}</a> {{if eq .Type 34}}{{$.i18n.Tr "repo.pulls.pull_request_scheduled_auto_merge" $createdStr | Safe}} {{else}}{{$.i18n.Tr "repo.pulls.pull_request_canceled_scheduled_auto_merge" $createdStr | Safe}}{{end}} </span> </div> {{end}} {{end}} {{end}}
-
-
-
@@ -8015,6 +8015,51 @@"$ref": "#/responses/error" } } }, "delete": { "produces": [ "application/json" ], "tags": [ "repository" ], "summary": "Cancel the scheduled auto merge for the given pull request", "operationId": "repoCancelScheduledAutoMerge", "parameters": [ { "type": "string", "description": "owner of the repo", "name": "owner", "in": "path", "required": true }, { "type": "string", "description": "name of the repo", "name": "repo", "in": "path", "required": true }, { "type": "integer", "format": "int64", "description": "index of the pull request to merge", "name": "index", "in": "path", "required": true } ], "responses": { "204": { "$ref": "#/responses/empty" }, "403": { "$ref": "#/responses/forbidden" }, "404": { "$ref": "#/responses/notFound" } } } }, "/repos/{owner}/{repo}/pulls/{index}/requested_reviewers": {
-
@@ -16298,6 +16343,10 @@"head_commit_id": { "type": "string", "x-go-name": "HeadCommitID" }, "merge_when_checks_succeed": { "type": "boolean", "x-go-name": "MergeWhenChecksSucceed" } }, "x-go-name": "MergePullRequestForm",
-