关于版本控制:Git工作流和Rebase vs合并问题

Git workflow and rebase vs merge questions

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

我听说Git非常适合分支和合并,到目前为止,我还没有看到它。当然,分支是非常简单的,但是当我尝试合并时,一切都会陷入地狱。现在,我已经习惯了来自SVN的方法,但在我看来,我只是用一个次标准版本系统换另一个。

我的搭档告诉我,我的问题源于我不情愿地合并的愿望,我应该在很多情况下使用REBASE而不是合并。例如,他制定的工作流程如下:

1
2
3
4
5
6
7
8
9
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

基本上,创建一个功能分支,总是从主分支到分支重定基,并从分支合并回主分支。需要注意的是,分支始终保持在本地。

以下是我开始使用的工作流

1
2
3
4
5
6
7
8
9
10
11
12
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

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

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

我对合并而不是重新平衡的推理是,合并似乎是标准的,重新平衡似乎是一个高级特性。我的直觉是,我正在尝试做的不是一个先进的设置,所以重新平衡应该是不必要的。我甚至读过关于Git的新的实用编程书籍,它们涉及广泛的合并,几乎没有提到Rebase。

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

什么是这样的"正确"工作流程?Git应该让分支和合并变得非常简单,我只是没有看到它。

更新2011-04-15

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

原来的工作流程是正确的,至少在我们的例子中是这样。换言之,这就是我们所做的,它起作用:

1
2
3
4
5
6
7
8
9
10
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的承诺,即使它们在我们的分支上有点混乱。所以,这就是我们要做的:

1
2
3
4
5
6
7
8
9
10
11
12
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(其他?)拉请求-如果您想知道合并/重新平衡与拉请求之间的关系,我建议您按照以上所有步骤进行操作,直到准备好合并回master。与手动与Git合并不同,您只接受pr。请注意,这不会进行挤压合并(至少在默认情况下不会),但非挤压、非快进是pull请求社区(据我所知)中接受的合并约定。具体来说,它的工作方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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

我爱上了吉特,不想回到SVN。如果你在挣扎,坚持下去,最终你会看到隧道尽头的光明。


DR

Git-Rebase工作流并不能保护您免受那些不擅长解决冲突的人或那些习惯于SVN工作流的人的伤害,就像在避免Git灾难时建议的那样:一个血淋淋的故事。这只会使冲突解决变得更为繁琐,并使他们很难从糟糕的冲突解决中恢复过来。相反,使用diff3,这样一开始就不那么困难了。好的。重新平衡工作流并不能更好地解决冲突!

我非常支持清理历史。但是,如果我遇到冲突,我会立即中止REBASE并进行合并!人们推荐一个Rebase工作流作为解决冲突的合并工作流的更好替代方案(这正是这个问题的目的),这真的让我丧命。好的。

如果它在合并过程中"全部下地狱",那么在重新平衡过程中它将"全部下地狱",而且可能会有更多的地狱!这就是为什么:好的。原因1:解决冲突一次,而不是每次提交一次

当您重新平衡而不是合并时,对于同一个冲突,您必须执行冲突解决,直到您提交重新平衡时为止!好的。真实场景

我从master分支以重构分支中的复杂方法。我的重构工作总共由15个提交组成,我的工作是重构它并获得代码审查。重构的一部分涉及修复master中以前存在的混合选项卡和空格。这是必要的,但不幸的是,它将与以后在master中对此方法所做的任何更改相冲突。当然,当我处理这个方法时,有人对主分支中的同一个方法做了一个简单的、合法的更改,该更改应该与我的更改合并在一起。好的。

当需要将分支与master合并时,我有两个选项:好的。

Git合并:我有冲突。我看到他们对master所做的更改,并将其与我的分支(分支的最终产品)合并。完成。好的。

Git ReBase:我的第一个承诺与我的冲突。我解决了冲突,继续重新平衡。我和第二次承诺有冲突。我解决了冲突,继续重新平衡。我和我的第三次承诺有冲突。我解决了冲突,继续重新平衡。我和我的第四次承诺有冲突。我解决了冲突,继续重新平衡。我和我的第五个承诺有冲突。我解决了冲突,继续重新平衡。我和我的第六个承诺有冲突。我解决了冲突,继续重新平衡。我和我的第七个承诺有冲突。我解决了冲突,继续重新平衡。我和我的第八个承诺有冲突。我解决了冲突,继续重新平衡。我和我第九次承诺有冲突。我解决了冲突,继续重新平衡。我和我的第十个承诺有冲突。我解决了冲突,继续重新平衡。我和我的第十一个承诺有冲突。我解决了冲突,继续重新平衡。我和我的第十二个承诺有冲突。我解决了冲突,继续重新平衡。我和我的第十三个承诺有冲突。我解决了冲突,继续重新平衡。我和我的第十四次承诺有冲突。我解决了冲突,继续重新平衡。我和我的第十五个承诺有冲突。我解决了冲突,继续重新平衡。好的。

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

通过你需要做的所有额外的冲突解决,它只会增加你犯错误的可能性。但在Git中错误是可以纠正的,对吧?当然,除了……好的。原因2:使用REBASE,没有撤消!

我认为我们都同意解决冲突是困难的,而且有些人很不擅长解决冲突。它很容易出错,这就是为什么它如此伟大,Git使得它很容易撤销!好的。

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

当你重新平衡一个分支时,在冲突解决可能是错误的情况下,你就完蛋了。现在每次提交都包含错误的合并,您不能只重新执行REBASE*。充其量,你必须回去修改每一个受影响的承诺。不好玩。好的。

在重新平衡之后,就不可能确定什么是最初的承诺的一部分,什么是由于糟糕的冲突解决而引入的。好的。

>如果可以从Git的内部日志中挖掘旧的refs,或者如果创建第三个分支,指向重新定位前的最后一次提交,则可以撤消重新定位。好的。解决冲突:使用diff3

以这种冲突为例:好的。

1
2
3
4
5
<<<<<<< HEAD
TextMessage.send(:include_timestamp => true)
=======
EmailMessage.send(:include_timestamp => false)
>>>>>>> feature-branch

从冲突的角度来看,不可能知道每个分支的变化或意图是什么。在我看来,这是冲突解决令人困惑和困难的最大原因。好的。

救不了你!好的。

1
git config --global merge.conflictstyle diff3

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

1
2
3
4
5
6
7
<<<<<<< HEAD
TextMessage.send(:include_timestamp => true)
||||||| merged common ancestor
EmailMessage.send(:include_timestamp => true)
=======
EmailMessage.send(:include_timestamp => false)
>>>>>>> feature-branch

首先检查合并的共同祖先。然后比较每一边以确定每个分支的意图。你可以看到那个头把emailmessage改成了textmessage。它的目的是改变用于textmessage的类,传递相同的参数。您还可以看到featurebranch的目的是为:include_timestamp选项传递false而不是true。要合并这些更改,请结合以下两种意图:好的。

1
TextMessage.send(:include_timestamp => false)

一般来说:好的。

  • 比较每个分支的共同祖先,并确定哪个分支的更改最简单
  • 将这个简单的更改应用到另一个分支的代码版本,这样它就包含了更简单和更复杂的更改
  • 删除冲突代码的所有部分,而不是将更改合并到一起的部分
  • 替代:通过手动应用分支的更改来解决

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

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

  • 决定我们当前签出的分支机构(HEAD--ours或我们要合并的分支机构(origin/feature1--theirs是一个更简单的变更。使用带三点的diff(git diff a...b显示了b上一次与a偏离以来发生的变化,也就是说,将a和b的共同祖先与b进行比较。好的。

    1
    2
    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
  • 查看文件的更复杂版本。这将删除所有冲突标记并使用您选择的边。好的。

    1
    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
  • 签出复杂更改后,拉起简单更改的差异(请参见步骤1)。将此差异中的每个更改应用于冲突文件。好的。

  • 好啊。


    "冲突"是指"相同内容的平行演变"。所以,如果在合并过程中它"全部下地狱",就意味着在同一组文件上有大量的进化。

    那么,Rebase优于Merge的原因是:

    • 您用一个主服务器重写本地提交历史(然后重新应用您的工作,解决任何冲突)
    • 最后的合并肯定是一个"快进"合并,因为它将具有主服务器的所有提交历史记录,并且只包含要重新应用的更改。

    我确认在这种情况下,正确的工作流程(通用文件集上的演进)是先重置基,然后合并。

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

    关于这个主题(重新平衡然后合并工作流),Barraponto在评论中提到了两个有趣的帖子,都来自randyfay.com:

    • Git的Rebase工作流:提醒我们先获取,Rebase:

    Using this technique, your work always goes on top of the public branch like a patch that is up-to-date with current HEAD.

    (集市也有类似的技术)

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


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

    然而,即使在一个基本上基于REBASE的工作流中,也有合并的地方。

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

    接下来我要做的是:

  • 在A的顶部创建(和签出)分支C。
  • 与B合并
  • 现在,分支C包含了来自A和B的更改,我可以继续开发它。如果我对a做了任何更改,那么我将按以下方式重建分支图:

  • 在A的新顶部创建分支T
  • 合并T与B
  • 重新设置为T
  • 删除分支T
  • 通过这种方式,我实际上可以维护任意的分支图,但是如果在父级更改时没有自动工具来执行重新平衡,那么做比上面描述的情况更复杂的事情已经太复杂了。


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

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

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


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

    1
    2
    3
    git checkout master
    git pull origin
    git checkout my_new_feature

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

    因为您的主分支不会从您朋友的存储库中自动更新。你得用git pull origin来做。也就是说,也许你总是会从一个永不改变的本地主分支机构重新获得资金?然后到了推送时间,您将推送到一个存储库中,该存储库具有您从未见过的(本地)提交,因此推送失败。


    在你的情况下,我认为你的搭档是对的。重新平衡的好处在于,对于局外人来说,你的改变看起来都是以一个干净的顺序发生的,所有的都是自己发生的。这意味着

    • 您的更改很容易查看
    • 您可以继续进行漂亮的小提交,但是您可以同时公开这些提交集(通过合并到master)。
    • 当您查看public master分支时,您会看到不同的开发人员对不同功能的不同系列提交,但它们不会混合在一起。

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

    文章使用git打包软件,很好地解释了合并与重新平衡的权衡。这是一个有点不同的上下文,但是主体是相同的——它基本上取决于您的分支是公共的还是私有的,以及您计划如何将它们集成到主线中。


    Anyway, I was following my workflow on a recent branch, and when I tried to merge it back to master, it all went to hell. There were tons of conflicts with things that should have not mattered. The conflicts just made no sense to me. It took me a day to sort everything out, and eventually culminated in a forced push to the remote master, since my local master has all conflicts resolved, but the remote one still wasn't happy.

    无论是你的合作伙伴还是你建议的工作流程,你都不应该遇到没有意义的冲突。即使你已经这样做了,如果你正在遵循建议的工作流程,那么在解决后,不需要"强制"推送。这表明您实际上没有将要推到的分支合并到一起,但必须推一个不是远程尖端后代的分支。

    我想你得仔细看看发生了什么。是否有其他人(故意或不故意)在创建本地分支和尝试将其合并回本地分支的点之间重绕了远程主分支?

    与许多其他版本控制系统相比,我发现使用Git可以减少对该工具的攻击,并使您能够处理对源代码流至关重要的问题。Git不执行魔法,因此冲突的更改会导致冲突,但它应该通过跟踪提交父级来简化写操作。


    从我观察到的情况来看,Git合并倾向于在合并后保持分支分离,而Rebase然后合并将其合并为一个分支。后者更清晰,而前者则更容易发现哪一个提交在合并之后属于哪个分支。


    "即使您是一个只有几个分支的开发人员,也有必要养成正确使用REBASE和合并的习惯。基本工作模式如下:

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

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

    • 重新设置分支A的更新

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

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


    对于Git,没有"正确"的工作流。用任何能浮在船上的东西。但是,如果在合并分支时经常遇到冲突,那么您应该更好地与其他开发人员协调您的工作吗?听起来你们俩一直在编辑同一个文件。另外,注意空格和Subversion关键字(即"$id$"和其他关键字)。