我已经和另一个开发人员在一个项目中使用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的新Pragmatic Programming书,其中涉及了大量的merge,而很少提到rebase。
不管怎样,我在最近的分支上遵循我的工作流,当我试图将它合并回master时,一切都糟透了。与本不重要的事情有很多冲突。这些冲突对我来说毫无意义。我花了一天的时间来整理所有的事情,最终在强制推送到远程的主人,因为我的本地主人已经解决了所有的冲突,但远程的主人仍然不高兴。
对于这样的事情,“正确的”工作流是什么?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上的单个提交。然后我们删除我们的特征分支。这允许我们在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。而不是用git手动合并,你只是接受PR。注意,这不会做挤压合并(至少默认情况下不会),但非挤压,非快进是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;
git rebase工作流并不能保护您免受那些不擅长解决冲突的人或习惯于SVN工作流的人的伤害,就像在避免git灾难:一个血腥的故事中建议的那样。这只会让解决冲突变得更加乏味,也会让他们更难从糟糕的冲突解决中恢复过来。相反,使用diff3,这样就不会那么难了。
重构工作流并不利于解决冲突!
我非常支持用rebase来清理历史。然而,如果我遇到冲突,我立即放弃重基,而是做一个合并!人们推荐重构工作流作为解决冲突的合并工作流的更好选择,这真的让我很生气(这正是这个问题的问题所在)。
如果它在合并期间“彻底完蛋”,那么在重基期间它也会“彻底完蛋”,而且可能会更糟!原因如下:
原因1:一次性解决冲突,而不是每次提交都解决一次
当你重基而不是合并时,你将不得不执行冲突解决,因为你已经提交了多次重基,对于相同的冲突!
真实的场景
I branch off of master to refactor a complicated method in a branch. My refactoring work is comprised of 15 commits total as I work to refactor it and get code reviews. Part of my refactoring involves fixing the mixed tabs and spaces that were present in master before. This is necessary, but unfortunately it will conflict with any change made afterward to this method in master. Sure enough, while I'm working on this method, someone makes a simple, legitimate change to the same method in the master branch that should be merged in with my changes.
当需要将我的分支合并回master时,我有两个选择:
git合并:
我得到一个冲突。我看到了他们所做的更改,以掌握并将其合并到我的分支(最终产品)中。完成了。
git rebase:
I get a conflict with my first commit. I resolve the conflict and continue the rebase.
I get a conflict with my second commit. I resolve the conflict and continue the rebase.
I get a conflict with my third commit. I resolve the conflict and continue the rebase.
I get a conflict with my fourth commit. I resolve the conflict and continue the rebase.
I get a conflict with my fifth commit. I resolve the conflict and continue the rebase.
I get a conflict with my sixth commit. I resolve the conflict and continue the rebase.
I get a conflict with my seventh commit. I resolve the conflict and continue the rebase.
I get a conflict with my eighth commit. I resolve the conflict and continue the rebase.
I get a conflict with my ninth commit. I resolve the conflict and continue the rebase.
I get a conflict with my tenth commit. I resolve the conflict and continue the rebase.
I get a conflict with my eleventh commit. I resolve the conflict and continue the rebase.
I get a conflict with my twelfth commit. I resolve the conflict and continue the rebase.
I get a conflict with my thirteenth commit. I resolve the conflict and continue the rebase.
I get a conflict with my fourteenth commit. I resolve the conflict and continue the rebase.
I get a conflict with my fifteenth commit. I resolve the conflict and continue the rebase.
如果这是你喜欢的工作流程,你一定是在跟我开玩笑。它所需要的只是一个与master上的一个更改相冲突的空白修复,并且每次提交都会发生冲突,必须解决。这是一个只有空格冲突的简单场景。但愿你不会遇到涉及跨文件的重大代码更改的真正冲突,并且不得不多次解决这个问题。
你需要做的所有额外的冲突解决方案,只会增加你犯错的可能性。但是错误在git中没有问题,因为你可以撤销,对吧?当然,除了……
原因2:有了rebase,就没有撤销了!
我想我们都同意解决冲突是困难的,而且有些人非常不擅长解决冲突。它很容易出错,这就是为什么git让它很容易撤销的原因!
当合并一个分支时,git会创建一个合并提交,如果冲突解决不顺利,可以将其丢弃或修改。即使您已经将错误的合并提交推到了公共/权威的回购中,您也可以使用git revert来撤销合并引入的更改,并在新的合并提交中正确地重做合并。
当您重新建立分支时,很可能发生冲突解决方法错误的情况,您就完蛋了。现在每次提交都包含错误的合并,你不能只是重做rebase*。充其量,您必须返回并修改每个受影响的提交。不好玩。
在重基之后,不可能确定哪些是最初提交的一部分,哪些是由于错误的冲突解决而引入的。
*如果你能从git的内部日志中挖掘出旧的引用,或者如果你创建了第三个分支,指向rebase之前的最后一次提交,就可以撤销rebase。
使用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)
一般来说:
比较每个分支的公共祖先,并确定哪个分支具有最简单的更改
将这个简单的更改应用到其他分支的代码版本,这样它就包含了更简单的更改和更复杂的更改
删除冲突代码的所有部分,除了您刚刚将更改合并到的部分
可选:通过手动应用分支的更改来解决
最后,有些冲突即使使用diff3也难以理解。当diff发现语义上不通用的相同行时,这种情况尤其会发生。两个分支恰好在同一位置有一个空行!)。例如,一个分支更改类主体的缩进或重新排序类似的方法。在这些情况下,更好的解决策略是从合并的任何一方检查更改,并手动将差异应用到另一个文件。
让我们看看在合并origin/feature1和lib/message的场景中如何解决冲突。rb冲突。
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
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
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.