Git工作流程和rebase与合并问题

时间:2009-01-19 15:16:53

标签: git version-control git-merge git-rebase

我和其他开发人员在一个项目上使用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上阅读了新的实用编程书,它们涵盖了广泛的合并,几乎没有提到变基。

无论如何,我在最近的一个分支上关注我的工作流程,当我试图将它合并回主人时,一切都进入了地狱。对于应该不重要的事情存在大量冲突。冲突对我来说毫无意义。我花了一天的时间来解决所有事情,并最终被强制推向远程主人,因为我的当地主人解决了所有冲突,但是远程主人仍然不满意。

这样的“正确”工作流程是什么? 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

事实上,我们的工作流程略有不同,因为我们倾向于 squash merges 而不是原始合并。 (注意:这是有争议的,见下文。)这允许我们将整个功能分支转换为主服务器上的单个提交。然后我们删除我们的功能分支。这允许我们在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(其他人?)拉请求 - 如果您想知道合并/ rebase如何与Pull请求相关,我建议您按照上述所有步骤进行操作,直到您准备合并为止回到主人。您只需接受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。如果你正在努力,只要坚持下去,最终你会看到隧道尽头的光线。

11 个答案:

答案 0 :(得分:372)

TL; DR

git rebase工作流程不能保护您免受冲突解决能力差的人或习惯SVN工作流的人的攻击,如Avoiding Git Disasters: A Gory Story中所述。它只会使冲突解决对他们来说更加乏味,并且使得从糟糕的冲突解决中恢复更加困难。相反,使用diff3以便它首先不那么困难。


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

我非常支持清理历史。然而,如果我曾遇到过冲突,我会立即中止rebase并进行合并!这真的让我失望,人们建议将rebase工作流作为解决冲突的合并工作流程的更好替代方案(正是这个问题的内容。)

如果它发生了#34;所有地狱"在合并期间,它将全部变成地狱"在一次变革中,也可能更加地狱!这就是原因:

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

当您进行rebase而不是merge时,为了同样的冲突,您必须执行冲突解决的次数,直到您提交rebase的次数为止!

真实场景

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

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

git merge: 我发生了冲突。我看到他们要掌握的变化并将其与我的分支(最终产品)合并。完成。

git rebase: 我与第一次提交发生冲突。我解决冲突并继续改变。 我与第二次提交发生冲突。我解决冲突并继续改变。 我与第三次提交发生冲突。我解决冲突并继续改变。 我与第四次提交发生冲突。我解决冲突并继续改变。 我与第五次提交发生冲突。我解决冲突并继续改变。 我与第六次提交发生冲突。我解决冲突并继续改变。 我与第七次提交发生冲突。我解决冲突并继续改变。 我与第八次提交发生冲突。我解决冲突并继续改变。 我与第九次提交发生冲突。我解决冲突并继续改变。 我与第十次提交发生冲突。我解决冲突并继续改变。 我与第十一次提交发生冲突。我解决冲突并继续改变。 我与第十二次提交发生冲突。我解决冲突并继续改变。 我与第十三次提交发生冲突。我解决冲突并继续改变。 我与第十四次提交发生冲突。我解决冲突并继续改变。 我与第十五次提交发生冲突。我解决冲突并继续改变。

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

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

原因#2:使用rebase,没有撤消!

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

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

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

经过重组后,无法确定最初提交的内容以及由于解决冲突而引入的内容。

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

解决冲突解决方案:使用diff3

以此冲突为例:

<<<<<<< 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)

一般来说:

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

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

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

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

    1. 确定我们当前已检出的分支(HEAD--ours)或我们正在合并的分支(origin/feature1--theirs)是一个更简单的应用变化。使用带有三点的差异(git diff a...b)显示自b的最后一次偏离后a发生的变化,或者换句话说,将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. 查看更复杂的文件版本。这将删除所有冲突标记并使用您选择的一侧。

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

答案 1 :(得分:358)

“冲突”是指“相同内容的并行演变”。因此,如果它在合并期间“完全地狱”,则意味着您在同一组文件上进行了大规模的演变。

因为rebase比合并更好的原因是:

  • 您使用其中一个重写您的本地提交历史记录(然后重新应用您的工作,然后解决任何冲突)
  • 最终合并肯定会是一个“快进”的合并,因为它将拥有主人的所有提交历史记录,以及只有你重新申请的更改。

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

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


关于该主题(rebase然后合并工作流程),barraponto在评论中提到两个有趣的帖子,均来自randyfay.com

  

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

(类似技术exists for bazaar

答案 2 :(得分:31)

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

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

回想一下,merge实际上创建了一个有两个父节点的节点。现在考虑以下情况:我有两个独立的特征B和A,现在想要在特征分支C上开发东西,这取决于A和B,而A和B正在接受审查。

我做的是以下内容:

  1. 在A。
  2. 之上创建(和结帐)分支C.
  3. 与B合并
  4. 现在分支C包括A和B的变化,我可以继续开发它。如果我对A做了任何更改,那么我将按以下方式重建分支图:

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

答案 3 :(得分:21)

请勿使用git push origin --mirror几乎任何情况。

它不会询问您是否确定要执行此操作,并且最好确定,因为它会清除所有不在本地盒子上的远程分支。

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

答案 4 :(得分:14)

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

git checkout master
git pull origin
git checkout my_new_feature

在您的功能分支中执行'git rebase / merge master'之前?

因为您的主分支不会自动从您朋友的存储库更新。您必须使用git pull origin执行此操作。即也许你总是会从一个永不改变的本地主分支变回来?然后来推送时间,你正在推进一个存储库,它有你从未见过的(本地)提交,因此推送失败。

答案 5 :(得分:13)

在你的情况下,我认为你的伴侣是正确的。关于变基的好处是,对于局外人来说,你的变化看起来就像他们所有人都是以干净的顺序发生的。这意味着

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

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

文章Packaging software using Git做了相当不错的工作,解释了合并与变基的权衡。这是一个不同的背景,但主体是相同的 - 它主要归结为您的分支机构是公共的还是私有的,以及您计划如何将它们集成到主线。

答案 6 :(得分:12)

  

无论如何,我在最近的一个分支上关注我的工作流程,当我试图将它合并回主人时,一切都进入了地狱。对于应该不重要的事情存在大量冲突。冲突对我来说毫无意义。我花了一天的时间来解决所有事情,并最终被强制推向远程主人,因为我的当地主人解决了所有冲突,但是远程主人仍然不满意。

在您的合作伙伴或建议的工作流程中,您都不应该遇到没有意义的冲突。即使您有,如果您遵循建议的工作流程,那么在解决后不应该要求“强制”推送。它表明你实际上没有合并你所推动的分支,但是必须推动一个不是远程技巧后代的分支。

我认为您需要仔细查看发生的事情。其他人是否(有意或无意)在创建本地分支和尝试将其合并回本地分支的点之间重绕远程主分支?

与许多其他版本控制系统相比,我发现使用Git可以减少对工具的影响,并且可以让您开始处理源流的基础问题。 Git不会执行魔法,因此冲突的更改会导致冲突,但它应该通过跟踪提交父级来轻松完成写入事务。

答案 7 :(得分:7)

从我观察到的情况来看,git merge趋向于在合并之后保持分支分离,而rebase然后merge将它组合成一个单独的分支。 后者更清晰,而在前者中,即使在合并之后,也更容易找出哪些提交属于哪个分支。

答案 8 :(得分:6)

“即使你是一个只有少数分支机构的开发人员,也值得养成使用rebase并正确合并的习惯。基本工作模式如下:

  • 从现有分支A

  • 创建新分支B.
  • 在分支B上添加/提交更改

  • 来自分支A的重新更新

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

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

答案 9 :(得分:2)

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

答案 10 :(得分:0)

我仅使用rebase工作流,因为它在视觉上更加清晰(不仅在GitKraken中,而且在Intellij和gitk中,但我最推荐第一个):您有一个分支,它起源于主人,然后又回到主人那里。当图表干净漂亮时,您将知道一无所有

enter image description here

我的工作流程与您的工作流程几乎相同,但只有一点点不同:我squashrebase分支之前在本地分支中提交了master上的最新更改,因为:

  

rebase基于每次提交

这意味着,如果您有15次提交更改了与master相同的行,则您必须检查15次是否不挤压,但是最后的结果是正确的吗?

因此,整个工作流程是:

  1. 签出到master并拉出以确保您具有最新版本

  2. 从此处创建一个新分支

  3. 在那里工作,您可以自由地提交几次,然后放到远程,不用担心,因为它是您的分支。

  4. 如果有人告诉您,“嘿,我的PR / MR已获批准,现在已合并到母版中”,您可以提取/拉取它们。您可以随时执行第6步。

  5. 完成所有工作后,提交它们,并如果有几次提交,将它们压扁(它们都是您的工作,而更改代码行的次数却没有事项;唯一重要的是最终版本)。推或不推都没关系。

  6. 再次签出到masterpull,以确保您在本地拥有最新的master。您的图表应与此类似:

enter image description here

如您所见,您位于本地分支机构,该分支机构源于master的过时状态,而master(本地和远程)都随着同事的变化而向前发展。 / p>

  1. 结帐回到您的分支机构,然后重新建立基础。现在,您只需执行一次提交,就可以仅解决一次。(在GitKraken中,您只需将分支拖到master上,然后选择“ Rebase”即可;这是另一个原因为什么我喜欢它。) 之后,您将像:

enter image description here

  1. 因此,现在,您具有最新master上的所有更改以及分支上的更改。现在,您可以推到遥控器了,如果您以前曾推过,则必须强行推入; Git会告诉您,您不能简单地快进。这是正常的,由于有了基础,您已经更改了分支的起点。但您不必担心:明智地使用武力。最后,遥控器也是您的分支您的分支,因此即使您做错了任何事情,也不会影响master

  2. 创建PR / MR并等待其被批准,因此master将有您的贡献。恭喜!因此,您现在可以签出到master,进行更改,并删除本地分支以清理该图。如果将远程分支合并到主分支中时没有执行此操作,则也应删除该分支。

最后的图又干净又清晰:

enter image description here