Git的工作stream程和rebase vs合并问题

我已经和其他开发者一起使用Git几个月了。 我有几年的SVN经验,所以我想我带了很多的关系的包袱。

我听说Git非常适合分支和合并,到目前为止,我只是没有看到它。 当然,分支是简单的,但是当我尝试合并的时候,所有的事情都变成了地狱。 现在,我已经习惯了SVN,但是在我看来,我只是把一个子版本系统换成另一个。

我的合作伙伴告诉我,我的问题源自我渴望融合的愿望,在很多情况下我应该使用rebase而不是merge。 例如,下面是他制定的工作stream程:

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 

从本质上讲,创build一个function分支,总是重新从主分支,并从分支合并到主。 需要注意的是,分支始终保持在本地。

这是我开始的工作stream程

 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 

有两个本质的区别(我认为):我总是使用合并而不是重新绑定,我把我的function分支(和我的function分支提交)到远程存储库。

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

我的合并而不是rebase的推理是合并似乎是标准的,rebase似乎是一个先进的function。 我的直觉是,我想要做的不是一个高级的设置,所以rebase应该是不必要的。 我甚至仔细阅读了Git上的新语用编程书,它们涵盖了广泛的合并,几乎没有提到rebase。

无论如何,我在最近的一个分支上关注着我的工作stream程,当我试图把它合并回主人时,这一切都变成了地狱。 与本来不应有的事情有很多冲突。 冲突对我来说是没有意义的。 我花了一整天的时间把所有的事情都整理出来,最终导致了对远程主人的强迫推行,因为当地的主人已经解决了所有的冲突,但是远程的主人仍然不高兴。

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

更新2011-04-15

这似乎是一个非常受欢迎的问题,所以我想我会从我第一次问到自己两年的经验更新。

事实certificate,原来的工作stream程是正确的,至less在我们的情况下。 换句话说,这就是我们所做的工作:

 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 

事实上,我们的工作stream程有点不同,因为我们倾向于压缩合并而不是原始合并。 ( 注意:这是有争议的,见下面)。这使我们可以把我们的整个特性分支变成一个单独的提交。 然后我们删除我们的function分支。 这使我们能够在逻辑上构build主人的承诺,即使他们在我们的分支上有点混乱。 所以,这就是我们所做的:

 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 

壁球合并争议 – 正如几位评论者指出,壁球合并将扔掉你的function分支上的所有历史。 顾名思义,它把所有的提交压缩成一个单一的。 对于小function,这是有意义的,因为它把它凝聚成一个单一的包。 对于更大的function,这可能不是一个好主意,特别是如果你的个人提交已经是primefaces的。 这真的归结为个人喜好。

Github和Bitbucket(其他?)拉取请求 – 如果您想知道合并/重新分配与合并请求的关系,我build议您按照上述步骤进行操作,直到您准备好合并回主数据库。 而不是手动合并混帐,你只是接受公关。 具体来说,它是这样工作的:

 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。 如果你正在挣扎,就坚持下去,最后你会看到隧道尽头的灯光。

“冲突”是指“同一内容的平行演变”。 因此,如果在合并期间它“全部到了地狱”,这意味着你在同一组文件上有巨大的进化。

为什么一个rebase然后比合并更好的原因是:

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

我确认在这种情况下正确的工作stream程(通用文件集上的演变) 首先rebase,然后合并

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


在这个话题上(rebase然后合并工作stream), barraponto在评论中提到了两个有趣的post,都来自randyfay.com :

  • Git的Rebase工作stream程 :提醒我们先获取,rebase:

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

(类似的技术存在集市 )

  • 避免Git灾难:一个血腥的故事 :关于git push --force git pull --rebase的危险(而不是git pull --rebase例如)

TL; DR

git rebase工作stream并不能保护您免受冲突解决不好的人员或习惯于SVN工作stream程的用户,就像避免Git灾难:血腥故事中的build议一样。 它只会使冲突的解决变得更乏味,使得从不好的冲突解决中恢复更加困难。 相反,使用diff3,这样一开始就不是那么难。


Rebase工作stream程不会更好地解决冲突!

我为清理历史非常亲睐。 但是,如果我遇到冲突,我立即放弃rebase并且合并! 这真的让我感到害怕,人们推荐使用rebase工作stream,作为解决冲突的合并工作stream程(这正是这个问题所关注的)的更好select。

如果在合并过程中“全部到了地狱”,那么在重新绑定的过程中,它将“全部下地狱”,而且可能还会更加糟糕! 原因如下:

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

当你改变而不是合并时,你必须执行冲突解决的次数,尽可能多的次数,因为同样的冲突!

真实情况

我在分支上重新分解一个复杂的方法。 我的重构工作总共包含15个提交,因为我正在重构它并获得代码评论。 我重构的一部分涉及修复之前在master中存在的混合选项卡和空格。 这是必要的,但不幸的是,它将与之后在master中对此方法所做的任何更改相冲突。 果然,当我正在处理这个方法时,有人对主分支中的同一个方法进行了一个简单的,合法的改变,这个方法应该和我的改变合并在一起。

当我把分支和主人合并的时候,我有两个select:

混帐合并:我得到一个冲突。 我看到他们所做的改变,把它们与我的分支(最终产品)合并在一起。 完成。

git rebase:我与第一次提交有冲突。 我解决冲突并继续进行改组。 我与第二次提交有冲突。 我解决冲突并继续进行改组。 我与第三次提交发生冲突。 我解决冲突并继续进行改组。 我与第四次提交发生冲突。 我解决冲突并继续进行改组。 我与第五次提交发生冲突。 我解决冲突并继续进行改组。 我与第六次提交发生冲突。 我解决冲突并继续进行改组。 我与第七次提交有冲突。 我解决冲突并继续进行改组。 我与第八次提交有冲突。 我解决冲突并继续进行改组。 我与第九次提交发生冲突。 我解决冲突并继续进行改组。 我与第十次提交发生冲突。 我解决冲突并继续进行改组。 我和第十一个承诺有冲突。 我解决冲突并继续进行改组。 我与我的第十二个承诺有冲突。 我解决冲突并继续进行改组。 我与第十三次承诺发生冲突。 我解决冲突并继续进行改组。 我与第十四次承诺发生冲突。 我解决冲突并继续进行改组。 我与第十五次提交发生冲突。 我解决冲突并继续进行改组。

如果是你的首选工作stream程,你一定是在开玩笑吧。 所需的只是一个空白修补与主机上的一个更改相冲突,每个提交都会发生冲突,必须解决。 这是一个简单的情况,只有空白冲突。 天堂禁止你有一个真正的冲突涉及跨文件的主要代码更改,必须多次解决。

所有额外的冲突解决你需要做的,它只是增加了你犯了一个错误的可能性。 但是,因为你可以撤消,git中的错误是好的,对吧? 除了…

原因#2:有了rebase,没有任何撤销!

我认为我们都可以同意冲突的解决是困难的,而且有些人是非常不好的。 它可能非常容易出错,这就是为什么它如此之好以至于git可以很容易地撤销!

当你合并一个分支时,git会创build一个合并提交,如果冲突解决的效果不好,可以放弃或修改。 即使您已经将错误的合并提交推送到公共/权威的仓库,也可以使用git revert来撤销合并引入的更改,并在新的合并提交中正确地重新进行合并。

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

经过重新devise之后,不可能确定提交的内容是什么,以及由于不良的冲突解决而导致的内容。

*如果你可以从git的内部日志中挖掘旧的ref,或者如果你创build了第三个分支,指向rebasing之前的最后一个commit,那么可以撤销rebase。

拿出冲突解决scheme:使用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的类,传递相同的参数。 您还可以看到,function分支的意图是传递false,而不是true:include_timestamp选项。 要合并这些更改,请结合两者的意图:

 TextMessage.send(:include_timestamp => false) 

一般来说:

  1. 比较共同祖先与每个分支,并确定哪个分支具有最简单的变化
  2. 将简单的更改应用到其他分支的代码版本,以便它包含更简单和更复杂的更改
  3. 删除除了刚刚合并到一起的更改之外的所有冲突代码部分

备用:通过手动应用分支的更改来解决

最后,即使是diff3,一些冲突也是难以理解的。 发生这种情况,尤其是当difffind共同的语义上不常见的行时(例如,两个分支碰巧在同一个地方有空行!)。 例如,一个分支改变了一个类的缩进或重新sorting类似的方法。 在这些情况下,更好的解决策略可以是从合并的任一侧检查变化,并手动将差异应用于其他文件。

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

  1. 确定我们目前签出的分支( HEAD ,或--ours )还是我们合并的分支( origin/feature1 ,或--theirs )是一个更简单的更改。 使用diff与三重点( git diff a...b )显示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. 检查出更复杂的文件版本。 这将删除所有的冲突标记,并使用你select的一面。

     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)。 将这个差异的每个更改应用到冲突的文件。

在我的工作stream程中,我尽可能地重新devise(我经常尝试这样做,不要让这些差异累积起来,从而大大减less分支之间碰撞的数量和严重程度)。

但是,即使在大部分基于rebase的工作stream程中,也有合并的地方。

回想一下,合并实际上创build了一个有两个父母的节点。 现在考虑以下情况:我有两个独立的functionB和B,现在想要开发function分支C上的东西,这个function是依赖于A和B,而A和B正在被审查。

我所做的是:

  1. 在A之上创build(并签出)分支C.
  2. 与B合并

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

  1. 在A的新顶部创build分支T.
  2. 把T和B合并
  3. 将C重新粘贴到T上
  4. 删除分支T

这样,我可以实际上维护分支的任意graphics,但是做一些比上面描述的情况更复杂的事情已经太复杂了,因为当父母更改时没有自动工具进行重新绑定。

不要在任何情况下使用git push origin –mirror。

它不问你是否确定要这样做,而且最好确定一下,因为它会清除所有不在本地盒子上的远程分支。

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

在你的情况下,我认为你的伴侣是正确的。 重新定义的好处在于,对于外部人来说,你所做的改变看起来像是他们都是以一个干净的顺序发生的。 意即

  • 您的更改很容易审查
  • 你可以继续做出很好的小提交,但是你可以把这些提交集合一起公开(通过合并成主)
  • 当您查看公共主分支时,您会看到不同开发人员针对不同function提交的不同系列提交,但不会混合在一起

您仍然可以继续将您的私人开发分支推送到远程存储库以备份,但其他人不应该将其视为“公共”分支,因为您将重新分配。 顺便说一句,这样做的一个简单的命令是git push --mirror origin

使用Git的文章包装软件做了一个相当不错的工作,解释合并与rebasing的权衡。 这是一个不同的背景,但校长是相同的 – 这基本上取决于您的分支机构是公共还是私人,以及您计划如何将它们整合到主线。

在阅读你的解释之后,我有一个问题:难道你没有做过吗?

 git checkout master git pull origin git checkout my_new_feature 

在你的function分支做'git rebase / merge master'之前?

因为您的主分支不会自动从您朋友的存储库中更新。 你必须用git pull origin来完成。 也许你会永远从一个不断变化的本地主分支变质? 然后来推时间,你推着一个仓库(本地)提交你从来没有看到,因此推失败。

无论如何,我在最近的一个分支上关注着我的工作stream程,当我试图把它合并回主人时,这一切都变成了地狱。 与本来不应有的事情有很多冲突。 冲突对我来说是没有意义的。 我花了一整天的时间把所有的事情都整理出来,最终导致了对远程主人的强迫推行,因为当地的主人已经解决了所有的冲突,但是远程的主人仍然不高兴。

在您的合作伙伴或您build议的工作stream程中,您都不应该遇到无意义的冲突。 即使你有,如果你正在按照build议的工作stream程,那么在解决之后,不应该要求“强制”推送。 这表明你并没有真正融合你所推动的分支,而是不得不推动一个不是远程提示的后代的分支。

我想你需要仔细看看发生了什么事。 其他人是否有意(或不是)在您创build本地分支和尝试将其合并到本地分支之间的远程主分支重新缠绕?

与其他许多版本控制系统相比,我发现使用Git不需要对付这个工具,并且可以让你开始处理源码stream的基本问题。 Git不会执行魔术,所以冲突的变化会引起冲突,但是通过跟踪提交的身份,它可以很容易地完成写入操作。

从我所观察到的情况来看,即使合并之后,git merge也会使分支保持分离,而rebase然后merge将它合并成一个分支。 后者变得更清洁,而前者更容易找出哪些提交属于哪个分支,即使合并后也是如此。

“即使你是一个只有几个分支的开发者,也应该习惯使用rebase和合并,基本的工作模式如下:

  • 从现有的分支A创build新的分支B.

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

  • 分支A的重新更新

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

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

使用Git没有“正确的”工作stream程。 使用任何漂浮你的船。 但是,如果您在合并分支时经常发生冲突,您应该与您的开发人员协调更好的工作吗? 听起来像你们两个不断编辑相同的文件。 此外,请注意空白和颠覆关键字(即“$ Id $”等)。