mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-26 04:17:08 +00:00 
			
		
		
		
	Add merge style fast-forward-only (#28954)
				
					
				
			With this option, it is possible to require a linear commit history with the following benefits over the next best option `Rebase+fast-forward`: The original commits continue existing, with the original signatures continuing to stay valid instead of being rewritten, there is no merge commit, and reverting commits becomes easier. Closes #24906
This commit is contained in:
		| @@ -1044,7 +1044,7 @@ LEVEL = Info | ||||
| ;; List of keywords used in Pull Request comments to automatically reopen a related issue | ||||
| ;REOPEN_KEYWORDS = reopen,reopens,reopened | ||||
| ;; | ||||
| ;; Set default merge style for repository creating, valid options: merge, rebase, rebase-merge, squash | ||||
| ;; Set default merge style for repository creating, valid options: merge, rebase, rebase-merge, squash, fast-forward-only | ||||
| ;DEFAULT_MERGE_STYLE = merge | ||||
| ;; | ||||
| ;; In the default merge message for squash commits include at most this many commits | ||||
|   | ||||
| @@ -126,7 +126,7 @@ In addition, there is _`StaticRootPath`_ which can be set as a built-in at build | ||||
|  keywords used in Pull Request comments to automatically close a related issue | ||||
| - `REOPEN_KEYWORDS`: **reopen**, **reopens**, **reopened**: List of keywords used in Pull Request comments to automatically reopen | ||||
|  a related issue | ||||
| - `DEFAULT_MERGE_STYLE`: **merge**: Set default merge style for repository creating, valid options: `merge`, `rebase`, `rebase-merge`, `squash` | ||||
| - `DEFAULT_MERGE_STYLE`: **merge**: Set default merge style for repository creating, valid options: `merge`, `rebase`, `rebase-merge`, `squash`, `fast-forward-only` | ||||
| - `DEFAULT_MERGE_MESSAGE_COMMITS_LIMIT`: **50**: In the default merge message for squash commits include at most this many commits. Set to `-1` to include all commits | ||||
| - `DEFAULT_MERGE_MESSAGE_SIZE`: **5120**: In the default merge message for squash commits limit the size of the commit messages. Set to `-1` to have no limit. Only used if `POPULATE_SQUASH_COMMENT_WITH_COMMIT_MESSAGES` is `true`. | ||||
| - `DEFAULT_MERGE_MESSAGE_ALL_AUTHORS`: **false**: In the default merge message for squash commits walk all commits to include all authors in the Co-authored-by otherwise just use those in the limited list | ||||
|   | ||||
| @@ -125,7 +125,7 @@ menu: | ||||
| - `CLOSE_KEYWORDS`: **close**, **closes**, **closed**, **fix**, **fixes**, **fixed**, **resolve**, **resolves**, **resolved**: 在拉取请求评论中用于自动关闭相关问题的关键词列表。 | ||||
| - `REOPEN_KEYWORDS`: **reopen**, **reopens**, **reopened**: 在拉取请求评论中用于自动重新打开相关问题的 | ||||
| 关键词列表。 | ||||
| - `DEFAULT_MERGE_STYLE`: **merge**: 设置创建仓库的默认合并方式,可选: `merge`, `rebase`, `rebase-merge`, `squash` | ||||
| - `DEFAULT_MERGE_STYLE`: **merge**: 设置创建仓库的默认合并方式,可选: `merge`, `rebase`, `rebase-merge`, `squash`, `fast-forward-only` | ||||
| - `DEFAULT_MERGE_MESSAGE_COMMITS_LIMIT`: **50**: 在默认合并消息中,对于`squash`提交,最多包括此数量的提交。设置为 -1 以包括所有提交。 | ||||
| - `DEFAULT_MERGE_MESSAGE_SIZE`: **5120**: 在默认的合并消息中,对于`squash`提交,限制提交消息的大小。设置为 `-1`以取消限制。仅在`POPULATE_SQUASH_COMMENT_WITH_COMMIT_MESSAGES`为`true`时使用。 | ||||
| - `DEFAULT_MERGE_MESSAGE_ALL_AUTHORS`: **false**: 在默认合并消息中,对于`squash`提交,遍历所有提交以包括所有作者的`Co-authored-by`,否则仅使用限定列表中的作者。 | ||||
|   | ||||
| @@ -493,6 +493,23 @@ func (err ErrMergeUnrelatedHistories) Error() string { | ||||
| 	return fmt.Sprintf("Merge UnrelatedHistories Error: %v: %s\n%s", err.Err, err.StdErr, err.StdOut) | ||||
| } | ||||
|  | ||||
| // ErrMergeDivergingFastForwardOnly represents an error if a fast-forward-only merge fails because the branches diverge | ||||
| type ErrMergeDivergingFastForwardOnly struct { | ||||
| 	StdOut string | ||||
| 	StdErr string | ||||
| 	Err    error | ||||
| } | ||||
|  | ||||
| // IsErrMergeDivergingFastForwardOnly checks if an error is a ErrMergeDivergingFastForwardOnly. | ||||
| func IsErrMergeDivergingFastForwardOnly(err error) bool { | ||||
| 	_, ok := err.(ErrMergeDivergingFastForwardOnly) | ||||
| 	return ok | ||||
| } | ||||
|  | ||||
| func (err ErrMergeDivergingFastForwardOnly) Error() string { | ||||
| 	return fmt.Sprintf("Merge DivergingFastForwardOnly Error: %v: %s\n%s", err.Err, err.StdErr, err.StdOut) | ||||
| } | ||||
|  | ||||
| // ErrRebaseConflicts represents an error if rebase fails with a conflict | ||||
| type ErrRebaseConflicts struct { | ||||
| 	Style     repo_model.MergeStyle | ||||
|   | ||||
| @@ -21,6 +21,8 @@ const ( | ||||
| 	MergeStyleRebaseMerge MergeStyle = "rebase-merge" | ||||
| 	// MergeStyleSquash squash commits into single commit before merging | ||||
| 	MergeStyleSquash MergeStyle = "squash" | ||||
| 	// MergeStyleFastForwardOnly fast-forward merge if possible, otherwise fail | ||||
| 	MergeStyleFastForwardOnly MergeStyle = "fast-forward-only" | ||||
| 	// MergeStyleManuallyMerged pr has been merged manually, just mark it as merged directly | ||||
| 	MergeStyleManuallyMerged MergeStyle = "manually-merged" | ||||
| 	// MergeStyleRebaseUpdate not a merge style, used to update pull head by rebase | ||||
|   | ||||
| @@ -122,6 +122,7 @@ type PullRequestsConfig struct { | ||||
| 	AllowRebase                   bool | ||||
| 	AllowRebaseMerge              bool | ||||
| 	AllowSquash                   bool | ||||
| 	AllowFastForwardOnly          bool | ||||
| 	AllowManualMerge              bool | ||||
| 	AutodetectManualMerge         bool | ||||
| 	AllowRebaseUpdate             bool | ||||
| @@ -148,6 +149,7 @@ func (cfg *PullRequestsConfig) IsMergeStyleAllowed(mergeStyle MergeStyle) bool { | ||||
| 		mergeStyle == MergeStyleRebase && cfg.AllowRebase || | ||||
| 		mergeStyle == MergeStyleRebaseMerge && cfg.AllowRebaseMerge || | ||||
| 		mergeStyle == MergeStyleSquash && cfg.AllowSquash || | ||||
| 		mergeStyle == MergeStyleFastForwardOnly && cfg.AllowFastForwardOnly || | ||||
| 		mergeStyle == MergeStyleManuallyMerged && cfg.AllowManualMerge | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -96,7 +96,7 @@ func (err ErrBranchNotExist) Unwrap() error { | ||||
| 	return util.ErrNotExist | ||||
| } | ||||
|  | ||||
| // ErrPushOutOfDate represents an error if merging fails due to unrelated histories | ||||
| // ErrPushOutOfDate represents an error if merging fails due to the base branch being updated | ||||
| type ErrPushOutOfDate struct { | ||||
| 	StdOut string | ||||
| 	StdErr string | ||||
|   | ||||
| @@ -87,7 +87,11 @@ func CreateRepositoryByExample(ctx context.Context, doer, u *user_model.User, re | ||||
| 			units = append(units, repo_model.RepoUnit{ | ||||
| 				RepoID: repo.ID, | ||||
| 				Type:   tp, | ||||
| 				Config: &repo_model.PullRequestsConfig{AllowMerge: true, AllowRebase: true, AllowRebaseMerge: true, AllowSquash: true, DefaultMergeStyle: repo_model.MergeStyle(setting.Repository.PullRequest.DefaultMergeStyle), AllowRebaseUpdate: true}, | ||||
| 				Config: &repo_model.PullRequestsConfig{ | ||||
| 					AllowMerge: true, AllowRebase: true, AllowRebaseMerge: true, AllowSquash: true, AllowFastForwardOnly: true, | ||||
| 					DefaultMergeStyle: repo_model.MergeStyle(setting.Repository.PullRequest.DefaultMergeStyle), | ||||
| 					AllowRebaseUpdate: true, | ||||
| 				}, | ||||
| 			}) | ||||
| 		} else { | ||||
| 			units = append(units, repo_model.RepoUnit{ | ||||
|   | ||||
| @@ -98,6 +98,7 @@ type Repository struct { | ||||
| 	AllowRebase                   bool             `json:"allow_rebase"` | ||||
| 	AllowRebaseMerge              bool             `json:"allow_rebase_explicit"` | ||||
| 	AllowSquash                   bool             `json:"allow_squash_merge"` | ||||
| 	AllowFastForwardOnly          bool             `json:"allow_fast_forward_only_merge"` | ||||
| 	AllowRebaseUpdate             bool             `json:"allow_rebase_update"` | ||||
| 	DefaultDeleteBranchAfterMerge bool             `json:"default_delete_branch_after_merge"` | ||||
| 	DefaultMergeStyle             string           `json:"default_merge_style"` | ||||
| @@ -195,6 +196,8 @@ type EditRepoOption struct { | ||||
| 	AllowRebaseMerge *bool `json:"allow_rebase_explicit,omitempty"` | ||||
| 	// either `true` to allow squash-merging pull requests, or `false` to prevent squash-merging. | ||||
| 	AllowSquash *bool `json:"allow_squash_merge,omitempty"` | ||||
| 	// either `true` to allow fast-forward-only merging pull requests, or `false` to prevent fast-forward-only merging. | ||||
| 	AllowFastForwardOnly *bool `json:"allow_fast_forward_only_merge,omitempty"` | ||||
| 	// either `true` to allow mark pr as merged manually, or `false` to prevent it. | ||||
| 	AllowManualMerge *bool `json:"allow_manual_merge,omitempty"` | ||||
| 	// either `true` to enable AutodetectManualMerge, or `false` to prevent it. Note: In some special cases, misjudgments can occur. | ||||
| @@ -203,7 +206,7 @@ type EditRepoOption struct { | ||||
| 	AllowRebaseUpdate *bool `json:"allow_rebase_update,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". | ||||
| 	// set to a merge style to be used by this repository: "merge", "rebase", "rebase-merge", "squash", or "fast-forward-only". | ||||
| 	DefaultMergeStyle *string `json:"default_merge_style,omitempty"` | ||||
| 	// set to `true` to allow edits from maintainers by default | ||||
| 	DefaultAllowMaintainerEdit *bool `json:"default_allow_maintainer_edit,omitempty"` | ||||
|   | ||||
| @@ -1775,6 +1775,7 @@ pulls.merge_pull_request = Create merge commit | ||||
| pulls.rebase_merge_pull_request = Rebase then fast-forward | ||||
| pulls.rebase_merge_commit_pull_request = Rebase then create merge commit | ||||
| pulls.squash_merge_pull_request = Create squash commit | ||||
| pulls.fast_forward_only_merge_pull_request = Fast-forward only | ||||
| pulls.merge_manually = Manually merged | ||||
| pulls.merge_commit_id = The merge commit ID | ||||
| pulls.require_signed_wont_sign = The branch requires signed commits but this merge will not be signed | ||||
|   | ||||
| @@ -885,6 +885,7 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error { | ||||
| 					AllowRebase:                   true, | ||||
| 					AllowRebaseMerge:              true, | ||||
| 					AllowSquash:                   true, | ||||
| 					AllowFastForwardOnly:          true, | ||||
| 					AllowManualMerge:              true, | ||||
| 					AutodetectManualMerge:         false, | ||||
| 					AllowRebaseUpdate:             true, | ||||
| @@ -911,6 +912,9 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error { | ||||
| 			if opts.AllowSquash != nil { | ||||
| 				config.AllowSquash = *opts.AllowSquash | ||||
| 			} | ||||
| 			if opts.AllowFastForwardOnly != nil { | ||||
| 				config.AllowFastForwardOnly = *opts.AllowFastForwardOnly | ||||
| 			} | ||||
| 			if opts.AllowManualMerge != nil { | ||||
| 				config.AllowManualMerge = *opts.AllowManualMerge | ||||
| 			} | ||||
|   | ||||
| @@ -35,6 +35,7 @@ func TestRepoEdit(t *testing.T) { | ||||
| 	allowRebase := false | ||||
| 	allowRebaseMerge := false | ||||
| 	allowSquashMerge := false | ||||
| 	allowFastForwardOnlyMerge := false | ||||
| 	archived := true | ||||
| 	opts := api.EditRepoOption{ | ||||
| 		Name:                      &ctx.Repo.Repository.Name, | ||||
| @@ -50,6 +51,7 @@ func TestRepoEdit(t *testing.T) { | ||||
| 		AllowRebase:               &allowRebase, | ||||
| 		AllowRebaseMerge:          &allowRebaseMerge, | ||||
| 		AllowSquash:               &allowSquashMerge, | ||||
| 		AllowFastForwardOnly:      &allowFastForwardOnlyMerge, | ||||
| 		Archived:                  &archived, | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -1862,6 +1862,8 @@ func ViewIssue(ctx *context.Context) { | ||||
| 				mergeStyle = repo_model.MergeStyleRebaseMerge | ||||
| 			} else if prConfig.AllowSquash { | ||||
| 				mergeStyle = repo_model.MergeStyleSquash | ||||
| 			} else if prConfig.AllowFastForwardOnly { | ||||
| 				mergeStyle = repo_model.MergeStyleFastForwardOnly | ||||
| 			} else if prConfig.AllowManualMerge { | ||||
| 				mergeStyle = repo_model.MergeStyleManuallyMerged | ||||
| 			} | ||||
|   | ||||
| @@ -576,6 +576,7 @@ func SettingsPost(ctx *context.Context) { | ||||
| 					AllowRebase:                   form.PullsAllowRebase, | ||||
| 					AllowRebaseMerge:              form.PullsAllowRebaseMerge, | ||||
| 					AllowSquash:                   form.PullsAllowSquash, | ||||
| 					AllowFastForwardOnly:          form.PullsAllowFastForwardOnly, | ||||
| 					AllowManualMerge:              form.PullsAllowManualMerge, | ||||
| 					AutodetectManualMerge:         form.EnableAutodetectManualMerge, | ||||
| 					AllowRebaseUpdate:             form.PullsAllowRebaseUpdate, | ||||
|   | ||||
| @@ -93,6 +93,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR | ||||
| 	allowRebase := false | ||||
| 	allowRebaseMerge := false | ||||
| 	allowSquash := false | ||||
| 	allowFastForwardOnly := false | ||||
| 	allowRebaseUpdate := false | ||||
| 	defaultDeleteBranchAfterMerge := false | ||||
| 	defaultMergeStyle := repo_model.MergeStyleMerge | ||||
| @@ -105,6 +106,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR | ||||
| 		allowRebase = config.AllowRebase | ||||
| 		allowRebaseMerge = config.AllowRebaseMerge | ||||
| 		allowSquash = config.AllowSquash | ||||
| 		allowFastForwardOnly = config.AllowFastForwardOnly | ||||
| 		allowRebaseUpdate = config.AllowRebaseUpdate | ||||
| 		defaultDeleteBranchAfterMerge = config.DefaultDeleteBranchAfterMerge | ||||
| 		defaultMergeStyle = config.GetDefaultMergeStyle() | ||||
| @@ -219,6 +221,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR | ||||
| 		AllowRebase:                   allowRebase, | ||||
| 		AllowRebaseMerge:              allowRebaseMerge, | ||||
| 		AllowSquash:                   allowSquash, | ||||
| 		AllowFastForwardOnly:          allowFastForwardOnly, | ||||
| 		AllowRebaseUpdate:             allowRebaseUpdate, | ||||
| 		DefaultDeleteBranchAfterMerge: defaultDeleteBranchAfterMerge, | ||||
| 		DefaultMergeStyle:             string(defaultMergeStyle), | ||||
|   | ||||
| @@ -151,6 +151,7 @@ type RepoSettingForm struct { | ||||
| 	PullsAllowRebase                      bool | ||||
| 	PullsAllowRebaseMerge                 bool | ||||
| 	PullsAllowSquash                      bool | ||||
| 	PullsAllowFastForwardOnly             bool | ||||
| 	PullsAllowManualMerge                 bool | ||||
| 	PullsDefaultMergeStyle                string | ||||
| 	EnableAutodetectManualMerge           bool | ||||
| @@ -598,8 +599,8 @@ func (f *InitializeLabelsForm) Validate(req *http.Request, errs binding.Errors) | ||||
| // swagger:model MergePullRequestOption | ||||
| type MergePullRequestForm struct { | ||||
| 	// required: true | ||||
| 	// enum: merge,rebase,rebase-merge,squash,manually-merged | ||||
| 	Do                     string `binding:"Required;In(merge,rebase,rebase-merge,squash,manually-merged)"` | ||||
| 	// enum: merge,rebase,rebase-merge,squash,fast-forward-only,manually-merged | ||||
| 	Do                     string `binding:"Required;In(merge,rebase,rebase-merge,squash,fast-forward-only,manually-merged)"` | ||||
| 	MergeTitleField        string | ||||
| 	MergeMessageField      string | ||||
| 	MergeCommitID          string // only used for manually-merged | ||||
|   | ||||
| @@ -267,6 +267,10 @@ func doMergeAndPush(ctx context.Context, pr *issues_model.PullRequest, doer *use | ||||
| 		if err := doMergeStyleSquash(mergeCtx, message); err != nil { | ||||
| 			return "", err | ||||
| 		} | ||||
| 	case repo_model.MergeStyleFastForwardOnly: | ||||
| 		if err := doMergeStyleFastForwardOnly(mergeCtx); err != nil { | ||||
| 			return "", err | ||||
| 		} | ||||
| 	default: | ||||
| 		return "", models.ErrInvalidMergeStyle{ID: pr.BaseRepo.ID, Style: mergeStyle} | ||||
| 	} | ||||
| @@ -377,6 +381,13 @@ func runMergeCommand(ctx *mergeContext, mergeStyle repo_model.MergeStyle, cmd *g | ||||
| 				StdErr: ctx.errbuf.String(), | ||||
| 				Err:    err, | ||||
| 			} | ||||
| 		} else if mergeStyle == repo_model.MergeStyleFastForwardOnly && strings.Contains(ctx.errbuf.String(), "Not possible to fast-forward, aborting") { | ||||
| 			log.Debug("MergeDivergingFastForwardOnly %-v: %v\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String()) | ||||
| 			return models.ErrMergeDivergingFastForwardOnly{ | ||||
| 				StdOut: ctx.outbuf.String(), | ||||
| 				StdErr: ctx.errbuf.String(), | ||||
| 				Err:    err, | ||||
| 			} | ||||
| 		} | ||||
| 		log.Error("git merge %-v: %v\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String()) | ||||
| 		return fmt.Errorf("git merge %v: %w\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String()) | ||||
|   | ||||
							
								
								
									
										21
									
								
								services/pull/merge_ff_only.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								services/pull/merge_ff_only.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| // Copyright 2023 The Gitea Authors. All rights reserved. | ||||
| // SPDX-License-Identifier: MIT | ||||
|  | ||||
| package pull | ||||
|  | ||||
| import ( | ||||
| 	repo_model "code.gitea.io/gitea/models/repo" | ||||
| 	"code.gitea.io/gitea/modules/git" | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| ) | ||||
|  | ||||
| // doMergeStyleFastForwardOnly merges the tracking into the current HEAD - which is assumed to be staging branch (equal to the pr.BaseBranch) | ||||
| func doMergeStyleFastForwardOnly(ctx *mergeContext) error { | ||||
| 	cmd := git.NewCommand(ctx, "merge", "--ff-only").AddDynamicArguments(trackingBranch) | ||||
| 	if err := runMergeCommand(ctx, repo_model.MergeStyleFastForwardOnly, cmd); err != nil { | ||||
| 		log.Error("%-v Unable to merge tracking into base: %v", ctx.pr, err) | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
| @@ -9,7 +9,7 @@ import ( | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| ) | ||||
|  | ||||
| // doMergeStyleMerge merges the tracking into the current HEAD - which is assumed to tbe staging branch (equal to the pr.BaseBranch) | ||||
| // doMergeStyleMerge merges the tracking branch into the current HEAD - which is assumed to be the staging branch (equal to the pr.BaseBranch) | ||||
| func doMergeStyleMerge(ctx *mergeContext, message string) error { | ||||
| 	cmd := git.NewCommand(ctx, "merge", "--no-ff", "--no-commit").AddDynamicArguments(trackingBranch) | ||||
| 	if err := runMergeCommand(ctx, repo_model.MergeStyleMerge, cmd); err != nil { | ||||
|   | ||||
| @@ -197,7 +197,7 @@ | ||||
| 				{{if .AllowMerge}} {{/* user is allowed to merge */}} | ||||
| 					{{$prUnit := .Repository.MustGetUnit $.Context $.UnitTypePullRequests}} | ||||
| 					{{$approvers := (.Issue.PullRequest.GetApprovers ctx)}} | ||||
| 					{{if or $prUnit.PullRequestsConfig.AllowMerge $prUnit.PullRequestsConfig.AllowRebase $prUnit.PullRequestsConfig.AllowRebaseMerge $prUnit.PullRequestsConfig.AllowSquash}} | ||||
| 					{{if or $prUnit.PullRequestsConfig.AllowMerge $prUnit.PullRequestsConfig.AllowRebase $prUnit.PullRequestsConfig.AllowRebaseMerge $prUnit.PullRequestsConfig.AllowSquash $prUnit.PullRequestsConfig.AllowFastForwardOnly}} | ||||
| 						{{$hasPendingPullRequestMergeTip := ""}} | ||||
| 						{{if .HasPendingPullRequestMerge}} | ||||
| 							{{$createdPRMergeStr := TimeSinceUnix .PendingPullRequestMerge.CreatedUnix ctx.Locale}} | ||||
| @@ -268,6 +268,13 @@ | ||||
| 									'mergeMessageFieldText': {{.GetCommitMessages}} + defaultSquashMergeMessage, | ||||
| 									'hideAutoMerge': generalHideAutoMerge, | ||||
| 								}, | ||||
| 								{ | ||||
| 									'name': 'fast-forward-only', | ||||
| 									'allowed': {{and $prUnit.PullRequestsConfig.AllowFastForwardOnly (eq .Issue.PullRequest.CommitsBehind 0)}}, | ||||
| 									'textDoMerge': {{ctx.Locale.Tr "repo.pulls.fast_forward_only_merge_pull_request"}}, | ||||
| 									'hideMergeMessageTexts': true, | ||||
| 									'hideAutoMerge': generalHideAutoMerge, | ||||
| 								}, | ||||
| 								{ | ||||
| 									'name': 'manually-merged', | ||||
| 									'allowed': {{$prUnit.PullRequestsConfig.AllowManualMerge}}, | ||||
|   | ||||
| @@ -35,6 +35,10 @@ | ||||
| 			<div>git checkout {{.PullRequest.BaseBranch}}</div> | ||||
| 			<div>git merge --squash {{$localBranch}}</div> | ||||
| 		</div> | ||||
| 		<div class="gt-hidden" data-pull-merge-style="fast-forward-only"> | ||||
| 			<div>git checkout {{.PullRequest.BaseBranch}}</div> | ||||
| 			<div>git merge --ff-only {{$localBranch}}</div> | ||||
| 		</div> | ||||
| 		<div class="gt-hidden" data-pull-merge-style="manually-merged"> | ||||
| 			<div>git checkout {{.PullRequest.BaseBranch}}</div> | ||||
| 			<div>git merge {{$localBranch}}</div> | ||||
|   | ||||
| @@ -528,6 +528,12 @@ | ||||
| 								<label>{{ctx.Locale.Tr "repo.pulls.squash_merge_pull_request"}}</label> | ||||
| 							</div> | ||||
| 						</div> | ||||
| 						<div class="field"> | ||||
| 							<div class="ui checkbox"> | ||||
| 								<input name="pulls_allow_fast_forward_only" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.AllowFastForwardOnly)}}checked{{end}}> | ||||
| 								<label>{{ctx.Locale.Tr "repo.pulls.fast_forward_only_merge_pull_request"}}</label> | ||||
| 							</div> | ||||
| 						</div> | ||||
| 						<div class="field"> | ||||
| 							<div class="ui checkbox"> | ||||
| 								<input name="pulls_allow_manual_merge" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.AllowManualMerge)}}checked{{end}}> | ||||
| @@ -545,6 +551,7 @@ | ||||
| 									<option value="rebase" {{if or (not $pullRequestEnabled) (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "rebase")}}selected{{end}}>{{ctx.Locale.Tr "repo.pulls.rebase_merge_pull_request"}}</option> | ||||
| 									<option value="rebase-merge" {{if or (not $pullRequestEnabled) (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "rebase-merge")}}selected{{end}}>{{ctx.Locale.Tr "repo.pulls.rebase_merge_commit_pull_request"}}</option> | ||||
| 									<option value="squash" {{if or (not $pullRequestEnabled) (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "squash")}}selected{{end}}>{{ctx.Locale.Tr "repo.pulls.squash_merge_pull_request"}}</option> | ||||
| 									<option value="fast-forward-only" {{if or (not $pullRequestEnabled) (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "fast-forward-only")}}selected{{end}}>{{ctx.Locale.Tr "repo.pulls.fast_forward_only_merge_pull_request"}}</option> | ||||
| 								</select>{{svg "octicon-triangle-down" 14 "dropdown icon"}} | ||||
| 								<div class="default text"> | ||||
| 									{{if (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "merge")}} | ||||
| @@ -559,12 +566,16 @@ | ||||
| 									{{if (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "squash")}} | ||||
| 										{{ctx.Locale.Tr "repo.pulls.squash_merge_pull_request"}} | ||||
| 									{{end}} | ||||
| 									{{if (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "fast-forward-only")}} | ||||
| 										{{ctx.Locale.Tr "repo.pulls.fast_forward_only_merge_pull_request"}} | ||||
| 									{{end}} | ||||
| 								</div> | ||||
| 								<div class="menu"> | ||||
| 									<div class="item" data-value="merge">{{ctx.Locale.Tr "repo.pulls.merge_pull_request"}}</div> | ||||
| 									<div class="item" data-value="rebase">{{ctx.Locale.Tr "repo.pulls.rebase_merge_pull_request"}}</div> | ||||
| 									<div class="item" data-value="rebase-merge">{{ctx.Locale.Tr "repo.pulls.rebase_merge_commit_pull_request"}}</div> | ||||
| 									<div class="item" data-value="squash">{{ctx.Locale.Tr "repo.pulls.squash_merge_pull_request"}}</div> | ||||
| 									<div class="item" data-value="fast-forward-only">{{ctx.Locale.Tr "repo.pulls.fast_forward_only_merge_pull_request"}}</div> | ||||
| 								</div> | ||||
| 							</div> | ||||
| 						</div> | ||||
|   | ||||
							
								
								
									
										12
									
								
								templates/swagger/v1_json.tmpl
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										12
									
								
								templates/swagger/v1_json.tmpl
									
									
									
										generated
									
									
									
								
							| @@ -19195,6 +19195,11 @@ | ||||
|       "description": "EditRepoOption options when editing a repository's properties", | ||||
|       "type": "object", | ||||
|       "properties": { | ||||
|         "allow_fast_forward_only_merge": { | ||||
|           "description": "either `true` to allow fast-forward-only merging pull requests, or `false` to prevent fast-forward-only merging.", | ||||
|           "type": "boolean", | ||||
|           "x-go-name": "AllowFastForwardOnly" | ||||
|         }, | ||||
|         "allow_manual_merge": { | ||||
|           "description": "either `true` to allow mark pr as merged manually, or `false` to prevent it.", | ||||
|           "type": "boolean", | ||||
| @@ -19251,7 +19256,7 @@ | ||||
|           "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\".", | ||||
|           "description": "set to a merge style to be used by this repository: \"merge\", \"rebase\", \"rebase-merge\", \"squash\", or \"fast-forward-only\".", | ||||
|           "type": "string", | ||||
|           "x-go-name": "DefaultMergeStyle" | ||||
|         }, | ||||
| @@ -20650,6 +20655,7 @@ | ||||
|             "rebase", | ||||
|             "rebase-merge", | ||||
|             "squash", | ||||
|             "fast-forward-only", | ||||
|             "manually-merged" | ||||
|           ] | ||||
|         }, | ||||
| @@ -22036,6 +22042,10 @@ | ||||
|       "description": "Repository represents a repository", | ||||
|       "type": "object", | ||||
|       "properties": { | ||||
|         "allow_fast_forward_only_merge": { | ||||
|           "type": "boolean", | ||||
|           "x-go-name": "AllowFastForwardOnly" | ||||
|         }, | ||||
|         "allow_merge_commits": { | ||||
|           "type": "boolean", | ||||
|           "x-go-name": "AllowMerge" | ||||
|   | ||||
| @@ -65,6 +65,7 @@ func getRepoEditOptionFromRepo(repo *repo_model.Repository) *api.EditRepoOption | ||||
| 	allowRebase := false | ||||
| 	allowRebaseMerge := false | ||||
| 	allowSquash := false | ||||
| 	allowFastForwardOnly := false | ||||
| 	if unit, err := repo.GetUnit(db.DefaultContext, unit_model.TypePullRequests); err == nil { | ||||
| 		config := unit.PullRequestsConfig() | ||||
| 		hasPullRequests = true | ||||
| @@ -73,6 +74,7 @@ func getRepoEditOptionFromRepo(repo *repo_model.Repository) *api.EditRepoOption | ||||
| 		allowRebase = config.AllowRebase | ||||
| 		allowRebaseMerge = config.AllowRebaseMerge | ||||
| 		allowSquash = config.AllowSquash | ||||
| 		allowFastForwardOnly = config.AllowFastForwardOnly | ||||
| 	} | ||||
| 	archived := repo.IsArchived | ||||
| 	return &api.EditRepoOption{ | ||||
| @@ -92,6 +94,7 @@ func getRepoEditOptionFromRepo(repo *repo_model.Repository) *api.EditRepoOption | ||||
| 		AllowRebase:               &allowRebase, | ||||
| 		AllowRebaseMerge:          &allowRebaseMerge, | ||||
| 		AllowSquash:               &allowSquash, | ||||
| 		AllowFastForwardOnly:      &allowFastForwardOnly, | ||||
| 		Archived:                  &archived, | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -365,6 +365,90 @@ func TestCantMergeUnrelated(t *testing.T) { | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func TestFastForwardOnlyMerge(t *testing.T) { | ||||
| 	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { | ||||
| 		session := loginUser(t, "user1") | ||||
| 		testRepoFork(t, session, "user2", "repo1", "user1", "repo1") | ||||
| 		testEditFileToNewBranch(t, session, "user1", "repo1", "master", "update", "README.md", "Hello, World 2\n") | ||||
|  | ||||
| 		// Use API to create a pr from update to master | ||||
| 		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) | ||||
| 		req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", "user1", "repo1"), &api.CreatePullRequestOption{ | ||||
| 			Head:  "update", | ||||
| 			Base:  "master", | ||||
| 			Title: "create a pr that can be fast-forward-only merged", | ||||
| 		}).AddTokenAuth(token) | ||||
| 		session.MakeRequest(t, req, http.StatusCreated) | ||||
|  | ||||
| 		user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ | ||||
| 			Name: "user1", | ||||
| 		}) | ||||
| 		repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ | ||||
| 			OwnerID: user1.ID, | ||||
| 			Name:    "repo1", | ||||
| 		}) | ||||
|  | ||||
| 		pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ | ||||
| 			HeadRepoID: repo1.ID, | ||||
| 			BaseRepoID: repo1.ID, | ||||
| 			HeadBranch: "update", | ||||
| 			BaseBranch: "master", | ||||
| 		}) | ||||
|  | ||||
| 		gitRepo, err := git.OpenRepository(git.DefaultContext, repo_model.RepoPath(user1.Name, repo1.Name)) | ||||
| 		assert.NoError(t, err) | ||||
|  | ||||
| 		err = pull.Merge(context.Background(), pr, user1, gitRepo, repo_model.MergeStyleFastForwardOnly, "", "FAST-FORWARD-ONLY", false) | ||||
|  | ||||
| 		assert.NoError(t, err) | ||||
|  | ||||
| 		gitRepo.Close() | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func TestCantFastForwardOnlyMergeDiverging(t *testing.T) { | ||||
| 	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { | ||||
| 		session := loginUser(t, "user1") | ||||
| 		testRepoFork(t, session, "user2", "repo1", "user1", "repo1") | ||||
| 		testEditFileToNewBranch(t, session, "user1", "repo1", "master", "diverging", "README.md", "Hello, World diverged\n") | ||||
| 		testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World 2\n") | ||||
|  | ||||
| 		// Use API to create a pr from diverging to update | ||||
| 		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) | ||||
| 		req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", "user1", "repo1"), &api.CreatePullRequestOption{ | ||||
| 			Head:  "diverging", | ||||
| 			Base:  "master", | ||||
| 			Title: "create a pr from a diverging branch", | ||||
| 		}).AddTokenAuth(token) | ||||
| 		session.MakeRequest(t, req, http.StatusCreated) | ||||
|  | ||||
| 		user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ | ||||
| 			Name: "user1", | ||||
| 		}) | ||||
| 		repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ | ||||
| 			OwnerID: user1.ID, | ||||
| 			Name:    "repo1", | ||||
| 		}) | ||||
|  | ||||
| 		pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ | ||||
| 			HeadRepoID: repo1.ID, | ||||
| 			BaseRepoID: repo1.ID, | ||||
| 			HeadBranch: "diverging", | ||||
| 			BaseBranch: "master", | ||||
| 		}) | ||||
|  | ||||
| 		gitRepo, err := git.OpenRepository(git.DefaultContext, repo_model.RepoPath(user1.Name, repo1.Name)) | ||||
| 		assert.NoError(t, err) | ||||
|  | ||||
| 		err = pull.Merge(context.Background(), pr, user1, gitRepo, repo_model.MergeStyleFastForwardOnly, "", "DIVERGING", false) | ||||
|  | ||||
| 		assert.Error(t, err, "Merge should return an error due to being for a diverging branch") | ||||
| 		assert.True(t, models.IsErrMergeDivergingFastForwardOnly(err), "Merge error is not a diverging fast-forward-only error") | ||||
|  | ||||
| 		gitRepo.Close() | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func TestConflictChecking(t *testing.T) { | ||||
| 	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { | ||||
| 		user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Chris Copeland
					Chris Copeland