如何签出仅在"git ls-remote"中列出的分支



我遇到无法切换到仅在git ls-remote中列出的分支的情况,以下是详细信息:

我将 github 存储库 A 分叉为 repoB,创建自己的分支并将其推送到 ComputerA 中的 repoB,在 ComputerB 中,我将分叉的存储库克隆到本地磁盘,添加远程上游,并尝试切换到我创建的分支但失败了,我可以成功切换到 github 网页中的同一分支。

以下结果来自计算机 B 中的存储库 B。

LS-远程分支:

$ git ls-remote --heads
2da2080ea7201fc7928e947dc3214dd89d86c4ba        refs/heads/enable-vim-better-whitespace
433cedd84bba8bcdf3584734906b2c0fd3b6dc3a        refs/heads/fix-lsp-cache-dir
ff65e1cd687d0c144e98b09e4d7a164f8b6bfd3e        refs/heads/gh-pages
17e53cf01badebc2abef7df375903da71bf884d8        refs/heads/master
7b8f8a2dccb0715ff1c1c411abf40b2ff6cec30b        refs/heads/vim-plug
26b8a0ba594af1068997c70c4ef0f503571557b3        refs/heads/vundle

列出分支:

$ git branch
abc
* master
$ git branch -r
origin/HEAD -> origin/master
origin/master
upstream/gh-pages
upstream/master
upstream/vim-plug
upstream/vundle
$ git branch -a
abc
* master
remotes/origin/HEAD -> origin/master
remotes/origin/master
remotes/upstream/gh-pages
remotes/upstream/master
remotes/upstream/vim-plug
remotes/upstream/vundle

abc分行是我还没有推的本地分行。

我尝试了几种切换到分支的方法,例如fix-lsp-cache-dir

$ git checkout fix-lsp-cache-dir
error: pathspec 'fix-lsp-cache-dir' did not match any file(s) known to gi
$ git checkout -t origin/fix-lsp-cache-dir
fatal: 'origin/fix-lsp-cache-dir' is not a commit and a branch 'fix-lsp-cache-dir' cannot be created from it

我尝试了谷歌,但所有建议的方法都失败了。

那么我该怎么做才能切换到git ls-remote中的仅分支列表

您在评论中提到您有多个遥控器originupstream。 这会干扰(嗯,可能会干扰)人们通常不知道他们所依赖的 Git 功能:git checkout所谓的DWIM 模式。 这不是问题,但我们不妨解决它(在下面的长节中)。

您在第二条评论中提到git config -l包含以下输出:

remote.origin.fetch=+refs/heads/master:refs/remotes/origin/master

这不是具有origin的典型标准克隆的正常设置。 正常设置为:

remote.origin.fetch=+refs/heads/*:refs/remotes/origin/*

如果您最初运行git clone --single-branchgit clone --depth=...(这意味着--single-branch),您拥有的设置是常态。

为了使事情顺利进行,您需要更改或添加到remote.origin.fetch设置。 例如,如果您首先将其更改为+refs/heads/*:refs/remotes/origin/*(请参阅 VonC 的更新答案),则可以运行:

git fetch origin

其次:

git checkout -t origin/fix-lsp-cache-dir

甚至只是:

git checkout fix-lsp-cache-dir

如果您只有一个 远程origin.如果您有多个遥控器,它有时会失败,在这种情况下,您需要使用稍长的git checkout -t origin/fix-lsp-cache-dir或单独的git branch命令来创建自己的分支名称fix-lsp-cache-dir

无论如何,您都需要一个首先从origin获取的git fetch。 您可以在git fetch中明确命名origin,或者使用从所有远程获取的选项之一(git fetch --allgit remote update,尽管使用git remote误入新的领域,带来了许多新选项)。

龙:幕后发生了什么

要理解所有这些,您需要了解以下所有信息:

  • 分支名称,您已经熟悉,但在内部存储,refs/heads/卡在前面(如您在git ls-remote中看到的那样);

  • 远程跟踪名称— Git 调用此远程跟踪分支名称,但它们实际上不是分支名称,所以我更喜欢从中间删除这个词:它们在内部存储,refs/remotes/卡在前面,后跟远程名称本身;

  • 远程,它们是像originupstream这样的短字符串,如果没有别的(通常还有其他东西),会存储一个 URL;

  • refsreferences,它们是分支名称、标签名称 (refs/tags/*)、远程跟踪名称和其他不太常见的名称(如refs/notes/*refs/stash)的长形式;

  • refspecs,它们大多只是由冒号分隔的 ref 对:并可选择以加号+为前缀; 最后,

  • git checkout的"DWIM模式"功能。 DWIM代表做我的意思(而不是我输入的内容)。 这个特殊的首字母缩略词可以追溯到施乐PARC和Warren Teitelman:参见Eric Raymond的行话文件条目和维基百科关于Teitelman的文章。

参考

、参考规范和远程

真的,你已经知道参考了。 它们只是每种引用的全名。 他们让像git fetch这样的命令知道他们是否正在处理分支名称(refs/heads/master)或远程跟踪名称(refs/remotes/origin/master)或其他什么,如果他们甚至关心的话。1

最简单的 refspec形式只是一对带有冒号的 ref。 左边的名称是,右边的名称是目标。 对于git fetch部分意味着:使用您在输出中看到的相同内容git ls-remote在我正在获取的存储库中找到名称和值。目标部分意味着在我自己的存储库中创建或更新目标名称。

前导加号(如果出现)将为由于该 refspec发生的任何更新设置--force标志。 因此:

+refs/heads/master:refs/remotes/origin/master

是一个 refspec 说:抓住他们的master分支,并使用它来创建或更新我的origin/master远程跟踪名称。 如果需要,强制进行此更新。您将获得他们在master的任何新提交,然后创建或更新您的origin/master。 您将对自己的origin/master进行此更新,即使这意味着某些提交在此过程中会"从"您的origin/master"掉落"(--force)。

我提到遥控器不仅仅包含 URL。每个远程都列出了一定数量的默认获取引用规范。 通常它只是一个,但通常那个是:

+refs/heads/*:refs/remotes/<remote>/*

填写remote部分。 这个特殊的 refspec 说:获取他们所有的分支名称——所有与refs/heads/*匹配的字符串——并强制创建或更新我所有相应的远程跟踪名称。远程origin的相应名称是refs/remotes/origin/*,所以这就是这里显示的内容。

单分支克隆的工作原理是在 refspec 中使用单个分支名称。 现在,您的git fetch不会创建或更新其余潜在的远程跟踪名称。 修复此问题后,您的git fetch创建或更新其余的远程跟踪名称。

请注意,使用refs/heads/*会启用另一个功能:--prune。 将--prune添加到git fetch命令中,或将fetch.prune设置为配置中的truegit fetch不仅会创建或更新正确的远程跟踪名称集,还会删除任何不再具有源的剩余远程跟踪名称。

例如,如果 Git onorigin有一个名为X的分支一小段时间,并且您运行git fetch,则您的 Git 会创建自己的origin/X。 但是,无论谁在原点上控制 Git,都会删除分支X。 如果您没有启用修剪,则继续携带origin/X:您的 Git 在它存在时创建并更新了它,但现在它没有,您的 Git 对此没有任何作用。 启用修剪,你的 Git 对自己说:啊哈,我有一个剩余的垃圾origin/X! 我会自动剪掉它。修剪可能是默认的,带有"不修剪"选项,但事实并非如此。


1Fetch 实际上确实很关心,因为它试图用标签做一堆神奇的奇怪事情。


Checkout 的"DWIM 模式",以及何时以及为什么使用两个或多个遥控器失败

首次克隆 Git 存储库(不带--single-branch)时,您自己的 Git 会获取origin存储库中每个分支的远程跟踪名称

git clone https://github.com/git/git/

例如,在 GitHub 上为 Git 存储库中的五个分支提供五个远程跟踪名称。

作为本git clone的最后一,您的 Git有效 2运行git checkout master在此阶段,您没有名为master的分支。 事实上,你根本没有分支名称! 那么git checkout怎么检查呢? 如何:

git checkout <name>

曾经工作过,当根本没有分支名称时?

答案是git checkout实际上master创建分支名称。 另请参阅下面的侧边栏(格式化为额外部分,因为我无法做真正的侧边栏)。 当git checkout被赋予看似可能是分支名称但事实并非如此时,它会查看您所有的远程跟踪名称:origin/masterorigin/maintorigin/next等,例如,如果您使用的是 Git 存储库。 如果只有一个名称匹配,那么您的 Git 就像您实际运行一样:

git checkout -t origin/<name>

它告诉git checkout创建分支,将远程跟踪名称设置为其上游。现在这个名字已经存在了,现在git checkout可以查看它了。

如果有两个或多个匹配的名称,则此过程将失败。例如,假设您没有fix-lsp-cache-dir作为分支名称,但在您自己的 Git 存储库中确实origin/fix-lsp-cache-dirupstream/fix-lsp-cache-dir。 您运行:

git checkout fix-lsp-cache-dir

找不到fix-lsp-cache-dir但确实找到了origin/fix-lsp-cache-dirupstream/fix-lsp-cache-dir. 它发现的不是一个而是两个远程跟踪名称。 它应该使用origin的,还是upstream的? 它不知道。

在这一点上,git checkout干脆放弃并说它不知道你所说的fix-lsp-cache-dir是什么意思。 所以现在你需要,例如,git checkout -t origin/fix-lsp-cache-dir,这是一个明确的指令:查找远程跟踪名称origin/fix-lsp-cache-dir,使用它来创建fix-lsp-cache-dir,然后签出fix-lsp-cache-dir这提供了有关使用哪个上游远程跟踪名称以及要创建哪个分支名称的答案。


2我在这里说"有效"是因为git clone里面的代码这样做,并没有真正git checkout运行,也不会打扰很多 DWIM 模式的东西:它确切地知道它已经放入存储库并可以作弊。 如果将git clone拆分为一系列单独的命令:

git init
git remote add origin <url>
git fetch
git checkout master

您将从字面上运行git checkout master并调用我所描述的 DWIM 模式。

(心理锻炼:比较和对比 Git 的分支 DWIM 和智能手机的自动更正。

超长侧边栏:Git 分支如何真正工作

每个 Git 分支名称(实际上,每个 Git引用)实际上只存储一个哈希 ID。 对于分支名称(以及隐含的远程跟踪名称),哈希 ID 被约束为提交哈希 ID;其他一些 ref 具有更大的灵活性,例如,标签名称可以指向 Git 的四种内部对象类型中的任何一种。

问题是,当我们说"分支master",或者"这个提交在分支master",或者任何类似的内容时,我们通常不是指一个特定的提交,即使实际的分支名称master只能识别一个特定的提交。 它是如何工作的解释了很多关于 Git 的信息。

胶囊形式:

  • 为了创建一个分支,我们将一些现有的有效提交的哈希 ID 写入以前不存在的名称中。

  • 为了更新分支,我们将一些现有的有效提交的哈希 ID 写入已经存在的名称中。 它不再识别它刚才记住的提交。 现在它标识了我们选择的那个。

但是,无论如何,我们从提交哈希 ID 开始。 所以从某种意义上说,重要的是提交,而不是分支名称(当然我们也想要这些!

在 Git 中,每个提交都由自己唯一的、丑陋的大哈希 ID 标识。 例如,Git 的 Git 存储库中的一个提交是9c9b961d7eb15fb583a2a812088713a68a85f1c0。 (这是一个为 Git 版本 2.23 做准备的提交,但不是任何特定版本。 这些哈希 ID 对于Git来说很好用——它是一个计算机程序,在将这些东西用作键值数据库中的键时不会出错——但它们对人类来说毫无用处。 我们在名称上做得更好,例如master. 如果我们创建分支名称master并使该名称表示"提交9c9b961d7eb15fb583a2a812088713a68a85f1c0",我们可以运行:

git log master

或:

git diff my-branch master

什么的。master每次都会选择提交9c9b961d7eb15fb583a2a812088713a68a85f1c0的名称。 但是 Git 怎么知道提交8619522ad1670ea82c0895f2bfe6c75e06df32e7——另一个随机的哈希 ID——是master(9c9b961d7eb15fb583a2a812088713a68a85f1c0)之前的提交呢?

答案是8619522ad1670ea82c0895f2bfe6c75e06df32e7存储在9c9b961d7eb15fb583a2a812088713a68a85f1c0

$ git cat-file -p 9c9b961d7eb15fb583a2a812088713a68a85f1c0 | sed 's/@/ /'
tree 33bba5e893986797fd68c4515bfafd709c6f69e5
parent 8619522ad1670ea82c0895f2bfe6c75e06df32e7
author Junio C Hamano <gitster@pobox.com> 1563561263 -0700
committer Junio C Hamano <gitster@pobox.com> 1563561263 -0700
The sixth batch
Signed-off-by: Junio C Hamano <gitster@pobox.com>

此处的parent行给出了上一次提交的原始哈希 ID。

每个 Git 提交——嗯,几乎每一个——都至少有一个父级3Git 可以在历史记录中向后移动一步,从提交到其父级。 父级本身有另一个父级,因此 Git 可以再移动一步。 通过逐步从提交移动到父级获得的路径Git 存储库中的历史记录。

对于简单的线性链,我们可以通过假装 Git 为每个提交使用一个字母名称而不是大丑陋的哈希 ID 来绘制这个:

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

链中的最后一个提交是提交H。 这是存储在名称下的哈希 IDmaster. 我们说master指向H.H反过来存储G的哈希ID ,所以我们说H指向G.G存储F的哈希 ID,因此G指向FF指向F的父母。 这种情况一直持续到我们遇到没有父级的提交,例如此存储库的第一次提交......这些是"在"分支master上的提交。

要添加新提交,我们让 Git 保存所有源文件的快照,添加我们的名称和电子邮件地址以及git log显示的其他内容,使用 commitH的实际哈希 ID 作为父级,然后写出一个新的提交。 这个新提交会得到一个新的、唯一的哈希 ID,但我们只称它为I。 然后 Git 只是用这个新的哈希 ID 覆盖名称master

... <-F <-G <-H <-I   <--master

master分支现在提交时间更长。 链中的最后一次提交称为提示提交。 我们知道(或找到)提示通过从分支名称中读取哈希 ID 在 Git 存储库中提交。

分支名称master只是标识链中的最后一个提交。移动分支名称或远程跟踪名称的各种 Git 命令,例如git resetgit branch -f,或者 - 对于远程跟踪名称 -git fetch- 实际上只是使名称指向一个特定的提交。

如果我们可以从提示开始,并使用内部的向后箭头找到提示,那么我们所做的就是向分支添加一些提交。 当我们使用git commit创建一个提交时,它就是这样做的:它创建一个新的提交,它成为 tip,并且将旧 tip 作为其父级。

当我们使用git fetch并且我们得到,比如说,我们的远程跟踪名称origin/master的三到五个新提交,其中最后一个- 提示 - 最终回到我们运行git fetch之前我们的origin/master指向的位置。 因此,新提交只是新添加到origin/master远程跟踪名称中。

Git 称这种名称更新,只会添加一些东西,快进。 您可以使用git fetch进行快进,更新您的远程跟踪名称,并使用git push向其他一些 Git 提供新提交并让它们更新其分支名称。 在这两种情况下,您的 Git 和/或他们的 Git 都没有丢失任何提交,因为从新提示开始并向后工作,您或他们到达旧提示。

你也可以 - 有一些额外的皱纹 - 用git merge快进。 如果git merge执行快进而不是合并,则使用您已有的提交,而无需实际进行任何新提交。 例如,在git fetch origin之后,您可能有:

...--F--G--H   <-- master (HEAD)

I--J   <-- origin/master

在这里,您实际上是在自己的master,通过将特殊名称HEAD附加到名称master来表示。 您的 Git 现在可以通过将名称移动到master使其指向提交J,并同时执行git checkout提交J执行快进而不是真正的合并:

...--F--G--H--I--J   <-- master (HEAD), origin/master

这就是快进合并:它实际上根本不是合并,而只是一个git checkout,它也向前拖动当前分支名称,就像git fetch快进您的origin/master一样。

当操作不是快进时,需要--force标志。 例如,假设您刚刚执行了上述操作,那么现在masterorigin/master都标识提交J。 同时,谁在origin控制存储库,谁说:哦,废话! 提交J是不好的! 我用git reset --hard扔掉它,而是添加一个新的提交K现在,您再次运行git fetch并获得:

K   <-- origin/master
/
...--H--I--J   <-- master (HEAD)

仍然有提交J:它在您的master上。他们试图抛弃提交J(无论它的实际哈希 ID 是什么——你的 Git 和他们的 Git 同意它的哈希 ID)。 你的origin/master现在指向KK的父母是I,而不是J。 您的origin/master刚刚被强制更新

您将git fetch输出中看到以下内容:

$ git fetch
...
+ a83509d9fc...0ddebcb508 pu          -> origin/pu  (forced update)

Git 的 Git 存储库中的pu分支是每个人都同意定期强制更新的分支。 所以我的origin/pu用来识别a83509d9fc,但现在它可以识别0ddebcb508。 请注意,+,单词(forced update),以及两个哈希ID之间有三个点而不是两个点的事实:这是git fetch宣布我的origin/pu刚刚被强制更新的三种方式。 我现在可以这样做:

$ git rev-list --left-right --count a83509d9fc...0ddebcb508
79  214

这告诉我 79 个提交被删除(从我的旧origin/pu),并添加了 214 个提交(到我的新更新origin/pu)。 在这种情况下,我实际上并不在乎,但是如果我出于某种原因这样做,我可以看到他们在origin做了什么。

(稍微有用一点:

$ git rev-list --count master..origin/master
210

告诉我,现在我可以将 210 个新提交带入我的master。 要实际查看这些提交,我可能需要git log


3没有父级的提交定义为根提交。 当您在一个新的、完全空的 Git 存储库中进行第一次提交时,这就是您所做的那种提交。 第一次提交不能有父级,所以它没有。

具有两个或更多父项的提交定义为合并提交。 这是git merge通常会做出的那种承诺。第一个父母一切照旧;任何其他父级都会告诉 Git 合并了哪些提交。

您需要先git fetch

检查你的 git config remote.origin 确实显示了一个获取引用规范,如下所示:

fetch = +refs/heads/*:refs/remotes/origin/*

这会将fix-lsp-cache-dir导入您的存储库,您将能够签出该分支。
结帐或...很快git switch.

OP CodyChan在评论中证实:

remote.origin.fetch=+refs/heads/master:refs/remotes/origin/master

那只会得到主人,没有别的。

cd /path/to/my/repo
git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*"

这应该可以解决它。