Git工作流和rebase vs合并问题

我已经与其他开发人员一起在一个项目上使用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

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

我对远程分支的推理是,我希望在工作时备份我的工作。 我们的存储库会自动备份,并在出现问题时恢复。 我的笔记本电脑不是,或者不是很完善。 因此,我讨厌在我的笔记本电脑上使用其他地方没有镜像的代码。

我对合并而不是重新合并的推理是合并似乎是标准的,并且重新分配似乎是一个先进的特征。 我的直觉是,我试图做的不是高级设置,所以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

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

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(其他?)拉取请求 - 如果您想知道合并/重新分配与合并请求的关系,我建议您按照上述步骤操作,直到您准备好合并回主数据库。 不要用git手动合并,你只需接受PR。 具体来说,它是这样工作的:

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比合并更好的原因是:

  • 你用主人之一重写本地提交历史记录(然后重新应用你的工作,然后解决任何冲突)
  • 最终的合并肯定会是一个“快进”的合并,因为它将拥有主人的所有提交历史记录,再加上您重新申请的更改。
  • 我确认在这种情况下正确的工作流程(通用文件集合的演变) 首先rebase,然后合并

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


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

  • Git的Rebase工作流程 :提醒我们先获取,rebase:
  • 使用这种技术,你的工作总是在公共分支之上,就像当前HEAD的最新补丁一样。

    (类似的技术存在集市)

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

  • TL; DR

    git rebase工作流不能保护您免受冲突解决不好的人员或习惯了SVN工作流程的用户,就像避免Git灾难:血腥故事中的建议一样。 它只会使解决冲突更加繁琐,并且使得难以从不良冲突解决中恢复。 相反,请使用diff3,以便首先不会如此困难。


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

    我对清理历史非常亲睐。 但是,如果我遇到冲突,我立即放弃rebase并进行合并! 它真的让我感到害怕,人们推荐使用rebase工作流程作为解决冲突的合并工作流程的一个更好的替代方案(这正是这个问题的原因)。

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

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

    当你重新组合而不是合并时,你必须执行冲突解决的次数达到你承诺重新分配的次数,因为同样的冲突!

    真实场景

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

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

    混帐合并:我得到一个冲突。 我看到他们为了掌握和合并(我的分支的最终产品)所做的更改。 完成。

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

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

    如果您需要解决所有额外的冲突解决方案,则会增加您犯错的可能性。 但是,因为你可以撤消,所以git中的错误是好的,对吧? 当然......

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

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

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

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

    重新绑定之后,不可能确定提交的原始内容以及由于解决冲突问题而导致的内容。

    *如果你可以从git的内部日志中挖掘旧的refs,或者如果你创建第三个分支指向rebasing之前的最后一次提交,那么可以撤销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的类更改为传递相同的参数。 您还可以看到,功能分支的意图是为:include_timestamp选项传递false而不是true。 要合并这些更改,请结合两者的意图:

    TextMessage.send(:include_timestamp => false)
    

    一般来说:

  • 将共同的祖先与每个分支进行比较,并确定哪个分支具有最简单的变化
  • 将该简单更改应用于其他分支的代码版本,以便它包含更简单和更复杂的更改
  • 删除除了刚刚合并到一起的更改之外的所有冲突代码部分
  • 备用:通过手动应用分支的更改来解决

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

    让我们来看看如何在合并origin/feature1其中lib/message.rb发生冲突)的场景中解决冲突。

  • 确定我们目前签出的分支( 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的工作流程中,也有合并的地方。

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

    我所做的是:

  • 在A之上创建(并签出)分支C.
  • 与B合并
  • 现在分支C包括来自A和B的变化,并且我可以继续对它进行开发。 如果我对A做任何改变,那么我按照以下方式重新构建分支图:

  • 在A的新顶部创建分支T.
  • 将T与B合并
  • 将C重新转化为T
  • 删除分支T
  • 这样,我可以实际上维护分支的任意图形,但是做比上述情况更复杂的事情已经太复杂了,因为在父级更改时没有自动工具来进行重新绑定。

    链接地址: http://www.djcxy.com/p/1347.html

    上一篇: Git workflow and rebase vs merge questions

    下一篇: How do I push amended commit to the remote Git repository?