1020

几个月来,我和其他开发人员一起在一个项目中使用了 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

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

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

我对合并而不是 rebase 的理由是,merge 似乎是标准的,而 rebase 似乎是一个高级功能。我的直觉是我想做的不是高级设置,所以 rebase 应该是不必要的。我什至仔细阅读了关于 Git 的新实用程序设计书,它们广泛地涵盖了合并,几乎没有提到变基。

无论如何,我在最近的一个分支上遵循我的工作流程,当我试图将它合并回 master 时,一切都变得糟糕透顶。与本应无关紧要的事情发生了很多冲突。这些冲突对我来说毫无意义。我花了一天的时间整理一切,最终以强制推送到远程 master 达到高潮,因为我的本地 master 已经解决了所有冲突,但远程 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

事实上,我们的工作流程有点不同,因为我们倾向于进行squash 合并而不是原始合并。(注意:这是有争议的,见下文。)这允许我们将整个功能分支变成 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

Squash Merge Controversy - 正如一些评论者所指出的,squash 合并将丢弃您功能分支上的所有历史记录。顾名思义,它将所有提交压缩为一个。对于小功能,这是有道理的,因为它将它压缩成一个包。对于更大的功能,这可能不是一个好主意,特别是如果您的个人提交已经是原子的。这真的取决于个人喜好。

Github 和 Bitbucket(其他?)Pull Requests - 如果您想知道合并/rebase 与 Pull Requests 的关系,我建议您按照上述所有步骤操作,直到您准备好合并回 master。无需手动与 git 合并,您只需接受 PR。请注意,这不会进行 squash 合并(至少默认情况下不会),但非 squash、非快进是 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。如果你在挣扎,那就坚持下去,最终你会看到隧道尽头的曙光。

4

11 回答 11

414

TL;博士

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


Rebase 工作流程并不适合解决冲突!

我非常支持清理历史记录。但是,如果我遇到冲突,我会立即中止变基并进行合并!人们推荐使用 rebase 工作流程作为解决冲突的合并工作流程的更好替代方案,这真的让我很生气(这正是这个问题的意义所在)。

如果它在合并期间“全部下地狱”,它会在 rebase 期间“下地狱”,并且可能还有更多的地狱!原因如下:

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

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

真实场景

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

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

git merge: 我遇到了冲突。我看到他们对 master 所做的更改并将其与我的分支(的最终产品)合并。完毕。

git rebase: 我的第一次提交发生了冲突。我解决了冲突并继续变基。我与第二次提交发生冲突。我解决了冲突并继续变基。我与第三次提交发生冲突。我解决了冲突并继续变基。我与第四次提交发生冲突。我解决了冲突并继续变基。我与第五次提交发生冲突。我解决了冲突并继续变基。我与第六次提交发生冲突。我解决了冲突并继续变基。我和我的第七个人发生冲突犯罪。我解决了冲突并继续变基。我与我的第八次提交发生冲突。我解决了冲突并继续变基。我与第九次提交发生冲突。我解决了冲突并继续变基。我与第十次提交发生冲突。我解决了冲突并继续变基。我与第十一次提交发生冲突。我解决了冲突并继续变基。我与第十二次提交发生冲突。我解决了冲突并继续变基。我与我的第十三次提交发生冲突。我解决了冲突并继续变基。我和我的十四岁发生冲突犯罪。我解决了冲突并继续变基。我与我的第十五次提交发生冲突。我解决了冲突并继续变基。

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

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

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

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

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

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

在 rebase 之后,无法确定提交的最初部分是什么,以及由于冲突解决不当而引入的内容。

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

摆脱冲突解决:使用 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. 删除所有冲突代码部分,而不是您刚刚将更改合并到的部分

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

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

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

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

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

于 2012-06-27T04:25:29.937 回答
386

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

变基比合并更好的原因是:

  • 你用主人之一重写你的本地提交历史(然后重新应用你的工作,然后解决任何冲突)
  • 最终的合并肯定是一个“快进”的合并,因为它将拥有 master 的所有提交历史,加上只有你的更改才能重新应用。

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

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


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

使用这种技术,您的工作总是在公共分支之上进行,就像一个与当前HEAD.

( bazaar 也存在类似的技术)

于 2009-01-19T15:32:24.947 回答
35

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

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

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

这样我实际上可以维护任意的分支图,但是做一些比上述情况更复杂的事情已经太复杂了,因为没有自动工具在父项更改时进行变基。

于 2009-05-12T20:54:12.417 回答
26

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

它不会询问您是否确定要这样做,而且您最好确定,因为它会清除所有不在本地机器上的远程分支。

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

于 2009-04-10T01:06:06.243 回答
15

在你的情况下,我认为你的伴侣是正确的。变基的好处在于,对于局外人来说,您的更改看起来就像它们都是以干净的顺序发生的一样。这表示

  • 您的更改很容易查看
  • 您可以继续进行良好的小型提交,但您可以一次将这些提交的集合公开(通过合并到 master 中)
  • 当您查看公共主分支时,您会看到不同开发人员针对不同功能的不同系列提交,但它们不会全部混合

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

使用 Git 打包软件一文很好地解释了合并与变基之间的权衡。这是一个有点不同的上下文,但原则是相同的——它基本上归结为你的分支是公共的还是私有的,以及你计划如何将它们集成到主线中。

于 2009-01-19T16:15:02.367 回答
15

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

git checkout master
git pull origin
git checkout my_new_feature

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

因为您的主分支不会从您朋友的存储库中自动更新。您必须使用git pull origin. 即,也许你总是从一个永不改变的本地主分支变基?然后到了推送时间,您正在推送一个存储库,其中包含您从未见过的(本地)提交,因此推送失败。

于 2009-05-25T20:52:22.290 回答
13

无论如何,我在最近的一个分支上遵循我的工作流程,当我试图将它合并回 master 时,一切都变得糟糕透顶。与本应无关紧要的事情发生了很多冲突。这些冲突对我来说毫无意义。我花了一天的时间整理一切,最终以强制推送到远程 master 达到高潮,因为我的本地 master 已经解决了所有冲突,但远程 master 仍然不高兴。

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

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

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

于 2009-01-19T19:30:25.967 回答
11

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

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

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

  • 来自分支 A 的变基更新

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

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

于 2012-03-22T16:16:53.250 回答
8

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

于 2011-08-25T05:40:35.623 回答
5

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

于 2009-01-19T15:25:30.750 回答
2

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

在此处输入图像描述

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

rebase基于每个提交工作

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

所以,整个工作流程是:

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

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

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

  4. 如果有人告诉你,“嘿,我的 PR/MR 被批准了,现在它被合并到 master”,你可以获取它们/拉取它们。您可以随时或在第 6 步中执行此操作。

  5. 完成所有工作后,提交它们,如果您有多个提交,则将它们压缩(它们都是您的工作,您更改一行代码多少次无关紧要;唯一重要的是最终版本)。推不推,无所谓。

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

在此处输入图像描述

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

  1. 结帐回您的分支,然后变基为 master。您现在将只有一次提交,因此您只需解决一次冲突。(在 GitKraken 中,您只需将分支拖到master并选择“Rebase”;我喜欢它的另一个原因。)之后,您将像:

在此处输入图像描述

  1. 所以现在,您拥有 latest 上的所有更改master,以及您的分支上的更改。您现在可以推送到您的遥控器,如果您之前推送过,则必须强制推送;Git 会告诉你不能简单地快进。这很正常,因为 rebase,你已经改变了你的分支的起点。但你不应该害怕:明智地使用武力,但不要害怕。最后,遥控器也是你的分支master,所以即使你做错了也不会影响。

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

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

在此处输入图像描述

于 2019-12-19T14:59:52.973 回答