为什么“git fetch origin branch:branch”只在非当前分支上起作用?

19

在使用特性分支时,我使用这个Git命令将我的“develop”分支更新到最新状态,紧接着在将我的特性分支与“develop”合并之前:

git fetch origin develop:develop

这个命令有效,即本地的“develop”指向与“origin/develop”相同的提交,并且处于最新状态。

但是,当“develop”分支被检出时,该命令会失败:

fatal: Refusing to fetch into current branch refs/heads/develop of non-bare repository
fatal: The remote end hung up unexpectedly

如果我知道为什么会发生这种情况,那么对我更好理解Git。

2个回答

24
错误信息来自builtin/fetch.c#check_not_current_branch()函数。
该函数可以追溯到2008年10月的提交8ee5d73,git 1.6.0.4
(也可以参考“Git拒绝将内容提取到当前分支”)。

注释很有启示性:

Some confusing tutorials suggested that it would be a good idea to fetch into the current branch with something like this:

git fetch origin master:master

(or even worse: the same command line with "pull" instead of "fetch").
While it might make sense to store what you want to pull, it typically is plain wrong when the current branch is "master".
This should only be allowed when (an incorrect) "git pull origin master:master" tries to work around by giving --update-head-ok to underlying "git fetch", and otherwise we should refuse it, but somewhere along the lines we lost that behavior.

The check for the current branch is now only performed in non-bare repositories, which is an improvement from the original behaviour.

考虑到函数check_not_current_branch()在以下情况下被调用的:

if (!update_head_ok)
        check_not_current_branch(ref_map);

那意味着 git fetch -u origin develop:develop 应该能够正常工作。
-u
--update-head-ok

默认情况下,git fetch 拒绝更新与当前分支对应的头部。此标志禁用了该检查。这仅用于 git pull 与 git fetch 之间的内部通信,除非您正在实现自己的 Porcelain,否则不应使用它。尽管您不应使用该选项,但它确实满足了您最初的要求,使“git fetch origin branch:branch”在当前分支上工作。
关于这个补丁的起源,请参阅讨论

尽管将想要拉取的内容存储起来可能是有道理的。

那就是 fetch 的部分:它会存储来自更新的 origin/master 的远程历史记录。
但是当当前本地分支也是 master 时,情况就特别糟糕了。
正如 这个答案 中所提到的:

我认为 "git fetch url side:master" 当 master 是当前分支,并且我们省略了 --update-head-ok 时是有问题的。
测试在当前的 master 上失败了。

它也无法更新工作目录,并且会将索引留下来,就好像你要删除所有东西一样。


以 "使用 refspec 的 git pull" 为例。
torek 展示了一个例子,其中:

suppose that I run git fetch and it brings in two new commits that I will label C and D.
C's parent is A, and D's is the node just before B:

                C
               /
   ...--o--o--A   <-- master
               \
                o--B   <-- develop
                 \
                  D

The output from this git fetch will list this as:

  aaaaaaa..ccccccc  master     -> origin/master
+ bbbbbbb...ddddddd develop    -> origin/develop  (forced update)
如果你当前的分支不是develop,那么强制更新可能正是你想要的。
但是,如果你在输入git fetch origin develop:develop时处于develop分支上,并且允许更新HEAD,则你当前的索引将反映D而不再是B
因此,在你的工作树中进行git diff会显示你的文件与D之间的差异,而不是之前的HEADB
这很糟糕,因为你最初的git checkout develop创建了一个与B HEAD文件相同的工作树。
即使你的git status干净(没有任何修改),如果git fetch origin develop:develop更新了HEAD(从B强制更新到D),git status现在也会报告之前不存在的差异。
这就是为什么默认情况下,git fetch拒绝更新对应于当前分支的HEAD
注意:Git 2.29中的一个错误也会触发类似的错误消息。
当 "git commit-graph"(man) 在合并层时检测到同一个提交记录多次记录时,它过去会崩溃。现在的代码忽略除其中一个以外的所有内容并继续运行,在Git 2.30(2021年第一季度)中修复。

查看 提交 85102ac提交 150f115(2020年10月9日)由Derrick Stolee (derrickstolee)进行。
(由Junio C Hamano -- gitster --于2020年11月2日合并到提交 307a53d中)

commit-graph:在合并层时忽略重复项

报告者:Thomas Braun
协助者:Taylor Blau
共同作者:Jeff King
签名者:Derrick Stolee

Thomas报告说一个 "git fetch"(手册) 命令失败了,出现了一个错误,提示“意外的重复提交ID”。

$ git fetch origin +refs/head/abcd:refs/remotes/origin/abcd fatal: 意外的重复提交ID 31a13139875bc5f49ddcbd42b4b4d3dc18c16576

根本原因是他们启用了 fetch.writeCommitGraph,它生成了 commit-graph 链,并且此实例合并了两个包含相同提交ID的层次结构。

最初的假设是,如果 Git 已经将提交ID写入较低的 commit-graph 层,则不会将其写入 commit-graph 层。
不知何故,这种特殊情况确实发生了,导致了这个错误。

虽然意外,但实际上这并不无效(只要两个层次结构在提交的元数据上达成一致)。当我们解析一个没有 graph_pos 的提交时,在 commit-graph 层次结构中使用二进制搜索来查找提交并设置 graph_pos
在这种情况下,该位置将不再使用。但是,当我们从 commit-graph 文件中解析提交时,我们从 commit-graph 中加载其父项,并在那时分配 graph_pos
如果这些父项已经从 commit-graph 中解析过了,则不需要做任何操作。否则,该 graph_poscommit-graph 中的一个有效位置,因此我们可以在必要时解析父项。

因此,这个 die() 过于激进。最简单的方法是忽略重复项。

如果只忽略重复项,那么我们将生成一个在相邻位置列出了相同提交ID的 commit-graph。这些多余的数据永远不会从 commit-graph 中删除,这可能会导致文件大小显著增大。

幸运的是,我们可以折叠列表以消除重复的提交指针。这使我们能够获得所需的最终结果,而不需要额外的内存成本和最小的 CPU 时间。

根本原因是禁用了 core.commitGraph,这会防止在 'git commit-graph write --split'(手册) 命令期间从较低层次结构解析提交。
由于我们使用 'graph_pos' 值来确定提交是否在较低层次结构中,因此我们永远不会发现那些提交已经在 commit-graph 链中并将它们添加到顶层。然后合并此层,从而创建重复项。

没有这个更改,t5324-split-commit-graph.sh 中添加的测试将失败。但是,我们仍然没有完全消除对此重复检查的需要。这将在后续更改中实现。

并且:

commit-graph:禁用时不写入提交图

报告者:Thomas Braun
协助者:Jeff King
协助者:Taylor Blau
签署者:Derrick Stolee

core.commitGraph配置设置为'false'可以防止从commit-graph文件中解析提交记录。但这会导致一个问题,当尝试使用"--split"进行写操作时,需要区分已存在的commit-graph层中的提交和不在其中的提交。
现有机制使用parse_commit(),并通过检查是否存在“graph_pos”来判断提交是否来自commit-graph文件。

core.commitGraph=false时,我们不会从commit-graph中解析提交,并且'graph_pos'表示没有提交在现有文件中。
--split逻辑继续向前创建一个新层,包含所有可达的提交,然后可能向这些层合并,导致重复提交。先前的更改使得合并过程更加健壮,以处理在写入的commit-graph数据中出现这种情况。

这里的简单答案是,如果禁用了读取commit-graph,则避免编写commit-graph。因为生成的commit-graph将不会被后续的Git进程读取。这比强制core.commitGraph对于“write”过程为true更自然。

git commit-graph现在在其手册页面中包含以下内容:

基于packfiles中的提交编写commit-graph文件。如果禁用config选项core.commitGraph,则该命令将输出警告,然后返回成功而不编写commit-graph文件。


1
你能更清楚地解释为什么git默认情况下会阻止git fetch origin master:master吗?我觉得理解这一点的关键可能在于这句话中,但是它的意思对我来说不太清楚:“虽然将要拉取的内容存储起来可能是有道理的,但当当前分支是'master'时,通常是错误的。”作者所说的“存储要拉取的内容”是什么意思? - sleeparrow
@sleeparrow 我不得不回到原来的线程获取这个补丁:https://www.spinics.net/lists/git/msg82242.html。我已经更新了答案。 - VonC
@VonC,我知道他们将git fetch的默认行为设置为不更新HEAD。但是为什么?我看到了这个讨论,但是无法理解原因为何? - Number945
@BreakingBenjamin 我认为(重新阅读我3年前的答案)最后一句话很关键:“它也无法更新工作目录,并且会将索引留在好像你正在删除所有内容的状态。” 在非裸仓库中,其中索引,这不是您想要的! - VonC
@BreakingBenjamin 我已编辑我的回答,以说明为什么 git fetch 不会更新 HEAD 如果被获取的分支是当前分支。如果这回答了你的评论问题,请告诉我。 - VonC
显示剩余2条评论

0

git fetch 只从远程仓库获取数据

  1. 它不会更新您的本地分支,即使它们已设置为跟踪远程分支
  2. (由于1) 它无法将远程分支提取到本地分支。这只是 不是 git fetch 的功能

根据 man:

git fetch [ < options > ] [ < repository > [ < refspec > … ] ]

您可以像这样运行 git fetch origin develop,它将仅更新您的远程分支引用 origin/develop

为了更新您的本地分支,您可以通过以下方式之一进行操作:

  1. 明确指定应将哪个远程分支拉取到哪个本地分支:git pull origin develop:develop
  2. 如果现在已经检出了develop分支,并且已设置为跟踪origin/develop,则只需运行git pull,它就会自动确定要拉取什么内容。

更新

更多来自man的内容:

当在命令行上使用明确的分支和/或标签运行git fetch时,例如git fetch origin master,命令行中给出的s确定要获取的内容(例如示例中的master,它是master:的简写,这意味着“获取主分支,但我没有明确指定要从命令行更新哪个远程跟踪分支”),并且示例命令将仅获取主分支。remote..fetch值确定是否更新任何远程跟踪分支。以这种方式使用时,remote..fetch值不会影响决定获取什么(即当命令行列出refspecs时,这些值不用作refspecs);它们仅用于决定被获取的引用存储在何处,充当映射。

可以更新本地分支,但我仍然不明白为什么在您想要更新的分支上执行此操作是不可能的。


1
它不能将远程分支提取到本地分支中 - 但它确实可以!请查看我的更新问题。它可以工作,前提是本地的“develop”没有被签出。 - MaDa
@MaDa,没错,你说得对。我漏掉了这个……现在我也感到困惑:) - Max Komarychev

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接