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:
Jimmy Praet 2021-07-13 01:26:25 +02:00 committed by GitHub
parent 46a4c6835d
commit 78118a3b02
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 182 additions and 44 deletions

View file

@ -91,14 +91,15 @@ func (cfg *IssuesConfig) ToDB() ([]byte, error) {
// PullRequestsConfig describes pull requests config // PullRequestsConfig describes pull requests config
type PullRequestsConfig struct { type PullRequestsConfig struct {
IgnoreWhitespaceConflicts bool IgnoreWhitespaceConflicts bool
AllowMerge bool AllowMerge bool
AllowRebase bool AllowRebase bool
AllowRebaseMerge bool AllowRebaseMerge bool
AllowSquash bool AllowSquash bool
AllowManualMerge bool AllowManualMerge bool
AutodetectManualMerge bool AutodetectManualMerge bool
DefaultMergeStyle MergeStyle DefaultDeleteBranchAfterMerge bool
DefaultMergeStyle MergeStyle
} }
// FromDB fills up a PullRequestsConfig from serialized format. // FromDB fills up a PullRequestsConfig from serialized format.

View file

@ -172,6 +172,8 @@ type EditRepoOption struct {
AllowManualMerge *bool `json:"allow_manual_merge,omitempty"` 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. // 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"` 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`. // 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"` DefaultMergeStyle *string `json:"default_merge_style,omitempty"`
// set to `true` to archive this repository. // set to `true` to archive this repository.

View file

@ -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_squash_commits = Enable Squashing to Merge Commits
settings.pulls.allow_manual_merge = Enable Mark PR as manually merged 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.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.projects_desc = Enable Repository Projects
settings.admin_settings = Administrator Settings settings.admin_settings = Administrator Settings
settings.admin_enable_health_check = Enable Repository Health Checks (git fsck) settings.admin_enable_health_check = Enable Repository Health Checks (git fsck)

View file

@ -5,6 +5,7 @@
package repo package repo
import ( import (
"errors"
"fmt" "fmt"
"math" "math"
"net/http" "net/http"
@ -25,6 +26,7 @@ import (
"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"
repo_service "code.gitea.io/gitea/services/repository"
) )
// ListPullRequests returns a list of all PRs // ListPullRequests returns a list of all PRs
@ -878,6 +880,38 @@ func MergePullRequest(ctx *context.APIContext) {
} }
log.Trace("Pull request merged: %d", pr.ID) 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) ctx.Status(http.StatusOK)
} }

View file

@ -833,14 +833,15 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
if err != nil { if err != nil {
// Unit type doesn't exist so we make a new config file with default values // Unit type doesn't exist so we make a new config file with default values
config = &models.PullRequestsConfig{ config = &models.PullRequestsConfig{
IgnoreWhitespaceConflicts: false, IgnoreWhitespaceConflicts: false,
AllowMerge: true, AllowMerge: true,
AllowRebase: true, AllowRebase: true,
AllowRebaseMerge: true, AllowRebaseMerge: true,
AllowSquash: true, AllowSquash: true,
AllowManualMerge: true, AllowManualMerge: true,
AutodetectManualMerge: false, AutodetectManualMerge: false,
DefaultMergeStyle: models.MergeStyleMerge, DefaultDeleteBranchAfterMerge: false,
DefaultMergeStyle: models.MergeStyleMerge,
} }
} else { } else {
config = unit.PullRequestsConfig() config = unit.PullRequestsConfig()
@ -867,6 +868,9 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
if opts.AutodetectManualMerge != nil { if opts.AutodetectManualMerge != nil {
config.AutodetectManualMerge = *opts.AutodetectManualMerge config.AutodetectManualMerge = *opts.AutodetectManualMerge
} }
if opts.DefaultDeleteBranchAfterMerge != nil {
config.DefaultDeleteBranchAfterMerge = *opts.DefaultDeleteBranchAfterMerge
}
if opts.DefaultMergeStyle != nil { if opts.DefaultMergeStyle != nil {
config.DefaultMergeStyle = models.MergeStyle(*opts.DefaultMergeStyle) config.DefaultMergeStyle = models.MergeStyle(*opts.DefaultMergeStyle)
} }

View file

@ -965,6 +965,22 @@ func MergePullRequest(ctx *context.Context) {
} }
log.Trace("Pull request merged: %d", pr.ID) 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)) 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 fullBranchName := pr.HeadRepo.Owner.Name + "/" + pr.HeadBranch
gitRepo, err := git.OpenRepository(pr.HeadRepo.RepoPath()) var gitBaseRepo *git.Repository
if err != nil {
ctx.ServerError(fmt.Sprintf("OpenRepository[%s]", pr.HeadRepo.RepoPath()), err)
return
}
defer gitRepo.Close()
gitBaseRepo, err := git.OpenRepository(pr.BaseRepo.RepoPath()) // Assume that the base repo is the current context (almost certainly)
if err != nil { if ctx.Repo != nil && ctx.Repo.Repository != nil && ctx.Repo.Repository.ID == pr.BaseRepoID && ctx.Repo.GitRepo != nil {
ctx.ServerError(fmt.Sprintf("OpenRepository[%s]", pr.BaseRepo.RepoPath()), err) gitBaseRepo = ctx.Repo.GitRepo
return } 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 gitBaseRepo.Close()
defer func() { defer func() {
ctx.JSON(http.StatusOK, map[string]interface{}{ ctx.JSON(http.StatusOK, map[string]interface{}{
@ -1208,6 +1240,11 @@ func CleanUpPullRequest(ctx *context.Context) {
return 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 { if err := repo_service.DeleteBranch(ctx.User, pr.HeadRepo, gitRepo, pr.HeadBranch); err != nil {
switch { switch {
case git.IsErrBranchNotExist(err): case git.IsErrBranchNotExist(err):
@ -1223,7 +1260,7 @@ func CleanUpPullRequest(ctx *context.Context) {
return 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 // Do not fail here as branch has already been deleted
log.Error("DeleteBranch: %v", err) log.Error("DeleteBranch: %v", err)
} }

View file

@ -416,14 +416,15 @@ func SettingsPost(ctx *context.Context) {
RepoID: repo.ID, RepoID: repo.ID,
Type: models.UnitTypePullRequests, Type: models.UnitTypePullRequests,
Config: &models.PullRequestsConfig{ Config: &models.PullRequestsConfig{
IgnoreWhitespaceConflicts: form.PullsIgnoreWhitespace, IgnoreWhitespaceConflicts: form.PullsIgnoreWhitespace,
AllowMerge: form.PullsAllowMerge, AllowMerge: form.PullsAllowMerge,
AllowRebase: form.PullsAllowRebase, AllowRebase: form.PullsAllowRebase,
AllowRebaseMerge: form.PullsAllowRebaseMerge, AllowRebaseMerge: form.PullsAllowRebaseMerge,
AllowSquash: form.PullsAllowSquash, AllowSquash: form.PullsAllowSquash,
AllowManualMerge: form.PullsAllowManualMerge, AllowManualMerge: form.PullsAllowManualMerge,
AutodetectManualMerge: form.EnableAutodetectManualMerge, AutodetectManualMerge: form.EnableAutodetectManualMerge,
DefaultMergeStyle: models.MergeStyle(form.PullsDefaultMergeStyle), DefaultDeleteBranchAfterMerge: form.DefaultDeleteBranchAfterMerge,
DefaultMergeStyle: models.MergeStyle(form.PullsDefaultMergeStyle),
}, },
}) })
} else if !models.UnitTypePullRequests.UnitGlobalDisabled() { } else if !models.UnitTypePullRequests.UnitGlobalDisabled() {

View file

@ -151,6 +151,7 @@ type RepoSettingForm struct {
PullsAllowManualMerge bool PullsAllowManualMerge bool
PullsDefaultMergeStyle string PullsDefaultMergeStyle string
EnableAutodetectManualMerge bool EnableAutodetectManualMerge bool
DefaultDeleteBranchAfterMerge bool
EnableTimetracker bool EnableTimetracker bool
AllowOnlyContributorsToTrackTime bool AllowOnlyContributorsToTrackTime bool
EnableIssueDependencies bool EnableIssueDependencies bool
@ -551,11 +552,12 @@ func (f *InitializeLabelsForm) Validate(req *http.Request, errs binding.Errors)
type MergePullRequestForm struct { type MergePullRequestForm struct {
// required: true // required: true
// enum: merge,rebase,rebase-merge,squash,manually-merged // enum: merge,rebase,rebase-merge,squash,manually-merged
Do string `binding:"Required;In(merge,rebase,rebase-merge,squash,manually-merged)"` Do string `binding:"Required;In(merge,rebase,rebase-merge,squash,manually-merged)"`
MergeTitleField string MergeTitleField string
MergeMessageField string MergeMessageField string
MergeCommitID string // only used for manually-merged MergeCommitID string // only used for manually-merged
ForceMerge *bool `json:"force_merge,omitempty"` ForceMerge *bool `json:"force_merge,omitempty"`
DeleteBranchAfterMerge bool `json:"delete_branch_after_merge,omitempty"`
} }
// Validate validates the fields // Validate validates the fields

View file

@ -303,7 +303,11 @@ func AddTestPullRequestTask(doer *models.User, repoID int64, branch string, isSy
for _, pr := range prs { for _, pr := range prs {
divergence, err := GetDiverging(pr) divergence, err := GetDiverging(pr)
if err != nil { if err != nil {
log.Error("GetDiverging: %v", err) 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 { } else {
err = pr.UpdateCommitDivergence(divergence.Ahead, divergence.Behind) err = pr.UpdateCommitDivergence(divergence.Ahead, divergence.Behind)
if err != nil { if err != nil {

View file

@ -141,10 +141,15 @@ func createTemporaryRepo(pr *models.PullRequest) (string, error) {
trackingBranch := "tracking" trackingBranch := "tracking"
// Fetch head branch // Fetch head branch
if err := git.NewCommand("fetch", "--no-tags", remoteRepoName, git.BranchPrefix+pr.HeadBranch+":"+trackingBranch).RunInDirPipeline(tmpBasePath, &outbuf, &errbuf); err != nil { 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 { if err := models.RemoveTemporaryPath(tmpBasePath); err != nil {
log.Error("CreateTempRepo: RemoveTemporaryPath: %s", err) 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()) 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() outbuf.Reset()

View file

@ -88,7 +88,9 @@ func GetDiverging(pr *models.PullRequest) (*git.DivergeObject, error) {
tmpRepo, err := createTemporaryRepo(pr) tmpRepo, err := createTemporaryRepo(pr)
if err != nil { if err != nil {
log.Error("CreateTemporaryPath: %v", err) if !models.IsErrBranchDoesNotExist(err) {
log.Error("CreateTemporaryRepo: %v", err)
}
return nil, err return nil, err
} }
defer func() { defer func() {

View file

@ -315,6 +315,12 @@
<button class="ui button merge-cancel"> <button class="ui button merge-cancel">
{{$.i18n.Tr "cancel"}} {{$.i18n.Tr "cancel"}}
</button> </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> </form>
</div> </div>
{{end}} {{end}}
@ -328,6 +334,12 @@
<button class="ui button merge-cancel"> <button class="ui button merge-cancel">
{{$.i18n.Tr "cancel"}} {{$.i18n.Tr "cancel"}}
</button> </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> </form>
</div> </div>
{{end}} {{end}}
@ -347,6 +359,12 @@
<button class="ui button merge-cancel"> <button class="ui button merge-cancel">
{{$.i18n.Tr "cancel"}} {{$.i18n.Tr "cancel"}}
</button> </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> </form>
</div> </div>
{{end}} {{end}}
@ -366,6 +384,12 @@
<button class="ui button merge-cancel"> <button class="ui button merge-cancel">
{{$.i18n.Tr "cancel"}} {{$.i18n.Tr "cancel"}}
</button> </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> </form>
</div> </div>
{{end}} {{end}}
@ -382,6 +406,12 @@
<button class="ui button merge-cancel"> <button class="ui button merge-cancel">
{{$.i18n.Tr "cancel"}} {{$.i18n.Tr "cancel"}}
</button> </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> </form>
</div> </div>
{{end}} {{end}}

View file

@ -445,6 +445,12 @@
<label>{{.i18n.Tr "repo.settings.pulls.enable_autodetect_manual_merge"}}</label> <label>{{.i18n.Tr "repo.settings.pulls.enable_autodetect_manual_merge"}}</label>
</div> </div>
</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"> <div class="field">
<p> <p>
{{.i18n.Tr "repo.settings.default_merge_style_desc"}} {{.i18n.Tr "repo.settings.default_merge_style_desc"}}

View file

@ -14058,6 +14058,11 @@
"type": "string", "type": "string",
"x-go-name": "DefaultBranch" "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": { "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`.", "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", "type": "string",
@ -15137,6 +15142,10 @@
"MergeTitleField": { "MergeTitleField": {
"type": "string" "type": "string"
}, },
"delete_branch_after_merge": {
"type": "boolean",
"x-go-name": "DeleteBranchAfterMerge"
},
"force_merge": { "force_merge": {
"type": "boolean", "type": "boolean",
"x-go-name": "ForceMerge" "x-go-name": "ForceMerge"