aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChris Copeland <chris@chrisnc.net>2024-02-12 14:37:23 -0800
committerGitHub <noreply@github.com>2024-02-12 23:37:23 +0100
commit47b59658629f47e0ac559559a305b867740cae9c (patch)
tree5bc0102a155565f1b813609082d9ae23712e0d65
parentf9c3459831659d37fd885dd1a9db32dcf19420e4 (diff)
downloadgitea-47b59658629f47e0ac559559a305b867740cae9c.tar.gz
gitea-47b59658629f47e0ac559559a305b867740cae9c.zip
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
-rw-r--r--custom/conf/app.example.ini2
-rw-r--r--docs/content/administration/config-cheat-sheet.en-us.md2
-rw-r--r--docs/content/administration/config-cheat-sheet.zh-cn.md2
-rw-r--r--models/error.go17
-rw-r--r--models/repo/git.go2
-rw-r--r--models/repo/repo_unit.go2
-rw-r--r--modules/git/error.go2
-rw-r--r--modules/repository/create.go6
-rw-r--r--modules/structs/repo.go5
-rw-r--r--options/locale/locale_en-US.ini1
-rw-r--r--routers/api/v1/repo/repo.go4
-rw-r--r--routers/api/v1/repo/repo_test.go2
-rw-r--r--routers/web/repo/issue.go2
-rw-r--r--routers/web/repo/setting/setting.go1
-rw-r--r--services/convert/repository.go3
-rw-r--r--services/forms/repo_form.go5
-rw-r--r--services/pull/merge.go11
-rw-r--r--services/pull/merge_ff_only.go21
-rw-r--r--services/pull/merge_merge.go2
-rw-r--r--templates/repo/issue/view_content/pull.tmpl9
-rw-r--r--templates/repo/issue/view_content/pull_merge_instruction.tmpl4
-rw-r--r--templates/repo/settings/options.tmpl11
-rw-r--r--templates/swagger/v1_json.tmpl12
-rw-r--r--tests/integration/api_repo_edit_test.go3
-rw-r--r--tests/integration/pull_merge_test.go84
25 files changed, 204 insertions, 11 deletions
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}}
@@ -269,6 +269,13 @@
'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}},
'textDoMerge': {{ctx.Locale.Tr "repo.pulls.merge_manually"}},
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 @@
<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>
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
@@ -530,6 +530,12 @@
</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}}>
<label>{{ctx.Locale.Tr "repo.pulls.merge_manually"}}</label>
</div>
@@ -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>
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})