mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-29 10:57:44 +09:00 
			
		
		
		
	Auto merge pull requests when all checks succeeded via API (#9307)
* Fix indention Signed-off-by: kolaente <k@knt.li> * Add option to merge a pr right now without waiting for the checks to succeed Signed-off-by: kolaente <k@knt.li> * Fix lint Signed-off-by: kolaente <k@knt.li> * Add scheduled pr merge to tables used for testing Signed-off-by: kolaente <k@knt.li> * Add status param to make GetPullRequestByHeadBranch reusable Signed-off-by: kolaente <k@knt.li> * Move "Merge now" to a seperate button to make the ui clearer Signed-off-by: kolaente <k@knt.li> * Update models/scheduled_pull_request_merge.go Co-authored-by: 赵智超 <1012112796@qq.com> * Update web_src/js/index.js Co-authored-by: 赵智超 <1012112796@qq.com> * Update web_src/js/index.js Co-authored-by: 赵智超 <1012112796@qq.com> * Re-add migration after merge * Fix frontend lint * Fix version compare * Add vendored dependencies * Add basic tets * Make sure the api route is capable of scheduling PRs for merging * Fix comparing version * make vendor * adopt refactor * apply suggestion: User -> Doer * init var once * Fix Test * Update templates/repo/issue/view_content/comments.tmpl * adopt * nits * next * code format * lint * use same name schema; rm CreateUnScheduledPRToAutoMergeComment * API: can not create schedule twice * Add TestGetBranchNamesForSha * nits * new go routine for each pull to merge * Update models/pull.go Co-authored-by: a1012112796 <1012112796@qq.com> * Update models/scheduled_pull_request_merge.go Co-authored-by: a1012112796 <1012112796@qq.com> * fix & add renaming sugestions * Update services/automerge/pull_auto_merge.go Co-authored-by: a1012112796 <1012112796@qq.com> * fix conflict relicts * apply latest refactors * fix: migration after merge * Update models/error.go Co-authored-by: delvh <dev.lh@web.de> * Update options/locale/locale_en-US.ini Co-authored-by: delvh <dev.lh@web.de> * Update options/locale/locale_en-US.ini Co-authored-by: delvh <dev.lh@web.de> * adapt latest refactors * fix test * use more context * skip potential edgecases * document func usage * GetBranchNamesForSha() -> GetRefsBySha() * start refactoring * ajust to new changes * nit * docu nit * the great check move * move checks for branchprotection into own package * resolve todo now ... * move & rename * unexport if posible * fix * check if merge is allowed before merge on scheduled pull * debugg * wording * improve SetDefaults & nits * NotAllowedToMerge -> DisallowedToMerge * fix test * merge files * use package "errors" * merge files * add string names * other implementation for gogit * adapt refactor * more context for models/pull.go * GetUserRepoPermission use context * more ctx * use context for loading pull head/base-repo * more ctx * more ctx * models.LoadIssueCtx() * models.LoadIssueCtx() * Handle pull_service.Merge in one DB transaction * add TODOs * next * next * next * more ctx * more ctx * Start refactoring structure of old pull code ... * move code into new packages * shorter names ... and finish **restructure** * Update models/branches.go Co-authored-by: zeripath <art27@cantab.net> * finish UpdateProtectBranch * more and fix * update datum * template: use "svg" helper * rename prQueue 2 prPatchCheckerQueue * handle automerge in queue * lock pull on git&db actions ... * lock pull on git&db actions ... * add TODO notes * the regex * transaction in tests * GetRepositoryByIDCtx * shorter table name and lint fix * close transaction bevore notify * Update models/pull.go * next * CheckPullMergable check all branch protections! * Update routers/web/repo/pull.go * CheckPullMergable check all branch protections! * Revert "PullService lock via pullID (#19520)" (for now...) This reverts commit 6cde7c9159a5ea75a10356feb7b8c7ad4c434a9a. * Update services/pull/check.go * Use for a repo action one database transaction * Apply suggestions from code review * Apply suggestions from code review Co-authored-by: delvh <dev.lh@web.de> * Update services/issue/status.go Co-authored-by: delvh <dev.lh@web.de> * Update services/issue/status.go Co-authored-by: delvh <dev.lh@web.de> * use db.WithTx() * gofmt * make pr.GetDefaultMergeMessage() context aware * make MergePullRequestForm.SetDefaults context aware * use db.WithTx() * pull.SetMerged only with context * fix deadlock in `test-sqlite\#TestAPIBranchProtection` * dont forget templates * db.WithTx allow to set the parentCtx * handle db transaction in service packages but not router * issue_service.ChangeStatus just had caused another deadlock :/ it has to do something with how notification package is handled * if we merge a pull in one database transaktion, we get a lock, because merge infoce internal api that cant handle open db sessions to the same repo * ajust to current master * Apply suggestions from code review Co-authored-by: delvh <dev.lh@web.de> * dont open db transaction in router * make generate-swagger * one _success less * wording nit * rm * adapt * remove not needed test files * rm less diff & use attr in JS * ... * Update services/repository/files/commit.go Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> * ajust db schema for PullAutoMerge * skip broken pull refs * more context in error messages * remove webUI part for another pull * remove more WebUI only parts * API: add CancleAutoMergePR * Apply suggestions from code review Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> * fix lint * Apply suggestions from code review * cancle -> cancel Co-authored-by: delvh <dev.lh@web.de> * change queue identifyer * fix swagger * prevent nil issue * fix and dont drop error * as per @zeripath * Update integrations/git_test.go Co-authored-by: delvh <dev.lh@web.de> * Update integrations/git_test.go Co-authored-by: delvh <dev.lh@web.de> * more declarative integration tests (dedup code) * use assert.False/True helper Co-authored-by: 赵智超 <1012112796@qq.com> Co-authored-by: 6543 <6543@obermui.de> Co-authored-by: delvh <dev.lh@web.de> Co-authored-by: zeripath <art27@cantab.net> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
		| @@ -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) { | func doAPIGetBranch(ctx APITestContext, branch string, callback ...func(*testing.T, api.Branch)) func(*testing.T) { | ||||||
| 	return func(t *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) | 		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("CreateAgitFlowPull", doCreateAgitFlowPull(dstPath, &httpContext, "master", "test/head")) | ||||||
| 		t.Run("BranchProtectMerge", doBranchProtectPRMerge(&httpContext, dstPath)) | 		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("CreatePRAndSetManuallyMerged", doCreatePRAndSetManuallyMerged(httpContext, httpContext, dstPath, "master", "test-manually-merge")) | ||||||
| 		t.Run("MergeFork", func(t *testing.T) { | 		t.Run("MergeFork", func(t *testing.T) { | ||||||
| 			defer PrintCurrentTest(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) { | func doCreateAgitFlowPull(dstPath string, ctx *APITestContext, baseBranch, headBranch string) func(t *testing.T) { | ||||||
| 	return func(t *testing.T) { | 	return func(t *testing.T) { | ||||||
| 		defer PrintCurrentTest(t)() | 		defer PrintCurrentTest(t)() | ||||||
|   | |||||||
| @@ -63,20 +63,13 @@ func TestPullCreate_CommitStatus(t *testing.T) { | |||||||
| 			api.CommitStatusWarning: "warning sign icon yellow", | 			api.CommitStatusWarning: "warning sign icon yellow", | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		testCtx := NewAPITestContext(t, "user1", "repo1") | ||||||
|  |  | ||||||
| 		// Update commit status, and check if icon is updated as well | 		// Update commit status, and check if icon is updated as well | ||||||
| 		for _, status := range statusList { | 		for _, status := range statusList { | ||||||
|  |  | ||||||
| 			// Call API to add status for commit | 			// Call API to add status for commit | ||||||
| 			token := getTokenForLoggedInUser(t, session) | 			t.Run("CreateStatus", doAPICreateCommitStatus(testCtx, commitID, status)) | ||||||
| 			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) |  | ||||||
|  |  | ||||||
| 			req = NewRequestf(t, "GET", "/user1/repo1/pulls/1/commits") | 			req = NewRequestf(t, "GET", "/user1/repo1/pulls/1/commits") | ||||||
| 			resp = session.MakeRequest(t, req, http.StatusOK) | 			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) { | func TestPullCreate_EmptyChangesWithCommits(t *testing.T) { | ||||||
| 	onGiteaRun(t, func(t *testing.T, u *url.URL) { | 	onGiteaRun(t, func(t *testing.T, u *url.URL) { | ||||||
| 		session := loginUser(t, "user1") | 		session := loginUser(t, "user1") | ||||||
|   | |||||||
| @@ -36,7 +36,6 @@ func doTestRepoCommitWithStatus(t *testing.T, state string, classes ...string) { | |||||||
| 	defer prepareTestEnv(t)() | 	defer prepareTestEnv(t)() | ||||||
|  |  | ||||||
| 	session := loginUser(t, "user2") | 	session := loginUser(t, "user2") | ||||||
| 	token := getTokenForLoggedInUser(t, session) |  | ||||||
|  |  | ||||||
| 	// Request repository commits page | 	// Request repository commits page | ||||||
| 	req := NewRequest(t, "GET", "/user2/repo1/commits/branch/master") | 	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) | 	assert.NotEmpty(t, commitURL) | ||||||
|  |  | ||||||
| 	// Call API to add status for commit | 	// Call API to add status for commit | ||||||
| 	req = NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/statuses/"+path.Base(commitURL)+"?token="+token, | 	t.Run("CreateStatus", doAPICreateCommitStatus(NewAPITestContext(t, "user2", "repo1"), path.Base(commitURL), api.CommitStatusState(state))) | ||||||
| 		api.CreateStatusOption{ |  | ||||||
| 			State:       api.CommitStatusState(state), |  | ||||||
| 			TargetURL:   "http://test.ci/", |  | ||||||
| 			Description: "", |  | ||||||
| 			Context:     "testci", |  | ||||||
| 		}, |  | ||||||
| 	) |  | ||||||
|  |  | ||||||
| 	resp = session.MakeRequest(t, req, http.StatusCreated) |  | ||||||
|  |  | ||||||
| 	req = NewRequest(t, "GET", "/user2/repo1/commits/branch/master") | 	req = NewRequest(t, "GET", "/user2/repo1/commits/branch/master") | ||||||
| 	resp = session.MakeRequest(t, req, http.StatusOK) | 	resp = session.MakeRequest(t, req, http.StatusOK) | ||||||
|   | |||||||
| @@ -110,6 +110,10 @@ const ( | |||||||
| 	CommentTypeDismissReview | 	CommentTypeDismissReview | ||||||
| 	// 33 Change issue ref | 	// 33 Change issue ref | ||||||
| 	CommentTypeChangeIssueRef | 	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{ | var commentStrings = []string{ | ||||||
| @@ -147,6 +151,8 @@ var commentStrings = []string{ | |||||||
| 	"project_board", | 	"project_board", | ||||||
| 	"dismiss_review", | 	"dismiss_review", | ||||||
| 	"change_issue_ref", | 	"change_issue_ref", | ||||||
|  | 	"pull_scheduled_merge", | ||||||
|  | 	"pull_cancel_scheduled_merge", | ||||||
| } | } | ||||||
|  |  | ||||||
| func (t CommentType) String() string { | func (t CommentType) String() string { | ||||||
|   | |||||||
| @@ -383,6 +383,8 @@ var migrations = []Migration{ | |||||||
| 	NewMigration("Add package tables", addPackageTables), | 	NewMigration("Add package tables", addPackageTables), | ||||||
| 	// v213 -> v214 | 	// v213 -> v214 | ||||||
| 	NewMigration("Add allow edits from maintainers to PullRequest table", addAllowMaintainerEdit), | 	NewMigration("Add allow edits from maintainers to PullRequest table", addAllowMaintainerEdit), | ||||||
|  | 	// v214 -> v215 | ||||||
|  | 	NewMigration("Add auto merge table", addAutoMergeTable), | ||||||
| } | } | ||||||
|  |  | ||||||
| // GetCurrentDBVersion returns the current db version | // GetCurrentDBVersion returns the current db version | ||||||
|   | |||||||
							
								
								
									
										23
									
								
								models/migrations/v214.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								models/migrations/v214.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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/setting" | ||||||
| 	"code.gitea.io/gitea/modules/timeutil" | 	"code.gitea.io/gitea/modules/timeutil" | ||||||
| 	"code.gitea.io/gitea/modules/util" | 	"code.gitea.io/gitea/modules/util" | ||||||
|  |  | ||||||
|  | 	"xorm.io/builder" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // PullRequestType defines pull request type | // PullRequestType defines pull request type | ||||||
| @@ -675,6 +677,18 @@ func (pr *PullRequest) IsSameRepo() bool { | |||||||
| 	return pr.BaseRepoID == pr.HeadRepoID | 	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 | // GetBaseBranchHTMLURL returns the HTML URL of the base branch | ||||||
| func (pr *PullRequest) GetBaseBranchHTMLURL() string { | func (pr *PullRequest) GetBaseBranchHTMLURL() string { | ||||||
| 	if err := pr.LoadBaseRepo(); err != nil { | 	if err := pr.LoadBaseRepo(); err != nil { | ||||||
|   | |||||||
							
								
								
									
										143
									
								
								models/pull/automerge.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								models/pull/automerge.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | 	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 | 	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", "") | ||||||
|  | } | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								modules/git/tests/repos/repo5_pulls/HEAD
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								modules/git/tests/repos/repo5_pulls/HEAD
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | ref: refs/heads/master | ||||||
							
								
								
									
										6
									
								
								modules/git/tests/repos/repo5_pulls/config
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								modules/git/tests/repos/repo5_pulls/config
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  | [core] | ||||||
|  | 	repositoryformatversion = 0 | ||||||
|  | 	filemode = true | ||||||
|  | 	bare = true | ||||||
|  | [receive] | ||||||
|  | 	advertisePushOptions = true | ||||||
							
								
								
									
										1
									
								
								modules/git/tests/repos/repo5_pulls/description
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								modules/git/tests/repos/repo5_pulls/description
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | Unnamed repository; edit this file 'description' to name the repository. | ||||||
							
								
								
									
										6
									
								
								modules/git/tests/repos/repo5_pulls/info/exclude
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								modules/git/tests/repos/repo5_pulls/info/exclude
									
									
									
									
									
										Normal file
									
								
							| @@ -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] | ||||||
|  | # *~ | ||||||
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | x%<25><>n<EFBFBD>0<0C>;<3B>)<29>0H<30><48>1	P<>]<5D><>(<28>F2<46>T<><54>k<EFBFBD>7|<7C>wu]<5D>{O<>қ<19>H<><48>p<EFBFBD><70>8<EFBFBD>$A<>1<EFBFBD>"\<5C><>a<EFBFBD>Rf<52><66>f<EFBFBD>4<><0B><>#ZL:J<>\-<2D><>#fO2s<32><73>N<EFBFBD><4E><EFBFBD>6<EFBFBD><36>ӯ<EFBFBD><D3AF>N<>;<3B>v<><02>#<23><>	3p<><70>5<D7BA><35><EFBFBD>p<EFBFBD>y^<5E><><EFBFBD>y<1F><>L)x<>ۼs_<0F>n<EFBFBD>1]<5D>ާa<DEA7>_<EFBFBD>)@X | ||||||
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							| @@ -0,0 +1,2 @@ | |||||||
|  | x<01><>MN<4D>0<0C>Y<EFBFBD><14>l<06><06>'<27><><1C> <20><>i%<25><>4ܟ<16> | ||||||
|  | <EFBFBD><=<3D>}~<7E><>2Mcc<>M<EFBFBD>"<22><><14>h֬z<D6AC><7A><EFBFBD>)q(<28><>CRI<52>O<EFBFBD><4F>tk<74>27Ƚ1=<3D>GrL&]<5D>Y<EFBFBD>BFt<46>'&o<><6F>?^<5E>/<2F>u<EFBFBD><75><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ѿ<EFBFBD><D1BE>*<2A>L<EFBFBD><4C><EFBFBD>ݛ<EFBFBD>ů6,<15>\ǵ<>O<EFBFBD><4F><EFBFBD><EFBFBD> | ||||||
| @@ -0,0 +1,2 @@ | |||||||
|  | x<01><>AJAE]<5D>)<29><>"<22>VwW<77>t E<>čz<C48D>NU5<06>$<24>T<EFBFBD>9<EFBFBD><39><EFBFBD>&<26>$'1<>+<2B>y|<7C><><EFBFBD><EFBFBD><EFBFBD>f<EFBFBD>6=^XS<58>NpE̅"<22>R<EFBFBD>1v>W<>(<28><17><>gD<67><44><EFBFBD>J<EFBFBD><4A>@%W<>PKZ | ||||||
|  | <EFBFBD>c<EFBFBD>2<EFBFBD><EFBFBD><EFBFBD><EFBFBD>D2)<29>r<EFBFBD><72><EFBFBD><EFBFBD>m<EFBFBD>`<60><><EFBFBD>Yy<59>f<EFBFBD><66><1D><>h<EFBFBD>:j<>\<5C><>)<29>۩<EFBFBD>=<3D><><EFBFBD><05>."<22>><3E>W<1D>~6<><36>5w<|>><3E>/<2F><><EFBFBD><EFBFBD><EFBFBD>| | ||||||
										
											Binary file not shown.
										
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | x<01><>AJAE]<5D>)<29><>!V<><56>tM<74><4D>"Y<08>F=@uw5<06>$<24><><EFBFBD>D\yo<><6F>h<EFBFBD> | ||||||
|  | n<EFBFBD><EFBFBD>?<3F><><EFBFBD><EFBFBD>l<EFBFBD><06>xbMd<>,<2C>T<EFBFBD><54><EFBFBD>C7f%<25><>uĔ<75>P<EFBFBD><50>3Jr;i:ԎJ,<2C>`<60><>5<EFBFBD>P)<29>a<EFBFBD>̔<EFBFBD><CC94>1ƞ | ||||||
|  | 9y<EFBFBD><EFBFBD>m<EFBFBD>9<EFBFBD><EFBFBD><EFBFBD><EFBFBD>U<EFBFBD><EFBFBD>.n<>Ig<49><67>Y<EFBFBD><59><EFBFBD><EFBFBD>O<EFBFBD><4F><EFBFBD>l<EFBFBD>G,<2C><>:<3A>=<3D>q<EFBFBD>s$D<><EFBFBD>M<EFBFBD><4D><EFBFBD><1D><1F>w<EFBFBD><77><EFBFBD><EFBFBD><EFBFBD>a<EFBFBD>_<>S<EFBFBD>6<EFBFBD>o9X<39> | ||||||
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										2
									
								
								modules/git/tests/repos/repo5_pulls/objects/info/packs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								modules/git/tests/repos/repo5_pulls/objects/info/packs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | |||||||
|  | P pack-81423f591973f5d9dab89cc45afa1c544448133e.pack | ||||||
|  |  | ||||||
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										5
									
								
								modules/git/tests/repos/repo5_pulls/packed-refs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								modules/git/tests/repos/repo5_pulls/packed-refs
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||||
							
								
								
									
										1
									
								
								modules/git/tests/repos/repo5_pulls/refs/heads/master
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								modules/git/tests/repos/repo5_pulls/refs/heads/master
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | d8e0bbb45f200e67d9a784ce55bd90821af45ebd | ||||||
| @@ -0,0 +1 @@ | |||||||
|  | d8e0bbb45f200e67d9a784ce55bd90821af45ebd | ||||||
| @@ -0,0 +1 @@ | |||||||
|  | 58a4bcc53ac13e7ff76127e0fb518b5262bf09af | ||||||
							
								
								
									
										1
									
								
								modules/git/tests/repos/repo5_pulls/refs/pull/4/head
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								modules/git/tests/repos/repo5_pulls/refs/pull/4/head
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | 58a4bcc53ac13e7ff76127e0fb518b5262bf09af | ||||||
| @@ -1560,6 +1560,14 @@ pulls.squash_merge_pull_request = Create squash commit | |||||||
| pulls.merge_manually = Manually merged | pulls.merge_manually = Manually merged | ||||||
| pulls.merge_commit_id = The merge commit ID | 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.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.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 = Merge Failed: There was a conflict whilst merging. Hint: Try a different strategy | ||||||
| pulls.merge_conflict_summary = Error Message | pulls.merge_conflict_summary = Error Message | ||||||
| @@ -1588,9 +1596,16 @@ pulls.outdated_with_base_branch = This branch is out-of-date with the base branc | |||||||
| pulls.closed_at = `closed this pull request <a id="%[1]s" href="#%[1]s">%[2]s</a>` | pulls.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.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_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_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_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.new = New Milestone | ||||||
| milestones.open_tab = %d Open | milestones.open_tab = %d Open | ||||||
|   | |||||||
| @@ -984,7 +984,8 @@ func Routes() *web.Route { | |||||||
| 						m.Post("/update", reqToken(), repo.UpdatePullRequest) | 						m.Post("/update", reqToken(), repo.UpdatePullRequest) | ||||||
| 						m.Get("/commits", repo.GetPullRequestCommits) | 						m.Get("/commits", repo.GetPullRequestCommits) | ||||||
| 						m.Combo("/merge").Get(repo.IsPullRequestMerged). | 						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.Group("/reviews", func() { | ||||||
| 							m.Combo(""). | 							m.Combo(""). | ||||||
| 								Get(repo.ListPullReviews). | 								Get(repo.ListPullReviews). | ||||||
|   | |||||||
| @@ -15,6 +15,7 @@ import ( | |||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/models" | 	"code.gitea.io/gitea/models" | ||||||
| 	issues_model "code.gitea.io/gitea/models/issues" | 	issues_model "code.gitea.io/gitea/models/issues" | ||||||
|  | 	pull_model "code.gitea.io/gitea/models/pull" | ||||||
| 	repo_model "code.gitea.io/gitea/models/repo" | 	repo_model "code.gitea.io/gitea/models/repo" | ||||||
| 	"code.gitea.io/gitea/models/unit" | 	"code.gitea.io/gitea/models/unit" | ||||||
| 	user_model "code.gitea.io/gitea/models/user" | 	user_model "code.gitea.io/gitea/models/user" | ||||||
| @@ -28,6 +29,7 @@ import ( | |||||||
| 	"code.gitea.io/gitea/modules/web" | 	"code.gitea.io/gitea/modules/web" | ||||||
| 	"code.gitea.io/gitea/routers/api/v1/utils" | 	"code.gitea.io/gitea/routers/api/v1/utils" | ||||||
| 	asymkey_service "code.gitea.io/gitea/services/asymkey" | 	asymkey_service "code.gitea.io/gitea/services/asymkey" | ||||||
|  | 	"code.gitea.io/gitea/services/automerge" | ||||||
| 	"code.gitea.io/gitea/services/forms" | 	"code.gitea.io/gitea/services/forms" | ||||||
| 	issue_service "code.gitea.io/gitea/services/issue" | 	issue_service "code.gitea.io/gitea/services/issue" | ||||||
| 	pull_service "code.gitea.io/gitea/services/pull" | 	pull_service "code.gitea.io/gitea/services/pull" | ||||||
| @@ -805,6 +807,22 @@ func MergePullRequest(ctx *context.APIContext) { | |||||||
| 		return | 		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 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) { | 		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))) | 			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) | 	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 | // GetPullRequestCommits gets all commits associated with a given PR | ||||||
| func GetPullRequestCommits(ctx *context.APIContext) { | func GetPullRequestCommits(ctx *context.APIContext) { | ||||||
| 	// swagger:operation GET /repos/{owner}/{repo}/pulls/{index}/commits repository repoGetPullRequestCommits | 	// swagger:operation GET /repos/{owner}/{repo}/pulls/{index}/commits repository repoGetPullRequestCommits | ||||||
|   | |||||||
| @@ -39,6 +39,7 @@ import ( | |||||||
| 	web_routers "code.gitea.io/gitea/routers/web" | 	web_routers "code.gitea.io/gitea/routers/web" | ||||||
| 	"code.gitea.io/gitea/services/auth" | 	"code.gitea.io/gitea/services/auth" | ||||||
| 	"code.gitea.io/gitea/services/auth/source/oauth2" | 	"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/cron" | ||||||
| 	"code.gitea.io/gitea/services/mailer" | 	"code.gitea.io/gitea/services/mailer" | ||||||
| 	repo_migrations "code.gitea.io/gitea/services/migrations" | 	repo_migrations "code.gitea.io/gitea/services/migrations" | ||||||
| @@ -147,6 +148,7 @@ func GlobalInitInstalled(ctx context.Context) { | |||||||
| 	mirror_service.InitSyncMirrors() | 	mirror_service.InitSyncMirrors() | ||||||
| 	mustInit(webhook.Init) | 	mustInit(webhook.Init) | ||||||
| 	mustInit(pull_service.Init) | 	mustInit(pull_service.Init) | ||||||
|  | 	mustInit(automerge.Init) | ||||||
| 	mustInit(task.Init) | 	mustInit(task.Init) | ||||||
| 	mustInit(repo_migrations.Init) | 	mustInit(repo_migrations.Init) | ||||||
| 	eventsource.GetManager().Init() | 	eventsource.GetManager().Init() | ||||||
|   | |||||||
| @@ -24,6 +24,7 @@ import ( | |||||||
| 	issues_model "code.gitea.io/gitea/models/issues" | 	issues_model "code.gitea.io/gitea/models/issues" | ||||||
| 	"code.gitea.io/gitea/models/organization" | 	"code.gitea.io/gitea/models/organization" | ||||||
| 	project_model "code.gitea.io/gitea/models/project" | 	project_model "code.gitea.io/gitea/models/project" | ||||||
|  | 	pull_model "code.gitea.io/gitea/models/pull" | ||||||
| 	repo_model "code.gitea.io/gitea/models/repo" | 	repo_model "code.gitea.io/gitea/models/repo" | ||||||
| 	"code.gitea.io/gitea/models/unit" | 	"code.gitea.io/gitea/models/unit" | ||||||
| 	user_model "code.gitea.io/gitea/models/user" | 	user_model "code.gitea.io/gitea/models/user" | ||||||
| @@ -1662,6 +1663,13 @@ func ViewIssue(ctx *context.Context) { | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		ctx.Data["StillCanManualMerge"] = stillCanManualMerge() | 		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 | 	// Get Dependencies | ||||||
|   | |||||||
							
								
								
									
										241
									
								
								services/automerge/automerge.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										241
									
								
								services/automerge/automerge.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | 	MergeCommitID          string // only used for manually-merged | ||||||
| 	HeadCommitID           string `json:"head_commit_id,omitempty"` | 	HeadCommitID           string `json:"head_commit_id,omitempty"` | ||||||
| 	ForceMerge             *bool  `json:"force_merge,omitempty"` | 	ForceMerge             *bool  `json:"force_merge,omitempty"` | ||||||
|  | 	MergeWhenChecksSucceed bool   `json:"merge_when_checks_succeed,omitempty"` | ||||||
| 	DeleteBranchAfterMerge bool   `json:"delete_branch_after_merge,omitempty"` | 	DeleteBranchAfterMerge bool   `json:"delete_branch_after_merge,omitempty"` | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -137,5 +137,13 @@ func GetPullRequestCommitStatusState(ctx context.Context, pr *models.PullRequest | |||||||
| 		return "", errors.Wrap(err, "GetLatestCommitStatus") | 		return "", 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" | ||||||
| 	"code.gitea.io/gitea/models/db" | 	"code.gitea.io/gitea/models/db" | ||||||
|  | 	pull_model "code.gitea.io/gitea/models/pull" | ||||||
| 	repo_model "code.gitea.io/gitea/models/repo" | 	repo_model "code.gitea.io/gitea/models/repo" | ||||||
| 	"code.gitea.io/gitea/models/unit" | 	"code.gitea.io/gitea/models/unit" | ||||||
| 	user_model "code.gitea.io/gitea/models/user" | 	user_model "code.gitea.io/gitea/models/user" | ||||||
| @@ -46,6 +47,11 @@ func Merge(pr *models.PullRequest, doer *user_model.User, baseGitRepo *git.Repos | |||||||
| 	pullWorkingPool.CheckIn(fmt.Sprint(pr.ID)) | 	pullWorkingPool.CheckIn(fmt.Sprint(pr.ID)) | ||||||
| 	defer pullWorkingPool.CheckOut(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) | 	prUnit, err := pr.BaseRepo.GetUnit(unit.TypePullRequests) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Error("pr.BaseRepo.GetUnit(unit.TypePullRequests): %v", err) | 		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) { | 	graceful.GetManager().RunWithShutdownContext(func(ctx context.Context) { | ||||||
| 		// There is no sensible way to shut this down ":-(" | 		// There is no sensible way to shut this down ":-(" | ||||||
| 		// If you don't let it run all the way then you will lose data | 		// 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) | 		prs, err := models.GetUnmergedPullRequestsByHeadInfo(repoID, branch) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
|   | |||||||
| @@ -14,6 +14,7 @@ import ( | |||||||
| 	user_model "code.gitea.io/gitea/models/user" | 	user_model "code.gitea.io/gitea/models/user" | ||||||
| 	"code.gitea.io/gitea/modules/git" | 	"code.gitea.io/gitea/modules/git" | ||||||
| 	"code.gitea.io/gitea/modules/structs" | 	"code.gitea.io/gitea/modules/structs" | ||||||
|  | 	"code.gitea.io/gitea/services/automerge" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // CreateCommitStatus creates a new CommitStatus given a bunch of parameters | // CreateCommitStatus creates a new CommitStatus given a bunch of parameters | ||||||
| @@ -44,6 +45,12 @@ func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, creato | |||||||
| 		return fmt.Errorf("NewCommitStatus[repo_id: %d, user_id: %d, sha: %s]: %v", repo.ID, creator.ID, sha, err) | 		return 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 | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -10,7 +10,8 @@ | |||||||
| 		22 = REVIEW, 23 = ISSUE_LOCKED, 24 = ISSUE_UNLOCKED, 25 = TARGET_BRANCH_CHANGED, | 		22 = REVIEW, 23 = ISSUE_LOCKED, 24 = ISSUE_UNLOCKED, 25 = TARGET_BRANCH_CHANGED, | ||||||
| 		26 = DELETE_TIME_MANUAL, 27 = REVIEW_REQUEST, 28 = MERGE_PULL_REQUEST, | 		26 = DELETE_TIME_MANUAL, 27 = REVIEW_REQUEST, 28 = MERGE_PULL_REQUEST, | ||||||
| 		29 = PULL_PUSH_EVENT, 30 = PROJECT_CHANGED, 31 = PROJECT_BOARD_CHANGED | 		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}} | 		{{if eq .Type 0}} | ||||||
| 			<div class="timeline-item comment" id="{{.HashTag}}"> | 			<div class="timeline-item comment" id="{{.HashTag}}"> | ||||||
| 			{{if .OriginalAuthor }} | 			{{if .OriginalAuthor }} | ||||||
| @@ -837,6 +838,15 @@ | |||||||
| 					{{end}} | 					{{end}} | ||||||
| 				</span> | 				</span> | ||||||
| 			</div> | 			</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}} | 	{{end}} | ||||||
| {{end}} | {{end}} | ||||||
|   | |||||||
| @@ -8015,6 +8015,51 @@ | |||||||
|             "$ref": "#/responses/error" |             "$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": { |     "/repos/{owner}/{repo}/pulls/{index}/requested_reviewers": { | ||||||
| @@ -16298,6 +16343,10 @@ | |||||||
|         "head_commit_id": { |         "head_commit_id": { | ||||||
|           "type": "string", |           "type": "string", | ||||||
|           "x-go-name": "HeadCommitID" |           "x-go-name": "HeadCommitID" | ||||||
|  |         }, | ||||||
|  |         "merge_when_checks_succeed": { | ||||||
|  |           "type": "boolean", | ||||||
|  |           "x-go-name": "MergeWhenChecksSucceed" | ||||||
|         } |         } | ||||||
|       }, |       }, | ||||||
|       "x-go-name": "MergePullRequestForm", |       "x-go-name": "MergePullRequestForm", | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user