From 47b59658629f47e0ac559559a305b867740cae9c Mon Sep 17 00:00:00 2001 From: Chris Copeland Date: Mon, 12 Feb 2024 14:37:23 -0800 Subject: [PATCH] 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 --- custom/conf/app.example.ini | 2 +- .../config-cheat-sheet.en-us.md | 2 +- .../config-cheat-sheet.zh-cn.md | 2 +- models/error.go | 17 ++++ models/repo/git.go | 2 + models/repo/repo_unit.go | 2 + modules/git/error.go | 2 +- modules/repository/create.go | 6 +- modules/structs/repo.go | 5 +- options/locale/locale_en-US.ini | 1 + routers/api/v1/repo/repo.go | 4 + routers/api/v1/repo/repo_test.go | 2 + routers/web/repo/issue.go | 2 + routers/web/repo/setting/setting.go | 1 + services/convert/repository.go | 3 + services/forms/repo_form.go | 5 +- services/pull/merge.go | 11 +++ services/pull/merge_ff_only.go | 21 +++++ services/pull/merge_merge.go | 2 +- templates/repo/issue/view_content/pull.tmpl | 9 +- .../view_content/pull_merge_instruction.tmpl | 4 + templates/repo/settings/options.tmpl | 11 +++ templates/swagger/v1_json.tmpl | 12 ++- tests/integration/api_repo_edit_test.go | 3 + tests/integration/pull_merge_test.go | 84 +++++++++++++++++++ 25 files changed, 204 insertions(+), 11 deletions(-) create mode 100644 services/pull/merge_ff_only.go diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 363bbcb151..4aae1c497f 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -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 diff --git a/docs/content/administration/config-cheat-sheet.en-us.md b/docs/content/administration/config-cheat-sheet.en-us.md index 33732d080b..415176d4ff 100644 --- a/docs/content/administration/config-cheat-sheet.en-us.md +++ b/docs/content/administration/config-cheat-sheet.en-us.md @@ -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 diff --git a/docs/content/administration/config-cheat-sheet.zh-cn.md b/docs/content/administration/config-cheat-sheet.zh-cn.md index 8236852ad3..01906930cb 100644 --- a/docs/content/administration/config-cheat-sheet.zh-cn.md +++ b/docs/content/administration/config-cheat-sheet.zh-cn.md @@ -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`,否则仅使用限定列表中的作者。 diff --git a/models/error.go b/models/error.go index 83dfe29805..75c53245de 100644 --- a/models/error.go +++ b/models/error.go @@ -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 diff --git a/models/repo/git.go b/models/repo/git.go index 610c554296..388bf86522 100644 --- a/models/repo/git.go +++ b/models/repo/git.go @@ -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 diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go index 8a3ba1ee89..31a2a2e248 100644 --- a/models/repo/repo_unit.go +++ b/models/repo/repo_unit.go @@ -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 } diff --git a/modules/git/error.go b/modules/git/error.go index dc10d451b3..91d25eca69 100644 --- a/modules/git/error.go +++ b/modules/git/error.go @@ -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 diff --git a/modules/repository/create.go b/modules/repository/create.go index 7c954a1412..ca2150b972 100644 --- a/modules/repository/create.go +++ b/modules/repository/create.go @@ -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{ diff --git a/modules/structs/repo.go b/modules/structs/repo.go index 51e175fba8..56d6158bd8 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -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"` diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 9af4d70171..96345f51f8 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -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 diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index d1b2c99d0c..40de8853d8 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -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 } diff --git a/routers/api/v1/repo/repo_test.go b/routers/api/v1/repo/repo_test.go index 29e2d1f21d..08ba7fabac 100644 --- a/routers/api/v1/repo/repo_test.go +++ b/routers/api/v1/repo/repo_test.go @@ -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, } diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index aa0cad98b7..a85f6e7666 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -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 } diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go index 8c1daf52bc..3b11638a92 100644 --- a/routers/web/repo/setting/setting.go +++ b/routers/web/repo/setting/setting.go @@ -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, diff --git a/services/convert/repository.go b/services/convert/repository.go index c16180c0af..9184bc05c7 100644 --- a/services/convert/repository.go +++ b/services/convert/repository.go @@ -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), diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 845eccf817..60fa0ab363 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -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 diff --git a/services/pull/merge.go b/services/pull/merge.go index 63f0268beb..d4c0c821d6 100644 --- a/services/pull/merge.go +++ b/services/pull/merge.go @@ -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()) diff --git a/services/pull/merge_ff_only.go b/services/pull/merge_ff_only.go new file mode 100644 index 0000000000..f57c732104 --- /dev/null +++ b/services/pull/merge_ff_only.go @@ -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 +} diff --git a/services/pull/merge_merge.go b/services/pull/merge_merge.go index 0f7664297a..bf56c071db 100644 --- a/services/pull/merge_merge.go +++ b/services/pull/merge_merge.go @@ -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 { diff --git a/templates/repo/issue/view_content/pull.tmpl b/templates/repo/issue/view_content/pull.tmpl index 2b5776ea03..f1ab53eb67 100644 --- a/templates/repo/issue/view_content/pull.tmpl +++ b/templates/repo/issue/view_content/pull.tmpl @@ -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}}, diff --git a/templates/repo/issue/view_content/pull_merge_instruction.tmpl b/templates/repo/issue/view_content/pull_merge_instruction.tmpl index 3dab44710e..a214f29786 100644 --- a/templates/repo/issue/view_content/pull_merge_instruction.tmpl +++ b/templates/repo/issue/view_content/pull_merge_instruction.tmpl @@ -35,6 +35,10 @@
git checkout {{.PullRequest.BaseBranch}}
git merge --squash {{$localBranch}}
+
+
git checkout {{.PullRequest.BaseBranch}}
+
git merge --ff-only {{$localBranch}}
+
git checkout {{.PullRequest.BaseBranch}}
git merge {{$localBranch}}
diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl index dfb909e743..f7f448fdf2 100644 --- a/templates/repo/settings/options.tmpl +++ b/templates/repo/settings/options.tmpl @@ -528,6 +528,12 @@
+
+
+ + +
+
@@ -545,6 +551,7 @@ + {{svg "octicon-triangle-down" 14 "dropdown icon"}}
{{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}}
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 403f241d72..a881afaf0e 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -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" diff --git a/tests/integration/api_repo_edit_test.go b/tests/integration/api_repo_edit_test.go index c4fc2177b4..7de8910ee0 100644 --- a/tests/integration/api_repo_edit_test.go +++ b/tests/integration/api_repo_edit_test.go @@ -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, } } diff --git a/tests/integration/pull_merge_test.go b/tests/integration/pull_merge_test.go index fcd7fecd52..5205df2f8e 100644 --- a/tests/integration/pull_merge_test.go +++ b/tests/integration/pull_merge_test.go @@ -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})