首页 > 解决方案 > 如何签出仅在`git ls-remote`中列出的分支?

问题描述

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

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

以下结果来自 ComputerB 中的 repoB。

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

标签: gitgithub

解决方案


在评论中提到您有多个遥控器origin并且upstream. 这会干扰——嗯,可能会干扰——人们通常不知道他们依赖的 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-branch,或git 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-dirgit branchfix-lsp-cache-dir

无论如何,您将需要一个git fetch从第一个获取的origin。您可以在您的 , 或使用从所有远程获取的选项之一origin显式命名(或者,尽管使用引入许多新选项的新领域的流浪)。git fetchgit fetch --allgit remote updategit remote

龙:幕后发生了什么

要理解所有这些,您需要了解以下所有内容:

  • 分支名称,您已经熟悉但内部存储refs/heads/在前面(如您所见git ls-remote);

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

  • remotes,它们是类似的短字符串originupstream如果没有别的东西——通常还有别的东西——存储一个 URL;

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

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

  • git checkout的“DWIM 模式”功能。DWIM 代表Do What I Mean(与我输入的相反)。这个特殊的首字母缩略词可以追溯到 Xerox PARC 和 Warren Teitelman:请参阅Eric Raymond 的术语文件条目关于 Teitelman 的 Wikipedia 文章

参考、参考规范和遥控器

真的,你已经知道 refs 了。它们只是各种参考的全名。他们让命令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。每个远程都列出了一些默认的 fetch refspecs。通常只有一个,但通常那个是:

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

remote填写部分。这个特定的参考规范说:获取他们所有的分支名称- 所有匹配的字符串refs/heads/*-<em> 并强制创建或更新我所有相应的远程跟踪名称。remote 的对应名称originrefs/remotes/origin/*,所以这就是这里显示的内容。

单分支克隆通过在 refspec 中使用单分支名称的简单权宜之计来工作。现在您git fetch不会创建或更新其余的潜在远程跟踪名称。解决这个问题,您git fetch 创建或更新其余的远程跟踪名称。

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

例如,如果 Git onorigin有一个名为X短暂的分支,并且您运行git fetch,您的 Git 会创建您自己的origin/X. 但是,无论谁在 origin 上控制 Git,都会删除branch X。如果你没有启用修剪,你继续携带origin/X:你的 Git在它存在时创建并更新它,但现在它没有,你的 Git 对此什么也不做。启用修剪,你的 Git 对自己说:啊哈,我有一个剩余的垃圾origin/X!我会自动剪掉的。 修剪可能应该是默认设置,带有“不修剪”选项,但事实并非如此。


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


Checkout 的“DWIM 模式”,以及在两个或多个遥控器上失败的时间和原因

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

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

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

作为这一步的最后一步git clone,您的 Git 有效地运行了2git checkout master在这个阶段,你没有一个名为master. 实际上,您根本没有分支名称!那么如何git checkout检查呢?怎么能:

git checkout <name>

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

答案是git checkout实际上创建了您的分支名称master。请参阅下面的侧边栏(格式为额外部分,因为我不能做真正的侧边栏)。当git checkout给出看起来可能是一个分支名称但实际上不是的时候,它会查看您的所有远程跟踪名称:例如origin/master,如果您使用 Git 的 Git 存储库origin/maint,它会查看您的所有远程跟踪名称。origin/next如果只有一个名称匹配,那么您的 Git 就像您实际运行一样:

git checkout -t origin/<name>

它告诉git checkout创建分支,将远程跟踪名称设置为其上游。 现在名称存在,现在 git checkout可以检查它。

如果有两个或更多匹配的名称,此过程将失败。例如 ,假设您没有分支名称,但在您自己的 Git 存储库中有,并且. 你跑:fix-lsp-cache-dirorigin/fix-lsp-cache-dir upstream/fix-lsp-cache-dir

git checkout fix-lsp-cache-dir

没有找到fix-lsp-cache-dir但确实找到origin/fix-lsp-cache-dirand upstream/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;其他一些引用具有更大的灵活性,例如,标签名称可以指向 Git 的四种内部对象类型中的任何一种。

问题是,当我们说“分支master”或“此提交在分支上master”或类似的任何内容时,我们通常并不是指一个特定的提交,即使实际的分支名称 master只能识别一个特定的提交. 它的工作原理解释了很多关于 Git 的内容。

胶囊形式:

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

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

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

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

git log master

或者:

git diff my-branch master

管他呢。该名称将每次master选择提交。9c9b961d7eb15fb583a2a812088713a68a85f1c0但是 Git 是如何知道提交8619522ad1670ea82c0895f2bfe6c75e06df32e7(另一个看起来很随机的哈希 ID)是在( )之前的提交呢? master9c9b961d7eb15fb583a2a812088713a68a85f1c0

答案是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 提交——嗯,几乎每个提交——都至少有一个parent3 Git 可以在历史上倒退一步,从提交到其父级。父级本身还有另一个父级,因此 Git 可以再移动一步。从 commit 到 parent 一步一步移动得到的路径,就是Git 仓库中的历史。

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

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

链中的最后一个提交是 commit H。那是存储在 name 下的哈希 ID master。我们说那master 指向 HH反过来存储 的哈希 ID G,所以我们说H指向GG存储 的哈希 ID F,因此G指向FF指向F的父级。这一直持续下去,直到我们遇到一个没有父级的提交,例如这个存储库的第一次提交......并且那些是“在”分支上的提交master

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

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

并且master分支现在是一个提交时间更长。链中的最后一个提交称为提示提交。我们通过从分支名称中读取哈希 ID 来了解或找到 Git 存储库中的提示提交。

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

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

当我们使用git fetch并获得三到五个新的远程跟踪 name 提交时origin/master,其中的最后一个- 提示 - 最终会回到我们在运行之前origin/master指定的位置。因此,新提交只是新添加到远程跟踪名称中。git fetchorigin/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使其指向 commitJ并同时执行 a git checkoutof commit来执行快进非真正合并J

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

这就是快进合并的含义:它实际上根本不是合并,而只是git checkout将当前分支名称向前拖动,就像刚才git fetch快进你origin/master的方式一样。

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

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

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

您将在git fetch输出中看到:

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

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

告诉我master现在可以带入 210 个新的提交。要真正查看这些提交,我可能想要git log。)


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

具有两个或多个父级的提交被定义为合并提交git merge这就是通常所做的那种提交。第一父母照常营业任何其他父项都会告诉 Git 合并了哪些提交。


推荐阅读