您提到in a comment 有多个遥控器、origin 和upstream。这会干扰——嗯,可能会干扰——人们通常不知道他们依赖的 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-branch 或 git 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 --all 或git remote update,尽管使用git remote 会误入新领域带来了许多新的选择)。
Long:幕后发生了什么
要理解所有这些,您需要了解以下所有内容:
-
分支名称,你已经很熟悉了,但在内部存储着 refs/heads/ 贴在前面(正如你看到的 git ls-remote);
-
remote-tracking names——Git 称它为 remote-tracking 分支名称,但它们实际上并不是 branch 名称,所以我更喜欢从中间去掉那个词:它们在内部存储,refs/remotes/ 贴在前面,后面是远程名称本身;
-
remotes,它们是像 origin 和 upstream 这样的短字符串,如果没有别的东西——通常还有别的东西——存储一个 URL;
-
refs 或 references,它们是分支名称、标签名称 (refs/tags/*)、远程跟踪名称和其他不太常见的名称的长形式,例如refs/notes/* 和 refs/stash;
-
refspecs,它们大多只是由冒号: 分隔的一对引用,并且可以选择以加号+ 作为前缀;最后,
-
git checkout 的“DWIM 模式”功能。 DWIM 代表 Do What I Mean(与我输入的相反)。这个特殊的首字母缩略词可以追溯到 Xerox PARC 和 Warren Teitelman:请参阅 Eric Raymond's Jargon File entry 和 the Wikipedia article on Teitelman。
参考、参考规范和遥控器
真的,您已经知道 refs。它们只是各种参考文献的全名。他们让git fetch 之类的命令知道他们是在处理分支名称 (refs/heads/master) 还是远程跟踪名称 (refs/remotes/origin/master) 或其他任何东西,如果他们关心的话。1
refspec 的最简单形式就是一对带有冒号的 ref。左边的名字是来源,右边的名字是目的地。对于git fetch,source 部分意味着:使用您在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 设置为true,git 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/master、origin/maint、origin/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-dir 和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 被限制为 commit 哈希 ID;其他一些 refs 具有更大的灵活性,例如,标签名称可以指向 Git 的四种内部对象类型中的任何一种。
问题是,当我们说“分支master”或“此提交在分支master”或类似的任何内容时,我们通常不是指一个特定的提交,即使实际的分支 name master 只能识别一个特定的提交。它的工作原理解释了很多关于 Git 的内容。
胶囊形式:
无论如何,我们从提交哈希 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 提交——嗯,几乎每一个——至少有一个 parent。3 Git 可以在历史上倒退一步,从提交给它的父母。父级本身还有另一个父级,因此 Git 可以再移动一步。从commit到parent一步一步移动得到的路径,就是Git仓库中的历史。
对于简单的线性链,我们可以通过暂时假设 Git 使用一个字母名称而不是大而丑陋的哈希 ID 来绘制每个提交:
... <-F <-G <-H <--master
链中的最后一个提交是提交H。那是存储在名称 master 下的哈希 ID。我们说master 指向 H。 H 又存储G 的哈希ID,所以我们说H 指向G。 G 存储 F 的哈希 ID,因此 G 指向 F。 F 指向 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 reset 或git 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 标志。例如,假设您刚刚执行了上述操作,那么现在master 和origin/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 合并了哪些提交。