【问题标题】:What is the exact difference when you execute `git fetch upstream master:master` vs `git pull upstream master:master`执行 `git fetch upstream master:master` 与 `git pull upstream master:master` 的确切区别是什么
【发布时间】:2018-09-21 21:20:17
【问题描述】:

我知道git fetchgit pull 之间的区别。 git pull 基本上是一个命令中的 git fetch + git merge

但是,我正在研究如何在不检查 master 分支的情况下使用上游更新我的 fork(master 分支)。我遇到了这个 SO 答案 - Merge, update and pull Git branches without checkouts

但是当我在master上签出后使用git fetch upstream master:master时,我遇到了这个错误-

fatal: Refusing to fetch into current branch refs/heads/master of non-bare repository

所以,我尝试了git pull upstream master:master,它成功了。有趣的是,git pull upstream master:master 使用上游更新我的 fork 无论我是否在 master 上。而git fetch upstream master:master 仅在我 不在 master 分支上时有效。

从这里有知识的人那里阅读对此的解释会非常有趣。

【问题讨论】:

标签: git git-pull git-fetch


【解决方案1】:

git pull 基本上是一个命令中的git fetch + git merge

是的——但是,正如您所怀疑的那样,它的作用远不止于此。

Bennett McElwee's comment,在您链接到的答案中,实际上有一个关键项目。他提到您可以:

使用fetch origin branchB:branchB,如果合并不是快进的,它将安全失败。

另一个没有很好的记录:它是git fetch 中的-u 又名--update-head-ok 选项,git pull 设置了该选项。 The documentation 确实定义了它的作用,但有点神秘和可怕:

默认git fetch拒绝更新对应的head 到当前分支。此标志禁用检查。这纯粹是 供 git pullgit fetch 通信的内部使用, 除非你正在实施你自己的瓷器,否则你不是 应该使用它。

这让我们得到你的观察:

所以,我尝试了 git pull upstream master:master,它成功了。有趣的是,无论我是否在 master 上,git pull upstream master:master 都会使用上游更新我的 fork。而git fetch upstream master:master 仅在我不在 master 分支时有效。

这是由于 -u 标志。如果您运行git fetch upstream master:master,这会起作用,从某种意义上说work,但会给您带来不同的问题。警告是有原因的。让我们看看这个原因是什么,看看警告是否过于严厉。警告:这里有很多!下面的大部分复杂功能是为了弥补历史错误,同时保持向后兼容性。

分支名称、引用和快进

首先,我们来谈谈引用快进操作

在 Git 中,reference 只是谈论分支名称(如 master)或标签名称(如 v1.2)或远程跟踪名称(如 origin/master)的一种奇特方式,或者,好吧,任何数量的其他名称,都以一种常见且合理的方式:我们将每个特定的 kind 名称分组为name space, or as a single word, namespace。分支名称位于refs/heads/ 下,标签名称位于refs/tags/ 下,依此类推,所以master 实际上只是refs/heads/master

这些名称中的每一个都以refs/ 开头,是一个引用。还有一些额外的引用不是以 refs 开头的,尽管 Git 在内部决定 HEADORIG_HEADMERGE_HEAD 这样的名称是否实际上是引用时有点不稳定。1 最后,引用主要用作为 Git 对象哈希 ID 提供有用名称的一种方式。特别是分支名称有一个有趣的属性:它们从一个提交移动到另一个提交,通常以 Git 称为 快进的方式。

也就是说,给定一个带有一些提交的分支,这里用大写字母表示,另一个分支有更多的提交,包括第一个分支上的所有提交:

...--E--F--G   <-- branch1
            \
             H--I   <-- branch2

Git 允许将 name branch1 向前滑动,以便它指向之前只能通过名称 branch2 访问的任一提交。2 sup> 比较一下,比方说:

...--E--F--G------J   <-- branch1
            \
             H--I   <-- branch2

如果我们将名称branch1移动到commit I而不是commit J,那么commit J本身会发生什么?3这种动议,留下提交,是对分支名称的非快进操作。

这些名称可以通过省略refs/ 部分来缩短,或者通常甚至是refs/heads/ 部分或refs/tags/ 部分或其他部分。 Git 将使用the gitrevisions documentation 中描述的六步规则,在其引用名称数据库4 中查找第一个匹配的。例如,如果您有refs/tags/masterrefs/heads/master,并说master,Git 将首先匹配refs/tags/master 并使用标签。5


1如果引用是具有或可以具有reflog的名称,则HEAD 引用而@ 987654366@ 和其他 *_HEAD 名称不是。不过,这里的边缘有点模糊。

2这些提交可能可以通过更多名称访问。重要的是,他们在快进之前无法通过branch1 访问,而在快进之后。

3直接的答案实际上是 什么都没有发生,但最终,如果提交 I 无法通过 some 名称访问,Git将垃圾收集提交。

4这个“数据库”实际上只是目录.git/refs 加上文件.git/packed-refs 的组合,至少目前是这样。如果 Git 同时找到文件条目 路径名,则路径名的哈希值会覆盖 packed-refs 文件中的哈希值。

5例外:git checkout 尝试将参数作为分支名称​​first,如果可行,则将master 视为分支名称时间>。 Git 中的其他所有内容都将其视为标签名称,因为前缀为 refs/tags 是第 3 步,而第 4 步是分支名称。


参考规范

现在我们知道引用只是一个指向提交的名称,而分支名称是一种特定类型的引用,快进是普通的日常事物,让我们看看 refspec .让我们从最常见且可解释的形式开始,即两个引用名称,用冒号分隔,例如master:masterHEAD:branch

每当您将两个 Git 相互连接时,Git 都会使用 refspecs,例如在 git fetchgit push 期间。左边的名字是source,右边的名字是destination。如果你在做git fetch,源是 other Git 存储库,目标是你自己的。如果你在做git push,源是你的存储库,目标是他们的。 (在使用 . 的特殊情况下,这意味着 此存储库,源和目标都是您自己,但一切仍然有效,就像您的 Git 正在与另一个独立的 Git 对话一样。)

如果您使用完全限定名称(以refs/ 开头),您肯定会知道您会得到哪个名称:分支、标签或其他名称。如果你使用部分限定或非限定名称,Git 通常会弄清楚你的意思。您偶尔会遇到 Git 无法理解您的意思的情况;在这种情况下,请使用完全限定名称。

您可以通过省略两个名称之一来进一步简化 refspec。 Git 知道您在冒号的哪一侧省略了哪个名称::<em>dst</em> 没有源名称,而 <em>src</em>: 没有目标名称。如果你写<em>name</em>,Git 会将其视为<em>name</em>::一个没有目的地的源。

这些意思各不相同。 git push 的空源意味着 delete: git push origin :branch 让您的 Git 要求他们的 Git 完全删除该名称。 git push 的空目的地表示使用默认值,这通常是相同的分支名称:git push origin branch 通过要求他们的 Git 设置名为 branch 的分支来推送你的 branch 6 请注意,将git push 直接发送到他们的分支 是正常的:您将您的提交发送给他们,然后要求他们设置他们的refs/heads/branch。这和普通的fetch很不一样!

对于git fetch,一个空的目的地意味着不要更新我的任何引用。非空目的地意味着更新我提供的参考。git push 不同的是,您可能在此处使用的通常目的地是远程跟踪名称: 您将获取他们的refs/heads/master 变成你自己的refs/remotes/origin/master。这样一来,您的分支名称 master(您的 refs/heads/master)就不会受到影响。

但是,由于历史原因,git fetch 的通常形式只是写为git fetch <em>remote</em> <em>branch</em>,省略了目的地。在这种情况下,Git 做了一些看似自相矛盾的事情:

  • 它写入分支名称更新nowhere。缺少目的地意味着没有(本地)分支得到更新。
  • 它将哈希 ID 写入.git/FETCH_HEADgit fetch 获取的所有内容总是在这里。这就是git pull 找出git fetch 所做的事情的位置和方式。
  • 它会更新远程跟踪名称,例如refs/remotes/origin/master,即使它没有被告知这样做。 Git 将此称为机会主义更新

(其中大部分实际上是通过 default refspec 控制的,您可以在 .git/config 文件中找到它。)

您还可以通过添加前导加号 + 来使 refspec 复杂化。这会设置“强制”标志,它会覆盖分支名称运动的默认“快进”检查。这是您的远程跟踪名称的正常情况:您希望您的 Git 更新您的 refs/remotes/origin/master 以匹配他们的 Git 的 refs/heads/master 即使这是一个非快进更改,以便您的Git 总是记得 他们的 master 在哪里,最后一次你的 Git 与他们的 Git 交谈。

请注意,前导加号仅在有要更新的目标时才有意义。这里有三种可能:

  • 您正在创建一个新名称。这通常没问题。7
  • 您没有更改名称:它用于映射到提交哈希 H,请求说将其设置为映射到提交哈希 H。这显然没问题。
  • 正在更改名称。这又分为三个子可能性:
    • 这根本不是一个类似分支的名称,例如,它是一个标签,不应该移动。您将需要一个强制标志来覆盖默认拒绝。8
    • 这是一个类似分支的名称,分支动作是快进。您不需要强制标志。
    • 这是一个类似分支的名称,但动作不是快进。您将需要强制标志。

这涵盖了更新引用的所有规则,除了最后一条规则,我们需要更多背景知识。


6您可以通过将push.default 设置为upstream 来复杂化。在这种情况下,如果您的分支 fred 的上游设置为 origin/barneygit push origin fred 会要求其 Git 将其分支设置为 barney

7对于各种更新情况,您可以编写挂钩来执行您喜欢的任何操作来验证名称和/或更新。

8在 1.8.3 之前的 Git 版本中,Git 不小心使用了分支规则进行标签更新。所以这只适用于 1.8.3 及更高版本。


HEAD 很特别

请记住,像 master 这样的分支名称只是标识了一些特定的提交哈希:

$ git rev-parse master
468165c1d8a442994a825f3684528361727cd8c0

您还看到git checkout <em>branchname</em> 以一种方式运行,而git checkout --detach <em>branchname</em>git checkout <em>hash</em> 以另一种方式运行,给出了关于“分离的 HEAD”的可怕警告。虽然HEAD 在大多数方面都像参考,但在某些方面,它非常特别。特别是,HEAD 通常是一个符号引用,其中包含一个分支名称的全名。那就是:

$ git checkout master
Switched to branch 'master'
$ cat .git/HEAD
ref: refs/heads/master

告诉我们当前分支名称masterHEAD 附加到master。但是:

$ git checkout --detach master
HEAD is now at 468165c1d... Git 2.17
$ cat .git/HEAD
468165c1d8a442994a825f3684528361727cd8c0

之后git checkout master 让我们像往常一样回到master

这意味着当我们有一个分离的 HEAD 时,Git 知道我们检查了哪个提交,因为正确的哈希 ID 就在那里,名称为 HEAD。如果我们对存储在refs/heads/master 中的值进行任意更改,Git 仍然会知道我们检查了哪个提交。

但如果HEAD 只包含名称 master,那么Git 知道当前 提交的唯一方法就是468165c1d8a442994a825f3684528361727cd8c0,那就是refs/heads/master 映射到 468165c1d8a442994a825f3684528361727cd8c0。如果我们做了一些更改 refs/heads/master 到其他哈希 ID 的操作,Git 会认为我们已经检查了其他提交。

这有关系吗?是的,它确实!让我们看看为什么:

$ git status
... nothing to commit, working tree clean
$ git rev-parse master^
1614dd0fbc8a14f488016b7855de9f0566706244
$ echo 1614dd0fbc8a14f488016b7855de9f0566706244 > .git/refs/heads/master
$ git status
...
Changes to be committed:
...
        modified:   GIT-VERSION-GEN
$ echo 468165c1d8a442994a825f3684528361727cd8c0 > .git/refs/heads/master
$ git status
...
nothing to commit, working tree clean

更改存储在 master 中的哈希 ID 改变了 Git 对状态的看法!

状态涉及 HEAD vs index plus index vs work-tree

git status 命令运行两个git diffs(嗯,git diff --name-statuses,在内部):

  • 比较 HEAD 与索引
  • 比较索引与工作树

记住,索引,也就是暂存区缓存,保存着当前提交的内容 直到我们开始修改它以保存 我们将进行的下一次提交的内容。工作树只是整个更新索引,然后提交过程的一个小帮手。我们只需要它,因为索引中的文件是特殊的 Git-only 格式,我们系统上的大多数程序都无法使用。

如果HEAD 持有当前提交的原始哈希 ID,那么无论我们如何处理我们的分支名称,比较 HEAD 与索引都会保持不变。但是如果HEAD 持有一个特定的分支名称,并且我们更改那个特定分支名称的值,然后进行比较,我们将比较一个不同的 提交到我们的索引。 索引和工作树将保持不变,但 Git 对(不同)当前提交和索引之间的相对差异的想法将会改变。

这就是 为什么 git fetch 默认拒绝更新当前分支名称。这也是您不能推送到非裸存储库的当前分支的原因:该非裸存储库有一个索引和工作树,其内容可能与当前提交匹配。如果你改变 Git 对当前提交的概念,通过更改存储在分支名称中的哈希,索引和工作树可能会停止匹配提交。

这不是致命的——事实上一点也不。这正是git reset --soft 所做的:它更改了HEAD 所附加的分支名称,而不涉及索引和工作树中的内容。同时git reset --mixed 更改分支名称​​和索引,但保持工作树不变,git reset --hard 一次性更改分支名称、索引和工作树。

快进“合并”基本上是git reset --hard

当您使用git pull 运行git fetch 然后git merge 时,git merge 步骤通常能够执行 Git 所谓的 快进合并。不过,这根本不是合并:它是对 当前分支名称 的快进操作,然后立即将索引和工作树内容更新为新提交,与 @987654461 相同@ 会。主要区别在于git pull 检查——嗯,应该 检查9——没有正在进行的工作会被这个git reset --hard 破坏,而@987654464 @ 本身故意检查,让你扔掉你不再想要的正在进行的工作。


9从历史上看,git pull 一直犯这个错误,并且在有人丢失大量工作后得到修复。避免git pull


将所有这些放在一起

当你运行 git pull upstream master:master 时,Git 首先运行:

git fetch --update-head-ok upstream master:master

让您的 Git 在为 upstream 列出的 URL 上调用另一个 Git,并从它们那里收集提交,如通过它们的名称 mastermaster:master 参考规范的左侧)找到的。然后你的 Git 使用 refspec 的右侧更新你自己的master,大概是refs/heads/master。如果master 是您当前的分支,则fetch 步骤通常会失败(如果您的.git/HEAD 包含ref: refs/heads/master),但-u--update-head-ok 标志可以防止失败。

(如果一切顺利,您的git pull 将运行第二个,git merge,步骤:

git merge -m <message> <hash ID extracted from .git/FETCH_HEAD>

但让我们先完成第一步。)

快进规则确保您的master 更新是快进操作。如果不是,则获取失败并且您的master 保持不变,pull 将在此处停止。所以到目前为止我们没问题:当且仅当从upstream 获得的新提交(如果有)可能的话,您的master 会被快速转发。

此时,如果您的 master 已更改并且这是您当前的分支,那么您的存储库现在不同步:您的索引和工作树不再匹配您的 master。但是,git fetch.git/FETCH_HEAD 中也留下了正确的哈希ID,您的git pull 现在继续进行类似重置的更新。这实际上使用了 git read-tree 而不是 git reset 的等价物,但只要它成功——考虑到pull 之前的检查,它应该成功——最终效果是一样的:你的索引和工作树将匹配新的提交。

或者,也许master 不是您当前的分支。也许您的.git/HEAD 包含ref: refs/heads/branch。在这种情况下,即使没有--update-head-ok,您的refs/heads/master 也会像git fetch 那样安全地快进。您的.git/FETCH_HEAD 包含与更新的master 相同的哈希ID,并且您的git pull 运行git merge 以尝试合并——这可能是也可能不是快进操作,具体取决于您的分支的提交立即命名branch 点。如果合并成功,Git 要么进行提交(真正的合并),要么像以前一样调整索引和工作树(快进“合并”)并将适当的哈希 ID 写入.git/refs/heads/branch。如果合并失败,Git 会因合并冲突而停止,并让您像往常一样清理混乱。

最后一种可能的情况是您的HEAD 已分离,但这与ref: refs/heads/branch 情况的工作方式相同。唯一的区别是,新的哈希 ID 会直接进入 .git/HEAD 而不是 .git/refs/heads/branch

【讨论】:

    猜你喜欢
    • 2015-07-20
    • 2016-07-12
    • 2013-03-14
    • 1970-01-01
    • 2011-12-06
    • 2012-08-06
    • 1970-01-01
    • 2019-10-30
    • 2010-12-01
    相关资源
    最近更新 更多