Git工作流程和rebase与merge的问题

1042
我已经和另一个开发者在一个项目上使用Git几个月了。我有数年的SVN经验,所以我想我对这种关系带来了很多包袱。
我听说Git在分支和合并方面非常出色,但到目前为止,我并没有看到它的优势。当然,分支非常简单,但是当我尝试合并时,一切都变得一团糟。现在,我从SVN中习惯了这种情况,但我觉得我只是把一个次优的版本控制系统换成了另一个。
我的合作伙伴告诉我,我的问题源于我想随意合并,而且在许多情况下应该使用rebase而不是merge。例如,他制定了以下工作流程:
clone the remote repository
git checkout -b my_new_feature
..work and commit some stuff
git rebase master
..work and commit some stuff
git rebase master
..finish the feature
git checkout master
git merge my_new_feature

基本上,创建一个功能分支,始终从主分支到分支进行变基,然后从分支合并回主分支。重要的是要注意分支始终保留在本地。
这是我开始使用的工作流程。
clone remote repository
create my_new_feature branch on remote repository
git checkout -b --track my_new_feature origin/my_new_feature
..work, commit, push to origin/my_new_feature
git merge master (to get some changes that my partner added)
..work, commit, push to origin/my_new_feature
git merge master
..finish my_new_feature, push to origin/my_new_feature
git checkout master
git merge my_new_feature
delete remote branch
delete local branch

我认为有两个关键的区别:我总是使用合并而不是变基,并且将我的特性分支(以及我的特性分支提交)推送到远程仓库。
我选择远程分支的原因是我希望在工作时备份我的工作。我们的存储库会自动备份,并且如果出现问题,可以进行恢复。我的笔记本电脑没有这样彻底的备份。因此,我不喜欢在我的笔记本电脑上有未在其他地方镜像的代码。
我选择合并而不是变基的原因是合并似乎是标准操作,而变基似乎是高级功能。我的直觉告诉我,我尝试做的事情不是高级设置,因此不需要变基。我甚至研究了 Git 的新编程书籍,它详细介绍了合并,几乎没有提到变基。
无论如何,我最近在遵循我的工作流程时遇到了问题,当我尝试将其合并回主分支时,一切都变得混乱。有大量与不应该有影响的事情发生冲突。这些冲突对我来说毫无意义。我花了一天时间解决所有问题,并最终强制将其推送到远程主分支,因为我的本地主分支已经解决了所有冲突,但远程主分支仍然不满意。
在这种情况下,“正确”的工作流程是什么?Git 应该使分支和合并变得超级简单,但我却看不到它。
clone the remote repository
git checkout -b my_new_feature
..work and commit some stuff
git rebase master
..work and commit some stuff
git rebase master
..finish the feature, commit
git rebase master
git checkout master
git merge my_new_feature

实际上,我们的工作流略有不同,因为我们倾向于使用“压缩合并”而不是原始合并。(注意:这是有争议的,请参见下文。)这使我们能够将整个特性分支转换为主分支上的单个提交。然后我们删除我们的特性分支。即使在我们的分支上有些混乱,这也使我们能够在主分支上逻辑地构建我们的提交。所以,这就是我们做的:
clone the remote repository
git checkout -b my_new_feature
..work and commit some stuff
git rebase master
..work and commit some stuff
git rebase master
..finish the feature, commit
git rebase master
git checkout master
git merge --squash my_new_feature
git commit -m "added my_new_feature"
git branch -D my_new_feature
压缩合并争议 - 正如几位评论者所指出的那样,压缩合并将会丢弃您的特性分支上的所有历史记录。顾名思义,它将所有提交压缩成一个单一的提交。对于小型特性而言,这是有意义的,因为它将其压缩成一个单独的包。对于大型特性而言,这可能不是一个好主意,尤其是如果您的单个提交已经是原子提交。这真的取决于个人偏好。 Github 和 Bitbucket(其他?)拉取请求 - 如果您想知道合并/变基与拉取请求的关系,我建议您按照以上所有步骤操作,直到准备合并回主分支。您只需接受 PR,而不是手动使用 git 合并。请注意,这不会进行压缩合并(至少不是默认情况下),但非压缩、非快进式是拉取请求社区中公认的合并约定(据我所知)。具体而言,它的工作方式如下:
clone the remote repository
git checkout -b my_new_feature
..work and commit some stuff
git rebase master
..work and commit some stuff
git rebase master
..finish the feature, commit
git rebase master
git push # May need to force push
...submit PR, wait for a review, make any changes requested for the PR
git rebase master
git push # Will probably need to force push (-f), due to previous rebases from master
...accept the PR, most likely also deleting the feature branch in the process
git checkout master
git branch -d my_new_feature
git remote prune origin

我已经爱上了Git,再也不想回到SVN。如果你正在挣扎,请坚持下去,最终你会看到隧道尽头的光明。


34
很遗憾,新的《Pragmstic编程》书籍大多数是在使用Git的同时仍然思考SVN,这种情况会误导您。在Git中,当事情可以简化时,rebase能够保持简单。您的经验可能告诉您,您的工作流在Git中不起作用,而不是说Git本身不起作用。 - Paul
18
在这种情况下,我不建议使用“挤压式合并”,因为它不会保存有关合并内容的任何信息(就像 svn 一样,但没有合并信息)。 - Marius K
7
底部的注释很棒,我也有过类似的Git使用困难经历,但现在却无法想象不使用它会怎样。感谢最后的解释,对理解rebase有很大帮助。 - Jon Phenow
6
在你完成功能后,应该在将新功能合并到主分支之前进行最后一次变基操作吗? - softarn
17
您的工作流将丢失已删除分支的所有提交历史记录 :( - Max Nanasy
显示剩余8条评论
11个回答

438

简述

使用git rebase工作流程并不能保护你免受那些不擅长冲突解决或者习惯于SVN工作流的人的影响,如避免Git灾难:一个恐怖的故事中所建议的。相反,它只会使那些人的冲突解决更加繁琐,并且使从糟糕的冲突解决中恢复更加困难。因此,最好使用diff3,这样一开始就不会那么困难。


Rebase工作流程并不适合冲突解决!

我非常支持使用rebase来清理历史记录。然而,如果我遇到了冲突,我会立即放弃rebase并改为使用merge!真的很让我感到失望的是,有人推荐使用rebase工作流程作为比merge工作流程更好的冲突解决方案(这正是这个问题所涉及的内容)。

如果在merge期间出现“大混乱”,那么在rebase期间也会出现“大混乱”,并且可能会更加混乱!以下是原因:

原因1:仅需解决一次冲突,而不是每个提交都要解决

当你使用rebase而不是merge时,你将不得不为每个提交解决冲突,直到出现与相同冲突!

真实场景

我从主分支中分支出来,在一个分支中重构了一个复杂的方法。我的重构工作总共包括15个提交,因为我需要重构并进行代码审查。我的重构工作包括修复主分支中存在的混合制表符和空格。这是必要的,但不幸的是,它会与主分支中此方法之后进行的任何更改发生冲突。果然,在我正在处理这个方法时,有人在主分支中对同一个方法进行了简单而合法的更改,应该与我的更改合并。

当我需要将我的分支与主分支合并时,我有两个选择:

git merge:我遇到了冲突。我看到他们对主分支所做的更改,并将其与(最终产品的)我的分支合并。完成。

git rebase: 我的第一个提交出现了冲突。我解决了冲突并继续了rebase。我的第二个提交出现了冲突。我解决了冲突并继续了rebase。我的第三个提交出现了冲突。我解决了冲突并继续了rebase。我的第四个提交出现了冲突。我解决了冲突并继续了rebase。我的第五个提交出现了冲突。我解决了冲突并继续了rebase。我的第六个提交出现了冲突。我解决了冲突并继续了rebase。我的第七个提交出现了冲突。我解决了冲突并继续了rebase。我的第八个提交出现了冲突。我解决了冲突并继续了rebase。我的第九个提交出现了冲突。我解决了冲突并继续了rebase。我的第十个提交出现了冲突。我解决了冲突并继续了rebase。我的第十一个提交出现了冲突。我解决了冲突并继续了rebase。我的第十二个提交出现了冲突。我解决了冲突并继续了rebase。我的第十三个提交出现了冲突。我解决了冲突并继续了rebase。我的第十四个提交出现了冲突。我解决了冲突并继续了rebase。我的第十五个提交出现了冲突。我解决了冲突并继续了rebase。
如果是你的首选工作流程,那你一定是在开玩笑吧。只要有一个空格修复与主分支上的一个更改发生冲突,每个提交都会冲突并必须解决。而且这只是一个涉及空格冲突的简单场景。如果涉及跨文件的重大代码更改并需要多次解决,那就更加麻烦了。这增加了出错的可能性。 使用rebase需要进行额外的冲突解决,这增加了出错的可能性。但在git中犯错误是可以撤销的,对吗?除非...。

原因 #2:使用rebase时,没有撤销!

冲突解决可能很困难,而且有些人非常不擅长。容易出现错误,这就是为什么git让撤销变得如此简单的原因!
当你合并一个分支时,Git会创建一个合并提交(merge commit)。如果冲突解决进行得不好,该提交可以舍弃或修改。即使你已经将不良合并提交推送到公共/权威仓库,你也可以使用“git revert”撤销合并引入的更改,并在新的合并提交中正确执行合并。
当你变基(rebase)一个分支时,很可能冲突解决出现问题,此时你就被卡住了。每个提交都包含了错误的合并内容,你不能仅仅重新进行变基。最好的情况是,你必须返回并修改受影响的每个提交。这不好玩。在变基之后,无法确定哪些内容是原始提交的一部分,哪些是由于错误的冲突解决而引入的。
从 Git 的内部日志中挖掘旧的引用,或者创建指向变基前最后一个提交的第三个分支,可能可以撤销变基。
摆脱冲突解决的地狱:使用 diff3 以这个冲突为例:
<<<<<<< HEAD
TextMessage.send(:include_timestamp => true)
=======
EmailMessage.send(:include_timestamp => false)
>>>>>>> feature-branch

看到冲突时,很难分辨每个分支所做的更改以及其意图。在我看来,这是冲突解决变得混乱和困难的最大原因。

diff3来拯救!

git config --global merge.conflictstyle diff3

当您使用diff3时,每个新的冲突都会有一个第三部分,即合并的共同祖先。
<<<<<<< HEAD
TextMessage.send(:include_timestamp => true)
||||||| merged common ancestor
EmailMessage.send(:include_timestamp => true)
=======
EmailMessage.send(:include_timestamp => false)
>>>>>>> feature-branch

首先检查合并的共同祖先。然后比较每一侧以确定每个分支的意图。您可以看到HEAD将EmailMessage更改为TextMessage。它的意图是更改用于TextMessage的类,并传递相同的参数。您还可以看到feature-branch的意图是将 :include_timestamp 选项设置为 false 而不是 true。要合并这些更改,请结合两者的意图:

TextMessage.send(:include_timestamp => false)

一般来说:

  1. 将公共祖先与每个分支进行比较,并确定哪个分支有最简单的更改
  2. 将这个简单的更改应用到另一个分支的代码版本中,以便它包含更简单和更复杂的更改
  3. 删除除了你刚刚合并更改的那个冲突代码部分之外的所有冲突代码部分

备选方案:手动应用分支的更改来解决冲突

最后,有些冲突即使使用diff3也很难理解。特别是当diff找到不语义相同的常见行时(例如,两个分支恰好在相同的位置具有空行!)。例如,一个分支更改类的正文缩进或重新排序相似的方法。在这些情况下,更好的解决策略可能是从合并的任一侧检查更改,并手动将diff应用于另一个文件。

让我们看看如何解决冲突,在合并origin/feature1时,lib/message.rb发生冲突的情况。

  1. Decide whether our currently checked out branch (HEAD, or --ours) or the branch we're merging (origin/feature1, or --theirs) is a simpler change to apply. Using diff with triple dot (git diff a...b) shows the changes that happened on b since its last divergence from a, or in other words, compare the common ancestor of a and b with b.

    git diff HEAD...origin/feature1 -- lib/message.rb # show the change in feature1
    git diff origin/feature1...HEAD -- lib/message.rb # show the change in our branch
    
  2. Check out the more complicated version of the file. This will remove all conflict markers and use the side you choose.

    git checkout --ours -- lib/message.rb   # if our branch's change is more complicated
    git checkout --theirs -- lib/message.rb # if origin/feature1's change is more complicated
    
  3. With the complicated change checked out, pull up the diff of the simpler change (see step 1). Apply each change from this diff to the conflicting file.


4
一次性解决所有冲突比单独提交更好的原因是什么?我已经在合并单个提交时遇到了问题(尤其是当人们没有将提交分为逻辑部分并提供足够的测试以进行验证时)。此外,在备份选项方面,rebase 与 merge 并没有什么区别,智能使用交互式 rebase 和像 tortoisegit 这样的工具(允许选择包括哪些提交)会大有帮助。 - prusswan
9
我觉得我在 #1 中已经解释了原因。如果单个提交不具有逻辑一致性,更应该合并逻辑一致的分支,这样你才能真正理解冲突。如果提交 1 有 bug,而提交 2 可以修复它,合并提交 1 将会很混乱。像我上面概述的那样,有合理的理由可能会连续出现15个冲突。此外,你认为 rebase 不会更糟糕的论点有些站不住脚。Rebase 会将错误的合并混入原本良好的提交中,不会留下良好提交让你重新尝试,但合并可以。 - Edward Anderson
6
我完全同意你的观点,nilbus。很棒的帖子;这澄清了一些事情。不过我不知道rerere是否会对此有所帮助。另外,感谢你关于使用diff3的建议,我现在肯定会切换它的。 - derick
55
谢谢你告诉我 diff3 命令,这让我很受用。以前不理解的冲突现在变得清晰了,我感激不已。我会好好利用这个命令。 - John
4
这应该是被接受的答案。重新基础流程也很糟糕,因为它隐藏了代码库在某个时间点上有巨大分歧的事实,如果你想理解你正在查看的代码是如何编写的,这可能是有用的。只有那些不冲突的小分支才应该被合并到主分支。 - Robert Rüger
显示剩余27条评论

387

"冲突"指的是“同一内容的平行演进”。如果在合并过程中出现问题,那么就意味着你对同一组文件进行了大量的演进。

之所以重新设置比合并更好,是因为:

  • 您将使用主分支的提交历史重写本地提交历史(然后重新应用您的工作,解决任何冲突)
  • 最终合并肯定会是“快进”合并,因为它将拥有主分支的所有提交历史,加上您要重新应用的修改。

我确认,在这种情况下(对共同文件集的演变),正确的工作流程是先重新设置,然后再合并

然而,这意味着如果您推送本地分支(出于备份原因),其他人不应该拉取(或至少不应该使用)该分支(因为提交历史将被重写)。


关于这个话题(重新设置然后合并的工作流程),barraponto 在评论中提到了两篇有趣的文章,均来自 randyfay.com:

使用这种技术,您的工作始终位于公共分支的顶部,就像一个与当前 HEAD 同步的补丁。

(类似的技术也适用于bazaar


27
如果你想要一种既可以进行 rebase 又可以进行分享的技术,请参阅 http://softwareswirl.blogspot.com/2009/04/truce-in-merge-vs-rebase-war.html。 - mhagger
2
http://www.randyfay.com/node/91 和 http://www.randyfay.com/node/89 是非常棒的阅读材料。这些文章让我明白了我的工作流程存在哪些问题,以及理想的工作流程应该是什么样子的。 - Capi Etheriel
只是为了明确,从主分支重新定位到您的本地基本上是更新您的本地可能错过的任何历史记录,而主分支在合并后具有知识? - hellatan
@dtan 这里我描述的是将本地分支在主分支之上进行变基。你并不是直接更新本地历史记录,而是重新应用本地历史记录在主分支之上,以解决本地分支内的任何冲突。 - VonC

35
在我的工作流中,我尽可能地使用变基(并且我经常试着这样做)。不让差异累积会大大减少分支之间碰撞的数量和严重程度。即使是在主要采用变基的工作流中,合并也有其存在的意义。
请记住,合并实际上创建了一个具有两个父节点的节点。现在考虑以下情况:我有两个独立的特性分支A和B,并且现在想要在依赖于A和B的特性分支C上开发东西,同时A和B正在接受审查。
然后我做如下操作:
1. 在A的基础上创建(并检出)分支C。 2. 将C与B合并。 现在分支C包含来自A和B的更改,我可以继续在其上进行开发。如果我对A进行任何更改,那么我可以按以下方式重构分支图:
1. 在新的A顶部创建T分支。 2. 将T与B合并。 3. 变基C到T。 4. 删除T分支。
这样,我实际上可以维护任意的分支图。但是进行比上述情况更复杂的操作就太复杂了,因为没有自动工具可以在父节点更改时执行变基。

1
你可以只使用rebase来实现相同的效果。在这里,合并实际上是不必要的(除非你不想复制提交 - 但我几乎看不出这是一个论点)。 - odwl
1
实际上我并不想复制提交记录。我希望保持我的工作进展状态尽可能清晰。但这是个人口味问题,并不适合每个人。 - Alex Gontmakher
我完全同意第一段。 (@Edward的答案适用于不是这种情况的情况,但我宁愿让世界上所有项目都像你建议的那样工作)。答案的其余部分似乎有点牵强附会,因为在A和B正在进行的过程中处理C已经有点冒险了(至少在很大程度上真正取决于A和B),即使最终您也可能不会保留合并(C将在最新和最好的基础上重新定位)。 - Alois Mahdal

27

严禁在几乎任何情况下使用 git push origin --mirror 命令。

该命令不会询问您是否确定执行此操作,因此您必须非常确定,因为它将擦除您的本地计算机上没有的所有远程分支。

http://twitter.com/dysinger/status/1273652486


9
不确定结果的事情最好不要做?我曾经管理的一台机器上有这样的提示:「对该机器进行操作可能会导致意外后果、工作/数据丢失,甚至可能被系统管理员杀死。请记住,您对行动的后果负全部责任。」 - richo
如果你有一个镜像仓库的话,可以使用它(虽然在我的情况下,它现在由源仓库上的特殊用户在 post-receive 钩子中执行)。 - prusswan

15
阅读了您的解释后,我有一个问题:难道您从未进行过一项技术操作吗?
git checkout master
git pull origin
git checkout my_new_feature

在你的特性分支中执行“git rebase/merge master”之前,为什么要这样做呢?因为你的主分支不会自动从你朋友的代码库中更新。你需要使用git pull origin完成这一操作。例如,你可以总是从一个固定的本地主分支中进行rebase操作吗?然后在推送时,你正在向一个包含(本地)未见过的提交的代码库推送,因此推送失败。

14
在你的情况下,我认为你的伴侣是正确的。使用变基的好处是,在外部人看来,你的更改就像一个干净的序列自己完成了一样。这意味着:
  • 你的更改非常容易审核
  • 你可以继续进行美好的、小的提交,但你可以将这些提交的集合(通过合并到主分支)一次性公开
  • 当你查看公共主分支时,你会看到不同的功能系列有不同的开发者提交,但它们不会被混杂在一起
你仍然可以继续将私有开发分支推送到远程仓库作为备份,但其他人不应将其视为“公共”分支,因为你将进行变基。顺便说一下,一个简单的命令是 git push --mirror origin
文章《使用 Git 打包软件》做了一个相当不错的工作,解释了合并和变基之间的权衡。虽然上下文略有不同,但原则是相同的,基本上取决于你的分支是公共还是私有,以及你计划如何将它们集成到主线中。

1
打包软件使用git的链接已经失效了。我找不到一个好的链接来编辑原始答案。 - Chetan
2
你不应该将镜像推送到 origin,而应该将其推送到第三个专门备份的仓库。 - Miral

13
无论如何,我按照最近的一个分支进行了工作流程,但是当我试图将其合并回主干时,一切都变得混乱了。有很多冲突,这些冲突本来不应该有影响。这些冲突对我来说毫无意义。我花了一天时间整理一切,最终导致远程主干强制推送,因为我的本地主干已经解决了所有冲突,但远程主干仍然感到不满意。
根据你和你的伙伴建议的工作流程,你不应该遇到没有意义的冲突。即使你在解决后遇到了这种情况,如果你按照建议的工作流程进行,就不需要进行“强制”推送。这表明你实际上并没有合并你要推送的分支,而是必须推送一个不是远端顶点的分支。
我认为你需要仔细查看发生了什么事情。是不是有其他人(无论是故意还是无心)在你创建本地分支之后,在尝试将其合并回本地分支的某一点之间,将远程主干分支倒回了之前的版本?
与许多其他版本控制系统相比,我发现使用Git可以减少与工具的斗争,让您可以集中精力解决与源流相关的问题。Git不会执行魔法,因此发生冲突会引发冲突,但是它应该通过跟踪提交父子关系使正确的事情变得容易。

你是在暗示 OP 的过程中存在一些未发现的 rebase 或错误,对吧? - krosenvold

11

"即使您是一名只有几个分支的单独开发人员,养成使用rebase和merge的好习惯也是值得的。基本的工作模式如下:

  • 从现有分支A创建新分支B

  • 在分支B上进行添加/提交更改

  • 将来自分支A的更新进行rebase操作

  • 将分支B上的更改合并到分支A上

https://www.atlassian.com/git/tutorials/merging-vs-rebasing/

"


8

根据我的观察,git merge 在合并后倾向于保持分支的独立性,而 rebase 后再合并则会将其合并为一个单一的分支。 后者更加清晰简洁,而前者在合并后更容易找出哪些提交属于哪个分支。


5

使用Git时,不存在“正确”的工作流程。可以使用任何适合你的方式。但是,如果在合并分支时经常出现冲突,也许你应该更好地与你的同事协调工作?听起来像是你们两个人都在编辑相同的文件。此外,注意空格和子版本关键字(即“$Id$”等)。


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