为什么 Git 合并无法与本地分支正常工作



我有一个有局部更改的local_branch,我希望我的主分支合并到其中。但是当我做"git merge origin/main"时,它显示我已经是最新的。但我确信我不是,因为我可以看到我在当地没有的主要变化。也可能是我不小心解决了我的冲突,这就是为什么它已经显示最新。我怎样才能摆脱这种情况?我希望我的本地分支合并主分支

我有一个具有本地更改的local_branch,我希望我的主分支合并到其中。但是当我做"git merge origin/main"时,它显示我已经是最新的......

这正是它所说的意思:你已经合并了origin/main,所以没有工作要做。

这里值得注意的是一些事情:

  • 首先,git merge并不意味着制造相同。 如果它这样做,我们将失去工作! 相反,它意味着结合工作

  • 其次,工作组合的定义很棘手。 一旦你弄清楚"找到已经完成的工作"意味着什么,"组合"部分就不那么难了,但部分有点难。

  • 最后,合并的"方向"有些重要,尽管比大多数人最初想象的要少。 我们将在下面看到这一点。

为了理解为什么"工作"的定义很棘手,让我们从你需要了解的一些关于 Git 的非常基本的事情开始。

Git 是关于提交的

那些刚接触 Git 的人经常认为 Git 是关于文件的,但事实并非如此。 这是关于提交。 每个提交都保存文件——如果他们不这样做,Git 将毫无用处——但 Git 在这里真正关心的是提交。 你得到所有的提交,或者没有:你永远不会得到一半的提交。1

那些刚接触 Git 的人经常认为 Git 是关于分支的,事实并非如此。 这是关于提交。 Git 将提交组织到分支,我们(人类)使用分支名称让 Git 帮助我们找到提交,但同样,Git 真正关心的是提交

旁白:我还发现分支在 Git 中是一个坏词——不像那种"四个字母的单词"亵渎那样糟糕,而是糟糕的,因为一开始没有人知道你说分支是什么意思,就像坏词带来亵渎一样,或者">"有时意味着"好"。 也就是说,单词分支在 Git 中有多种含义,人们会在几个句子中使用这个词——或者在一个句子中多次——每次出现的含义都不同!

所以,好吧,Git 是关于提交的。 可以在没有任何分支名称的情况下使用 Git(但这不是一个好主意,至少如果你是人类的话)。 这意味着我们需要确切地知道提交是什么以及为您做什么。 不过,让我从这个开始:

  • Git存储库的核心是两个数据库的集合,一个数据库通常比另一个大得多。

  • 通常较大的数据库包含 Git 的提交和其他 Git对象(支持让 Git 保留提交和/或使提交更好地工作的对象)。 这个大数据库中的所有内容都是只读的,数据库本身大部分(尽管不是 100%)是仅追加的。 也就是说,提交通常不会消失,任何提交一旦完成,就永远不会改变。 相反,我们只是添加新的

  • 这个大数据库中的对象是有编号的,具有大的、丑陋的、看起来随机的哈希ID 或(更正式的)Git对象 IDOID。 这些曾经是完全的 SHA-1 哈希(现在不再是真的),所以旧文档也会调用这些 SHA 等。 Git实际上需要哈希 ID(OID)来查找对象。 因此,每个提交都有一个唯一的哈希 ID;从某种意义上说,哈希 ID就是提交。

  • (通常较小的)第二个数据库包含名称分支名称、标签名称、远程跟踪名称和许多其他类型的名称。 Git 为我们人类提供了这些,对于他们来说,哈希 ID 是无法使用的,除非通过鼠标剪切粘贴或其他方式。 相反,我们可以使用分支名称之类的东西:像maindevelop这样的分支名称转换为(一个,单个)哈希ID,对于分支名称,该ID被定义为"在"该分支上的最新提交。

因此,通过使用这两个数据库,我们可以给 Git 一个名字(例如local_branchorigin/main)。 Git 将根据需要使用该名称查找提交哈希 ID。 然后,Git 使用哈希 ID 在大型全 Git 对象数据库中查找实际提交。

当我说提交的哈希 ID 是唯一的时,我的意思是唯一的:例如,我们将使用git clone"克隆"一个 Git 存储库,当我们这样做时,我们会从其他存储库数据库中复制所有 Git 对象。 这些副本具有与原件相同的OID。 (这就是对象是只读的原因:OID 是数据的加密校验和。 更改数据,并且您更改了校验和并具有其他对象。 由于对象数据库在很大程度上是仅追加的,因此您所做的只是添加一个对象:旧对象仍然存在,在旧 ID 下。

这就是我们需要了解的有关数据库的信息:

  • 大的包含按其哈希ID索引的对象,包括提交。 这就是 Git 找到任何特定提交的方式:你给 Git 哈希 ID,Git 只是查找它。 它要么在那里 - 整个事情都在那里,而不仅仅是它的一部分 - 或者它不是,因为它不能改变,如果你有一个具有一些已知哈希ID的提交,而其他人在其他Git存储库中具有相同的哈希ID,你们都必须具有相同的提交阿拉伯数字

  • 小家伙意味着大多数时候,我们不必使用原始哈希 ID:我们可以使用名称,Git 将为我们查找原始哈希 ID。

现在让我们继续提交。Git 中的提交:

  • 正如我们刚刚看到的,已编号。
  • 包含两件事:每个文件的完整快照,以及一些元数据或有关提交本身的信息。

当听说每个提交都有每个文件的完整快照时,人们经常反对:这不会使存储库变得非常庞大吗?它会,除了:

  • 快照中的文件是压缩的,有时是高度压缩的;和
  • 文件经过重复数据删除,这在许多方面更为重要。

(Git 通过将每个文件的内容作为对象存储在大对象数据库中来实现重复数据消除。 高度压缩发生在游戏后期,当对象被打包到一个"包文件"中时,我们在这里根本不会介绍,因为对于简单地使用Git 来说,这些都不重要。

虽然文件是我们在工作时关心的,但目前,它实际上是我们需要关心的元数据。 任何一个给定提交的元数据包含以下内容:

  • 提交者的用户名和电子邮件地址;
  • 日期和时间戳;
  • 日志消息,git log可以显示或汇总;以及
  • 对于 Git 的操作至关重要的是,以前的提交哈希 ID 列表

此列表通常正好有一个条目长。 因此存储在任何给定提交中的(单个)提交哈希 ID 称为提交的父级

这种在每次提交中存储提交哈希 ID 的技巧意味着,如果我们可以在某个提交链中找到最后一个提交,我们可以使用它向后工作。 假设H代表某个提交链中最后一个提交的哈希 ID。 我们说存储在H中的哈希 ID指向上一个提交 — 为简单起见,我们将其称为G。 我们可以这样画:

<-G <-H

也就是说,提交H实际上是向后指向提交G。 当然,G也是一个提交,所以它也有一个父级,我们可以称之为F

... <-F <-G <-H

由于F有一个父级,Git 可以从H开始,然后回到G,然后F等等。 最终,所有这些向后工作将出现有史以来第一次提交,它不会有父级,因为它不能。 它的父项列表在其元数据中将为空,这为我们提供了整个链:

A--B--C--D--E--F--G--H

(我们的仓库显然只有八个提交)。

我们如何找到H? 我们已经知道:我们使用像main这样的分支名称来做到这一点:

A--B--C--D--E--F--G--H   <-- main

请注意,我在提交之间绘制箭头有点懒惰。 这部分是因为它们实际上无法更改 - 它们是提交中的元数据,任何提交的任何部分都无法更改 - 还因为我即将绘制更复杂的图形。


1一个仍在开发中的功能称为部分克隆,可让您获得"一半"(或某些片段)提交,并承诺在需要时稍后会显示其余部分。 这对你最初应该思考 Git 的方式造成了很大的损害,所以让我们假装它不存在。 无论如何,它还没有真正为普通用户做好准备。 一旦它,我相信它将是非常无缝的 - 也就是说,你不必意识到它甚至存在 - 除了如果你的网络关闭并且当你需要一个完整的提交时你有一个部分提交,你的Git将停止工作在这一点。 如果你的网络启动了,你的 Git 软件只会变慢(并且可能会打印出有关下载内容的进度消息)。

2这实际上不能永远工作,也不会。 从理论上讲,哈希 ID 的大小将失败的日子放到了足够远的未来,以至于我们不在乎。 这个理论有一个缺陷,但它正在得到解决。

<小时 />分支

,几种

假设我们采用我们简单、相对较新的 Git 存储库,只有 8 个提交和一个分支名称main,并添加另外两个分支名称br1br2。 Git 中的分支名称必须只存储一个哈希 ID;这是提交的哈希 ID,它是分支"上"或"在">分支中的最后一次提交。 如果我们让这三个名字都指向H,我们得到:

...--G--H   <-- br1, br2, main

现在,我们使用哪个名称并不重要,因为这三个名称都指向H. 但一会儿就不是真的了。 为了记住我们使用的名称,让我们将特殊名称HEAD(像这样大写)附加到一个分支名称:

...--G--H   <-- br1, br2, main (HEAD)

这说明我们正在使用名称main来查找提交H

如果我们现在运行:

git switch br1     # or git checkout br1

我们告诉 Git 从分支main,即提交H,切换到分支br1,即提交H。 我们不会更改提交(我们稍后会看到这一点非常重要),但我们正在更改分支。 我们现在有:

...--G--H   <-- br1 (HEAD), br2, main

现在让我们进行新的提交。 我们稍后会回到有关进行新提交的一些重要事情,但现在让我们假设这会产生一个新的快照和元数据(确实如此),让我们称之为新的提交I。 Git 将安排新的提交I向后指向现有的提交H

I
/
...--G--H

然后,作为git commit的最后一步,Git 会将I的哈希 ID(无论是什么)写入名称br1,以便我们得到:

I   <-- br1 (HEAD)
/
...--G--H   <-- br2, main

也就是说,当前分支名称(我们通过查看HEAD附加到哪个名称来找到)将更新为指向新提交。 新提交现在是分支上的最后一个提交。 如果我们进行另一个新提交,我们会得到:

I--J   <-- br1 (HEAD)
/
...--G--H   <-- br2, main

提交I-J现在仅在"分支br1上"。 提交到H现在都在所有三个分支上。

我现在一直在说,部分原因是:

  • 我们可以随时添加新名称,例如使用git branch;
  • 我们可以随时删除分支名称,也可以使用git branch(带 -dor-D');
  • 我们可以随时使用具有各种选项的git branchgit reset移动任何分支名称。

无论分支名称指向什么提交,这都是该分支中的最后一个提交。 我们可以通过父级向后移动找到的其他提交也"在"分支中,并且许多提交通常位于许多分支中,第一次提交通常在每个分支中。

无论如何,让我们现在git switch br2,或者git checkout br2,然后运行git branch -d main,得到这个:

I--J   <-- br1
/
...--G--H   <-- br2 (HEAD)

名称main完全消失了(这使得绘制图形更容易,这里没有特别的其他原因将其删除),但我们不需要它:它只是为我们提供了一种快速查找提交H的方法。 我们有一个缓慢的方法:我们可以找到提交J,使用名称br1,然后向后工作两跳到I然后H。 我们现在还有另一种快速的方法:br2找到H的名字。

请注意,此时,我们用于提交的文件J消失,取而代之的是用于提交H的文件。 我们所做的更改似乎消失了! 但它们并没有消失:在提交I中有一个完整的快照,在提交J中有一个完整的快照,如果我们git switch br2,我们会取回这些文件(并且H中的文件消失)。 是时候简要地谈谈你的工作树Git 的索引了。

Git 的索引和你的工作树

出于空间原因,我们将快速浏览,但重要的是要知道:标准("非裸")存储库包括工作树或工作树存储库本身位于隐藏的.git目录(或文件夹,如果您更喜欢该术语)中,您可以在此工作树的顶部找到该目录。 在这个.git里面有各种控制和辅助文件,它们实现了两个大数据库和 Git 想要拥有的所有其他东西。 你可以查看它们的内部,但一般来说,除非你真的知道你在做什么,否则你不应该更改这些文件中的任何一个:Git 对它在.git存储库中的文件非常挑剔。 将其存储在云同步文件夹中也是一个坏主意,因为云同步软件和 Git 对重要内容有非常不同的想法。 云同步软件最终损坏 Git 存储库(墨菲定律意味着这通常发生在一些大型演示之前)。 别这样;使用git fetchgit push同步 Git 存储库。

相比之下,工作树包含您看到和处理/处理的文件。 我们提到过,提交存储每个文件的完整快照(Git 知道,在您或任何人制作快照时),并且提交的内容采用特殊格式,只有 Git 可以读取,实际上没有任何内容可以覆盖(不成功,也就是说:物理覆盖只会损坏数据,因为哈希 ID 将不再匹配: 这是云同步软件破坏存储库的一种方式)。 但是为了完成工作,我们需要程序可以读写的常规(非 Git 化)文件。 Git 将这些文件放入我们的工作树中:它们可以从提交中复制出来,就像提取存档一样,然后我们有普通文件。

重要提示:工作树中的文件不在 Git 中!它们可能刚刚从 Git中出来,但 Git(在提交中)中的文件是特殊的 Git 化文件。 这些是普通文件;当你处理它们时,Git 与它们无关,如果你删除一个,Git就无法取回那个。 Git 只能在提交中取回 Git 化的。

如果 Git 像其他版本控制系统一样,它会停在这里:会有永久冻结的已提交文件,并且会有你的工作树文件,你可以随心所欲地处理这些文件。 每个"活动"文件将有两个副本:当前提交中的冻结文件和您正在摆弄的文件。 时不时地运行git commit,Git 会获取这些工作树文件并冻结它们。 但 Git 不像其他版本控制系统。 这不是 Git 进行新提交的方式。

相反,Git 有第三个区域——有点提交和工作树之间——Git 在其中存储第三个副本,位于提交中的冻结副本和工作树中的可用副本之间。 不过,我们可以称之为"副本"而不是副本。 第三个"副本",介于两者之间的副本,位于 Git 称为索引暂存区域或缓存(目前很少见)缓存中。 索引中的"副本"采用 Git 化和去重复的格式,因此至少在最初,此"副本"永远不会占用任何空间,因为它来自当前提交

请记住,我们运行了git switch br2或其他任何东西,这就是我们得到 Git 用来填充工作树的提交的地方。 Git将从你的工作树和它的索引中删除所有与你要离开的提交相关的文件。 然后,Git 会将要移动到的提交附带的所有文件放入其索引和工作树中。 因此,当我们有:

I--J   <-- br1 (HEAD)
/
...--G--H   <-- br2

(其他一切都是"干净的",因为任何地方都没有未提交的工作),Git 的索引和你的工作树匹配提交J的快照。 然后我们运行:

git switch br2

Git删除提交J文件并安装提交H文件:

I--J   <-- br1
/
...--G--H   <-- br2 (HEAD)

再一次,你当前的提交、Git 的索引和你的工作树都匹配

要立即进行新提交,您需要:

  • 修改工作树文件;
  • 运行git add:通过将更新的文件复制回 Git 的索引来更新建议的新提交快照;3
  • 最终,运行git commit.

当您运行git commit时,Git 将索引文件(冻结格式的文件,但在此之前可替换)打包到新的冻结快照中,并将其用于新提交。 Git 也会收集或生成适当的元数据,并将所有这些写出来,现在我们有了新的提交,它获得了一个新的、唯一的哈希 ID,然后将新的哈希 IDgit commit填充到当前分支名称中:

I--J   <-- br1
/
...--G--H

K   <-- br2 (HEAD)

我们像往常一样添加了另一个提交,并注意到现在 - 由于索引和工作树在我们git add- 编辑文件后匹配,并且新的提交K匹配索引,因为它是从索引创建的- 当前提交,Git 的索引和你的工作树都再次匹配。

我们现在可以进行另一个新提交:

I--J   <-- br1
/
...--G--H

K--L   <-- br2 (HEAD)

我们有一个很好的对称设置,都可以合并了。 请注意,通过H和包括提交的提交方式分别在两个分支上,而提交I-JK-L分别仅在br1br2上提交。


3我们将跳过有关其工作原理的所有详细信息。 它在某些方面很简单,在其他方面很复杂。 最终结果是索引已准备好提交,并保存更新的建议新提交,并且添加的文件已更改。

<小时 />

git showgit diffgit log -p

在我们开始之前git merge,让我们注意有关这些提交序列的另一件事:它们都有快照,但是如果我们查看一些带有git showgit log -p的提交,我们会看到一个差异。 如果我们运行:

git diff <hash-of-I> <hash-of-J>

我们看到了一个差异。 这些差异是什么,为什么我们会看到它们?

答案很简单:差异是一种配方。 它显示了两个快照之间的更改。 如果执行显示的更改,并将这些更改应用于较早的或左侧的快照,则会获得较晚或右侧的快照。

Git 本质上可以玩一个发现差异的游戏。 我们选择一个提交,比如I,把它放在左边,另一个像J一样,放在右边,然后说:告诉我发生了什么变化。Git 不存储更改 - 它存储整个快照 - 但通过比较两个快照,Git 可以提供更改配方:例如,在README.txt的第 12 行之后添加此内容。 Git 将更改显示为一系列差异大块,或者在某些情况下,显示为全新添加的文件,或完全删除的文件,或重命名(可能是重命名后跟一些更改)。 我们不会涵盖所有这些可能的情况,主要只关注简单的差异大块头。

通过比较IJ中的文件,Git 可以向我们展示更改的内容。 这通常比两个完整快照更有趣,这就是为什么git showhash-of-J或者更简单地说,git show br1,将运行此特定git diffgit log -p也是如此. 差异向我们显示了我们更改的内容,而不是快照中的内容,这就是我们希望看到的内容。

git showgit log -p都使用信息:为了显示一些特定的提交,Git 找到父级,提取(到内存中的临时区域)两个快照中的每一个,并将它们与git diff进行比较。 (重复数据消除在这里有很大帮助:Git 可以立即判断两个文件是否 100% 相同,甚至无需提取文件本身,因此根本不费心去比较它们。

但是使用命令行,我们可以手动挑选出任何两个提交并在其上运行git diff。 例如,如果我们查找提交H的哈希 ID——或者有其他方法可以找到它(如果我们没有删除它,可能是分支名称main)——我们可以运行:

git diff <hash-of-H> br1

这将向我们展示提交HJ之间的变化。 这是IJ所有更改的摘要,它方便地消除了我们在J中"撤消"的I所做的任何更改(例如,如果我们插入一个拼写错误,然后修复它,这不会出现在差异中,因为 Git 会跳过提交I)。

这将是我们对"工作"的定义。从某个起始点提交到分支结束所做的工作只是从该起点提交到分支的提示提交(使用分支名称找到)的git diff

我们现在可以继续git merge.

简而言之,git merge的工作原理

让我们回到这个存储库,但是git switch br1

I--J   <-- br1 (HEAD)
/
...--G--H

K--L   <-- br2

我们已签出提交J,因此我们会看到这些文件。 如果我们运行git diff L我们将看到此提交和提交L之间有什么不同(向后,因为这将L放在左边,我们的工作树放在右边)。 但这不是我们所做的工作,不是br1.br1上发生了一些工作,但这不是这个差异显示的内容。 而且,在br2上也发生了一些工作。

我们想要从 Git 中得到的是将我们在br1上所做的工作与我们(或某人)在br2上所做的工作结合起来。 我们如何做到这一点? 好吧,我们已经看到了:我们发现了一些提交,例如 提交H,它们在两个分支上。 每个提交到包括提交H都可以在这里工作,但有一个显然是最好的,那就是提交本身H。 这就是两个分支分开的地方。

因此,我们将运行 Git:

git diff <hash-of-H> <hash-of-J>   # what we did on br1

然后运行:

git diff <hash-of-H> <hash-of-L>   # what they did on br2

这些让我们"完成工作",并且因为我们仔细选择了相同的起始提交,因此两组"完成的工作"都可以应用于该起始提交中的快照。 也就是说,Git 只需要将两组"完成的工作">结合起来

(请注意,如果我们早点开始,例如在提交G,这仍然有效,但现在H中的所有内容在两个提交中都是"完成的工作"。 这使得合并工作更加困难,没有任何优势。 这就是为什么很明显,提交H最好的共享提交。

Git 的"组合工作"是通过这样做来实现的:

  • 对于我们更改的每个文件,如果他们根本没有触及它,请使用我们的文件版本;
  • 对于他们更改的每个文件,如果我们根本没有触及它,请使用他们的版本;
  • 对于无人更改的文件,请使用任何版本 - 所有三个版本都匹配;
  • 对于双方都更改的文件,请更加努力。

"努力工作">部分,Git 介绍了差异:对于我们更改的每个大块头,如果它们没有触及相同的行,请接受我们的更改。 对于他们改变的每一块大块头,如果我们没有触及相同的线条,请接受他们的改变。 如果我们都进行了完全相同的更改,请复制该更改的一个副本如果我们进行了不一致的重叠更改,Git 会抱怨:我不知道如何组合这些!Git 说存在合并冲突。 而且,作为一个特例,如果我们和他们对不同的行进行了不同的更改,但这两个更改相邻,Git 也会声明合并冲突。

如果存在合并冲突,Git 会留下一团糟。 由于篇幅原因,我们不会在这里讨论任何细节,只是提到这种"混乱"涉及在您的工作树中留下部分合并结果,并在Git 的索引中保留文件的多个副本。 作为操作 Git 机器的程序员,您负责清理混乱。

但是,如果没有任何合并冲突,Git 将继续使用它获得的快照进行自己的合并提交:

  • 组合差异
  • 组合的差异应用于合并库中的快照,在本例中为提交H

这个新快照像往常一样获取元数据,因此它就像其他所有提交(快照和元数据)一样,但有一个区别:它的哈希 ID 列表包含两个哈希 ID。 新提交像往常一样在当前分支上进行,因此它看起来像这样:

I--J
/    ₁
...--G--H      M   <-- br1 (HEAD)
    /²
K--L   <-- br2

M点提交回两个较早的提交,而不仅仅是一个。 父母的名单是有序的,所以有时 - 虽然不是经常 - 我们关心哪个是第一个,哪个不是。 这就是小12进来的地方:M的第一个父母是J,因为当我们开始时br1指出JM第二个父级是L,因为提交L是我们合并的提交。

请注意,我们合并的是提交,而不是分支! 我们通常使用分支名称来选择该提交,即git merge br2。 但是我们可以使用原始哈希 ID 运行它,并获得相同的结果。 提交K-L现在"在br1",以及br2。 (提交I-J不是on br2:如果我们运行git switch br2git log,我们将看不到IJ;我们将看到L,然后是K,然后是H,然后是G,然后是比这更早出现的东西。

如果我们使用git switch br2; git merge br1,我们会得到这个:

I--J   <-- br1
/    ₂
...--G--H      M   <-- br2 (HEAD)
    /¹
K--L

也就是说,新的合并提交M将在br2上,而所有提交现在都将在br2上,并且br1仍然指向L并且不会添加任何提交。M的第一个父代是LM第二个父代是J。 但是快照是相同的,因为 Git 具有相同的更改集要组合(我们假设没有冲突)。


4练习:这总是正确的做法吗? 如果更改是添加应该汇总的数字列(例如要计费的美元),该怎么办? 假设我们所做的更改是添加 10 美元,因为我们出售了 10 美元的商品,他们所做的更改是添加 10 美元,因为他们工作了 10 美元。 只保留一笔 10 美元的费用是对的吗?


已保持最新;快进

git merge说你已经是最新的时,你就是。 下面是一个插图:

...--G--H   <-- main, origin/main

I--J   <-- feature (HEAD)

运行git merge maingit merge origin/main会指示 Git 查看您当前的提交J,以及提交H,并找出哪个提交是最佳共享提交。 这意味着我们从J开始,向后工作到IH,然后从H开始,嘿,看,一个共享的提交! 最好的共享提交是提交H。 但。。。挂起,提交H已在feature分支。 如果我们跑git diff H H看看"他们改变了什么",我们绝对什么也得不到。 如果我们跑git diff H J看看我们改变了什么,我们会看到我们改变了什么,并将其添加到H得到......J. 所以实际上无事可做。

我们可以得到一种相反的情况,它实际上经常发生:

...--G--H   <-- main (HEAD), origin/main

I--J   <-- feature

请注意我们现在是如何on branch main的,正如git status所说。 我们运行git merge feature,Git 去寻找最好的共享提交。 好吧,这又是提交H。 将HH的差异将再次表明什么都没有。 但这一次,我们希望将他们的更改添加到我们缺乏更改的内容中。 如果我们将其作为真正的合并来执行,我们将得到一个新的提交 - 让我们再次称之为M- 如下所示:

(origin/main still points to H)
/
...--G--H------M   <-- main (HEAD)
    /
I--J   <-- feature

提交M中的快照将与J中的快照完全匹配,因为添加某些内容(从HJ的变化,如果有的话)会产生某些内容。 这种"加法",加零,是轻而易举的。 因此,如果您不阻止它,Git 将采取捷径并执行此操作:

...--G--H   <-- origin/main

I--J   <-- feature, main (HEAD)

也就是说,Git 会将名称main移动到指向J,并将签出提交J。 就好像你使用J的哈希 ID 或名称feature做了一个git switchgit checkout,除了HEAD保持附加到main,并且名称main"向前滑动",Git 称之为快进操作。 Git 特别将这种快进操作称为快进合并,但不涉及实际的合并

您可能需要通过git fetch更新main分支的本地版本:

# from local_branch
git fetch origin
git merge origin/main

上述git merge将使用最新的远程跟踪分支,该分支应反映远程分支main上的最新更改。