Git工作流程和变基与合并问题


970

我已经在与另一个开发人员的项目中使用Git几个月了。我在SVN方面拥有多年的经验,所以我想我为这段感情带来了很多麻烦。

我听说Git非常适合分支和合并,到目前为止,我只是看不到它。当然,分支是非常简单的,但是当我尝试合并时,一切都会陷入困境。现在,我已经习惯了SVN,但是在我看来,我只是将一个低于标准的版本控制系统换成了另一个。

我的伴侣告诉我,我的问题源于我想要合并Willy-nilly的愿望,在许多情况下,我应该使用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

本质上,创建一个功能分支,始终将其从master转移到master,然后从分支合并回到master。需要注意的重要一点是,分支始终位于本地。

这是我开始的工作流程

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而不是重新设置基础,并且将功能分支(和功能分支的提交)推送到远程存储库。

我之所以选择远程分支机构,是因为我希望在工作时备份自己的工作。我们的存储库将自动备份,如果出现问题可以将其还原。我的笔记本电脑不是,还是不够彻底。因此,我讨厌笔记本电脑上没有其他地方镜像的代码。

我之所以选择合并而不是重新设置基准,是因为合并似乎是标准的,而重新确定基准似乎是一项高级功能。我的直觉是我要尝试的不是高级设置,因此不需要重新设置基准。我什至仔细阅读过有关Git的新《实用程序设计》一书,它们涵盖了合并的内容,几乎没有提及重新设置基础。

无论如何,我在最近的分支上跟踪我的工作流程,当我尝试将其合并回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(其他?)拉取请求 -如果您想知道合并/变迁与拉取请求之间的关系,我建议您按照上述所有步骤进行操作,直到准备好合并回主服务器为止。您只需接受PR,而不是与git手动合并。请注意,这不会进行壁球合并(至少默认情况下不是),但是在请求请求社区中,非壁球,非快进是可接受的合并约定(据我所知)。具体来说,它的工作方式如下:

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了。如果您在挣扎,请坚持下去,最终您会在隧道尽头看到光明。


31
不幸的是,新的Pragmstic编程手册主要是在仍然使用SVN进行思考的同时使用Git编写的,在这种情况下,它误导了您。在Git中,rebase使事情尽可能简单。您的经验可以告诉您,您的工作流程在Git中不起作用,而不是Git不起作用。
保罗

18
在这种情况下,我不建议壁球合并,因为它不保存任何有关合并内容的信息(就像svn一样,但这里没有mergeinfo)。
Marius K

7
喜欢底部的音符,我曾有过与Git斗争的类似经历,但现在却很难想象不使用它。感谢您的最终解释过,帮助了很多与rebase理解
乔恩Phenow

6
完成功能后,是否不应该在合并new_feature和master之前重新设置基础?
softarn 2011年

17
您的工作流将从已删除的分支中丢失所有提交历史记录:(
Max Nanasy 2012年

Answers:


371

“冲突”是指“相同内容的平行演变”。因此,如果在合并过程中“全部陷入困境”,则意味着您在同一组文件上进行了大量改进。

相比于合并,重组之所以更好的原因是:

  • 您使用主数据库之一重写了本地提交历史记录(然后重新应用您的工作,然后解决了所有冲突)
  • 最后的合并肯定是“快进”的合并,因为它将具有主数据库的所有提交历史记录,并且仅包含您要重新应用的更改。

我确认在这种情况下,正确的工作流程(在一组通用文件上的进化)首先要重新设置基础,然后再合并

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


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

使用此技术,您的工作将始终像最新的补丁一样在公共分支的顶部HEAD

存在与集市类似的技术)


27
有关允许重新
定级

2
randyfay.com/node/91randyfay.com/node/89不错的读物。这些文章使我了解了我的工作流程所带来的麻烦以及理想的工作流程。
Capi Etheriel'3

只是为了弄清楚,从master分支重新定位到您的本地,基本上是为了更新您的本地可能错过的历史,因为在进行任何种类的合并后,master都不会知道?
hellatan 2012年

@dtan我在这里描述的是在master之上重新建立本地。您不是完全更新本地历史记录,而是在主数据库上重新应用本地历史记录,以解决本地分支机构中的任何冲突。
VonC 2012年

385

TL; DR

git rebase工作流无法保护您免受冲突解决能力不佳的人或习惯于SVN工作流的人们的攻击,如避免Git灾难:血腥的故事中建议的那样。这只会使冲突解决对他们而言更加乏味,并且使他们很难从不良的冲突解决中恢复。相反,请使用diff3,这样一来就不那么困难。


重新设置工作流不是解决冲突的更好方法!

我非常赞成清理历史记录。但是,如果遇到冲突,我会立即终止变基并改为合并!人们真的建议重新设置工作流作为冲突解决的合并工作流的更好替代方案(这正是这个问题的实质),这真让我感到震惊。

如果在合并过程中“全部变为地狱”,则在重新设置基准时将“全部变为地狱”,并且可能还会产生更多地狱!原因如下:

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

当您重新设置基准而不是合并时,对于相同的冲突,您将必须执行最多次要重新确定基准的冲突解决!

真实场景

我从master分支出来,以重构分支中的复杂方法。我的重构工作包括15个提交,这是我进行重构并获得代码审查的结果。我的重构工作包括修复master中以前存在的混合制表符和空格。这是必要的,但是不幸的是,它将与之后在master中对该方法进行的任何更改相冲突。确实,当我在使用此方法时,有人对应该与我的更改合并的master分支中的同一方法进行了简单,合法的更改。

是时候将分支与master合并回去了,我有两个选择:

git merge: 我遇到了冲突。我看到他们为掌握并将其与分支的最终产品合并而做出的更改。做完了

git rebase:第一次提交时发生冲突。我解决了冲突,并继续进行调整。我第二次提交时发生冲突。我解决了冲突,并继续进行调整。我与第三次提交发生冲突。我解决了冲突,并继续进行调整。我与第四次提交发生冲突。我解决了冲突,并继续进行调整。我与第五次提交发生冲突。我解决了冲突,并继续进行调整。我第六次提交时发生冲突。我解决了冲突,并继续进行调整。我和第七个孩子发生冲突承诺。我解决了冲突,并继续进行调整。我与第八次提交发生冲突。我解决了冲突,并继续进行调整。我与第九次提交发生冲突。我解决了冲突,并继续进行调整。我与第十次提交发生冲突。我解决了冲突,并继续进行调整。我与第十一次提交发生冲突。我解决了冲突,并继续进行调整。我与第十二次提交发生冲突。我解决了冲突,并继续进行调整。我与第十三次提交发生冲突。我解决了冲突,并继续进行调整。我与十四岁时发生冲突承诺。我解决了冲突,并继续进行调整。我与第十五次提交发生冲突。我解决了冲突,并继续进行调整。

如果是您的首选工作流程,您必须在跟我开玩笑。它所要做的只是一个空白修复程序,该修复程序与在master上所做的一个更改冲突,并且每次提交都将发生冲突并且必须解决。这是一个只有空格冲突的简单情况。但愿你有涉及到整个文件的主要代码更改一个真正的冲突,必须解决的是多次。

使用您需要做的所有额外冲突解决方案,这只会增加您犯错误的可能性。但是git中的错误很好,因为您可以撤消,对吗?当然除外...

原因2:有了基准,就无法撤消!

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

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

为分支重新设置基准时,如果冲突解决方案执行不正确,则很可能会陷入困境。现在,每个提交都包含错误的合并,您不能只重做rebase *。充其量,您必须返回并修改每个受影响的提交。不好玩。

重新设置基准之后,就无法确定最初是提交的一部分以及由于不良冲突解决而引入的内容。

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

TextMessage.send(:include_timestamp => false)

一般来说:

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

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

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

让我们看看我们怎样才能在一个场景合并解决冲突origin/feature1,其中lib/message.rb冲突。

  1. 确定我们当前签出的分支(HEAD--ours)或我们要合并的分支(origin/feature1--theirs)是更简单的更改。将diff与三点(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)。将此差异的每个更改应用到冲突文件。


4
一次合并所有冲突如何比单个提交更好地工作?我已经从合并单个提交中遇到了问题(尤其是那些不会将提交分解为逻辑部分并提供足够的测试以进行验证的人)。此外,在备份选项方面,rebase并没有比合并更糟糕,智能使用交互式rebase和诸如tortoisegit之类的工具(可以选择要包含的提交)将大有帮助。
prusswan 2012年

8
我觉得我已经解决了#1中的原因。如果单个提交在逻辑上不一致,则更需要合并逻辑上一致的分支,以便您可以真正理解冲突。如果提交1有问题并且提交2对其进行了修复,则合并提交1将造成混乱。有合理的理由,您可能会连续发生15次冲突,例如我上面概述的冲突。同样,您对于变基不恶化的论点也是没有根据的。Rebase将错误的合并混入原始的良好提交中,并且不会留下良好的提交让您重试。合并。
爱德华·安德森

6
我完全同意您的观点。好帖子 清除了一些东西。我不知道rerere是否会对您有所帮助。另外,感谢您提出使用diff3的建议,我现在肯定会切换到diff3。
德里克2012年

45
+1告诉我有关diff3的信息-经常在一次难以理解的冲突中咒骂谁负责不告诉我共同祖先必须说什么。非常感谢你。
约翰

4
这应该是公认的答案。变基工作流也很可怕,因为它掩盖了一个事实,即代码库在某个时间点上存在巨大差异,这对于了解您是否想了解正在编写的代码是如何编写的很有用。只有没有冲突的小分支才应重新建立为主节点。
罗伯特·鲁格(RobertRüger)2014年

32

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

但是,即使在主要基于rebase的工作流中,也有合并的地方。

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

接下来,我要做的是:

  1. 在A的顶部创建(和签出)分支C。
  2. 与B合并

现在,分支C包含来自A和B的更改,我可以继续对其进行开发。如果我对A进行任何更改,则可以通过以下方式重构分支图:

  1. 在A的新顶部创建分支T
  2. 将T与B合并
  3. 将C重设到T
  4. 删除分支T

通过这种方式,我实际上可以维护分支的任意图,但是要进行比上述情况更复杂的操作已经太复杂了,因为在父级发生更改时没有自动工具可以进行重新基准。


1
只需重新设置基准即可实现相同的目的。实际上,这里不需要合并(除非您不想重复提交-但我几乎不认为这是一个参数)。
猫头鹰2009年

1
确实,我不想重复提交。我想保持工作中的飞行结构尽可能整洁。但这是个人喜好的问题,不一定适合所有人。
Alex Gontmakher 2009年

我100%同意第一段。(@Edward的答案在并非如此的情况下有效,但是我宁愿让世界上所有的项目都按照您的建议工作)。其余的答案似乎有些牵强,因为在A和B进行时进行C的工作已经有点冒险了(至少在一定程度上确实取决于A和B),甚至最终可能不会保留合并(C将基于最新和最大的合并重新建立)。
Alois Mahdal '18

22

在几乎任何情况下都不要使用git push origin --mirror。

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

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


6
还是不要做一些不确定结果的事情?我以前用来管理的机器已安装 Instructions to this machine may lead to unintended consequences, loss of work/data, or even death (at the hands of the sysad). Remember that you are solely responsible for the consequences of your actions 在MOTD中。
richo 2011年

如果您有镜像的回购协议,请使用它(尽管在我的情况下,它现在由特殊用户在后接收挂钩上的源回购协议中执行)
prusswan 2012年

14

阅读您的解释后,我有一个问题:可能是您从未做过

git checkout master
git pull origin
git checkout my_new_feature

在功能分支中执行“ git rebase / merge master”之前?

因为您的 master分支不会从您朋友的存储库中自动更新。您必须使用git pull origin。也就是说,也许您总是会从一个不变的本地master分支中重新定位?然后是推送时间,您要推送的存储库中包含您从未见过的(本地)提交,因此推送失败。


13

根据您的情况,我认为您的伴侣是正确的。进行基础调整的好处是,对于外部人员而言,您的更改看起来像是全部都是干净整齐地自己完成的。这表示

  • 您的更改很容易检查
  • 您可以继续做出不错的小型提交,但是您可以一次将所有这些提交公开(通过合并到主提交中)
  • 当您查看public master分支时,您会看到不同开发人员针对不同功能进行的一系列提交,但不会完全混淆

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

使用Git打包软件一文做得很好,解释了合并与重新基准之间的权衡。上下文稍有不同,但原理相同–基本上取决于您的分支机构是公开的还是私有的,以及您打算如何将它们集成到主线中。


1
使用git到Packaging软件的链接不再起作用。我找不到很好的链接来编辑原始答案。
斯坦

您不应镜像到origin,而应镜像到第三个专用备份存储库。
Miral 2015年

12

无论如何,我在最近的分支上跟踪我的工作流程,当我尝试将其合并回master时,一切都陷入了困境。与本不应该发生的事情存在大量冲突。冲突对我来说毫无意义。我花了一天的时间来整理所有内容,最终以强迫推送到远程主机的方式达到了顶峰,因为我的本地主机解决了所有冲突,但是远程主机仍然不满意。

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

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

与许多其他版本控制系统相比,我发现使用Git涉及的工具较少,可以使您着手解决源流中最根本的问题。Git不会执行魔术操作,因此发生冲突的更改会引起冲突,但是它应该通过跟踪提交父母身份来简化写操作。


您是在暗示OP在他的流程中存在一些未发现的变基或错误,对吗?
krosenvold

7

从我观察到的情况来看,即使在合并之后,git merge也会使分支保持分离,而rebase然后合并会将其合并为一个分支。后者更干净,而在前者中,即使合并后,更容易找出哪些提交属于哪个分支。



3

使用Git,没有“正确的”工作流程。使用漂浮在船上的任何东西。但是,如果合并分支机构时经常发生冲突,也许应该与其他开发人员更好地协调工作?听起来你们两个一直在编辑相同的文件。另外,请注意空格和Subversion关键字(即“ $ Id $”和其他关键字)。


0

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

在此处输入图片说明

我的工作流程与您的工作流程几乎相同,但只有一点点不同:我squash先在本地分支中提交一个,然后在rebase分支上提交上的最新更改master,因为:

rebase每次提交的基础上工作

这意味着,如果您有15次提交更改了同一行master,那么您必须检查15次是否不挤压,但是最终结果是什么,对吗?

因此,整个工作流程是:

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

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

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

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

  5. 完成所有工作后,提交它们,如果有几次提交,请压缩它们(这些都是您的工作,更改代码行的次数无关紧要;唯一重要的是最终版本)。推或不推,都没关系。

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

在此处输入图片说明

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

  1. 结帐回到您的分支机构,并重新设置为master。现在,您只需执行一次提交,因此您只需解决一次冲突即可。(在GitKraken中,您只需将分支拖到master并选择“ Rebase”即可;我喜欢它的另一个原因。)之后,您将喜欢:

在此处输入图片说明

  1. 因此,现在,您将所有最新的更改master与分支上的更改结合在一起。现在,您可以推到遥控器了,如果您以前曾推过,则必须强行推入;Git会告诉您,您不能简单地快进。这是正常的,由于有了基础,您已经更改了分支的起点。但是您不应该害怕:明智地使用武力。最后,遥控器也是您的分支机构,因此master即使您做错了什么,您也不会受到影响。

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

最后的图又干净又清晰了:

在此处输入图片说明

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.