协慌网

登录 贡献 社区

Git 工作流程和 rebase 与合并问题

我和其他开发人员一起在一个项目上使用 Git 几个月了。我有几年的SVN经验,所以我想我给这段关系带来了很多包袱。

我听说 Git 非常适合分支和合并,到目前为止,我只是没有看到它。当然,分支很简单,但是当我尝试合并时,一切都变得很糟糕。现在,我已经习惯了 SVN,但在我看来,我只是将一个低于标准的版本系统换成了另一个。

我的搭档告诉我,我的问题源于我不顾一切地合并,并且我应该在很多情况下使用 rebase 而不是合并。例如,这是他所规定的工作流程:

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

有两个本质区别(我认为):我总是使用 merge 而不是 rebase,我将我的功能分支(和我的功能分支提交)推送到远程存储库。

我对远程分支的理由是,我希望在我工作时备份我的工作。我们的存储库会自动备份,如果出现问题可以恢复。我的笔记本电脑没有,或没有彻底。因此,我讨厌我的笔记本电脑上没有镜像的代码。

我合并而不是 rebase 的原因是合并似乎是标准的,而 rebase 似乎是一个高级功能。我的直觉是,我正在尝试做的不是高级设置,所以不应该使用 rebase。我甚至在 Git 上阅读了新的实用编程书,它们涵盖了广泛的合并,几乎没有提到 rebase。

无论如何,我在最近的一个分支上关注我的工作流程,当我试图将它合并回主人时,一切都进入了地狱。对于应该不重要的事情存在大量冲突。冲突对我来说毫无意义。我花了一天的时间来解决所有事情,并最终被强制推向远程主人,因为我的当地主人解决了所有冲突,但是远程主人仍然不高兴。

这样的事情的 “正确” 工作流程是什么? Git 应该让分支和合并超级简单,我只是没有看到它。

更新 2011-04-15

这似乎是一个非常受欢迎的问题,所以我认为自从我第一次提出问题以来,我会用自己两年的经验进行更新。

事实证明原始工作流程是正确的,至少在我们的情况下。换句话说,这就是我们所做的,它的工作原理:

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

事实上,我们的工作流程略有不同,因为我们倾向于进行压缩合并而不是原始合并。 ( 注意:这是有争议的,见下文。 )这允许我们将整个功能分支转换为主服务器上的单个提交。然后我们删除我们的功能分支。这允许我们在 master 上逻辑地构造我们的提交,即使它们在我们的分支上有点乱。所以,这就是我们所做的:

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(其他人?)Pull Requests - 如果您想知道 merge / rebase 如何与 Pull Requests 相关,我建议您按照上述所有步骤进行操作,直到您准备好合并回 master。您只需接受 PR,而不是手动与 git 合并。请注意,这不会进行压缩合并(至少不会默认),但非压缩,非快进是 Pull Request 社区中可接受的合并约定(据我所知)。具体来说,它的工作原理如下:

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。如果你正在努力,只要坚持下去,最终你会看到隧道尽头的光线。

答案

TL; DR

git rebase 工作流程不能保护您免受冲突解决能力差的人或习惯 SVN 工作流的人的攻击,例如避免 Git Disasters:A Gory Story 中的建议 。它只会使冲突解决对他们来说更加乏味,并且使得从糟糕的冲突解决中恢复更加困难。相反,使用 diff3,这样一开始就不那么困难了。


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

为了清理历史,我非常专业。但是,如果我遇到冲突,我会立即中止 rebase 并进行合并!我真的让我感到害怕的是,人们推荐使用 rebase 工作流程作为解决冲突的合并工作流程的更好替代方案(这正是这个问题的内容)。

如果它在合并过程中 “一切都变成地狱”,那么它将在变革期间 “彻底地变成地狱”,而且可能还会更加地狱!原因如下:

原因#1:解决冲突一次,而不是每次提交一次

当您进行 rebase 而不是 merge 时,对于同样的冲突,您必须执行冲突解决,直到您提交 rebase 的次数为止!

真实场景

我从 master 分支出来,在一个分支中重构一个复杂的方法。我的重构工作总共包含 15 个提交,因为我正在重构它并获得代码审查。我的重构的一部分涉及修复之前在 master 中存在的混合选项卡和空格。这是必要的,但遗憾的是,它会与 master 中此方法所做的任何更改发生冲突。果然,当我正在研究这种方法时,有人会对主分支中的同一方法进行简单合法的更改,该方法应与我的更改合并。

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

git merge:我遇到了冲突。我看到他们要掌握的变化并将其与我的分支(最终产品)合并。完成。

git rebase:我与第一次提交有冲突。我解决冲突并继续改变。我与第二次提交发生冲突。我解决冲突并继续改变。我与第三次提交发生冲突。我解决冲突并继续改变。我与第四次提交发生冲突。我解决冲突并继续改变。我与第五次提交发生冲突。我解决冲突并继续改变。我与第六次提交发生冲突。我解决冲突并继续改变。我与第七次提交发生冲突。我解决冲突并继续改变。我与第八次提交发生冲突。我解决冲突并继续改变。我与第九次提交发生冲突。我解决冲突并继续改变。我与第十次提交发生冲突。我解决冲突并继续改变。我与第十一次提交发生冲突。我解决冲突并继续改变。我与第十二次提交发生冲突。我解决冲突并继续改变。我与第十三次提交发生冲突。我解决冲突并继续改变。我与第十四次提交发生冲突。我解决冲突并继续改变。我与第十五次提交发生冲突。我解决冲突并继续改变。

如果是您首选的工作流程,您必须开玩笑。所需要的只是一个空白修复,与 master 上的一个更改冲突,每个提交都会发生冲突,必须解决。这是一个只有空白冲突的简单场景。 但愿你有涉及到整个文件的主要代码更改一个真正的冲突那些多次化解。

通过您需要做的所有额外冲突解决,它只会增加您犯错误的可能性。但是你可以撤消 git 中的错误,对吧?当然除外......

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

我想我们都同意解决冲突可能很困难,而且有些人也非常苛刻。它可能非常容易出错,这就是 git 让它易于撤消的原因!

当您合并分支时,git 会创建一个合并提交,如果冲突解决方案不佳,可以将其丢弃或修改。即使您已经将错误的合并提交推送到公共 / 权威仓库,您也可以使用git revert来撤消合并引入的更改,并在新的合并提交中正确地重做合并。

当您重新分支一个分支时,如果冲突解决错误,可能会被搞砸。现在每个提交都包含错误的合并,你不能只重做 rebase *。充其量,您必须返回并修改每个受影响的提交。不好玩。

在重组之后,无法确定最初提交的内容以及由于解决冲突而引入的内容。

* 如果您可以从 git 的内部日志中挖掘旧的 ref,或者如果您创建指向最后一次提交之前的第三个分支,则可以撤消 rebase。

从解决冲突中解脱出来:使用 diff3

以此冲突为例:

<<<<<<< HEAD
TextMessage.send(:include_timestamp => true)
=======
EmailMessage.send(:include_timestamp => false)
>>>>>>> feature-branch

看一下冲突,就不可能分辨出每个分支的变化或意图是什么。这是我认为解决冲突困难和困难的最大原因。

diff3 来救援!

git config --global merge.conflictstyle diff3

当你使用 diff3 时,每个新冲突都会有第 3 个部分,即合并的共同祖先。

<<<<<<< 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 应用于其他文件。

让我们看一下如何在lib/message.rb冲突时合并origin/feature1的场景中解决冲突。

  1. 确定我们当前检出的分支( HEAD ,或--ours )或我们正在合并的分支( origin/feature1--theirs )是否是更简单的应用变更。使用 DIFF 与三联点( git diff a...b )显示了所发生的变化b ,因为从上次发散a ,或者换句话说,比较的共同祖先和 b 用 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. 查看更复杂的文件版本。这将删除所有冲突标记并使用您选择的一侧。

    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. 通过检查复杂的更改,拉出更简单更改的差异(参见步骤 1)。将此差异中的每个更改应用于冲突文件。

“冲突” 是指 “相同内容的平行演变”。因此,如果它在合并期间 “完全地狱”,则意味着您在同一组文件上进行了大规模的演变。

因此,rebase 比合并更好的原因是:

  • 你用一个 master 重写你的本地提交历史(然后重新申请你的工作,然后解决任何冲突)
  • 最终合并肯定会是一个 “快进” 的合并,因为它将拥有主人的所有提交历史记录,以及只有你重新申请的更改。

我确认在这种情况下正确的工作流程(普通文件集的演变) 首先rebase,然后是 merge

但是,这意味着,如果您推送本地分支(出于备份原因),则不应该由其他任何人拉动(或至少使用)该分支(因为提交历史记录将由连续的 rebase 重写)。


关于该主题(rebase 然后合并工作流程), barraponto在评论中提到两个有趣的帖子,都来自randyfay.com

使用这种技术,您的工作总是在公共分支之上,就像当前HEAD最新的补丁一样。

(类似的技术存在集市

在我的工作流程中,我尽可能地重新设置(并且我经常尝试这样做。不要让差异累积大幅减少分支之间冲突的数量和严重程度)。

然而,即使在基于 rebase 的工作流程中,也有合并的地方。

回想一下,merge 实际上创建了一个有两个父节点的节点。现在考虑以下情况:我有两个独立的特征 B 和 A,现在想要在特征分支 C 上开发东西,它依赖于 A 和 B,而 A 和 B 正在被审查。

我当时做的是以下内容:

  1. 在 A 之上创建(和结帐)分支 C.
  2. 与 B 合并

现在分支 C 包括 A 和 B 的变化,我可以继续开发它。如果我对 A 做了任何更改,那么我将按以下方式重建分支图:

  1. 在 A 的新顶部创建分支 T.
  2. 将 T 与 B 合并
  3. 将 C 转换为 T
  4. 删除分支 T.

这样我实际上可以维护分支的任意图形,但是做一些比上述情况更复杂的事情已经太复杂了,因为在父改变时没有自动工具来进行改变。