ChatGPT解决这个技术问题 Extra ChatGPT

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

什么时候推荐使用 Git rebase vs. Git merge?

成功变基后我还需要合并吗?

喜欢使用 rebase 的人的一个问题是它阻止了他们定期推送他们的代码。所以想要干净的历史会阻止他们分享他们的代码,我认为这更重要。
@static_rtti:那不是真的。如果它阻止您定期推送更改,那么您使用的基于变基的流程是错误的。
我的启发式:“尝试变基,如果它溶解到冲突解决中,请放弃并将 master 合并到您的分支中并继续前进。” - 在我的帖子 timwise.co.uk/2019/10/14/merge-vs-rebase 中有更多详细信息(在商业开发的背景下)

C
Community

精简版

合并将一个分支中的所有更改合并到一个提交中的另一个分支中。

Rebase 说我希望我分支的点移动到一个新的起点

那么你什么时候使用其中之一呢?

合并

假设您创建了一个分支来开发单个功能。当您想将这些更改带回主控时,您可能需要合并(您不关心维护所有临时提交)。

变基

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


@Rob 提到在合并时维护临时提交。我相信默认情况下,将分支 B(您一直在处理的功能分支)合并到分支 M(主分支)中,将为在 B 中进行的每个提交在 M 中创建一个提交,因为这两个提交发生了分歧。但是,如果您使用 --squash 选项进行合并,则在分支 B 上所做的所有提交都将“集中在一起”并合并为分支 M 上的单个提交,从而使您的主分支上的日志保持整洁。如果您有许多开发人员独立工作并重新合并为 master,则 Squashing 可能是您想要的。
我相信@spaarky21 关于合并的假设是不正确的。如果将分支 B 合并到主 M 中,则 M 上只会有一个提交(即使 B 有多个提交),无论您使用普通合并还是 --squash 合并。 --squash 将做的是消除对 B 作为父级的引用。这里有一个很好的可视化:syntevo.com/smartgithg/howtos.html?page=workflows.merge
@jpeskin 那不是我所看到的。我只是做了一个快速测试来验证。创建一个包含文本文件、init 新存储库、add 文件和 commit 的目录。签出新功能分支 (checkout -b feature。) 更改文本文件,提交并重复,以便在功能分支上有两个新提交。然后是 checkout mastermerge feature。在 log 中,我看到了我在 master 上的初始提交,然后是从 feature 合并的两个。如果您merge --squash feature,功能会合并到 master 但未提交,因此 master 上唯一的新提交将是您自己创建的。
@spaarky21 看起来我们都对了一半。当可以进行快进合并时(如您的示例),git 将默认将所有提交包含在功能分支 B 中(或者按照您的建议,您可以使用 --squash 组合成一个提交)。但是在您合并两个不同的分支 M 和 B 的情况下,如果合并到 M 中,git 将不会包含来自分支 B 的所有单个提交(无论您是否使用 --squash)。
为什么“(你不关心维护所有临时提交)”仍然在这个答案中?这在 09 年没有意义,现在也没有意义。此外,当然,如果另一个开发人员进行了您需要的相关更改,您肯定只想重新设置基准 - 如果他们进行了不相关的更改,那么您的功能分支应该很容易合并而不会发生冲突,并且您的历史记录将得到维护。
A
Anton Menshov

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

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

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

$ git checkout master
$ git merge cool-feature

但是这样添加了一个新的虚拟提交。如果你想避免意大利面条的历史,你可以重新设置:

$ git checkout cool-feature
$ git rebase master

然后将其合并到 master 中:

$ git checkout master
$ git merge cool-feature

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


but this way a new dummy commit is added, if you want to avoid spaghetti-history - 情况如何?
此外,merge 的 --no-ff 标志非常有用。
@アレックス 用户 Sean Schofield 将它放在评论中:“rebase 也很好,因为一旦你最终将你的东西合并回 master (正如已经描述的那样微不足道),你就会把它放在你的提交历史的“顶部” . 在可能编写功能但几周后合并的较大项目中,您不想将它们合并到主控中,因为它们在历史上被“填充”到主控中。我个人喜欢能够做 git记录并在“顶部”查看最近的功能。请注意,提交日期被保留 - rebase 不会更改该信息。“
我认为这里值得重复——请记住,所有这些术语(mergerebasefast-forward 等)都指的是有向无环图的特定操作。考虑到这种心理模型,它们变得更容易推理。
@Aldo关于重新定位的历史没有什么“干净”或“整洁”的。它通常很肮脏,恕我直言,因为你不知道到底发生了什么。 “最干净”的 Git 历史是实际发生的历史。 :)
P
Peter Mortensen

TL;博士

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

简答

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

生成的历史树结构(通常仅在查看提交图时才明显)是不同的(一个会有分支,另一个没有)。

合并通常会创建一个额外的提交(例如树中的节点)。

合并和变基将以不同的方式处理冲突。 Rebase 会一次提交一个冲突,而 merge 会同时显示所有冲突。

所以简短的回答是根据你希望你的历史看起来像什么来选择变基或合并。

长答案

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

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

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

你的开发团队有多熟练?

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

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

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

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

我还曾在使用每个开发人员分支模型的团队工作过(我们都去过那里)。在这种情况下,分支本身不会传达任何其他信息(提交已经有作者)。重新定位不会有任何害处。

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

与还原合并相比,还原(如撤消)变基相当困难和/或不可能(如果变基有冲突)。如果您认为有可能想要还原,请使用合并。

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

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

常见的神话

合并破坏历史(压缩提交)

假设您有以下合并:

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

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

如果这是真的,我们就不会有 questions like this。基本上,您会看到 B 和 C,除非您明确要求不要看到它们(使用 --first-parent)。这很容易自己尝试。

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

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

Rebase 更酷/更性感/更专业

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

我的两分钱

我一直认为总有一天我会遇到这样一个场景,即 Git rebase 是解决问题的绝佳工具。就像我想我会遇到这样一个场景,Git reflog 是一个很棒的工具,可以解决我的问题。我已经使用 Git 工作了五年多了。它没有发生。

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

更新 (4/2017)

我觉得有义务提到我个人对使用 rebase 的态度有所缓和,尽管我的一般建议仍然有效。我最近与 Angular 2 Material 项目进行了很多互动。他们使用 rebase 来保持非常干净的提交历史。这让我可以很容易地看到哪些提交修复了给定的缺陷,以及该提交是否包含在发布中。它是正确使用 rebase 的一个很好的例子。


我最喜欢这个答案。但是:Rebase 不会创造“干净”的历史。它创造了一个更线性的历史,但这根本不是一回事,因为现在谁知道每个提交都隐藏了多少“污垢”?最干净、最清晰的 Git 历史是保持分支和提交完整性的历史。
值得一提的是,git 最近更改了它的 git pull 行为以默认包含 --rebase 标志。这意味着对多个开发人员使用的分支进行 rebase 的危险性要小一些。拉动您的更改的人可能会惊讶于在这样的操作期间需要解决一些冲突,但不会有灾难。
rebase 的另一个缺点,imo 在高速 git repos 中更难,尤其是在构建时间大于合并之间的平均时间的情况下。如果分支不断被合并,你需要不断地变基,直到轮到你,如果你还与构建协调通过,这可能会很棘手。例如,monorepos rebase 合并到共享分支可能很困难
P
Peter Mortensen

为了补充提到的 my own answer by TSamper

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

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

变基后的合并点?

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

另一种情况(例如 described in Git Ready)是通过 rebase 将您的工作直接带入 B(这确实保留了所有不错的提交,甚至让您有机会通过交互式 rebase 重新排序它们)。
在那种情况下(你在 B 分支中变基),你是对的:不需要进一步的合并:

当我们没有合并也没有重新定位时,默认的 Git 树

https://i.stack.imgur.com/7vsGZ.png

我们通过变基得到:

https://i.stack.imgur.com/yCxOO.png

第二种情况是关于:我如何让新功能回到主控。

我的意思是,通过描述第一个 rebase 场景,是为了提醒大家,rebase 也可以用作一个初步步骤(即“将新功能恢复为 master”)。
您可以使用 rebase 首先将 master “带入”新功能分支:rebase 将重播来自 HEAD master 的新功能提交,但仍在新功能分支中,有效地将您的分支起点从对 HEAD-master 的旧主提交。
这使您可以解决您的 分支中的任何冲突(这意味着,如果您的冲突解决阶段花费的时间过长,则允许 master 继续并行发展)。
然后您可以切换到 master 并合并 new-feature(或者如果您想保留在 new-feature 分支中完成的提交,则将 new-feature 重新设置为 master)。

所以:

“rebase vs. merge”可以看作是导入作品的两种方式,比如,master。

但是“rebase then merge”可能是一个有效的工作流程,首先单独解决冲突,然后恢复你的工作。


在 rebase 之后合并是一个微不足道的快进,无需解决冲突。
@obelcap:确实,这是一种想法:您将环境中的所有问题冲突(在您的新功能分支中重新设置主控),然后共同主控,合并新功能:1 皮秒(快速-向前)如果主人没有进化
Rebase 也很好,因为一旦你最终将你的东西合并回 master(正如已经描述的那样微不足道),你就会将它放在提交历史的“顶部”。在可能编写功能但几周后合并的较大项目中,您不希望仅将它们合并到主控中,因为它们在历史上被“填充”到主控中。就我个人而言,我喜欢能够执行 git log 并在“顶部”看到最近的功能。请注意,提交日期被保留 - rebase 不会更改该信息。
@Joe:从心理上讲,您是在说“在另一个分支之上重放我的任何更改(在我的私人分支中单独完成),但是一旦变基完成,就把我留在我的私人分支中”。这是一个清理本地历史的好机会,避免“检查点提交”、破碎的平分和错误的责备结果。请参阅“Git 工作流程”:sandofsky.com/blog/git-workflow.html
@scoarescoare 关键是看看你的本地更改如何在最新的上游分支之上兼容。如果您的一个提交引入了冲突,您将立即看到它。合并只引入一个(合并的)提交,这可能会触发许多冲突,而没有一种简单的方法可以查看您自己的本地提交中的哪一个确实添加了所述冲突。因此,除了更清晰的历史记录之外,您还可以更精确地查看您引入的更改,逐个提交(由 rebase 重放),而不是上游分支引入的所有更改(转储到一个合并中)。
A
Andrew Arnott

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

合并不会消除您的提交。合并保留历史! (只看 gitk) Rebase 重写历史,在你推送它之后这是一件坏事。

使用合并——当你已经推送时不要变基。

Here is Linus' (author of Git) take on it(现在托管在我自己的博客上,作为 recovered by the Wayback Machine)。这真是一本好书。

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

在 master 上重新建立一个分支:

提供了关于如何创建提交的错误想法

一堆可能没有经过良好测试的中间提交污染了 master

实际上可能会在这些中间提交上引入构建中断,因为在创建原始主题分支和重新建立基础分支之间对 master 进行了更改。

使得在 master 中找到好地方结帐变得困难。

导致提交的时间戳与其在树中的时间顺序不一致。所以你会看到提交 A 在 master 中的提交 B 之前,但提交 B 是首先编写的。 (什么?!)

产生更多的冲突,因为主题分支中的每个提交都可能涉及必须单独解决的合并冲突(在历史中进一步说明每个提交中发生了什么)。

是历史的改写。如果要重新设置的分支已被推送到任何地方(与您以外的任何人共享),那么自从您改写历史以来,您已经搞砸了拥有该分支的所有其他人。

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

保留创建主题分支的历史记录,包括从主分支到主题分支的任何合并,以帮助使其保持最新。您确实可以准确了解开发人员在构建时使用的代码。

master 是一个主要由合并组成的分支,这些合并提交中的每一个通常都是历史上可以安全检查的“好点”,因为那是主题分支准备好集成的地方。

主题分支的所有单独提交都被保留,包括它们在主题分支中的事实,因此隔离这些更改是很自然的,您可以在需要的地方进行钻取。

合并冲突只需解决一次(在合并时),因此在主题分支中进行的中间提交更改不必独立解决。

可以顺利完成多次。如果您定期将您的主题分支集成到主控,人们可以继续在主题分支上构建,并且可以保持独立合并。


此外,git merge 具有“--no-ff”(无快进)选项,可让您非常轻松地还原某个合并引入的所有更改。
只是说得更清楚一点:你指的是“每当你已经推动”的情况——这应该是大胆的。顺便说一句,Linus 帖子的链接很好,澄清了它。
但是,在通过 PR 将主题分支合并到主分支之前(以解决分支中的冲突,而不是主分支),从主分支“更新”到您的主题分支不是最佳实践吗?我们正在这样做,所以大多数主题分支都有作为最后一次提交“将分支主合并到主题-...”但是这里这被列为变基的“功能”并且没有人提到它用于合并...?
@AndrewArnott“大多数主题分支应该能够在没有冲突的情况下合并到它们的目标分支中”当 20 个开发人员在 30 个分支上工作时,这应该如何实现?在你处理你的工作时会有合并 - 所以当然你必须在创建 PR 之前从目标更新你的主题分支......不是吗?
通常不会,@Sumit。即使对其中一个或两个分支进行了更改,Git 也可以很好地合并任一方向。只有当跨两个分支修改相同的代码行(或非常接近)时,才会发生冲突。如果任何团队都经常发生这种情况,那么团队应该重新考虑他们如何分配工作,因为解决冲突是一种负担,会减慢他们的速度。
J
Jeehut

我刚刚用我自己的话为我的团队创建了一个常见问题解答,它回答了这个问题。让我分享:

什么是合并?

提交,将不同分支的所有更改组合到当前。

什么是变基?

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

合并和变基之间的主要区别是什么?

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

在什么情况下我们应该使用合并?

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

通常,您可以通过单击 Pull/Merge Requests 上的“Merge”按钮来执行此操作,例如在 GitHub 上。

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

每当您想将基础分支的更改添加回分支分支时,请使用 rebase

通常,只要 main 分支发生变化,您就会在 feature 分支中执行此操作。

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

git 历史将包括许多不必要的合并提交。如果在一个特性分支中需要多次合并,那么特性分支甚至可能比实际提交包含更多的合并提交!这会创建一个循环,破坏 Git 设计的心智模型,这会导致 Git 历史的任何可视化出现问题。想象有一条河(例如“尼罗河”)。水流向一个方向(Git 历史中的时间方向)。时不时地,想象那条河有一个分支,并假设这些分支中的大多数会汇回河流中。这就是河流的自然流动。这说得通。但是再想象一下那条河的一个小支流。然后,由于某种原因,河流汇入分支,分支从那里继续。从技术上讲,这条河现在已经消失了,它现在在分支中。但随后,不知何故神奇地,那根树枝又汇回了河里。你问哪条河?我不知道。河流现在实际上应该在分支中,但不知何故它仍然存在,我可以将分支合并回河流中。所以,河在河中。有点没有意义。当您将基本分支合并到功能分支中时,这正是发生的情况,然后当功能分支完成时,您再次将其合并回基本分支。心智模式被打破。正因为如此,您最终会得到一个不是很有帮助的分支可视化。

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

https://i.stack.imgur.com/Oqqmm.png

请注意以 Merge branch 'main' into ... 开头的许多提交。如果您变基,它们甚至都不存在(在那里,您只会有拉取请求合并提交)。还有许多视觉分支合并循环(mainfeaturemain)。

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

https://i.stack.imgur.com/0ZVla.png

更清晰的 Git 历史记录,更少的合并提交,也没有杂乱的视觉分支合并循环。

rebase 有什么缺点/陷阱吗?

是的:

因为变基会移动提交(技术上重新执行它们),所以所有移动提交的提交日期将是变基的时间,并且 git 历史记录会丢失初始提交时间。因此,如果出于某种原因需要提交的确切日期,那么合并是更好的选择。但通常情况下,干净的 git 历史记录比准确的提交日期有用得多。如果重新建立的分支有多个提交更改了同一行,并且该行在基础分支中也发生了更改,则您可能需要多次解决同一行的合并冲突,而在合并时您永远不需要这样做。因此,平均而言,有更多的合并冲突需要解决。

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

经常变基。我通常建议每天至少做一次。尽量将同一行上的更改压缩到一个提交中。


我会从您的列表中完全删除缺点(2),因为正如您所说,挤压是(2)的完美解决方案并且它总是有效
谢谢,很有用。这突出了一个重要的点,即使用 rebase 可以避免多个合并提交。
我在这里发表评论是因为我不知道在哪里抱怨这个。如果我 rebase main -> feature,GitHub 将显示更改到共同祖先,这是可恶的,因为审查该 PR 意味着审查所有已经合并到 main 的工作。这很恶心,这就是我使用合并的原因。
@tfrascaroli 我不明白您的意思,如果您将功能分支重新定位到主分支的最新提交,那么针对主分支的 PR 应该只显示共同祖先之后功能分支中的更改。只有当您的 PR 是针对另一个分支而不是主要分支时,您所描述的才有可能。还是我误解了什么?
嗯,这只是昨天发生在我身上。主要的分支功能。使用 Squash and Merge 将其他功能合并到主要功能。变基主要 -> 功能。 PR 功能 -> 主 = 不可能审查,因为检测到的更改会回到主分支。如果我改为 Merge main -> feature 然后 PR feature -> main 不会发生。
C
Carl

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

如果整洁的历史记录是最重要的,那么您将首先变基,然后合并您的更改,这样新代码到底是什么就很清楚了。如果你已经推送了你的分支,除非你能处理后果,否则不要变基。

如果序列的真实表示是最重要的,那么您将在没有变基的情况下合并。

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

Rebase 意味着:使用我当前的一组提交作为提示,创建一个全新的提交系列。换句话说,计算如果我从我要重新定位的点开始进行更改,我的更改会是什么样子。因此,在 rebase 之后,您可能需要重新测试您的更改,并且在 rebase 期间,您可能会遇到一些冲突。

鉴于此,你为什么要变基?只是为了保持发展历史清晰。假设您正在处理功能 X,当您完成后,您将更改合并到其中。目标现在将有一个提交,它会说出类似于“添加的功能 X”的内容。现在,如果您重新定位然后合并,而不是合并,目标开发历史将包含单个逻辑进程中的所有单个提交。这使得以后审查更改变得更加容易。想象一下,如果 50 位开发人员一直在合并各种功能,您会发现回顾开发历史是多么困难。

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

另一个你可能想要变基的时候是当你想在向上游推送之前从你的分支中删除提交。例如:在早期引入一些调试代码的提交和在清理该代码时进一步的其他提交。执行此操作的唯一方法是执行交互式变基:git rebase -i <branch/commit/tag>

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

更新 2:考虑变基的另一种方法是,它可以实现从您的开发风格到您正在提交的存储库中接受的风格的一种映射。假设你喜欢以小块的方式提交。你有一个提交来修复一个错字,一个提交来摆脱未使用的代码等等。当你完成你需要做的事情时,你有一系列的提交。现在假设您提交的存储库鼓励大型提交,因此对于您正在做的工作,人们会期望进行一次或两次提交。你如何接受你的提交字符串并将它们压缩到预期的值?您将使用交互式 rebase 并将您的微小提交压缩成更少的大块。如果需要相反的情况也是如此 - 如果您的风格是几个大提交,但存储库需要长字符串的小提交。你也可以使用变基来做到这一点。如果您已经合并,那么您现在已经将您的提交样式移植到主存储库中。如果有很多开发人员,您可以想象一段时间后要跟踪具有多种不同提交样式的历史是多么困难。

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


与 master 合并可能会导致快进。在功能分支中可能有一些提交,它们有小错误或者甚至没有编译。如果您只在功能分支中进行单元测试,那么集成中的一些错误会漏掉。在与 master 合并之前,需要进行集成测试,并且可能会显示一些错误。如果这些是固定的,则可以集成该功能。由于您不希望将错误代码提交给 master,因此似乎有必要进行 rebase 以防止所有提交快进。
@mbx git merge 支持强制它进行合并提交的 --no-ff 选项。
当您可以通过简单地使用搜索/过滤命令查看历史记录来获得与“整洁历史记录”相同的优点而没有它的巨大缺点时,它并不真正“依赖”。使变基实际上毫无用处。
P
Peter Mortensen

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

更好地理解合并

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

(1) 共同祖先提交。如果你跟踪一个项目中两个分支的历史,它们总是至少有一个共同的提交:在这个时间点,两个分支具有相同的内容,然后演变不同。

(2) + (3) 每个分支的端点。集成的目标是结合两个分支的当前状态。因此,它们各自的最新修订版具有特殊意义。结合这三个提交将导致我们的目标是集成。

快进或合并提交

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

https://i.stack.imgur.com/57Wt0.gif

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

https://i.stack.imgur.com/f1VTv.gif

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

https://i.stack.imgur.com/GlAgx.gif

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

https://i.stack.imgur.com/e3xJE.gif

人工提交和合并提交

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

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

与 Rebase 集成

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

https://i.stack.imgur.com/b1nud.gif

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

https://i.stack.imgur.com/Az5LO.gif

我们将分三步完成

git rebase branch-A // 将历史记录与分支-A 同步 git checkout branch-A // 将当前分支更改为分支-A git merge branch-B // 合并/获取从分支-B 到分支-A 的更改

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

https://i.stack.imgur.com/To3Iq.gif

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

https://i.stack.imgur.com/Vrfmo.gif

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

结果看起来就像直线发展一样。保留了原始提交结构,而不是包含所有组合更改的合并提交。

https://i.stack.imgur.com/4IKL0.gif

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

注意:摘自git-tower 的精彩postrebase缺点在同一篇文章中也很值得一读。


P
Peter Mortensen

在合并/变基之前:

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 The Basics Tutorial 中可以找到这个示例以及更多关于 Git 的详细说明信息。


stackoverflow.com/a/804178 ) Rebase 到我们自己的 dev 分支,然后合并到 master? “您可以使用 rebase 首先将 master 带入”新功能分支:rebase 将重播来自 HEAD master 的新功能提交,但仍在新功能分支中,有效地将您的分支起点从旧 master 移动提交到 HEAD-master。这允许您解决分支中的任何冲突(意思是,孤立地)。然后您可以切换到 master 并合并新功能(或者如果您想保留已完成的提交,则将新功能重新设置到 master 上在您的新功能分支中)。”
s
sp00m

这个答案广泛围绕Git Flow。表格是用漂亮的 ASCII Table Generator 生成的,历史树是用这个美妙的命令(aliased 作为 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 合并

命令:

Time          Branch "develop"             Branch "features/foo"
------- ------------------------------ -------------------------------
15:04   git merge features/foo
15:03                                  git commit -m "Third commit"
15:02                                  git commit -m "Second commit"
15:01   git checkout -b features/foo
15: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 合并 --no-ff

命令:

Time           Branch "develop"              Branch "features/foo"
------- -------------------------------- -------------------------------
15:04   git merge --no-ff features/foo
15:03                                    git commit -m "Third commit"
15:02                                    git commit -m "Second commit"
15:01   git checkout -b features/foo
15: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 合并 vs git rebase

第一点:总是将特性合并到开发中,永远不要从特性中变基开发。这是 Golden Rule of Rebasing 的结果:

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

In other words

永远不要改变你推到某个地方的任何东西。

我个人会补充:除非它是一个功能分支,并且您和您的团队都知道后果。

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

特征分支之间

git 合并

命令:

Time           Branch "develop"              Branch "features/foo"           Branch "features/bar"
------- -------------------------------- ------------------------------- --------------------------------
15:10   git merge --no-ff features/bar
15:09   git merge --no-ff features/foo
15:08                                                                    git commit -m "Sixth commit"
15:07                                                                    git merge --no-ff features/foo
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/bar
15:01   git checkout -b features/foo
15: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/bar
15:09   git merge --no-ff features/foo
15:08                                                                    git commit -m "Sixth commit"
15:07                                                                    git rebase features/foo
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/bar
15:01   git checkout -b features/foo
15: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

从开发到功能分支

git 合并

命令:

Time           Branch "develop"              Branch "features/foo"           Branch "features/bar"
------- -------------------------------- ------------------------------- -------------------------------
15:10   git merge --no-ff features/bar
15:09                                                                    git commit -m "Sixth commit"
15:08                                                                    git merge --no-ff develop
15:07   git merge --no-ff features/foo
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/bar
15:01   git checkout -b features/foo
15: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/bar
15:09                                                                    git commit -m "Sixth commit"
15:08                                                                    git rebase develop
15:07   git merge --no-ff features/foo
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/bar
15:01   git checkout -b features/foo
15: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 樱桃采摘

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

命令:

Time           Branch "develop"              Branch "features/foo"                Branch "features/bar"
------- -------------------------------- ------------------------------- -----------------------------------------
15:10   git merge --no-ff features/bar
15:09   git merge --no-ff features/foo
15: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/bar
15:01   git checkout -b features/foo
15: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

我不确定我能不能比 Derek Gourlay 更好地解释它...基本上,使用 git pull --rebase 而不是 git pull :) 但文章中缺少的是 you can enable it by default

git config --global pull.rebase true

git rerere

同样,很好地解释了 here。但简而言之,如果启用它,您将不再需要多次解决相同的冲突。


P
Peter Mortensen

这句话明白了:

一般来说,两全其美的方法是在你推送它们以清理你的故事之前,对你所做但尚未共享的本地更改进行 rebase,但永远不要对你推送的任何内容进行 rebase .

来源:3.6 Git Branching - Rebasing, Rebase vs. Merge


P
Peter Mortensen

Pro Git 这本书对 rebasing page 有很好的解释。

基本上,合并将需要两次提交并将它们组合起来。

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

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

仅出于这个原因,我几乎完全合并。 99% 的情况下,我的分支并没有那么大的差异,所以如果有冲突,它只会在一两个地方发生。


合并不合并提交——那将重写历史。 Rebase 就是这样做的。
我不确定为什么你不能基于功能分支,然后在公共分支上合并。
P
Peter Mortensen

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

它还用于保持您创建的分支的私密性,因为在重新设置基础并将更改推送到服务器之后,如果您删除了您的分支,则不会有您曾经处理过的分支的证据。因此,您的分支机构现在是您当地的关注点。

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

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


P
Peter Mortensen

一些实际示例,在一定程度上与使用 Gerrit 进行审查和交付集成的大规模开发相关:

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

git fetch
git checkout origin/my_feature
git merge origin/master
git commit
git push origin HEAD:refs/for/my_feature

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

git fetch
git checkout origin/master
git merge --squash origin/my_feature
git commit
git push origin HEAD:refs/for/master

当我的交付提交由于某种原因无法集成时,我会重新设置基准,并且我需要将其更新为新的远程主机。

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

J
Jeremy Benks

多次解释了什么是变基和什么是合并,但是什么时候应该使用呢?

什么时候应该使用变基?

变基“解除”您的更改并将变基分支的所有更改放入您当前的分支,然后将您的更改放在它之上。因此,它会更改您分支的历史记录。

当您还没有推送分支/没有其他人在处理它时

您希望在合并回源分支时同时查看所有更改

你想避免自动生成的“merged ..”提交消息

我说“您希望在一个地方查看所有更改”,因为有时合并操作会将您的所有更改放在一个提交中(一些:合并自...消息)。 Rebase 使您的更改看起来像您一个接一个地提交,中间没有其他人做任何事情。这使您更容易看到您对功能所做的更改。

但请确保您使用 git merge feature-branch --ff-only 来确保在将功能合并回开发/主时不会创建单个提交。

什么时候应该使用合并?

当您推送分支/其他人也在处理它时(如果其他人也在该分支上工作,rebase 会变得非常复杂!)

您不需要完整的历史记录(*)/您的功能不必将其全部提交到一个地方。

(*) 您可以通过首先将开发分支合并到您的功能然后将您的功能合并回开发来避免您的功能仅获得一个“合并的..”提交。这仍然为您提供“合并的 ..”提交,但至少您的功能的所有提交仍然可见。


反之亦然。如果你变基,你的历史会被重写,因此一些信息会丢失。合并不会改变或丢失任何历史,所以你的观点是错误的。您错过的最重要的一点是变基意味着您拥有线性历史。您的答案错过了重新定位的重点!
M
Marnen Laibow-Koser

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


@benjaminhull 谢谢! - 除了我希望我的回答是基于事实的。恕我直言,在这类事情中几乎没有意见:事实是,失去你的真实历史会让以后的生活变得更艰难。
同意。合并永远不会导致历史记录等损坏(当您重新设置推送的提交时)
我不知道为什么这个答案有很多反对意见。我不得不投票以尽量减少损失。哈哈。我部分同意,但我认为如果我们是唯一在分支中工作以保持一切清洁的人,我们可以重新设置基准。主要问题是,如果出于某种意想不到的原因,其他人也开始研究它。