Add checkbox to delete pull branch after successful merge (#16049)
* Add checkbox to delete pull branch after successful merge * Omit DeleteBranchAfterMerge field in json * Log a warning instead of error when PR head branch deleted * Add DefaultDeleteBranchAfterMerge to PullRequestConfig * Add support for delete_branch_after_merge via API * Fix for API: the branch should be deleted from the HEAD repo If head and base repo are the same, reuse the already opened ctx.Repo.GitRepo * Don't delegate to CleanupBranch, only reuse branch deletion code CleanupBranch contains too much logic that has already been performed by the Merge * Reuse gitrepo in MergePullRequest Co-authored-by: Andrew Thornton <art27@cantab.net>
This commit is contained in:
		
							parent
							
								
									46a4c6835d
								
							
						
					
					
						commit
						78118a3b02
					
				
					 14 changed files with 182 additions and 44 deletions
				
			
		|  | @ -98,6 +98,7 @@ type PullRequestsConfig struct { | |||
| 	AllowSquash                   bool | ||||
| 	AllowManualMerge              bool | ||||
| 	AutodetectManualMerge         bool | ||||
| 	DefaultDeleteBranchAfterMerge bool | ||||
| 	DefaultMergeStyle             MergeStyle | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -172,6 +172,8 @@ type EditRepoOption struct { | |||
| 	AllowManualMerge *bool `json:"allow_manual_merge,omitempty"` | ||||
| 	// either `true` to enable AutodetectManualMerge, or `false` to prevent it. `has_pull_requests` must be `true`, Note: In some special cases, misjudgments can occur.
 | ||||
| 	AutodetectManualMerge *bool `json:"autodetect_manual_merge,omitempty"` | ||||
| 	// set to `true` to delete pr branch after merge by default
 | ||||
| 	DefaultDeleteBranchAfterMerge *bool `json:"default_delete_branch_after_merge,omitempty"` | ||||
| 	// set to a merge style to be used by this repository: "merge", "rebase", "rebase-merge", or "squash". `has_pull_requests` must be `true`.
 | ||||
| 	DefaultMergeStyle *string `json:"default_merge_style,omitempty"` | ||||
| 	// set to `true` to archive this repository.
 | ||||
|  |  | |||
|  | @ -1664,6 +1664,7 @@ settings.pulls.allow_rebase_merge_commit = Enable Rebasing with explicit merge c | |||
| settings.pulls.allow_squash_commits = Enable Squashing to Merge Commits | ||||
| settings.pulls.allow_manual_merge = Enable Mark PR as manually merged | ||||
| settings.pulls.enable_autodetect_manual_merge = Enable autodetect manual merge (Note: In some special cases, misjudgments can occur) | ||||
| settings.pulls.default_delete_branch_after_merge = Delete pull request branch after merge by default | ||||
| settings.projects_desc = Enable Repository Projects | ||||
| settings.admin_settings = Administrator Settings | ||||
| settings.admin_enable_health_check = Enable Repository Health Checks (git fsck) | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ | |||
| package repo | ||||
| 
 | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"math" | ||||
| 	"net/http" | ||||
|  | @ -25,6 +26,7 @@ import ( | |||
| 	"code.gitea.io/gitea/services/forms" | ||||
| 	issue_service "code.gitea.io/gitea/services/issue" | ||||
| 	pull_service "code.gitea.io/gitea/services/pull" | ||||
| 	repo_service "code.gitea.io/gitea/services/repository" | ||||
| ) | ||||
| 
 | ||||
| // ListPullRequests returns a list of all PRs
 | ||||
|  | @ -878,6 +880,38 @@ func MergePullRequest(ctx *context.APIContext) { | |||
| 	} | ||||
| 
 | ||||
| 	log.Trace("Pull request merged: %d", pr.ID) | ||||
| 
 | ||||
| 	if form.DeleteBranchAfterMerge { | ||||
| 		var headRepo *git.Repository | ||||
| 		if ctx.Repo != nil && ctx.Repo.Repository != nil && ctx.Repo.Repository.ID == pr.HeadRepoID && ctx.Repo.GitRepo != nil { | ||||
| 			headRepo = ctx.Repo.GitRepo | ||||
| 		} else { | ||||
| 			headRepo, err = git.OpenRepository(pr.HeadRepo.RepoPath()) | ||||
| 			if err != nil { | ||||
| 				ctx.ServerError(fmt.Sprintf("OpenRepository[%s]", pr.HeadRepo.RepoPath()), err) | ||||
| 				return | ||||
| 			} | ||||
| 			defer headRepo.Close() | ||||
| 		} | ||||
| 		if err := repo_service.DeleteBranch(ctx.User, pr.HeadRepo, headRepo, pr.HeadBranch); err != nil { | ||||
| 			switch { | ||||
| 			case git.IsErrBranchNotExist(err): | ||||
| 				ctx.NotFound(err) | ||||
| 			case errors.Is(err, repo_service.ErrBranchIsDefault): | ||||
| 				ctx.Error(http.StatusForbidden, "DefaultBranch", fmt.Errorf("can not delete default branch")) | ||||
| 			case errors.Is(err, repo_service.ErrBranchIsProtected): | ||||
| 				ctx.Error(http.StatusForbidden, "IsProtectedBranch", fmt.Errorf("branch protected")) | ||||
| 			default: | ||||
| 				ctx.Error(http.StatusInternalServerError, "DeleteBranch", err) | ||||
| 			} | ||||
| 			return | ||||
| 		} | ||||
| 		if err := models.AddDeletePRBranchComment(ctx.User, pr.BaseRepo, pr.Issue.ID, pr.HeadBranch); err != nil { | ||||
| 			// Do not fail here as branch has already been deleted
 | ||||
| 			log.Error("DeleteBranch: %v", err) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	ctx.Status(http.StatusOK) | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -840,6 +840,7 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error { | |||
| 					AllowSquash:                   true, | ||||
| 					AllowManualMerge:              true, | ||||
| 					AutodetectManualMerge:         false, | ||||
| 					DefaultDeleteBranchAfterMerge: false, | ||||
| 					DefaultMergeStyle:             models.MergeStyleMerge, | ||||
| 				} | ||||
| 			} else { | ||||
|  | @ -867,6 +868,9 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error { | |||
| 			if opts.AutodetectManualMerge != nil { | ||||
| 				config.AutodetectManualMerge = *opts.AutodetectManualMerge | ||||
| 			} | ||||
| 			if opts.DefaultDeleteBranchAfterMerge != nil { | ||||
| 				config.DefaultDeleteBranchAfterMerge = *opts.DefaultDeleteBranchAfterMerge | ||||
| 			} | ||||
| 			if opts.DefaultMergeStyle != nil { | ||||
| 				config.DefaultMergeStyle = models.MergeStyle(*opts.DefaultMergeStyle) | ||||
| 			} | ||||
|  |  | |||
|  | @ -965,6 +965,22 @@ func MergePullRequest(ctx *context.Context) { | |||
| 	} | ||||
| 
 | ||||
| 	log.Trace("Pull request merged: %d", pr.ID) | ||||
| 
 | ||||
| 	if form.DeleteBranchAfterMerge { | ||||
| 		var headRepo *git.Repository | ||||
| 		if ctx.Repo != nil && ctx.Repo.Repository != nil && pr.HeadRepoID == ctx.Repo.Repository.ID && ctx.Repo.GitRepo != nil { | ||||
| 			headRepo = ctx.Repo.GitRepo | ||||
| 		} else { | ||||
| 			headRepo, err = git.OpenRepository(pr.HeadRepo.RepoPath()) | ||||
| 			if err != nil { | ||||
| 				ctx.ServerError(fmt.Sprintf("OpenRepository[%s]", pr.HeadRepo.RepoPath()), err) | ||||
| 				return | ||||
| 			} | ||||
| 			defer headRepo.Close() | ||||
| 		} | ||||
| 		deleteBranch(ctx, pr, headRepo) | ||||
| 	} | ||||
| 
 | ||||
| 	ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index)) | ||||
| } | ||||
| 
 | ||||
|  | @ -1170,19 +1186,35 @@ func CleanUpPullRequest(ctx *context.Context) { | |||
| 
 | ||||
| 	fullBranchName := pr.HeadRepo.Owner.Name + "/" + pr.HeadBranch | ||||
| 
 | ||||
| 	gitRepo, err := git.OpenRepository(pr.HeadRepo.RepoPath()) | ||||
| 	if err != nil { | ||||
| 		ctx.ServerError(fmt.Sprintf("OpenRepository[%s]", pr.HeadRepo.RepoPath()), err) | ||||
| 		return | ||||
| 	} | ||||
| 	defer gitRepo.Close() | ||||
| 	var gitBaseRepo *git.Repository | ||||
| 
 | ||||
| 	gitBaseRepo, err := git.OpenRepository(pr.BaseRepo.RepoPath()) | ||||
| 	// Assume that the base repo is the current context (almost certainly)
 | ||||
| 	if ctx.Repo != nil && ctx.Repo.Repository != nil && ctx.Repo.Repository.ID == pr.BaseRepoID && ctx.Repo.GitRepo != nil { | ||||
| 		gitBaseRepo = ctx.Repo.GitRepo | ||||
| 	} else { | ||||
| 		// If not just open it
 | ||||
| 		gitBaseRepo, err = git.OpenRepository(pr.BaseRepo.RepoPath()) | ||||
| 		if err != nil { | ||||
| 			ctx.ServerError(fmt.Sprintf("OpenRepository[%s]", pr.BaseRepo.RepoPath()), err) | ||||
| 			return | ||||
| 		} | ||||
| 		defer gitBaseRepo.Close() | ||||
| 	} | ||||
| 
 | ||||
| 	// Now assume that the head repo is the same as the base repo (reasonable chance)
 | ||||
| 	gitRepo := gitBaseRepo | ||||
| 	// But if not: is it the same as the context?
 | ||||
| 	if pr.BaseRepoID != pr.HeadRepoID && ctx.Repo != nil && ctx.Repo.Repository != nil && ctx.Repo.Repository.ID == pr.HeadRepoID && ctx.Repo.GitRepo != nil { | ||||
| 		gitRepo = ctx.Repo.GitRepo | ||||
| 	} else if pr.BaseRepoID != pr.HeadRepoID { | ||||
| 		// Otherwise just load it up
 | ||||
| 		gitRepo, err = git.OpenRepository(pr.HeadRepo.RepoPath()) | ||||
| 		if err != nil { | ||||
| 			ctx.ServerError(fmt.Sprintf("OpenRepository[%s]", pr.HeadRepo.RepoPath()), err) | ||||
| 			return | ||||
| 		} | ||||
| 		defer gitRepo.Close() | ||||
| 	} | ||||
| 
 | ||||
| 	defer func() { | ||||
| 		ctx.JSON(http.StatusOK, map[string]interface{}{ | ||||
|  | @ -1208,6 +1240,11 @@ func CleanUpPullRequest(ctx *context.Context) { | |||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	deleteBranch(ctx, pr, gitRepo) | ||||
| } | ||||
| 
 | ||||
| func deleteBranch(ctx *context.Context, pr *models.PullRequest, gitRepo *git.Repository) { | ||||
| 	fullBranchName := pr.HeadRepo.Owner.Name + "/" + pr.HeadBranch | ||||
| 	if err := repo_service.DeleteBranch(ctx.User, pr.HeadRepo, gitRepo, pr.HeadBranch); err != nil { | ||||
| 		switch { | ||||
| 		case git.IsErrBranchNotExist(err): | ||||
|  | @ -1223,7 +1260,7 @@ func CleanUpPullRequest(ctx *context.Context) { | |||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if err := models.AddDeletePRBranchComment(ctx.User, pr.BaseRepo, issue.ID, pr.HeadBranch); err != nil { | ||||
| 	if err := models.AddDeletePRBranchComment(ctx.User, pr.BaseRepo, pr.IssueID, pr.HeadBranch); err != nil { | ||||
| 		// Do not fail here as branch has already been deleted
 | ||||
| 		log.Error("DeleteBranch: %v", err) | ||||
| 	} | ||||
|  |  | |||
|  | @ -423,6 +423,7 @@ func SettingsPost(ctx *context.Context) { | |||
| 					AllowSquash:                   form.PullsAllowSquash, | ||||
| 					AllowManualMerge:              form.PullsAllowManualMerge, | ||||
| 					AutodetectManualMerge:         form.EnableAutodetectManualMerge, | ||||
| 					DefaultDeleteBranchAfterMerge: form.DefaultDeleteBranchAfterMerge, | ||||
| 					DefaultMergeStyle:             models.MergeStyle(form.PullsDefaultMergeStyle), | ||||
| 				}, | ||||
| 			}) | ||||
|  |  | |||
|  | @ -151,6 +151,7 @@ type RepoSettingForm struct { | |||
| 	PullsAllowManualMerge                 bool | ||||
| 	PullsDefaultMergeStyle                string | ||||
| 	EnableAutodetectManualMerge           bool | ||||
| 	DefaultDeleteBranchAfterMerge         bool | ||||
| 	EnableTimetracker                     bool | ||||
| 	AllowOnlyContributorsToTrackTime      bool | ||||
| 	EnableIssueDependencies               bool | ||||
|  | @ -556,6 +557,7 @@ type MergePullRequestForm struct { | |||
| 	MergeMessageField      string | ||||
| 	MergeCommitID          string // only used for manually-merged
 | ||||
| 	ForceMerge             *bool  `json:"force_merge,omitempty"` | ||||
| 	DeleteBranchAfterMerge bool   `json:"delete_branch_after_merge,omitempty"` | ||||
| } | ||||
| 
 | ||||
| // Validate validates the fields
 | ||||
|  |  | |||
|  | @ -303,7 +303,11 @@ func AddTestPullRequestTask(doer *models.User, repoID int64, branch string, isSy | |||
| 		for _, pr := range prs { | ||||
| 			divergence, err := GetDiverging(pr) | ||||
| 			if err != nil { | ||||
| 				if models.IsErrBranchDoesNotExist(err) && !git.IsBranchExist(pr.HeadRepo.RepoPath(), pr.HeadBranch) { | ||||
| 					log.Warn("Cannot test PR %s/%d: head_branch %s no longer exists", pr.BaseRepo.Name, pr.IssueID, pr.HeadBranch) | ||||
| 				} else { | ||||
| 					log.Error("GetDiverging: %v", err) | ||||
| 				} | ||||
| 			} else { | ||||
| 				err = pr.UpdateCommitDivergence(divergence.Ahead, divergence.Behind) | ||||
| 				if err != nil { | ||||
|  |  | |||
|  | @ -141,10 +141,15 @@ func createTemporaryRepo(pr *models.PullRequest) (string, error) { | |||
| 	trackingBranch := "tracking" | ||||
| 	// Fetch head branch
 | ||||
| 	if err := git.NewCommand("fetch", "--no-tags", remoteRepoName, git.BranchPrefix+pr.HeadBranch+":"+trackingBranch).RunInDirPipeline(tmpBasePath, &outbuf, &errbuf); err != nil { | ||||
| 		log.Error("Unable to fetch head_repo head branch [%s:%s -> tracking in %s]: %v:\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, tmpBasePath, err, outbuf.String(), errbuf.String()) | ||||
| 		if err := models.RemoveTemporaryPath(tmpBasePath); err != nil { | ||||
| 			log.Error("CreateTempRepo: RemoveTemporaryPath: %s", err) | ||||
| 		} | ||||
| 		if !git.IsBranchExist(pr.HeadRepo.RepoPath(), pr.HeadBranch) { | ||||
| 			return "", models.ErrBranchDoesNotExist{ | ||||
| 				BranchName: pr.HeadBranch, | ||||
| 			} | ||||
| 		} | ||||
| 		log.Error("Unable to fetch head_repo head branch [%s:%s -> tracking in %s]: %v:\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, tmpBasePath, err, outbuf.String(), errbuf.String()) | ||||
| 		return "", fmt.Errorf("Unable to fetch head_repo head branch [%s:%s -> tracking in tmpBasePath]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, err, outbuf.String(), errbuf.String()) | ||||
| 	} | ||||
| 	outbuf.Reset() | ||||
|  |  | |||
|  | @ -88,7 +88,9 @@ func GetDiverging(pr *models.PullRequest) (*git.DivergeObject, error) { | |||
| 
 | ||||
| 	tmpRepo, err := createTemporaryRepo(pr) | ||||
| 	if err != nil { | ||||
| 		log.Error("CreateTemporaryPath: %v", err) | ||||
| 		if !models.IsErrBranchDoesNotExist(err) { | ||||
| 			log.Error("CreateTemporaryRepo: %v", err) | ||||
| 		} | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	defer func() { | ||||
|  |  | |||
|  | @ -315,6 +315,12 @@ | |||
| 									<button class="ui button merge-cancel"> | ||||
| 										{{$.i18n.Tr "cancel"}} | ||||
| 									</button> | ||||
| 									{{if .IsPullBranchDeletable}} | ||||
| 										<div class="ui checkbox ml-2"> | ||||
| 											<input name="delete_branch_after_merge" type="checkbox" {{if $prUnit.PullRequestsConfig.DefaultDeleteBranchAfterMerge}}checked{{end}}> | ||||
| 											<label>{{$.i18n.Tr "repo.branch.delete" .HeadTarget}}</label> | ||||
| 										</div> | ||||
| 									{{end}} | ||||
| 								</form> | ||||
| 							</div> | ||||
| 							{{end}} | ||||
|  | @ -328,6 +334,12 @@ | |||
| 									<button class="ui button merge-cancel"> | ||||
| 										{{$.i18n.Tr "cancel"}} | ||||
| 									</button> | ||||
| 									{{if .IsPullBranchDeletable}} | ||||
| 										<div class="ui checkbox ml-2"> | ||||
| 											<input name="delete_branch_after_merge" type="checkbox" {{if $prUnit.PullRequestsConfig.DefaultDeleteBranchAfterMerge}}checked{{end}}> | ||||
| 											<label>{{$.i18n.Tr "repo.branch.delete" .HeadTarget}}</label> | ||||
| 										</div> | ||||
| 									{{end}} | ||||
| 								</form> | ||||
| 							</div> | ||||
| 							{{end}} | ||||
|  | @ -347,6 +359,12 @@ | |||
| 									<button class="ui button merge-cancel"> | ||||
| 										{{$.i18n.Tr "cancel"}} | ||||
| 									</button> | ||||
| 									{{if .IsPullBranchDeletable}} | ||||
| 										<div class="ui checkbox ml-2"> | ||||
| 											<input name="delete_branch_after_merge" type="checkbox" {{if $prUnit.PullRequestsConfig.DefaultDeleteBranchAfterMerge}}checked{{end}}> | ||||
| 											<label>{{$.i18n.Tr "repo.branch.delete" .HeadTarget}}</label> | ||||
| 										</div> | ||||
| 									{{end}} | ||||
| 								</form> | ||||
| 							</div> | ||||
| 							{{end}} | ||||
|  | @ -366,6 +384,12 @@ | |||
| 									<button class="ui button merge-cancel"> | ||||
| 										{{$.i18n.Tr "cancel"}} | ||||
| 									</button> | ||||
| 									{{if .IsPullBranchDeletable}} | ||||
| 										<div class="ui checkbox ml-2"> | ||||
| 											<input name="delete_branch_after_merge" type="checkbox" {{if $prUnit.PullRequestsConfig.DefaultDeleteBranchAfterMerge}}checked{{end}}> | ||||
| 											<label>{{$.i18n.Tr "repo.branch.delete" .HeadTarget}}</label> | ||||
| 										</div> | ||||
| 									{{end}} | ||||
| 								</form> | ||||
| 							</div> | ||||
| 							{{end}} | ||||
|  | @ -382,6 +406,12 @@ | |||
| 										<button class="ui button merge-cancel"> | ||||
| 											{{$.i18n.Tr "cancel"}} | ||||
| 										</button> | ||||
| 										{{if .IsPullBranchDeletable}} | ||||
| 											<div class="ui checkbox ml-2"> | ||||
| 												<input name="delete_branch_after_merge" type="checkbox" {{if $prUnit.PullRequestsConfig.DefaultDeleteBranchAfterMerge}}checked{{end}}> | ||||
| 												<label>{{$.i18n.Tr "repo.branch.delete" .HeadTarget}}</label> | ||||
| 											</div> | ||||
| 										{{end}} | ||||
| 									</form> | ||||
| 								</div> | ||||
| 							{{end}} | ||||
|  |  | |||
|  | @ -445,6 +445,12 @@ | |||
| 								<label>{{.i18n.Tr "repo.settings.pulls.enable_autodetect_manual_merge"}}</label> | ||||
| 							</div> | ||||
| 						</div> | ||||
| 						<div class="field"> | ||||
| 							<div class="ui checkbox"> | ||||
| 								<input name="default_delete_branch_after_merge" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.DefaultDeleteBranchAfterMerge)}}checked{{end}}> | ||||
| 								<label>{{.i18n.Tr "repo.settings.pulls.default_delete_branch_after_merge"}}</label> | ||||
| 							</div> | ||||
| 						</div> | ||||
| 						<div class="field"> | ||||
| 							<p> | ||||
| 								{{.i18n.Tr "repo.settings.default_merge_style_desc"}} | ||||
|  |  | |||
|  | @ -14058,6 +14058,11 @@ | |||
|           "type": "string", | ||||
|           "x-go-name": "DefaultBranch" | ||||
|         }, | ||||
|         "default_delete_branch_after_merge": { | ||||
|           "description": "set to `true` to delete pr branch after merge by default", | ||||
|           "type": "boolean", | ||||
|           "x-go-name": "DefaultDeleteBranchAfterMerge" | ||||
|         }, | ||||
|         "default_merge_style": { | ||||
|           "description": "set to a merge style to be used by this repository: \"merge\", \"rebase\", \"rebase-merge\", or \"squash\". `has_pull_requests` must be `true`.", | ||||
|           "type": "string", | ||||
|  | @ -15137,6 +15142,10 @@ | |||
|         "MergeTitleField": { | ||||
|           "type": "string" | ||||
|         }, | ||||
|         "delete_branch_after_merge": { | ||||
|           "type": "boolean", | ||||
|           "x-go-name": "DeleteBranchAfterMerge" | ||||
|         }, | ||||
|         "force_merge": { | ||||
|           "type": "boolean", | ||||
|           "x-go-name": "ForceMerge" | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue