Git工作流和rebase vs合并问题

我现在已经在另一个开发人员的项目中使用Git几个月了。我有几年的SVN经验,所以我想我给这种关系带来了很多包袱。

我听说Git在分支和合并方面非常出色,到目前为止,我只是没有看到它。当然,分支非常简单,但是当我尝试合并时,一切都完蛋了。现在,我已经习惯了SVN,但在我看来,我只是用一个低于标准的版本控制系统换了另一个。

我的搭档告诉我,我的问题源于我对合并的渴望,并且在许多情况下我应该使用rebase而不是合并。例如,这是他制定的工作流程:

clone the remote repositorygit checkout -b my_new_feature..work and commit some stuffgit rebase master..work and commit some stuffgit rebase master..finish the featuregit checkout mastergit merge my_new_feature

从本质上讲,创建一个功能分支,始终从master重新定位到分支,并从分支合并回master。重要的是要注意分支始终保持本地。

这是我开始的工作流程

clone remote repositorycreate my_new_feature branch on remote repositorygit checkout -b --track my_new_feature origin/my_new_feature..work, commit, push to origin/my_new_featuregit merge master (to get some changes that my partner added)..work, commit, push to origin/my_new_featuregit merge master..finish my_new_feature, push to origin/my_new_featuregit checkout mastergit merge my_new_featuredelete remote branchdelete local branch

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

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

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

不管怎样,我在最近的一个分支上遵循我的工作流程,当我试图将它合并回master时,一切都去了地狱。与不应该重要的事情发生了大量冲突。这些冲突对我来说毫无意义。我花了一天的时间来整理一切,最终我被迫推送到远程master,因为我的本地master已经解决了所有冲突,但是远程的仍然不高兴。

对于这样的事情,“正确”的工作流程是什么?Git应该使分支和合并变得超级容易,我只是没有看到它。

更新2011-04-15

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

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

clone the remote repositorygit checkout -b my_new_feature..work and commit some stuffgit rebase master..work and commit some stuffgit rebase master..finish the feature, commitgit rebase mastergit checkout mastergit merge my_new_feature

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

clone the remote repositorygit checkout -b my_new_feature..work and commit some stuffgit rebase master..work and commit some stuffgit rebase master..finish the feature, commitgit rebase mastergit checkout mastergit merge --squash my_new_featuregit commit -m "added my_new_feature"git branch -D my_new_feature

壁球合并争议-正如几位评论者所指出的,壁球合并将丢弃你的功能分支上的所有历史记录。顾名思义,它将所有提交压缩成一个。对于小功能,这是有道理的,因为它将其压缩成一个包。对于较大的功能,这可能不是一个好主意,特别是如果你的单个提交已经是原子的。这真的归结为个人偏好。

Github和Bitucket(其他?)拉取请求-如果你想知道合并/rebase与拉取请求有什么关系,我建议遵循上述所有步骤,直到你准备好合并回master。你只需接受PR,而不是手动使用git合并。请注意,这不会进行压扁合并(至少默认情况下不会),但非压扁,非快进是拉取请求社区中接受的合并约定(据我所知)。具体来说,它的工作方式是这样的:

clone the remote repositorygit checkout -b my_new_feature..work and commit some stuffgit rebase master..work and commit some stuffgit rebase master..finish the feature, commitgit rebase mastergit push # May need to force push...submit PR, wait for a review, make any changes requested for the PRgit rebase mastergit 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 processgit checkout mastergit branch -d my_new_featuregit remote prune origin

我已经爱上了Git,再也不想回到SVN了。如果你在挣扎,只要坚持下去,最终你会看到隧道尽头的光明。

223432 次浏览

使用Git没有“正确”的工作流程。使用任何漂浮你的船。但是,如果你在合并分支时经常遇到冲突,也许你应该更好地与你的开发伙伴协调工作?听起来你们两个一直在编辑相同的文件。此外,要注意空格和颠覆关键字(即“$Id$”等)。

“冲突”意味着“相同内容的并行演进”。因此,如果在合并过程中“全部下地狱”,则意味着您对同一组文件进行了大规模演进。

重新基础比合并更好的原因是:

  • 您可以使用主服务器重写本地提交历史记录(然后重新应用您的工作,解决任何冲突)
  • 最后的合并肯定是“快进”的,因为它将包含master的所有提交历史,加上您要重新应用的更改。

我确认在这种情况下正确的工作流程(公共文件集的演变)是先合并,再合并

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


在这个主题上(重新定位然后合并工作流),酒吧在评论中提到了两个有趣的帖子,都来自randyfay.com

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

(类似技术为集市而存在

在你的情况下,我认为你的搭档是正确的。重新建立基础的好处是,在外人看来,你的变化看起来都发生在一个干净的序列中。这意味着

  • 您的更改很容易审查
  • 你可以继续做出漂亮的、小的提交,但你可以一次公开这些提交的集合(通过合并到master)
  • 当您查看公共主分支时,您会看到不同开发人员针对不同功能的不同提交系列,但它们不会全部混合在一起

为了备份,您仍然可以继续将您的私有开发分支推送到远程存储库,但其他人不应该将其视为“公共”分支,因为您将重新建立基础。顺便说一句,执行此操作的简单命令是git push --mirror origin

文章使用Git打包软件很好地解释了合并和重置的权衡。它的上下文略有不同,但原则是相同的-它基本上归结为您的分支是公共的还是私有的,以及您计划如何将它们集成到主线中。

不管怎样,我在最近的一个分支上遵循我的工作流程,当我试图将它合并回master时,一切都去了地狱。与不应该重要的事情发生了大量冲突。这些冲突对我来说毫无意义。我花了一天的时间来整理一切,最终我被迫推送到远程master,因为我的本地master已经解决了所有冲突,但是远程的仍然不高兴。

在你的合作伙伴和你建议的工作流中,你都不应该遇到没有意义的冲突。即使你遇到了,如果你遵循建议的工作流,那么在解决后不应该需要“强制”推送。这表明你实际上并没有合并你正在推送的分支,但不得不推送一个不是远程提示后代的分支。

我认为您需要仔细看看发生了什么。在您创建本地分支和您尝试将其合并回本地分支之间,是否有其他人(故意或不故意)倒带了远程主分支?

与许多其他版本控制系统相比,我发现使用Git涉及更少的战斗工具,并允许您解决对源流至关重要的问题。Git不执行魔法,因此冲突的更改会导致冲突,但它应该通过跟踪提交父级来轻松完成写入工作。

几乎在任何情况下都不要使用git Push Orie-Mirror。

它不会询问您是否确定要执行此操作,您最好确定,因为它会擦除不在本地框中的所有远程分支。

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

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

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

回想一下,合并实际上创建了一个有两个父节点的节点。现在考虑以下情况:我有两个独立的特征分支A和B,现在想在特征分支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

通过这种方式,我实际上可以维护任意的分支图,但是做一些比上面描述的情况更复杂的事情已经太复杂了,因为没有自动工具可以在父级更改时进行重新设置。

读完你的解释后,我有一个问题:难道你从来没有做过

git checkout mastergit pull origingit checkout my_new_feature

在您的功能分支中执行“git rebase/合并主”之前?

因为主分支不会从你朋友的存储库自动更新。你必须用git pull origin来做这件事。也就是说,也许你总是会从一个从未改变的本地主分支重新定位?然后到了推送时间,你正在推送一个存储库,它有你从未见过的(本地)提交,因此推送失败。

根据我所观察到的,即使在合并后,git合并也倾向于保持分支分离,而rebase然后合并将其组合成一个分支。后者更清晰,而在前者中,即使在合并后也更容易找出哪些提交属于哪个分支。

“即使您是只有几个分支的单个开发人员,也值得养成正确使用rebase和合并的习惯。基本工作模式如下:

  • 从现有分支A创建新的分支B

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

  • 来自分支A的重新定位更新

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

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

太长别读

git rebase工作流并不能保护你免受不擅长解决冲突的人或习惯于SVN工作流的人的伤害,就像避免Git灾难:一个血腥的故事中建议的那样。它只是让冲突解决对他们来说更加乏味,并使得更难从糟糕的冲突解决中恢复过来。相反,使用Div3,这样一开始就不那么困难了。


重新构建工作流并不是解决冲突的更好方法!

然而,如果我曾经遇到过冲突,我立即中止rebase并进行合并!,人们推荐一个rebase工作流作为解决冲突的合并工作流的更好替代方案(这正是这个问题的目的),这真的让我很难过。

如果它在合并期间“全部下地狱”,那么它将在重新建立基础期间“全部下地狱”,并且可能还有更多地狱!这就是为什么:

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

当您rebase而不是合并时,您将不得不执行冲突解决,最多与您提交rebase的次数一样多,对于相同的冲突!

真实场景

我从master分支下来重构分支中的一个复杂方法。我的重构工作总共由15次提交组成,因为我的工作是重构它并进行代码审查。我的重构工作的一部分涉及修复master中以前存在的混合选项卡和空格。这是必要的,但不幸的是,它会与之后对master中这个方法所做的任何更改冲突。果然,当我在做这个方法的时候,有人对master分支中的同一个方法做了简单、合法的更改,应该与我的更改合并。

当需要将分支合并回master时,我有两个选择:

git合并:我得到一个冲突。我看到他们对master所做的更改并将其与我的分支(最终产品)合并。完成。

git rebase:我的第一提交发生了冲突。我解决了冲突并继续重新建立基础。我的第二提交发生了冲突。我解决了冲突并继续重新建立基础。我的第三提交发生了冲突。我解决了冲突并继续重新建立基础。我的第四提交发生了冲突。我解决了冲突并继续重新建立基础。我的第五提交发生了冲突。我解决了冲突并继续重新建立基础。我的第六提交发生了冲突。我解决了冲突并继续重新建立基础。我的第七提交发生了冲突。我解决了冲突并继续重新建立基础。我的第八提交发生了冲突。我解决了冲突并继续重新建立基础。我的第九提交发生了冲突。我解决了冲突并继续重新建立基础。我的第十提交发生了冲突。我解决了冲突并继续重新建立基础。我的第十一提交发生了冲突。我解决了冲突并继续重新建立基础。我的第十二提交发生了冲突。我解决了冲突并继续重新建立基础。我的第十三提交发生了冲突。我解决了冲突并继续重新建立基础。我的第十四提交发生了冲突。我解决了冲突并继续重新建立基础。我的第十五提交发生冲突。我解决了冲突并继续重新建立基础。

如果这个是你首选的工作流程,你一定是在开玩笑。所需要的只是一个与master上的一个更改冲突的空格修复,每个提交都会冲突,必须解决。这是一个只有空格冲突的简单场景。天堂禁止你有一个真正的冲突,涉及跨文件的主要代码更改,并且必须多次解决那个

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

理由#2:有了rebase,就没有撤消!

我想我们都同意解决冲突可能很困难,而且有些人非常不擅长解决冲突。它很容易出错,这就是为什么git让它很容易撤销的原因!

当你合并作为分支,git创建了一个合并提交,如果冲突解决效果不佳,可以丢弃或修改该合并提交。即使您已经将错误的合并提交推送到公共/权威存储库,您也可以使用git revert撤消合并引入的更改,并在新的合并提交中正确重做合并。

当你重新定位分支,如果冲突解决可能出错,你就完蛋了。现在每个提交都包含错误的合并,你不能只是重做rebase*。充其量,你必须返回并修改每个受影响的提交。不好玩。

在重新建立基础之后,不可能确定哪些是最初提交的一部分,哪些是由于糟糕的冲突解决而引入的。

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

把地狱从冲突解决中解脱出来:使用扩散3

以这个冲突为例:

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

看着冲突,不可能知道每个分支改变了什么,或者它的意图是什么。在我看来,这是冲突解决令人困惑和困难的最大原因。

快去营救!

git config --global merge.conflictstyle diff3

当您使用div3时,每个新的冲突都将有一个第三部分,即合并的共同祖先。

<<<<<<< HEADTextMessage.send(:include_timestamp => true)||||||| merged common ancestorEmailMessage.send(:include_timestamp => true)=======EmailMessage.send(:include_timestamp => false)>>>>>>> feature-branch

首先检查合并的共同祖先。然后比较每一端以确定每个分支的意图。您可以看到HEAD将Email Message更改为TextMessage。它的意图是更改用于TextMessage的类,传递相同的参数。您还可以看到功能分支的意图是为:include_timestamp选项传递false而不是true。要合并这些更改,请结合两者的意图:

TextMessage.send(:include_timestamp => false)

一般而言:

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

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

最后,有些冲突即使使用diff也很难理解。特别是当diff发现语义上不常见的行时(例如,两个分支碰巧在同一位置有一个空行!)。例如,一个分支更改了类正文的缩进或重新排序类似的方法。在这些情况下,更好的解决策略是从合并的任何一方检查变化,并手动将diff应用于另一个文件。

让我们看看在合并origin/feature1lib/message.rb冲突的场景中如何解决冲突。

  1. 确定我们当前签出的分支(HEAD--ours)还是我们正在合并的分支(origin/feature1--theirs)是更容易应用的更改。使用带有三重点(git diff a...b)的diff显示了b自上次与a分歧以来发生的变化,或者换句话说,将a和b的共同祖先与b进行比较。

    git diff HEAD...origin/feature1 -- lib/message.rb # show the change in feature1git diff origin/feature1...HEAD -- lib/message.rb # show the change in our branch
  2. 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 complicatedgit checkout --theirs -- lib/message.rb # if origin/feature1's change is more complicated
  3. 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.

我只使用rebase工作流,因为它在视觉上更清晰(不仅在GitKraken中,而且在Intellij和gitk中,但我最推荐第一个):你有一个分支,它起源于master,它回到master。当图表干净漂亮时,你就会知道没有什么会下地狱永远不会

输入图片描述

我的工作流程与你的几乎相同,但只有一个小区别:我squashrebase之前提交到我的本地分支中的一个分支到master的最新更改,因为:

rebase每次提交的基础上工作

这意味着,如果你有15个提交更改了与master相同的行,如果你不压扁,你必须检查15次,但重要的是最终结果,对吗?

所以,整个工作流程是:

  1. master

  2. 从那里创建一个新分支

  3. 在那里做你的工作,你可以自由提交几次,并推送到远程,不用担心,因为它是分支。

  4. 如果有人告诉你,嘿,我的PR/MR通过了,现在合并到master了,你可以获取他们/拉取他们。你可以随时执行,也可以在步骤6中执行。

  5. 做完所有的工作后,提交它们,并且如果你有几个提交,压扁他们(它们都是你的工作,一行代码改多少次并不重要;唯一重要的是最终版本)。推不推,都不重要。

  6. 再次签出到masterpull以确保您在本地有最新的master。你的图表应该如下所示:

输入图片描述

如您所见,您在本地分支上,该分支源自master上的过时状态,而master(本地和远程)已随着同事的更改向前移动。

  1. Checkout回到你的分支,然后rebase到master。你现在只有一次提交,所以你解决了冲突只有一次。(在GitKraken中,你只需要将分支拖到master并选择“Rebase”;我喜欢它的另一个原因。)在那之后,你会喜欢:

输入图片描述

  1. 所以现在,您拥有最新master上的所有更改,并结合您的分支上的更改。您现在可以推送到您的远程,并且如果你以前推过,你将不得不强迫推; Git会告诉您不能简单地快进。这很正常,因为rebase,您更改了分支的起点。但您不应该害怕:明智地使用武力,但不要害怕。最后,远程也是你的分支,所以即使您做错了什么,也不会影响master

  2. 创建PR/MR并等待它被批准,所以master将有你的贡献。恭喜!所以你现在可以签出到master,提取你的更改,然后删除本地分支以清理图表。远程分支也应该被删除,如果你合并到master时没有这样做。

最后的图表再次清晰明了:

输入图片描述