`--no-ff` 标志对于 `git merge` 有什么影响?

1318
使用gitk log,我无法发现在使用git mergegit merge --no-ff之间的区别。如何通过git命令或一些工具观察到这种区别?

3
可能是[Git快进VS非快进合并]的重复问题(https://dev59.com/TWw15IYBdhLWcg3wT55c) - Avijit Gupta
9个回答

1403
--no-ff标记防止git merge执行“快进”操作,如果检测到你当前的HEAD是要合并的提交的祖先,则不会进行快进。快进是指,Git不构造合并提交,而只是将你的分支指针移动到指向传入的提交。这通常在没有任何本地更改的情况下执行git pull时发生。
然而,有时你想阻止此行为发生,通常是因为你想维护特定的分支拓扑结构(例如,你正在合并一个主题分支,并且想确保查看历史记录时它的样子)。为了做到这一点,你可以传递--no-ff标记,git merge将始终构建合并而不是快进。
类似地,如果你想执行git pull或使用git merge以明确进行快进,并且希望如果无法快进则退出,则可以使用--ff-only标记。这样,你就可以经常执行像git pull --ff-only这样的操作,而不必思考,如果出现错误,你可以回退并决定是否合并或变基。

112
为了更直接地回答提问者的问题:它们并不总是不同的,但如果它们不同,从 gitkgit log --graph 中可以清楚地看出快进合并没有创建合并提交,而非快进合并则有。 - Cascabel
21
在避免使用“ff”(fast forward)的原因上进行解释会更好:作者提到了“特定的分支拓扑结构”,这意味着在“--no-ff”的情况下,会多出一个合并提交来作为合并的标记。优点是明确的合并标记,包括作者和合并者的姓名。缺点是非线性历史记录,看起来像一组汇聚的铁路轨道。然而,合并的一个可能的心理副作用是贡献者由于较长的审核过程而失去兴趣。参考链接:http://blog.spreedly.com/2014/06/24/merge-pull-request-considered-harmful/#.Vf2oMyBVikp - Vlad
10
使用 --no-ff 从 feature 分支合并到 develop 或者从 develop 合并到 master,可以说与合并 pull request 类似,这样说是否公平? - Merlin -they-them-
14
@merlinpatt 好的。如果您在GitHub上合并拉取请求,它会执行等效于“--no-ff”的操作。 - Lily Ballard
2
这是一个很好的解释。天啊,对于人们理解为什么干净的git历史记录非常重要,就是没有足够好的git解释(包括我自己!)。 - dudewad
显示剩余3条评论

1315

回答该问题的图形化说明

这里是一个网站,清晰地解释和图示了如何使用git merge --no-ff

difference between git merge --no-ff and git merge

在看到这个之前,我完全摸不着头脑。使用--no-ff允许审核历史记录的人清晰地看到你所检出的分支进行工作。 (该链接指向 github 的“network”可视化工具) 这里还有另一个精彩的参考资料,配有插图。这份参考资料与第一份相互补充,更专注于那些对 git 不太熟悉的人。


新手的基本信息

如果你像我一样,不是 Git 大师,请参考我在这里的答案,描述了如何处理从 Git 跟踪中删除文件而不从本地文件系统中删除它们的情况,这似乎文档不太好但经常出现。另一个新手情况是获取当前代码,这仍然让我困惑。


示例工作流程

我更新了一个包到我的网站,不得不回到我的笔记看我的工作流;我认为将一个例子添加到这个答案中会很有用。

我的 git 命令工作流:

git checkout -b contact-form
(do your work on "contact-form")
git status
git commit -am  "updated form in contact module"
git checkout master
git merge --no-ff contact-form
git branch -d contact-form
git push origin master

下面是:实际使用情况,包括解释。
注意:下面的输出被截断了;git非常冗长。

$ git status
# On branch master
# Changed but not updated:
#   (use "git add/rm <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#       modified:   ecc/Desktop.php
#       modified:   ecc/Mobile.php
#       deleted:    ecc/ecc-config.php
#       modified:   ecc/readme.txt
#       modified:   ecc/test.php
#       deleted:    passthru-adapter.igs
#       deleted:    shop/mickey/index.php
#
# Untracked files:
#   (use "git add <file>..." to include in what will be committed)
#
#       ecc/upgrade.php
#       ecc/webgility-config.php
#       ecc/webgility-config.php.bak
#       ecc/webgility-magento.php

从上面可以注意到三件事:
1)在输出中,您可以看到ECC软件包升级的更改情况,包括添加了新文件。
2)还要注意有两个文件(不在/ecc文件夹中),我独立于此更改删除了这些文件。为了避免将那些文件删除与ecc混淆,我稍后会创建一个不同的cleanup分支以反映这些文件的删除情况。
3)我没有按照我的工作流程进行操作!当我尝试重新使ecc工作时,我忘记了git。

下面:与其像我通常会做的那样使用全包含的git commit -am“updated ecc package”,我只想添加/ecc文件夹中的文件。那些已经被跟踪在git中的已删除文件并不是我特别加入git add的一部分,但因为它们已经被跟踪,所以我需要将它们从这个分支的提交中移除:

$ git checkout -b ecc
$ git add ecc/*
$ git reset HEAD passthru-adapter.igs
$ git reset HEAD shop/mickey/index.php
Unstaged changes after reset:
M       passthru-adapter.igs
M       shop/mickey/index.php

$ git commit -m "Webgility ecc desktop connector files; integrates with Quickbooks"

$ git checkout master
D       passthru-adapter.igs
D       shop/mickey/index.php
Switched to branch 'master'
$ git merge --no-ff ecc
$ git branch -d ecc
Deleted branch ecc (was 98269a2).
$ git push origin master
Counting objects: 22, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (14/14), done.
Writing objects: 100% (14/14), 59.00 KiB, done.
Total 14 (delta 10), reused 0 (delta 0)
To git@github.com:me/mywebsite.git
   8a0d9ec..333eff5  master -> master

自动化以上流程的脚本

由于我一天要使用这个过程10次以上,所以我开始编写批处理脚本来执行命令,并制作了一个几乎正式的 git_update.sh <分支> <"提交信息"> 脚本以完成以上步骤。 这是该脚本的Gist源代码

与其使用 git commit -am 命令,我会从通过 git status 生成的 "修改" 列表中选择文件,然后将它们粘贴到这个脚本中。这是因为我做了几十个编辑,但想要不同的分支名称来帮助分组更改。


17
在使用--no-ff选项合并后,您仍然可以安全地删除分支吗? - Andy Fleming
25
@DesignerGuy 是的,你可以安全地删除旧分支。把分支视为指向特定提交的指针即可。 - Yvo
4
我发现链接页面上的这段文字很有帮助:如果不加上 "--no-ff",就无法从 Git 历史记录中看出哪些提交对象一起实现了一个功能 — 您将不得不手动阅读所有日志信息。 - Lorne Laliberte
1
一张图片胜过千言万语! - rupps
4
为什么第二张图片中的图形没有显示出features branch,它还存在,对吗? - ADJenks
显示剩余2条评论

626

合并策略

显式合并(又称为非快进合并):创建一个新的合并提交。(如果你使用了 --no-ff,就会得到这个结果。)

enter image description here

快速前进合并:快速前进,而不创建新的提交:

enter image description here

重新基准化:建立一个新的基准水平。

enter image description here

注意:Rebase合并与快进合并相似,但与快进合并不同的是,分支的HEAD必须是我们要合并的提交的祖先。

压缩:以力量将(某物)压扁或挤压使其变平:

enter image description here


17
那么,Rebase和fast-forward合并有什么区别呢? - ThanosFisherman
5
对于快进合并(ff merge),当前分支的 HEAD 必须是你想要合并的提交的祖先。就像 Lily Ballard 在上面的回答中所说。@ThanosFisherman - lrxw
3
@ThanosFisherman,Fast-Forward保持相同的基础,并尝试将所有文件更改从共享基础后的相同更改合并。重新定位(rebase)“分离”您的分支,然后将提交一个接一个地应用于基础之上(就像您刚刚检出了一个新的主分支),然后尝试应用对新基础所做的更改。通常在您想要合并到主分支之前,会使用重新定位,以便它将主题提交应用于主分支之上,而不会出现合并冲突。 - MahNas92
你第一张图片中的“合并提交”是什么?两个红色的“feature”提交去哪了?你的“合并提交”看起来像是压缩提交,而不是合并提交。 - Green
@Green 我理解的是,在 squash 的情况下,通过合并提交,我们无法看到功能分支的历史记录,但是通过 no-ff 可以。https://stackoverflow.com/a/56583703/8762338 - Astha Garg

257
--no-ff 选项确保不会进行快速向前合并,而且始终会创建新的提交对象。如果您希望 git 维护功能分支的历史记录,则这是有利的。             git merge --no-ff vs git merge 在上图中,左侧是使用 git merge --no-ff 后的 git 历史记录示例,右侧是使用 git merge 的示例,其中可以进行 ff 合并。 编辑:此前版本的图片只显示合并提交的单个父提交。合并提交具有多个父提交,git 使用它们来维护 "功能分支" 和原始分支的历史记录。多个父链接以绿色突出显示。

3
绿色箭头和黑色箭头分别表示什么? - rtconner
12
绿色箭头表示提交记录与父提交记录之间的链接,其中提交记录具有多个父提交记录。普通的提交记录(黑色)只有一个父提交记录。 - Daniel Smith
10
@DanielSmith,你是怎么画这个优雅的图表的? - chancyWu
4
好的,我会尽力进行翻译并保持原意不变。以下是需要翻译的内容:@chancyWu with Adobe Illustrator - Daniel Smith
2
是的,在这两种情况下它仍然存在,并且在git merge之后,它与主分支完全相同,因为所有提交都已快进到主分支。在git日志中,您会看到feature和master指向相同的提交。在git merge --no-ff的情况下,feature分支将缺少最后一个合并提交。 - cb2
显示剩余5条评论

41

这是一个老问题,其他帖子中可能会略微提及,但使我恍然大悟的解释是非快进合并需要一个单独的提交


请您能否查看我上面的回答中的工作流程:这是否意味着我需要除了现在已有的提交之外的额外提交?谢谢。 - Krista K
3
如果你运行命令 git merge --no-ff ecc,则在主分支的 git log 中将出现一个额外的合并提交。如果主分支指向了提交ecc所在的直接祖先,则从技术上讲这是不必要的,但通过指定--no-ff选项,您正在强制创建该合并提交。它的标题将为:Merge branch 'ecc' - Ankur Agarwal
很棒的总结! :) - Eduard

12

--no-ff标志意味着即使可以使用快进模式合并,合并操作始终会创建一个新的提交对象。这样可以避免丢失有关功能分支历史存在的信息,并将一起添加功能的所有提交组合在一起。


10

其他回答已经很好地说明了--no-ff会产生一个合并提交。这将保留有关功能分支的历史信息,因为功能分支经常被清理和删除。

这个答案可能提供了何时使用或不使用--no-ff的上下文。

从功能分支合并到主分支:使用--no-ff

示例:

$ git checkout -b NewFeature
[work...work...work]
$ git commit -am  "New feature complete!"
$ git checkout main
$ git merge --no-ff NewFeature
$ git push origin main
$ git branch -d NewFeature

将主分支的更改合并到特性分支:不要使用--no-ff

示例:

$ git checkout -b NewFeature
[work...work...work]
[New changes made for HotFix in the main branch! Lets get them...]
$ git commit -am  "New feature in progress"
$ git pull origin main
[shortcut for "git fetch origin main", "git merge origin main"]

2

什么是快进(fast-forward)?

当您对比所选分支之前的分支进行合并或变基时,Git 所执行的操作就是快进。

假设有如下分支设置:

您拥有两个分支都引用同一次提交。它们都具有完全相同的历史记录。现在在feature 分支上进行了提交。

在此时,master 分支仍然引用 7ddac6c,而 feature 分支已经向前移动了两个提交。可以认为 feature 分支现在领先于 master 分支。

现在很容易看出当 Git 进行快进时会发生什么。它只需将 master 分支更新为引用与 feature 分支相同的精确提交即可。由于来自 feature 的提交已包含所有必要的更改,因此不会对存储库本身进行任何更改。

此时您的存储库历史记录应如下所示:

什么情况下不会发生快进?

当原始分支和新分支中都进行了更改时,就不会发生快进。

如果将 feature 分支合并或变基到 master 上,由于树已经分叉,Git 将无法进行快进。考虑到 Git 提交是不可变的,因此 Git 无法在不更改其父引用的情况下将 feature 分支提交合并到 master 上。


-1

Git合并和Git合并--no-ff:面对面比较

enter image description here


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接