TL;DR:
git fetch
命令从不合并任何内容。它可以更新引用,并且非常愿意以快进方式更新类似分支的引用。但是,如果要以非快进方式更新此类引用,则必须强制更新。
快进 - 一旦与合并的想法分离开来 - 是指引用更改所指向的提交的属性。更具体地说,我们通常关心的是分支名称值或远程跟踪名称值是否以快进方式更改。这意味着我们必须查看提交图,因为引用当前选择的提交和提交图中的新位置共同确定对该引用的更新是否为快进。
Long:
至少在一个重要方面,原始声明是错误的:
我知道git fetch
在从远程获取提交后,总是对分支和其远程跟踪执行快进合并。
让我们稍微分解一下,这样我们就有了正确的词语和短语可用。我们需要知道:
- reference是什么;
- refspec是什么;最重要的是,
- 做fast-forward更新与非fast-forward更新到引用意味着什么。
最后这部分还涉及force flag:每个引用更新都可以被强制或不被强制。你可能熟悉git push --force
,它为Git正在推送的每个引用设置了force标志。git fetch
命令也有相同的标志,具有相同的效果,但总的来说,“全有或全无”太广泛了,因此Git有一种方法在更个体化的基础上设置force标志。(git push
命令在这方面甚至有更多的细化,但我们只会简单提一下。)
reference和refspec的定义
在Git中,reference只是一个名称 - 理想情况下,是一个对某个特定提交或其他Git对象有意义的名称。1 引用始终以refs/
开头,并且大多数情况下都有第二个斜杠分隔的组件,声明它们是哪种引用,例如refs/heads/
是一个分支名称,refs/tags/
是一个标签名称,而refs/remotes/
是一个远程跟踪名称。3
在判断某个更新是否是快进时,我们关心的参考文献是那些我们希望表现出“分支方式”的参考文献:位于
refs/heads/
和
refs/remotes/
中的参考文献。我们将在接下来讨论的规则
可能适用于任何参考文献,但
确实适用于这些“分支方式”的参考文献。
如果您在Git需要或可以使用引用的地方使用了未经修饰的名称(如
master
),Git会使用
gitrevisions文档开头概述的六步过程来解析缩写名称以获取完整的引用名称。
在Git中,
refspec基本上是一对由冒号(
:
)字符分隔的引用,其前面可以有一个可选的加号
+
。左侧的引用是
源,右侧的引用是
目标。我们在使用
git fetch
和
git push
与连接两个不同的Git仓库时使用refspecs。源引用意味着将提交和其他Git对象发送到的Git所使用的引用,而目的地引用意味着接收Git所使用的引用。因此,对于
git fetch
,源是
其他 Git,而目标是我们自己。
如果refspec中的引用没有完全限定(不以
refs/
开头),Git可以使用上面的过程来限定它。如果单个refspec中的{{两个}}引用都未被限定,Git中有一些代码来尝试将它们都放入适当的名称空间中,但我从来不太信任这段代码。例如,在提取期间,究竟是谁真正限定了源和目标并不清楚:涉及两个Git,但另一个Git通常会向我们发送一个所有引用的完整列表,因此我们的Git可以使用此列表进行解析。在这里使用完全限定的引用显然更明智,以防{{他们的}}参考集与您自己的期望不匹配:如果他们只有一个
refs/tags/xyz
,而您希望
xyz
扩展为
refs/heads/xyz
,则当它未能实现时,您可能会感到惊讶。
在任何refspec中,可以省略源或目标部分。要省略目标,请编写不带冒号的refspec,例如
refs/heads/br
。要省略源,请编写具有冒号但源部分为空的refspec,例如
:refs/heads/br
。当您执行这些操作时,它们的含义与用途各不相同:与
git push
相比,
git fetch
会对它们进行不同的处理。目前,请注意存在源和目标部分,并可选择省略它们。
如果您选择使用加号,那么它始终放在前面。因此,
git push origin +:refs/heads/br
是一个推送操作,强制标志已设置,源为空,目的地为
refs/heads/br
,是完全合格的。由于这是一次推送操作,源表示我们 Git 的名称(没有),目的地表示他们 Git 的名称(名为
br
的分支)。类似的字符串
+refs/heads/br
设置了强制标志,具有完全合格的源,但没有目的地。如果我们关心
git push
,我们可以查看这两个推送 refspecs 的含义,但现在让我们继续。
1任何类似分支的引用必须指向一个提交。标签名称可以指向任何对象。其他引用名称可能具有其他约束。
2Git本身存在一些内部分歧,即是否每个引用都必须以与refs/*
匹配的完整名称形式拼写出来。如果是这样,HEAD
将永远不会是一个引用。实际上,像HEAD
、ORIG_HEAD
和MERGE_HEAD
这样的特殊名称有时像普通引用一样工作,有时则不然。对我来说,我大多数情况下都将它们排除在引用的概念之外,除非在方便包含它们的时候。每个Git命令都会自己决定如何以及是否更新这些*_HEAD
名称,因此没有像refs/
风格引用那样的正式系统方法。
3还有更多广为人知的子空间:例如,refs/replace
保留给git replace
使用。这里的想法很简单:refs/
后面跟着另一个可读字符串,告诉我们这个特定引用是什么类型的引用。根据种类,我们可能需要另一个子空间,就像在refs/remotes/
中一样,我们接下来想知道:哪个远程?
4一些Git命令知道或假设缩写引用必须是分支名称或标签名称。例如,git branch
在某些地方不允许您拼出refs/heads/
:它只是粗鲁地将refs/heads/
推入其中,因为它仅适用于分支名称。当没有明确的“必须是分支名称”或“必须是标签名称”的规则时,通常使用这个六步过程。
提交图
在定义什么是快进更新之前,我们需要查看提交图。快进与非快进只有在提交和提交图的情况下才有意义。因此,它只适用于特定于提交的引用。类似分支的名称-那些在refs/heads/
和refs/remotes/
中的名称-总是指向提交,并且这些是我们在此关心的。
提交通过其哈希ID唯一标识。5每个提交还存储一些父提交哈希ID的集合。大多数提交存储单个父ID;我们说这样的提交指向其父提交。这些指针组成一个向后查找的链,从最新提交到最旧提交:
A <-B <-C
例如,在一个只有三个提交的小仓库中。提交
C
将提交
B
作为其直接父项,因此
C
指向
B
。提交
B
将提交
A
作为其直接父项,因此
B
指向
A
。
A
是第一个提交,因此它没有父项:它是一个根提交,并且没有指向任何地方。
这些指针形成了祖先/后代关系。我们知道这些指针总是向后看,因此我们不需要绘制内部箭头。我们确实需要某些东西来标识数据结构的
tip提交,以便Git可以找到这些链的
ends:
o--o--C--o--o--o--G <-- master
\
o--o--J <-- develop
在这里,{{master}}指向某个提交{{G}},而{{develop}}则指向{{J}}。向后追溯{{J}}或{{G}}最终会导致提交{{C}}。因此,提交{{C}}是提交{{G}}和{{J}}的一个祖先。
请注意,{{G}}和{{J}}之间没有父子关系!它们彼此都不是后代,也不是父母;只有在我们回溯到足够久远的时间/历史时,它们才有一些共同的祖先。
实际上,每个 Git 对象都通过其哈希 ID 唯一标识。例如,这就是 Git 存储某些文件内容的唯一副本时所使用的方法,即使该文件的特定版本存储在数十个或数千个提交中:不更改文件内容的提交可以重复使用现有的 blob 对象。
{{“快进”}}的定义
{{“快进”}}是{{“移动标签”}}的属性。我们可以移动现有的名称({{master
}}和{{develop
}}),但让我们暂时避免这样做。相反,假设我们添加一个新名称,并将其指向提交{{C
}}。让我们为其余提交添加单字母哈希ID:
............ <-- temp
.
A--B--C--D--E--F--G <-- master
\
H--I--J <-- develop
现在我们可以要求Git将新名称从提交移动到任何其他提交。
这样做时,我们可以问一个关于此移动的问题。具体来说,temp当前指向提交C。我们从A到J可能提交的宇宙中选择另一个ID,并告诉Git将temp移动,以使其指向此新选择的提交。我们的问题很简单:新提交是指向标签指向的提交的后代吗?
如果此标签移动导致名称temp指向的提交是C的后代,则此移动是快进。如果不是-如果我们选择提交B或A-则此移动不是快进。
就是这样-这就是快进的全部。这是对我们即将进行的此标签的此更新是否导致该标签沿着我们指向后面的提交链向前移动的答案。
这对于{{分支}}名称尤其有趣——在
refs/heads/
空间中的名称——是因为
git commit
创建一个新的提交,其父提交是当前提交,并将此新提交添加到图形中,然后
更新当前分支名称以指向它刚刚创建的新提交。因此,一系列重复的
git commit
操作会导致分支标签向前单步移动。例如,如果我们检出
develop
并进行两个新提交,我们会得到:
A--B--C--D--E--F--G <-- master
\
H--I--J--K--L <-- develop
现在,名为develop
的指针指向这些新提交中的第二个。
如果我们在调整temp
时,将分支名称temp
指向提交J
,那么我们现在可以将temp
快进到指向提交L
。因为L
指向K
,K
又指向J
,所以跟随这些链的所有Git操作都将把提交K
视为仍然“在”分支temp
上。因此,快进是有趣的,因为它意味着我们不会“丢失”提交。
另一方面,如果我们改为让temp
指向E
,现在将temp
移动到指向K
将从分支temp
中“丢失”提交D
和E
。这些提交仍然安全地存储在master
上,因此它们在这里得到了保护。如果由于某种原因它们不再在master
上——例如,如果我们对master
进行了奇怪或不寻常的操作,比如删除分支名称——那么提交D
和E
将通过名称temp
得到保护,直到我们以非快进方式移动temp
。如果temp
是保护这些提交免受垃圾回收器攻击的唯一名称,它们就会变得脆弱。
将快进与作为动词的“合并”进行比较
Git确实有一些称之为“快进合并”的东西。我不喜欢“快进合并”这个短语,因为它根本不是真正的合并——它更像是运行git checkout
,只是分支名称移动了。但是git merge
文档使用了这个短语,在更正式地说出某些合并解决为快进之后,我们必须能够解释它。
在Git中,快进合并是通过运行git merge other
来实现的,其中other
是严格超前于(即是当前或HEAD
提交的后代)图表中的提交。这意味着附加到HEAD
的分支名称可以以快进方式移动。例如,对于指向提交C
的分支名称temp
,我们可以运行:
git checkout temp
git merge <hash-of-commit-E>
Git会意识到将标签temp从提交C移动到提交E是对该标签的快速转发操作。我们能够在此处使用动词合并的主要事项是我们刚刚使用git merge来实现它:因此,git merge命令更新我们的索引和工作树以及执行快速转发操作。
但这只是git merge借用了快速转发概念。快速转发本身并不是一个“合并”概念。如果您运行不同的git merge other,其中其他不是当前提交的后代,而是某个公共祖先(即合并基)的后代,则在这种情况下,git merge执行真正的合并,使用您的索引和工作树作为合并区域。这是一次合并,是一种真正填补动词短语“合并”的操作。
(我们的图形中没有这样的提交-我们必须使A或B的子级成为合并基础,之后提交A或提交B将是合并基础。)
git fetch和git push都从不合并。
正如我们刚才提到的,真正的合并至少可能需要使用索引和工作树。
git fetch
命令不会触及索引和工作树。
git push
通常用于一个
--bare
存储库,这甚至没有工作树!
git fetch
或
git push
操作可以进行快进操作。由于快进不是合并,因此这并不与我们的“永远不合并”声明相矛盾。
git fetch
或
git push
操作还可以对引用名称(包括分支名称)执行非快速转发操作,但要这样做,必须在特定操作上启用强制标志。 (
git push
命令不仅提供“普通”和“强制”,还提供“带租约的强制”,类似于多线程编程中的比较和交换或CAS指令。fetch命令没有此CAS选项,它只有普通或强制。)
git fetch如何使用refspecs更新引用
git fetch
命令有(至少,取决于您如何计算)两个部分:
- 从另一个Git传输提交(和其他Git对象)到我们的Git,增强我们的提交图;
- 可选地,更新我们存储库中的一些引用。
它的副作用是将其对新提交的所有了解写入.git/FETCH_HEAD中,这是一个特殊文件,绝对不是引用——这方面永远不会有任何歧义,与HEAD不同——但确实包含哈希ID(以及关于我们的Git从其他Git中看到的额外信息)。即使git fetch不更新任何引用,Git的其余部分也可以使用留在此文件中的数据。
现在,请记住,refspec可以列出源引用和目标引用,或仅列出源引用或仅列出目标引用。它还可以有一个前导+符号,表示“必要时强制执行”。
具体看git fetch,则在处理后半部分应该发生的事情时,我们有以下三种可能的情况:
1.既有源又有目标的refspec:使用源在其他Git存储库中定位名称;使用目标选择要在我们自己的存储库中更新的名称。
2.只有源而没有目标的refspec:使用源在其他Git存储库中定位名称,但不更新任何本地名称(但请参见下文)。
3.只有目标而没有源的refspec:错误。
在Git版本1.8.4之前的旧版本中,
git fetch
操作只是遵循命令行中给出的任何refspecs。如果没有指定refspecs,则使用并遵守配置中的
remote.remote.fetch
指令。也就是说,在这些旧版本的Git中,运行
git fetch origin xyz
会获取与
xyz
匹配的任何引用,并且由于没有目标,因此这不会更新我们自己存储库中的任何引用!(该命令仍会像往常一样将信息写入
.git/FETCH_HEAD
。)请注意,
xyz
可能是一个标签:其他Git可能会找到
refs/tags/xyz
而不是
refs/heads/xyz
。我们没有指定;如果要确保获取一个分支,需要指定
refs/heads/
。
然而,如果您的Git至少是1.8.4版本,当
git fetch
获取一个分支名称时,Git会使用您的
remote.remote.fetch
设置进行机会式更新。所以,假设正常的
remote.origin.fetch
设置,
git fetch origin refs/heads/xyz
:
- 不会更新任何内容,因为空目标部分;
- 但随后会更新
refs/remotes/origin/xyz
,因为有fetch
设置。
一旦git fetch
完成所有更新,每个更新:
- 根据引用规则,可以成功更新;或者
- 无法更新因为规则不允许且未设置强制标志;或者
- 即使规则不允许,但设置了强制标志,也可以成功更新。
假设我们运行:
git fetch origin refs/heads/xyz:refs/heads/abc
假设另一个Git上有一个名为
refs/heads/xyz
的分支,而且我们的Git至少是1.8.4,并且在
remote.origin.fetch
中有通常的refspec。那么我们的Git会:
- 如果需要,将与他们Git的
refs/heads/xyz
相关的提交带过来。
- 尝试更新我们的
refs/heads/abc
。此更新不是强制性的。该更新是由我们在命令行上告诉我们的Git导致的。
- 尝试更新我们的
refs/remotes/origin/xyz
。此更新是强制性的。该更新是由我们通过remote.origin.fetch
告诉我们的Git导致的。
由于
refs/heads/
和
refs/remotes/
都是分支样式的名称空间,我们的Git(我们知道至少是1.8.4)遵循此处的分支更新规则。这些规则告诉Git,如果这是一个快进,则可以自动允许更新。
对于第2项,需要更新的名称是
refs/heads/abc
(因为它在命令行上的refspec右侧)。再次强调,这里的
fast-forward与合并无关:Git只是检查
refs/heads/abc
的当前值是否是建议的
refs/heads/abc
新值的祖先。如果是,允许此更新。如果不是,则不允许。
对于第3项,需要更新的名称是
refs/remotes/origin/xyz
(因为左侧匹配的名称是
refs/heads/xyz
,默认的refspec读取
+refs/heads/*:refs/remotes/origin/*
)。该refspec设置了
force标志,因此将发生对
refs/remotes/origin/xyz
的更新。如果更改是快进,它将是一个正常的快进非强制更新。如果更改不是快进,则会进行非快进强制更新。
在Git 1.8.2及更早版本中,Git错误地将分支更新“必须是快进操作”的规则应用于标签名称。在Git 1.8.4中,此问题已得到修复。然而,
某个时间点引入了一个新的错误。Git中用于在
git fetch
期间更新引用的代码非常混乱,我认为可能应该将其丢弃并从头开始重新编码,但实际上这样做本身就是一场噩梦。
git fetch
中还有一个特殊的限制条件
我们在上面提到过,特殊名称HEAD
通常不是引用,通常会与某个分支名称关联。当您的HEAD与某个分支相关联时,该分支就是您的当前分支。这是所谓的内部定义,即拥有该分支作为当前分支的含义:该分支的名称必须在.git/HEAD
文件中。
默认情况下,git fetch
拒绝更新此分支名称。也就是说,如果HEAD
附加到master
上,则git fetch
根本不会更新refs/heads/master
。运行git fetch origin refs/heads/master:refs/heads/master
将无法更新您的refs/heads/master
。在您git checkout
其他分支之后,例如将HEAD
附加到develop
上,然后git fetch
愿意更新master
了,现在您可以运行git fetch origin master:master
(假设您更喜欢较短、稍微有风险、未经修饰的拼写)。
这个特殊限制的原因与我们上面提到的关于
git merge
如何解决快进合并的区别有关:
git merge
会像运行
git checkout
一样
更新索引和工作树,而
git fetch
命令
从不更新索引和工作树。如果
git fetch
允许你将
master
快进到一个新的提交,你的索引和工作树可能会
失控。
问题在于,你的索引和工作树应该与你当前的提交匹配,除了你自从运行
git checkout
更改索引和工作树以来所做的任何工作。如果
git fetch
更新了
HEAD
所附加的
refs/heads/
空间分支名称,你的索引和工作树就不再与你当前的提交匹配,因为你当前的提交是存储在该分支名称中的哈希 ID。(如果你确实设法进入了这种状态,那么修复起来很麻烦,但是它是
为什么Git允许向已添加的工作树的已检出分支推送?我该如何恢复?)
git fetch
命令有一个标志--update-head-ok
,它专门覆盖了这个检查。你不应该使用它。 git pull
代码确实使用它,因为git pull
立即运行第二个Git命令,即使在这些特殊情况下也会更正索引和工作树。此外,git pull
进行一些预fetch
检查,以确保第二个命令不会破坏一切。除非你确切知道自己在做什么,否则你不应该使用它。
7如果你这样做,通常只会给自己增加额外的思维负担。我建议不要将其作为日常实践。相反,使用git fetch origin && git checkout master && git merge --ff-only
。我定义了一个别名git mff
,它运行git merge --ff-only
,我用它来完成这些操作。
git pull --no-ff origin master
。在某些情况下,可以省略origin
和master
。而且 @Christoph 是正确的。 - ElpieKaymaster
不是当前分支,则无法通过git fetch
进行非快进式合并。先检查它,然后再合并。 - ElpieKay