我和其他开发人员一起在一个项目上使用 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。如果你正在努力,只要坚持下去,最终你会看到隧道尽头的光线。
git rebase 工作流程不能保护您免受冲突解决能力差的人或习惯 SVN 工作流的人的攻击,例如避免 Git Disasters:A Gory Story 中的建议 。它只会使冲突解决对他们来说更加乏味,并且使得从糟糕的冲突解决中恢复更加困难。相反,使用 diff3,这样一开始就不那么困难了。
为了清理历史,我非常专业。但是,如果我遇到冲突,我会立即中止 rebase 并进行合并!我真的让我感到害怕的是,人们推荐使用 rebase 工作流程作为解决冲突的合并工作流程的更好替代方案(这正是这个问题的内容)。
如果它在合并过程中 “一切都变成地狱”,那么它将在变革期间 “彻底地变成地狱”,而且可能还会更加地狱!原因如下:
当您进行 rebase 而不是 merge 时,对于同样的冲突,您必须执行冲突解决,直到您提交 rebase 的次数为止!
我从 master 分支出来,在一个分支中重构一个复杂的方法。我的重构工作总共包含 15 个提交,因为我正在重构它并获得代码审查。我的重构的一部分涉及修复之前在 master 中存在的混合选项卡和空格。这是必要的,但遗憾的是,它会与 master 中此方法所做的任何更改发生冲突。果然,当我正在研究这种方法时,有人会对主分支中的同一方法进行简单合法的更改,该方法应与我的更改合并。
当我的分支与主人合并时,我有两个选择:
git merge:我遇到了冲突。我看到他们要掌握的变化并将其与我的分支(最终产品)合并。完成。
git rebase:我与第一次提交有冲突。我解决冲突并继续改变。我与第二次提交发生冲突。我解决冲突并继续改变。我与第三次提交发生冲突。我解决冲突并继续改变。我与第四次提交发生冲突。我解决冲突并继续改变。我与第五次提交发生冲突。我解决冲突并继续改变。我与第六次提交发生冲突。我解决冲突并继续改变。我与第七次提交发生冲突。我解决冲突并继续改变。我与第八次提交发生冲突。我解决冲突并继续改变。我与第九次提交发生冲突。我解决冲突并继续改变。我与第十次提交发生冲突。我解决冲突并继续改变。我与第十一次提交发生冲突。我解决冲突并继续改变。我与第十二次提交发生冲突。我解决冲突并继续改变。我与第十三次提交发生冲突。我解决冲突并继续改变。我与第十四次提交发生冲突。我解决冲突并继续改变。我与第十五次提交发生冲突。我解决冲突并继续改变。
如果这是您首选的工作流程,您必须开玩笑。所需要的只是一个空白修复,与 master 上的一个更改冲突,每个提交都会发生冲突,必须解决。这是一个只有空白冲突的简单场景。 但愿你有涉及到整个文件的主要代码更改一个真正的冲突,和那些多次化解。
通过您需要做的所有额外冲突解决,它只会增加您犯错误的可能性。但是你可以撤消 git 中的错误,对吧?当然除外......
我想我们都同意解决冲突可能很困难,而且有些人也非常苛刻。它可能非常容易出错,这就是 git 让它易于撤消的原因!
当您合并分支时,git 会创建一个合并提交,如果冲突解决方案不佳,可以将其丢弃或修改。即使您已经将错误的合并提交推送到公共 / 权威仓库,您也可以使用git revert
来撤消合并引入的更改,并在新的合并提交中正确地重做合并。
当您重新分支一个分支时,如果冲突解决错误,可能会被搞砸。现在每个提交都包含错误的合并,你不能只重做 rebase *。充其量,您必须返回并修改每个受影响的提交。不好玩。
在重组之后,无法确定最初提交的内容以及由于解决冲突而引入的内容。
* 如果您可以从 git 的内部日志中挖掘旧的 ref,或者如果您创建指向最后一次提交之前的第三个分支,则可以撤消 rebase。
以此冲突为例:
<<<<<<< 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)
一般来说:
最后,即使使用 diff3,一些冲突也很难理解。特别是当 diff 找到非语义共同的共同行时(例如,两个分支碰巧在同一个地方都有一个空行!),就会发生这种情况。例如,一个分支更改类的主体的缩进或重新排序类似的方法。在这些情况下,更好的解决策略可以是从合并的任一侧检查更改并手动将 diff 应用于其他文件。
让我们看一下如何在lib/message.rb
冲突时合并origin/feature1
的场景中解决冲突。
确定我们当前检出的分支( 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
查看更复杂的文件版本。这将删除所有冲突标记并使用您选择的一侧。
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
通过检查复杂的更改,拉出更简单更改的差异(参见步骤 1)。将此差异中的每个更改应用于冲突文件。
“冲突” 是指 “相同内容的平行演变”。因此,如果它在合并期间 “完全地狱”,则意味着您在同一组文件上进行了大规模的演变。
因此,rebase 比合并更好的原因是:
我确认在这种情况下正确的工作流程(普通文件集的演变) 首先是rebase,然后是 merge 。
但是,这意味着,如果您推送本地分支(出于备份原因),则不应该由其他任何人拉动(或至少使用)该分支(因为提交历史记录将由连续的 rebase 重写)。
关于该主题(rebase 然后合并工作流程), barraponto在评论中提到两个有趣的帖子,都来自randyfay.com :
使用这种技术,您的工作总是在公共分支之上,就像当前
HEAD
最新的补丁一样。
(类似的技术存在集市 )
git push --force
的危险(而不是git pull --rebase
) 在我的工作流程中,我尽可能地重新设置(并且我经常尝试这样做。不要让差异累积大幅减少分支之间冲突的数量和严重程度)。
然而,即使在基于 rebase 的工作流程中,也有合并的地方。
回想一下,merge 实际上创建了一个有两个父节点的节点。现在考虑以下情况:我有两个独立的特征 B 和 A,现在想要在特征分支 C 上开发东西,它依赖于 A 和 B,而 A 和 B 正在被审查。
我当时做的是以下内容:
现在分支 C 包括 A 和 B 的变化,我可以继续开发它。如果我对 A 做了任何更改,那么我将按以下方式重建分支图:
这样我实际上可以维护分支的任意图形,但是做一些比上述情况更复杂的事情已经太复杂了,因为在父改变时没有自动工具来进行改变。