【问题标题】:How to checkout a branch only listed in `git ls-remote`?如何签出仅在`git ls-remote`中列出的分支?
【发布时间】:2019-12-11 15:21:21
【问题描述】:

我遇到了无法切换到仅在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-dirlike

$ 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中的仅分支列表

【问题讨论】:

  • 你能显示远程网址吗(git remote -v)?
  • @D.BenKnoble 我已经添加了远程上游,原始 url 和上游 url 不同。我可以从远程上游同步并从原始网址中提取。
  • 我的意思是你能把它们编辑到 Q 中吗?
  • @D.BenKnoble 你需要这些链接吗?我认为这些网址在这里并不重要。
  • 好吧,如果 fetching 不起作用,它可能 - 这里基本上有 4 个不同的 repos 并追踪哪个有哪些分支......显然 comp B 没有它们,所以我很好奇遥控器是否它从 dos 获取(你试过 fetch —all 吗?

标签: git github


【解决方案1】:

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

您在second comment 中提到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's updated answer),然后您可以运行:

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 会误入新领域带来了许多新的选择)。

Long:幕后发生了什么

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

  • 分支名称,你已经很熟悉了,但在内部存储着 refs/heads/ 贴在前面(正如你看到的 git ls-remote);

  • remote-tracking names——Git 称它为 remote-tracking 分支名称,但它们实际上并不是 branch 名称,所以我更喜欢从中间去掉那个词:它们在内部存储,refs/remotes/ 贴在前面,后面是远程名称本身;

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

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

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

  • git checkout 的“DWIM 模式”功能。 DWIM 代表 Do What I Mean(与我输入的相反)。这个特殊的首字母缩略词可以追溯到 Xerox PARC 和 Warren Teitelman:请参阅 Eric Raymond's Jargon File entrythe Wikipedia article on Teitelman

参考、参考规范和遥控器

真的,您已经知道 refs。它们只是各种参考文献的全名。他们让git fetch 之类的命令知道他们是在处理分支名称 (refs/heads/master) 还是远程跟踪名称 (refs/remotes/origin/master) 或其他任何东西,如果他们关心的话。1

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

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

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

是一个参考规范:抓住他们的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 不仅会创建或更新正确的远程跟踪名称集,而且 em> 删除任何不再有来源的剩余远程跟踪名称。

例如,如果 origin 上的 Git 在短时间内有一个名为 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最后一步,您的 Git2 有效地运行 git checkout master在这个阶段你没有一个名为master的分支。实际上,您根本没有分支名称!那么git checkout怎么查看呢?怎么可能:

git checkout <name>

在根本没有分支名称的情况下工作过吗?

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

git checkout -t origin/<name>

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

如果有两个或更多匹配的名称,此过程将失败。例如,假设您没有fix-lsp-cache-dir 作为分支 名称,但您确实在自己的 Git 存储库中拥有origin/fix-lsp-cache-dir upstream/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 被限制为 commit 哈希 ID;其他一些 refs 具有更大的灵活性,例如,标签名称可以指向 Git 的四种内部对象类型中的任何一种。

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

胶囊形式:

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

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

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

在 Git 中,每个提交都由它自己唯一的、又大又丑的哈希 ID 标识。例如,Git 存储库中用于 Git 的一个提交是 9c9b961d7eb15fb583a2a812088713a68a85f1c0。 (这是为 Git 版本 2.23 准备的提交,但不是任何特定版本。)这些哈希 ID 适合 Git 使用——它是一个计算机程序,它不会生成将这些东西用作键值数据库中的键是错误的——但它们对人类来说毫无用处。我们使用 names 做得更好,例如 master。如果我们创建分支名称 master 并将该名称 mean 设为“commit 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 提交——嗯,几乎每一个——至少有一个 parent3 Git 可以在历史上倒退一步,从提交给它的父母。父级本身还有另一个父级,因此 Git 可以再移动一步。从commit到parent一步一步移动得到的路径,就是Git仓库中的历史。

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

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

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

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

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

master 分支现在多了一个提交。链中的 last 提交称为 tip 提交。我们通过从分支名称中读取哈希 ID 来了解或发现提示在 Git 存储库中的提交。

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

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

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

Git 调用这种名称更新,只添加 东西,快进。您可以使用 git fetch 进行快进,更新您的远程跟踪名称,并使用 git push 向其他 Git 提交新提交并让它们更新其 branch 名称。在这两种情况下,您的 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 提交@987654498 来进行快进非真正合并@,同时:

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

这就是快进合并:它根本不是合并,而只是一个git checkout,它也将当前分支名称向前拖动,就像git fetch fast - 刚才转发了你的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 现在指向K,而K 的父级是I,而不是J。您的 origin/master 刚刚强制更新

你会在git fetch 输出中看到这个:

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

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

$ 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父提交no被定义为根提交。这就是你在一个新的、完全空的 Git 存储库中进行第一次提交时所做的那种提交。第一次提交不能有父级,所以没有。

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

【讨论】:

  • 哇,解释的真长,谢谢你的好意。问题是我用--depth=1 做了git clone 回购,即使在那之后我在回购中git pull --unshallow,我相信它仍然是single-branch。我从你的回复中学到了很多。我还有另一个问题,我如何拉/取所有远程分支,以便在我已经git clone --depth=1 之后可以切换,我尝试了谷歌,但git pull/fetch --all 不起作用,唯一的解决方案是stackoverflow.com/a/17714718/1528712,但那不方便,有没有带选项的git命令。提前致谢。
  • 您只需更新一次git config remote.origin.fetch 设置(对于每个具有单分支设置的遥控器,但在您的情况下可能只是origin)。这会将您的克隆从 --single-branch 更改为正常。 (--unshallow 步骤将您的克隆从浅层转换为完整,您可以按任意顺序执行这些操作。)
  • 虽然一个简单的git fetch origin 让我可以使用该分支,但我真的很喜欢你的解释。
【解决方案2】:

您需要先git fetch

检查您的 git config remote.origin 是否显示 fetch refspec 之类的:

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

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

OP CodyChan 确认in the comments

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/*"

这应该可以解决它。

【讨论】:

  • 我在I tried google, but all of the suggested methods failed. 部分做了git fetch,我又做了一次,我的git config -l 中这个repo 的fetch 部分是:remote.origin.fetch=+refs/heads/master:refs/remotes/origin/mastergit checkout fix-lsp-cache-dir 或@ 987654336@ 仍然失败。
  • 可能是你需要从“上游”获取(如果你不指定远程,获取默认为原点)
  • @CodyChan 您的 refspec 仅用于获取主文件。按照我的建议进行更改,您将获取每个远程分支。
  • @VonC 谢谢你,git clone --depth=1 是@torek 回复的原因,你的建议是我发现的唯一解决方案,可以解决已经用 --depth=1 克隆的回购问题,我'我想知道是否有任何其他更简单的解决方案,例如带有选项的 git 命令?
  • @CodyChan 如果您不想永久更改 refspec,您可以在 fetch 阶段更改它,git -c remote.origin.fetch="+refs/heads/*:refs/remotes/origin/*" fetch
猜你喜欢
  • 2020-02-25
  • 1970-01-01
  • 2020-04-12
  • 1970-01-01
  • 2010-12-19
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多