你什么时候使用Git rebase而不是Git合并?

什么时候建议使用Git rebase vs. Git合并?

成功重设后还需要合并吗?

772983 次浏览

简短版本

  • Merge接受一个分支中的所有更改,并在一次提交中将它们合并到另一个分支中。
  • Rebase表示我希望我分支的点移动到一个新的起点

你什么时候使用任何一个?

合并

  • 假设您为开发单个功能创建了一个分支。当您想将这些更改带回master时,您可能需要合并

重新定位

  • 第二种情况是,如果您开始进行一些开发,然后另一个开发人员进行了不相关的更改。您可能希望从存储库中拉取然后rebase以基于当前版本的更改。

挤压:在这两种情况下都保留所有提交(例如:“添加功能”,然后“错字”,然后“再次错字”…)。可以通过压扁将提交合并为单个提交。压扁可以作为合并或rebase操作(--squash标志)的一部分完成,在这种情况下,它通常称为压扁合并或压扁rebase。

拉取请求:流行的git服务器(比特桶,GitLab,GitHub等…)允许配置如何在每个存储库的基础上合并拉取请求。UI可能会按照约定显示“合并”按钮,但该按钮可以使用任何标志进行任何操作(关键字:合并,rebase,壁球,快进)。

为了补充由tsamper提到的我自己的答案

  • 在合并之前,rebase通常是一个好主意,因为这个想法是您在分支Y中集成您将合并的分支B的工作。
    但是,在合并之前,您解决了分支中的任何冲突(即:“rebase”,如“从分支B的最近点开始在我的分支中重播我的工作”)。如果操作正确,从分支到分支B的后续合并可以快进。

  • 合并直接影响目标分支B,这意味着合并最好是微不足道的,否则分支B可能需要很长时间才能回到稳定状态(解决所有冲突的时间)


在一个rebase之后合并的点?

在我描述的情况下,我将B重新定位到我的分支上,只是为了有机会从B的最近一点重播我的工作,但同时留在我的分支中。
在这种情况下,仍然需要合并才能将我的“重播”工作带到B上。

另一种情况(例如在Git就绪中描述)是通过rebase直接将您的工作带到B中(这确实保存了您所有的漂亮提交,甚至让您有机会通过交互式rebase重新排序)。
在这种情况下(你在B分支中重新建立基础),你是对的:不需要进一步合并:

当我们没有合并或重新基于时,默认为Git树

rebase

我们通过rebase得到:

rebase

第二个场景是关于:我如何让新功能回到master。

我的观点,通过描述第一个rebase场景,是提醒每个人,rebase也可以用作初步步骤(即“让新功能回到master”)。
您可以使用rebase首先将master“引入”新功能分支:rebase将重放来自HEAD master的新功能提交,但仍在新功能分支中,有效地将分支起点从旧的master提交移动到HEAD-master
这允许您解决分支中的任何冲突(这意味着,隔离,同时允许master继续并行发展,如果您的冲突解决阶段需要太长时间)。然后您可以切换到master并合并new-feature(如果您想保留在new-feature分支中完成的提交,则将new-feature重新设置为master)。

所以:

  • “rebase与合并”可以看作是导入工作的两种方式,例如master
  • 但是“重新定位然后合并”可以是一个有效的工作流程,首先单独解决冲突,然后恢复您的工作。

这很简单。使用rebase,您可以使用另一个分支作为您工作的新基地

例如,如果您有一个分支master,您创建一个分支来实现一个新功能,并说您将其命名为cool-feature,当然,主分支是新功能的基础。

现在,在某个时候,你想添加你在master分支中实现的新功能。你可以切换到master并合并cool-feature分支:

$ git checkout master$ git merge cool-feature

但是通过这种方式添加了一个新的虚拟提交。如果你想避免意大利面历史,你可以rebase

$ git checkout cool-feature$ git rebase master

然后将其合并到master中:

$ git checkout master$ git merge cool-feature

这一次,由于主题分支具有相同的master提交加上具有新功能的提交,因此合并将只是一个快进。

TLDR:这取决于什么是最重要的-一个整洁的历史或发展顺序的真实表示

如果整洁的历史记录是最重要的,那么您应该先重新建立基础,然后合并您的更改,这样新代码的确切内容就很清楚了。如果你已经推动了你的分支,除非你能处理好后果,否则不要重新建立基础。

如果序列的真实表示是最重要的,那么您可以合并而无需重新建立基础。

合并意味着:创建一个新的提交,将我的更改合并到目标中。备注:这个新提交将有两个父级-来自您的提交字符串的最新提交和您正在合并的另一个分支的最新提交。

rebase的意思是:创建一系列新的提交,使用我当前的提交集作为提示。换句话说,计算如果我从重新基于的点开始进行更改,我的更改会是什么样子。因此,在rebase之后,你可能需要重新测试你的更改,在rebase期间,你可能会有一些冲突。

鉴于此,为什么要rebase呢?只是为了保持开发历史的清晰。假设你正在处理功能X,完成后,你合并了你的更改。目标现在将有一个单一的提交,上面写着类似于“添加功能X”的内容。现在,如果你rebase然后合并,目标开发历史将包含单个逻辑进程中的所有单独提交,而不是合并。这使得以后审查更改变得更加容易。想象一下,如果50名开发人员一直在合并各种功能,你会发现审查开发历史是多么困难。

也就是说,如果你已经将你正在处理的分支推送到上游,你不应该rebase,而是合并。对于尚未上游推送的分支,rebase、测试和合并。

另一个你可能想要重写的时候是当你想在向上推之前摆脱分支中的提交时。例如:早期引入一些调试代码的提交和进一步清理该代码的其他提交。做到这一点的唯一方法是执行交互式重写:git rebase -i <branch/commit/tag>

更新:当您使用Git连接到不支持非线性历史记录的版本管理系统(例如Subversion)时,您还希望使用rebase。当使用git-svn桥时,您合并回Subversion的更改是主干中最新更改之上的更改的顺序列表,这一点非常重要。只有两种方法可以做到这一点:(1)手动重新创建更改和(2)使用rebase命令,这要快得多。

更新2:考虑rebase的另一种方式是,它支持从您的开发风格到您要提交的存储库中接受的风格的某种映射。假设您喜欢以很小的块提交。您有一个提交来修复错字,一个提交来删除未使用的代码等等。当您完成需要做的事情时,您有一长串提交。现在假设您要提交的存储库鼓励大型提交,因此对于您正在做的工作,人们可能会期望一次或两次提交。您如何获取您的提交字符串并将它们压缩到预期的内容?你可以使用交互式rebase,将微小的提交压缩成更少的大块。如果需要反过来,情况也是如此——如果你的风格是一些大的提交,但存储库需要长字符串的小提交。你也可以使用rebase来做到这一点。如果你改为合并,你现在已经将你的提交风格嫁接到主存储库上了。如果有很多开发人员,你可以想象一段时间后跟踪具有多种不同提交风格的历史是多么困难。

更新3:Does one still need to merge after a successful rebase?是的,你需要。原因是rebase本质上涉及到提交的“移动”。正如我上面所说,这些提交是计算出来的,但是如果你从分支开始有14次提交,那么假设你的rebase没有问题,在rebase完成后,你将领先(你rebase的点)14次提交。你在rebase之前有一个分支。之后你会有一个长度相同的分支。在发布更改之前,你仍然需要合并。换句话说,rebase随你需要多次(同样,只有当你没有将更改推向上游时)。只有在你rebase之后才合并。

合并/rebase之前:

A <- B <- C    [master]^\D <- E       [branch]

git merge master之后:

A <- B <- C^         ^\         \D <- E <- F

git rebase master之后:

A <- B <- C <- D' <- E'

(A, B, C, D, E和F是提交)

这个例子以及更多关于Git的说明信息可以在Git基础教程中找到。

Pro Git这本书对改基页有很好的解释。

基本上,合并将接受两个提交并将它们组合起来。

一个rebase将去到两者的共同祖先,并逐步将更改应用到彼此之上。这使得历史更干净,更线性。

但是当您重新建立基础时,您会放弃以前的提交并创建新的提交。因此,您永远不应该重新建立公共存储库的基础。在存储库上工作的其他人会讨厌您。

99%的时候,我的分支并没有太大的不同,所以如果有冲突,它只在一两个地方。

Git rebase用于使历史记录中的分支路径更清晰,存储库结构更线性。

它还用于保持您创建的分支的私有性,因为在重新建立基础并将更改推送到服务器后,如果您删除了您的分支,将没有您曾经处理过的分支的证据。所以你的分支现在是你本地的关注点。

在做了rebase之后,我们还摆脱了一个额外的提交,我们用来查看我们是否进行了正常的合并。

是的,在成功rebase后仍然需要进行合并,因为rebase命令只是将您的工作放在您在rebase期间提到的分支之上,例如master,并将您的分支的第一次提交作为master分支的直系后代。这意味着我们现在可以进行快进合并,将更改从该分支带到master分支。

这里的很多答案都说合并会将您的所有提交变成一个,因此建议使用rebase来保留您的提交。这是不正确的。和一个坏主意,如果你已经推你的提交

Merge没有会抹去你的提交。Merge保留了历史!(看看gitk)Rebase重写历史,这在你之后是一件坏事。

使用合并--而不是rebase当你已经推。

这是Linus(Git的作者)对它的看法(现在在我自己的博客上托管,作为由Wayback机器回收)。这是一个非常好的阅读。

或者你可以在下面阅读我自己的想法。

在master上重设分支:

  • 提供了关于如何创建提交的错误想法
  • 用一堆可能没有经过良好测试的中间提交来污染master
  • 实际上可以在这些中间提交上引入构建中断,因为在创建原始主题分支和重新基于它之间对master进行了更改。
  • 使得在Master中找到好地方结帐变得困难。
  • 导致提交的时间戳与树中的时间顺序不一致。所以你会看到提交A在master中先于提交B,但提交B是先创作的。(什么?!)
  • 产生更多的冲突,因为主题分支中的单个提交都可能涉及必须单独解决的合并冲突(进一步在历史中记录每个提交中发生的事情)。
  • 如果正在重写的分支被推到任何地方(与除您之外的任何人共享),那么自从您重写历史以来,您已经搞砸了拥有该分支的其他人。

相反,将主题分支合并到master中:

  • 保留创建主题分支的历史记录,包括从master到主题分支的任何合并,以帮助保持其最新。您可以真正准确地了解开发人员在构建时使用的代码。
  • master是一个主要由合并组成的分支,每个合并提交通常是历史上的“好点”,可以安全地检查,因为这是主题分支准备集成的地方。
  • 保留主题分支的所有单独提交,包括它们在主题分支中的事实,因此隔离这些更改是很自然的,您可以在需要的地方钻取。
  • 合并冲突只需解决一次(在合并时),因此在主题分支中进行的中间提交更改不必独立解决。
  • 可以多次顺利完成。如果您定期集成主题分支以掌握,人们可以继续在主题分支上构建,它可以继续独立合并。

这句话得到了它:

一般来说,两全其美的方法是重新定位本地你所做的改变,但还没有分享,在你把它们推进去之前清理你的故事,但永远不要重新建立你所推动的任何事情某处

来源:3.6 Git分支-Rebase,Rebase vs. Merge

一些实际示例,与大规模开发有些联系,其中Gerrit用于审查和交付集成:

当我将我的功能分支提升到一个新的远程主机时,我会合并。这提供了最小的提升工作,并且很容易跟踪功能开发的历史,例如gitk

git fetchgit checkout origin/my_featuregit merge origin/mastergit commitgit push origin HEAD:refs/for/my_feature

我在准备交付提交时合并。

git fetchgit checkout origin/mastergit merge --squash origin/my_featuregit commitgit push origin HEAD:refs/for/master

当我的交付提交由于某种原因未能集成时,我会重新调整基础,并且我需要将其更新为新的远程主服务器。

git fetchgit fetch <gerrit link>git checkout FETCH_HEADgit rebase origin/mastergit push origin HEAD:refs/for/master

太长别读

如果您有任何疑问,请使用合并。

简短回答

rebase和合并之间的唯一区别是:

  • 历史记录的结果树结构(通常只有在查看提交图时才会注意到)是不同的(一个会有分支,另一个不会)。
  • 合并通常会创建一个额外的提交(例如树中的节点)。
  • Merge和rebase将以不同的方式处理冲突。Rebase将一次提交一个冲突,而合并将一次呈现所有冲突。

简单的答案是选择rebase或合并基于你希望你的历史看起来像什么

长答案

在选择使用哪种操作时,您应该考虑几个因素。

您正在获得更改的分支是否与团队之外的其他开发人员共享(例如开源、公共)?

如果是这样,不要rebase。rebase会破坏分支,除非他们使用git pull --rebase,否则这些开发人员将拥有损坏/不一致的存储库。这是快速让其他开发人员感到不安的好方法。

你的开发团队有多熟练?

Rebase是一个破坏性的操作。这意味着,如果您没有正确应用它,您可能会丢失已提交的工作和/或破坏其他开发人员存储库的一致性。

我工作过的团队中,开发人员都来自一个公司能够负担得起专门的员工来处理分支和合并的时代。那些开发人员对Git了解不多,也不想知道太多。在这些团队中,我不会冒险以任何理由推荐rebase。

分支本身是否代表有用的信息

有些团队使用了每个分支代表一个功能(或错误修复、子功能等)的分支模型。在这种模型中,分支有助于识别相关的提交集。例如,可以通过恢复该分支的合并来快速恢复一个功能(公平地说,这是一个罕见的操作)。或者通过比较两个分支来比较一个功能(更常见)。rebase会破坏分支,这并不简单。

我也曾在使用每个开发人员分支模型的团队中工作过(我们都有过这种经历)。在这种情况下,分支本身不会传达任何额外的信息(提交已经有作者了)。重新建立基础没有坏处。

您可能出于任何原因想要恢复合并吗?

与恢复合并相比,恢复(如撤消)rebase相当困难和/或不可能(如果rebase有冲突)。如果您认为有机会想要恢复,请使用合并。

你在团队中工作吗?如果是,你愿意在这个分支上采取全有或全无的方法吗?

rebase操作需要使用相应的git pull --rebase拉取。如果你独自工作,你可能会记住在适当的时候应该使用哪个。如果你在团队中工作,这将很难协调。这就是为什么大多数rebase工作流建议对所有合并使用rebase(对所有拉取使用git pull --rebase)。

常见的神话

合并破坏历史(壁球提交)

假设您有以下合并:

    B -- C/      \A--------D

有些人会说合并“破坏”了提交历史,因为如果您只查看主分支(A-D)的日志,您将错过B和C中包含的重要提交消息。

如果这是真的,我们就不会有这样的问题了。基本上,你会看到B和C,除非你明确要求不要看到它们(使用--first-父)。这很容易自己尝试。

Rebase允许更安全/更简单的合并

这两种方法合并的方式不同,但并不清楚一种是否总是比另一种更好,这可能取决于开发人员的工作流程。例如,如果一个开发人员倾向于定期提交(例如,在他们从工作过渡到家庭的过程中,他们可能每天提交两次),那么给定分支可能会有很多提交。其中许多提交可能看起来与最终产品完全不同(我倾向于为每个功能重构一两次我的方法)。如果其他人在相关的代码领域工作,并且他们试图重新构建我的更改,那么这可能是一个相当乏味的操作。

Rebase更酷/更性感/更专业

如果您喜欢将rm别名为rm -rf以“节省时间”,那么rebase可能适合您。

我的两分钱

我总是想有一天我会遇到一个场景,Git rebase是解决问题的很棒的工具。就像我想我会遇到一个场景,Git reflg是一个解决我问题的很棒的工具。我使用Git已经超过五年了。它还没有发生。

凌乱的历史对我来说从来都不是一个问题。我从来不会像读一本令人兴奋的小说一样阅读我的提交历史。大多数时候,我需要一个历史,无论如何我都会使用Git指责或Git二分法。在这种情况下,合并提交对我来说实际上是有用的,因为如果合并引入了问题,这对我来说是有意义的信息。

更新(4/2017)

我觉得有义务提一下,尽管我的一般建议仍然有效,但我个人在使用rebase时已经软化了。我最近与角2材料项目进行了很多互动。他们使用rebase来保持非常干净的提交历史记录。这使我能够非常容易地看到什么提交修复了给定的缺陷,以及该提交是否包含在版本中。它是正确使用rebase的一个很好的例子。

这个答案广泛面向gitflow。这些表是用漂亮的ASCII表生成器生成的,历史树是用这个美妙的命令(别名作为git lg)生成的:

git log --graph --abbrev-commit --decorate --date=format:'%Y-%m-%d %H:%M:%S' --format=format:'%C(bold blue)%h%C(reset) - %C(bold cyan)%ad%C(reset) %C(bold green)(%ar)%C(reset)%C(bold yellow)%d%C(reset)%n''          %C(white)%s%C(reset) %C(dim white)- %an%C(reset)'

表格按相反的时间顺序排列,以便与历史树更一致。另请首先查看git mergegit merge --no-ff之间的区别(您通常希望使用git merge --no-ff,因为它使您的历史看起来更接近现实):

git merge

命令:

Time          Branch "develop"             Branch "features/foo"------- ------------------------------ -------------------------------15:04   git merge features/foo15:03                                  git commit -m "Third commit"15:02                                  git commit -m "Second commit"15:01   git checkout -b features/foo15:00   git commit -m "First commit"

结果:

* 142a74a - YYYY-MM-DD 15:03:00 (XX minutes ago) (HEAD -> develop, features/foo)|           Third commit - Christophe* 00d848c - YYYY-MM-DD 15:02:00 (XX minutes ago)|           Second commit - Christophe* 298e9c5 - YYYY-MM-DD 15:00:00 (XX minutes ago)First commit - Christophe

git merge --no-ff

命令:

Time           Branch "develop"              Branch "features/foo"------- -------------------------------- -------------------------------15:04   git merge --no-ff features/foo15:03                                    git commit -m "Third commit"15:02                                    git commit -m "Second commit"15:01   git checkout -b features/foo15:00   git commit -m "First commit"

结果:

*   1140d8c - YYYY-MM-DD 15:04:00 (XX minutes ago) (HEAD -> develop)|\            Merge branch 'features/foo' - Christophe| * 69f4a7a - YYYY-MM-DD 15:03:00 (XX minutes ago) (features/foo)| |           Third commit - Christophe| * 2973183 - YYYY-MM-DD 15:02:00 (XX minutes ago)|/            Second commit - Christophe* c173472 - YYYY-MM-DD 15:00:00 (XX minutes ago)First commit - Christophe

git merge vsgit rebase

第一点:始终将功能合并到开发中,永远不要从功能重新构建开发。这是重设基地的黄金法则的结果:

git rebase的黄金法则是永远不要在公共分支上使用它。

换句话说

永远不要重新建立你在某处推动的任何东西。

我个人补充说:除非你和你的团队意识到后果

因此,git merge vsgit rebase的问题几乎仅适用于特征分支(在以下示例中,合并时总是使用--no-ff)。请注意,由于我不确定是否有更好的解决方案(存在争论),我将只提供这两个命令的行为方式。在我的情况下,我更喜欢使用git rebase,因为它会产生更好的历史树:)

功能分支之间

git merge

命令:

Time           Branch "develop"              Branch "features/foo"           Branch "features/bar"------- -------------------------------- ------------------------------- --------------------------------15:10   git merge --no-ff features/bar15:09   git merge --no-ff features/foo15:08                                                                    git commit -m "Sixth commit"15:07                                                                    git merge --no-ff features/foo15:06                                                                    git commit -m "Fifth commit"15:05                                                                    git commit -m "Fourth commit"15:04                                    git commit -m "Third commit"15:03                                    git commit -m "Second commit"15:02   git checkout -b features/bar15:01   git checkout -b features/foo15:00   git commit -m "First commit"

结果:

*   c0a3b89 - YYYY-MM-DD 15:10:00 (XX minutes ago) (HEAD -> develop)|\            Merge branch 'features/bar' - Christophe| * 37e933e - YYYY-MM-DD 15:08:00 (XX minutes ago) (features/bar)| |           Sixth commit - Christophe| *   eb5e657 - YYYY-MM-DD 15:07:00 (XX minutes ago)| |\            Merge branch 'features/foo' into features/bar - Christophe| * | 2e4086f - YYYY-MM-DD 15:06:00 (XX minutes ago)| | |           Fifth commit - Christophe| * | 31e3a60 - YYYY-MM-DD 15:05:00 (XX minutes ago)| | |           Fourth commit - Christophe* | |   98b439f - YYYY-MM-DD 15:09:00 (XX minutes ago)|\ \ \            Merge branch 'features/foo' - Christophe| |/ /|/| /| |/| * 6579c9c - YYYY-MM-DD 15:04:00 (XX minutes ago) (features/foo)| |           Third commit - Christophe| * 3f41d96 - YYYY-MM-DD 15:03:00 (XX minutes ago)|/            Second commit - Christophe* 14edc68 - YYYY-MM-DD 15:00:00 (XX minutes ago)First commit - Christophe

git rebase

命令:

Time           Branch "develop"              Branch "features/foo"           Branch "features/bar"------- -------------------------------- ------------------------------- -------------------------------15:10   git merge --no-ff features/bar15:09   git merge --no-ff features/foo15:08                                                                    git commit -m "Sixth commit"15:07                                                                    git rebase features/foo15:06                                                                    git commit -m "Fifth commit"15:05                                                                    git commit -m "Fourth commit"15:04                                    git commit -m "Third commit"15:03                                    git commit -m "Second commit"15:02   git checkout -b features/bar15:01   git checkout -b features/foo15:00   git commit -m "First commit"

结果:

*   7a99663 - YYYY-MM-DD 15:10:00 (XX minutes ago) (HEAD -> develop)|\            Merge branch 'features/bar' - Christophe| * 708347a - YYYY-MM-DD 15:08:00 (XX minutes ago) (features/bar)| |           Sixth commit - Christophe| * 949ae73 - YYYY-MM-DD 15:06:00 (XX minutes ago)| |           Fifth commit - Christophe| * 108b4c7 - YYYY-MM-DD 15:05:00 (XX minutes ago)| |           Fourth commit - Christophe* |   189de99 - YYYY-MM-DD 15:09:00 (XX minutes ago)|\ \            Merge branch 'features/foo' - Christophe| |/| * 26835a0 - YYYY-MM-DD 15:04:00 (XX minutes ago) (features/foo)| |           Third commit - Christophe| * a61dd08 - YYYY-MM-DD 15:03:00 (XX minutes ago)|/            Second commit - Christophe* ae6f5fc - YYYY-MM-DD 15:00:00 (XX minutes ago)First commit - Christophe

develop到特性分支

git merge

命令:

Time           Branch "develop"              Branch "features/foo"           Branch "features/bar"------- -------------------------------- ------------------------------- -------------------------------15:10   git merge --no-ff features/bar15:09                                                                    git commit -m "Sixth commit"15:08                                                                    git merge --no-ff develop15:07   git merge --no-ff features/foo15:06                                                                    git commit -m "Fifth commit"15:05                                                                    git commit -m "Fourth commit"15:04                                    git commit -m "Third commit"15:03                                    git commit -m "Second commit"15:02   git checkout -b features/bar15:01   git checkout -b features/foo15:00   git commit -m "First commit"

结果:

*   9e6311a - YYYY-MM-DD 15:10:00 (XX minutes ago) (HEAD -> develop)|\            Merge branch 'features/bar' - Christophe| * 3ce9128 - YYYY-MM-DD 15:09:00 (XX minutes ago) (features/bar)| |           Sixth commit - Christophe| *   d0cd244 - YYYY-MM-DD 15:08:00 (XX minutes ago)| |\            Merge branch 'develop' into features/bar - Christophe| |/|/|* |   5bd5f70 - YYYY-MM-DD 15:07:00 (XX minutes ago)|\ \            Merge branch 'features/foo' - Christophe| * | 4ef3853 - YYYY-MM-DD 15:04:00 (XX minutes ago) (features/foo)| | |           Third commit - Christophe| * | 3227253 - YYYY-MM-DD 15:03:00 (XX minutes ago)|/ /            Second commit - Christophe| * b5543a2 - YYYY-MM-DD 15:06:00 (XX minutes ago)| |           Fifth commit - Christophe| * 5e84b79 - YYYY-MM-DD 15:05:00 (XX minutes ago)|/            Fourth commit - Christophe* 2da6d8d - YYYY-MM-DD 15:00:00 (XX minutes ago)First commit - Christophe

git rebase

命令:

Time           Branch "develop"              Branch "features/foo"           Branch "features/bar"------- -------------------------------- ------------------------------- -------------------------------15:10   git merge --no-ff features/bar15:09                                                                    git commit -m "Sixth commit"15:08                                                                    git rebase develop15:07   git merge --no-ff features/foo15:06                                                                    git commit -m "Fifth commit"15:05                                                                    git commit -m "Fourth commit"15:04                                    git commit -m "Third commit"15:03                                    git commit -m "Second commit"15:02   git checkout -b features/bar15:01   git checkout -b features/foo15:00   git commit -m "First commit"

结果:

*   b0f6752 - YYYY-MM-DD 15:10:00 (XX minutes ago) (HEAD -> develop)|\            Merge branch 'features/bar' - Christophe| * 621ad5b - YYYY-MM-DD 15:09:00 (XX minutes ago) (features/bar)| |           Sixth commit - Christophe| * 9cb1a16 - YYYY-MM-DD 15:06:00 (XX minutes ago)| |           Fifth commit - Christophe| * b8ddd19 - YYYY-MM-DD 15:05:00 (XX minutes ago)|/            Fourth commit - Christophe*   856433e - YYYY-MM-DD 15:07:00 (XX minutes ago)|\            Merge branch 'features/foo' - Christophe| * 694ac81 - YYYY-MM-DD 15:04:00 (XX minutes ago) (features/foo)| |           Third commit - Christophe| * 5fd94d3 - YYYY-MM-DD 15:03:00 (XX minutes ago)|/            Second commit - Christophe* d01d589 - YYYY-MM-DD 15:00:00 (XX minutes ago)First commit - Christophe

附带说明

git cherry-pick

当您只需要一个特定的提交时,git cherry-pick是一个很好的解决方案(-x选项在原始提交消息正文中附加一行“(樱桃采摘自提交…)”,因此通常使用它是一个好主意-git log <commit_sha1>查看它):

命令:

Time           Branch "develop"              Branch "features/foo"                Branch "features/bar"------- -------------------------------- ------------------------------- -----------------------------------------15:10   git merge --no-ff features/bar15:09   git merge --no-ff features/foo15:08                                                                    git commit -m "Sixth commit"15:07                                                                    git cherry-pick -x <second_commit_sha1>15:06                                                                    git commit -m "Fifth commit"15:05                                                                    git commit -m "Fourth commit"15:04                                    git commit -m "Third commit"15:03                                    git commit -m "Second commit"15:02   git checkout -b features/bar15:01   git checkout -b features/foo15:00   git commit -m "First commit"

结果:

*   50839cd - YYYY-MM-DD 15:10:00 (XX minutes ago) (HEAD -> develop)|\            Merge branch 'features/bar' - Christophe| * 0cda99f - YYYY-MM-DD 15:08:00 (XX minutes ago) (features/bar)| |           Sixth commit - Christophe| * f7d6c47 - YYYY-MM-DD 15:03:00 (XX minutes ago)| |           Second commit - Christophe| * dd7d05a - YYYY-MM-DD 15:06:00 (XX minutes ago)| |           Fifth commit - Christophe| * d0d759b - YYYY-MM-DD 15:05:00 (XX minutes ago)| |           Fourth commit - Christophe* |   1a397c5 - YYYY-MM-DD 15:09:00 (XX minutes ago)|\ \            Merge branch 'features/foo' - Christophe| |/|/|| * 0600a72 - YYYY-MM-DD 15:04:00 (XX minutes ago) (features/foo)| |           Third commit - Christophe| * f4c127a - YYYY-MM-DD 15:03:00 (XX minutes ago)|/            Second commit - Christophe* 0cf894c - YYYY-MM-DD 15:00:00 (XX minutes ago)First commit - Christophe

git pull --rebase

我不确定我能比DerekGourlay更好地解释它……基本上,使用git pull --rebase而不是git pull:)文章中缺少的是您可以默认启用它

git config --global pull.rebase true

git rerere

同样,很好地解释了这里。但简单地说,如果您启用它,您将不必再多次解决相同的冲突。

虽然合并绝对是集成更改的最简单和最常见的方法,但它不是唯一的方法:重定向是集成的替代方法。

理解合并更好一点

当Git执行合并时,它会查找三个提交:

  • (1)共同的祖先提交。如果你关注一个项目中两个分支的历史,它们总是至少有一个共同的提交:在这个时间点,两个分支都有相同的内容,然后演变不同。
  • (2)+(3)每个分支的端点。集成的目标是组合两个分支的当前状态。因此,它们各自的最新版本特别有趣。将这三个提交结合起来将导致我们想要的集成。

快进或合并提交

在非常简单的情况下,两个分支中的一个在分支发生后没有任何新的提交-它的最新提交仍然是共同的祖先。

在此处输入图片描述

在这种情况下,执行集成非常简单:Git可以在共同祖先提交之上添加另一个分支的所有提交。在Git中,这种最简单的集成形式称为“快进”合并。然后两个分支共享完全相同的历史记录。

在此处输入图片描述

然而,在很多情况下,两个分支都各自向前发展。

在此处输入图片描述

要进行集成,Git必须创建一个包含它们之间差异的新提交-合并提交。

在此处输入图片描述

人类提交和合并提交

通常,提交是由人精心创建的。它是一个有意义的单元,只包装相关的更改并用注释对其进行注释。

合并提交有点不同:它不是由开发人员创建的,而是由Git自动创建的。它的目的不是包装一组相关的更改,而是连接两个分支,就像一个结。如果你想稍后理解合并操作,你需要查看两个分支的历史和相应的提交图。

与Rebase集成

有些人更喜欢没有这种自动合并提交。相反,他们希望项目的历史看起来好像是在一条直线上发展的。没有迹象表明它在某个时候被分成多个分支。

在此处输入图片描述

让我们一步一步地浏览一个rebase操作。场景与前面的示例相同:我们想将分支B的更改集成到分支A,但现在使用rebase。

在此处输入图片描述

我们将分三步完成

  1. git rebase branch-A // Synchronises the history with branch-A
  2. git checkout branch-A // Change the current branch to branch-A
  3. git merge branch-B // Merge/take the changes from branch-B to branch-A

首先,Git将“撤消”在行开始分支后(在共同祖先提交之后)发生在分支A上的所有提交。但是,当然,它不会丢弃它们:相反,你可以将这些提交视为“暂时保存”。

在此处输入图片描述

接下来,它应用我们要集成的来自分支B的提交。此时,两个分支看起来完全相同。

在此处输入图片描述

在最后一步中,分支A上的新提交现在被重新应用-但在一个新的位置上,在分支B的集成提交之上(它们是重新基于的)。

结果看起来开发是直线进行的。不是包含所有组合更改的合并提交,而是保留了原始提交结构。

在此处输入图片描述

最后,你得到一个干净的分支A分支,没有不需要的和自动生成的提交。

备注:取自#0帖子rebase中的缺点在同一篇文章中也是一个很好的阅读。

我什么时候使用git rebase?几乎从不,因为它改写了历史。git merge几乎总是更好的选择,因为它尊重项目中实际发生的事情。

我刚刚用自己的话给团队做了一个FAQ,回答了这个问题,跟大家分享一下:

什么是merge

一个提交,将不同分支的所有更改合并到当前中。

什么是rebase

将当前分支的所有提交重新提交到不同的基提交上。

mergerebase的主要区别是什么?

  1. merge只执行一个个新提交。rebase通常执行多个(当前分支中的提交数)。
  2. merge生成新的生成的提交(所谓的合并提交)。rebase只移动现有提交。

在什么情况下我们应该使用merge

每当您想将分支的更改添加到基分支时,请使用merge

通常,您可以通过单击拉取/合并请求上的“合并”按钮来执行此操作,例如在GitHub上。

在什么情况下我们应该使用rebase

每当您想将基分支的变化添加回分支分支时,请使用rebase

通常,每当main分支发生更改时,您都会在feature分支中执行此操作。

为什么不使用merge将基本分支的更改合并到功能分支?

  1. git历史将包括许多不必要的合并提交。如果功能分支中需要多次合并,那么功能分支甚至可能包含比实际提交更多的合并提交!

  2. 这会创建一个循环破坏了Git设计的心理模型,这会导致Git历史记录的任何可视化出现问题。

    想象有一条河(例如“尼罗河”)。水朝一个方向流动(Git历史上的时间方向)。时不时地,想象这条河有一条支流,假设这些支流中的大部分汇回河流。这就是河流自然流动的样子。这是有道理的。

    但是想象一下这条河有一个小分支。然后,出于某种原因,河流汇入支流和分支从那里继续。这条河现在技术上已经消失了,它现在在分支中。但是,不知何故,那个分支被神奇地合并回河流中。你问哪条河?我不知道。这条河现在实际上应该在分支中,但不知何故它仍然存在,我可以将分支合并回河流中。所以,河流在河流中。有点没有意义。

    这正是当您将基本分支mergefeature分支时发生的情况,然后当feature分支完成时,您将其再次合并回基本分支。心理模型被打破了。正因为如此,您最终得到了一个不是很有帮助的分支可视化。

使用merge时的示例Git历史记录:

合并时的Git历史示例

请注意,许多以Merge branch 'main' into ...开头的提交。如果您重新定位,它们甚至不存在(在那里,您将只有拉取请求合并提交)。还有许多可视化分支合并循环(mainfeaturemain)。

使用rebase时的示例Git历史记录:

使用rebase时的示例Git历史记录

更干净的Git历史记录,更少的合并提交,没有混乱的视觉分支合并循环。

rebase有什么缺点/陷阱吗?

有:

  1. 因为rebase移动了提交(从技术上讲是重新执行它们),所有移动的提交的提交日期将是rebase和git历史可能看起来像是丢失了初始提交时间的时间。因此,如果出于某种原因,所有工具都需要确切的提交日期,那么merge是更好的选择。但通常,干净的git历史比确切的提交日期更有用。作者-Date字段将在需要的地方继续保存原始提交日期。

  2. 如果重新基于分支有多个更改同一行的提交,并且该行也在基分支中更改了,您可能需要多次解决同一行的合并冲突,而合并时您永远不需要这样做。因此,平均而言,有更多的合并冲突需要解决。

使用rebase时减少合并冲突的提示:

  1. 经常重新定位.我通常建议每天至少做一次。
  2. 尝试在同一行中尽可能多地壁球变化到一个提交中。

当您在分支上工作并在两者之间合并了一些其他工作时,rebase很有用-合并将创建更改,这将使您的diff受到污染,因此更难阅读。

如果您重新设置分支的基础,那么您的提交将应用于您重新设置分支的顶部,这使得审查更容易,并且diff输出更干净。