TL;DR
你的 refspec 是倒退的:你想要git push -f origin for-force-push:master。然而,这会让你得到与你所说的“彻底灾难”相同的东西。我不知道您在这里真正想要什么,因此无法就此提供任何进一步的建议。
长
git fetch 和 git push 都使用 refspec——嗯,一个或多个,但我们先描述一个。 refspec 的格式是 +<src>:<dst>,+ 是可选的——它设置强制标志——而 src 和/或 dst 是可选的也是。如果您删除某些部分(源和/或目标),事情会变得有点混乱,如果您将不合格的引用用于源和/或目标,事情会变得有点混乱。 + 与 git push --force 或 git push --force-with-lease 的交互也令人困惑。因此,出于讨论目的,最好从以下假设开始:
这消除了除了源和目标含义之外的所有混淆,所以现在我们可以简单地解释这些了。请记住,git fetch 表示从它们那里获取提交(和/或其他内部 Git 对象,但主要是提交),git push 表示将提交(和/或其他内部 Git 对象)发送到他们。 它们,顾名思义,是其他一些 Git 存储库,由在另一台机器上运行的各种 git <em>something</em> 命令操作1。
如果我们向他们发送内容,源将是我们的存储库中的提交(和其他 Git 对象)。我们使用 我们的名字 找到这些提交,例如,我们的分支名称:refs/heads/for-force-push 例如。 destination 是 他们的仓库 中的一个名称:例如refs/heads/master。所以,由于语法是<src>:<dst>,我们将使用:
git push <them> refs/heads/for-force-push:master
如果我们从他们那里得到东西——git fetch;名字不好的git pull 做的太多了,2 所以我们在这里忽略它——那么 他们 是“源”,我们是“目的地”,并且我们会使用:
git fetch <them> refs/heads/master:refs/remotes/origin/master
例如。
如果您提供多个 refspec,例如:
git push origin <spec1> <spec2>
Git 将对所有 refspec 进行操作,例如发送多个更新请求。推送操作首先发送任何提交和/或其他所需的内部对象,然后通过询问(常规推送)或命令(强制推送)其他 Git 创建或更新各种名称(引用)在 他们的 em> 存储库。他们的 Git 应该填充到这些新的或更新的引用中的值是您的 Git 通过解析 source 部分找到的值,这也决定了必须发送哪些提交和/或其他对象。因此:
git push origin refs/heads/br1:refs/heads/br1 refs/heads/br2:refs/heads/br2
发送我们在分支br1 上的提交和我们在分支br2 上的提交,然后(礼貌地)询问他们的Git 应该设置他们的分支名称br1匹配我们的分支名称br1,并设置他们的分支名称br2匹配我们的分支名称br2。
这可以让您一次推送多个分支,很多人似乎并不知道这一点。但重要的是,在推送前(发送方)和接收前和接收后挂钩(接收方)挂钩中:您必须通读所有正在请求或命令的更新。
1在退化的情况下,这些命令可能在您自己的机器上运行:例如,您可以从笔记本电脑向笔记本电脑推送或获取。如果您的笔记本电脑上有 ssh 服务器和/或 https 服务器,则可以通过 ssh 和/或 https 执行此操作。这有时是我们设置 VM 的方式,例如使用 Docker 或 VirtualBox 或其他任何东西。确切的细节很大程度上取决于您使用的软件。但大多数情况下,我们是从笔记本电脑发送到 GitHub 或其他任何东西,如果“他们的”Git 软件在 GitHub 等服务器上运行,并且“他们的”Git 存储库是 GitHub 上完全独立的存储库,则更容易考虑这一点。
2如果 git fetch 所做的事情被称为 git pull,我们会说“拉和合并”或“拉和变基”,这一切都会更有意义。相反,我们通过说git pull 来说“获取和合并或变基”,然后我们必须停下来,往回走一点,弄清楚我们是在合并还是变基,然后才能再次前进。与 revert-vs-backout 一样,Mercurial 做对了这一点,而 Git 做错了这一点,尽管 revert-vs-backout 的情况显然更糟。
缩写
写出refs/heads/master、refs/remotes/origin/master 等等有点痛苦。 Git不能解决这个问题吗?如果我想推分支br1,为什么我不能使用:
git push origin br1:br1
例如? Git 只需要为我在两边填写refs/heads。如果我:
git push origin v1.7:v1.7
Git 不知道v1.7 是一个标签,给我在两边填上refs/tags/?
这里的答案是:是的,Git 可以解决这个问题。 方法 它发现这一点很复杂。有人需要有名字。对于git push,我们正在发送——我们是源——所以我们需要有名称,以便Git 可以查找提交哈希ID。但是当我们使用git push 时,我们甚至不需要在左侧放置一个name作为来源:
git push origin a123456:refs/heads/newbranch
如果 a123456 是我们提交之一的(缩写)哈希 ID,则允许,并且:
git push origin HEAD:refs/heads/newbranch
即使我们在分离的 HEAD 上也是允许的。因此,如果git push refspec 的 left(源)侧不是完全限定的,则 right 侧 可用于限定名称——但现在 destination 名称必须与 other Git 中的某个现有名称匹配。它会匹配哪一个?你可能不知道。让 Git 自己匹配是一个坏主意,因为它可能会匹配 错误的东西(例如标签名称)。如果您不使用自己的 Git 通过源代码进行解析,我建议您在此处使用完全合格的参考资料。如果您尝试从 分离的 HEAD 创建一个 新分支(这是我自己的用例之一),您必须提供无论如何都是一个完全限定的名称:
$ git push origin $(git rev-parse HEAD):xyzzy # simulate detached HEAD
error: The destination you provided is not a full refname (i.e.,
starting with "refs/"). We tried to guess what you meant by:
- Looking for a ref that matches 'xyzzy' on the remote side.
- Checking if the <src> being pushed ('11ae6ca18f6325c858f1e3ea2b7e6a045666336d')
is a ref in "refs/{heads,tags}/". If so we add a corresponding
refs/{heads,tags}/ prefix on the remote side.
Neither worked, so we gave up. You must fully qualify the ref.
hint: The <src> part of the refspec is a commit object.
hint: Did you mean to create a new branch by pushing to
hint: '11ae6ca18f6325c858f1e3ea2b7e6a045666336d:refs/heads/xyzzy'?
git fetch 命令与git push 不同,部分原因是历史原因。当我们使用git push 时,我们必须向其他Git发送一些名称来设置。也就是说,git push origin master: 毫无意义。所以git push 的这种形式是完全无效的。然而,git push origin :master“意味着”git push --delete origin master:也就是说,我们的 Git 要求他们的 Git 删除他们的名字 master。和以前一样,由于我们没有在 我们的 端查找名称,因此我们依靠 他们的 Git 的名称来确定这是分支名称还是标签名称。在这里省略refs/heads/ 或refs/tags/ 部分也不是完全明智的——它们的名称可能会在我们的Git 在git push 期间查找它们之前更改——但至少这一次您可能希望与目的地 on 的这两种名称中的一种完全匹配(即,我们不会尝试创建 new 分支或标签,我们正在尝试删除一个现有的分支或标签)。
所以,在git push 期间,我们可以:
- 我们实际拥有的缩写名称,因为我们知道 Git 会查找我们的名称并找到完全限定的版本;
- 完全省略
:<em>dst</em> 部分,因为git push origin br1 意味着无论如何git push origin br1:br1。
这让我们可以将提交推送到另一个 Git 并在两边使用相同的分支名称,这可能是我们最常见的用例。而且,如果我们将origin/br1 设置为当前分支br1 的上游,我们可以运行git push 并完成它——前提是我们没有摆弄@无论如何,987654389@ 设置。3
特殊语法:
git push origin :
调用匹配模式。在这里,我们的 Git 调用他们的 Git 并让他们列出他们的分支名称。现在我们知道他们是否有一个名为br1 的分支,是否有一个名为master 的分支,等等。我们的 Git 也知道,因为它正在查看我们的存储库,是否有名为 br1 和 master 的分支等等。对于所有匹配的分支名称,我们的 Git 会尝试 git push 那对名称。
3这适用于simple 的default-since-Git-2.0 push.default,也适用于current 和upstream。它与matching 不同,这是一些古老版本的 Git 的默认设置;如果你正在使用这些,要么升级,要么坚持使用git push origin br1。如果你将push.default 设置为nothing,Git 会强制你拼写出来:我尝试了这种模式一段时间,但它太痛苦/太累了,我大部分时间都回到了simple .
git fetch 不一样
当我们使用 no 参数或仅 一个 参数(例如远程名称 (git push origin) 运行 git push 时,默认 ,在现代 Git 中是:
- 找到当前分支的上游设置;
- 确保它是
origin/<em>B</em>,其中B 是当前分支的名称(并根据需要将此处的origin 替换为适当的远程名称),并要求该分支 B 存在于另一个 Git 上;
- 执行
git push origin <em>B</em>:<em>B</em>(如果合适,请再次在此处替换origin)。
所以这会从当前分支推送 一个 组提交,并要求另一个 Git 更新其分支中的 一个:名称在当前分支中的分支。如果你觉得这里的前两个要点很烦人——分支 B 存在 on 远程的要求,并且被设置为当前分支的上游——你可以改变你的push.default 到 current。请注意,这很容易意外地公开一个您本来不想推送的私有分支,因此,如果您这样做,请小心。 (matching 模式没有这个问题,所以如果你喜欢 Git 在 1.x 中的行为,你可以将你的 push.default 更改为 matching;请注意,这通常会推送多个分支。)
但git fetch 不同。如果我们不带参数运行git fetch,Git 会:
- 找到当前分支的上游(如果已设置),并从那里获取远程的名称:如果我们在分支上
paradise 并且它的上游是 phloston/paradise,远程 git fetch 将使用 phloston;如果失败,回退到origin;
- 查找
remote.<em>remote</em>.fetch,这是一个多值配置选项;
- 在这些配置设置(复数)中使用这些 refspecs 作为 fetch 的 refspecs。
如果我们在命令行上提供一个或多个 refspecs — 如:
git fetch origin master
例如,git fetch 使用提供的遥控器(我们必须提供一个4)和 refspec(s)。 那些 refspecs 控制发生的事情——嗯,主要是。我们稍后会回到“大部分”。
如果我们将它们排除在外,使用git fetch 或git fetch origin,我们会从remote.origin.fetch(或任何其他设置)获得默认值。 这个默认值是 Git 中许多谜团的原因。特别是,单分支克隆是这样工作的。
如果我们创建一个 单分支 克隆(某个 URL),例如:
git clone -b somebranch --single-branch <url>
这个克隆中的remote.origin.fetch设置将是:
+refs/heads/somebranch:refs/remotes/origin/somebranch
如果我们不使用--single-branch(也不使用--depth,设置--single-branch),我们会得到:
+refs/heads/*:refs/remotes/origin/*
请注意,这些 refspecs 都是两部分:有一个 src 和一个 dst,用冒号分隔 :,并以+ 强制标志始终设置。 destination 是一个远程跟踪名称,在本例中为refs/remotes/origin/ 名称。 source 是一个 branch 名称。所以这就是我们的 Git 将它们的 branch 名称复制到我们的 remote-tracking 名称的原因。他们的master 或main 成为我们的origin/master 或origin/main。他们的dev 变成了我们的origin/dev。如果他们有一个origin/whatever,它在他们的存储库中就是refs/remotes/origin/whatever,所以我们根本不会复制它。
这告诉我们关于 refspec 的其他信息:它们可以包含通配符。我们大多只将这些与git fetch 一起使用,即使这样也仅与 default 获取值一起使用。但是可以在其他地方使用它们(随意尝试这个,但要小心,这很容易弄得一团糟:使用存储库的副本,或者使用垃圾临时的,而不是那些你想保留)。请注意并注意 shell * 扩展和 Git * 扩展之间的区别。
这些默认的 refspecs,对于git fetch,在命令行上没有 refspec,只有在你没有在命令行上放置 refspec 时才使用。还是他们?现在我们来看看 fetch 和 push 之间的另一个区别。
在git push 中,git push <em>remote</em> <em>src</em>: 形式的请求(没有目标的源)无效:
$ git push origin branch:
fatal: invalid refspec 'branch:'
(即使branch 是一个有效的分支,因为它在我在这里使用的测试存储库中)。但是对于git fetch,这不是错误:
$ git fetch origin branch:
From: <url>
* branch branch -> FETCH_HEAD
这里的输出中有这个时髦的FETCH_HEAD。现在看看当我删除 origin/branch 然后重新运行同样的git fetch 时会发生什么:
$ git branch -r -d origin/branch
Deleted remote-tracking branch origin/branch (was 222c4dd).
$ git fetch origin branch:
From <url>
* branch branch -> FETCH_HEAD
* [new branch] branch -> origin/branch
如果我使用不带冒号的branch,也会发生同样的事情。这是“大部分”部分崩溃的地方。如果我重复git branch -r -d删除origin/branch,然后直接从URL中获取,而不使用名称origin:
$ git branch -r -d origin/branch
Deleted remote-tracking branch origin/branch (was 222c4dd).
git fetch <url> branch
From <url>
* branch branch -> FETCH_HEAD
这一次,没有:
* [new branch] branch -> origin/branch
线。这里发生了什么?现在是另一个答案部分的时候了。
4从技术上讲,我们必须提供一个位置参数,但它不必是一个远程。如果它不是遥控器,那么所有关于遥控器的常用规则都会在这里消失。明显的替代规则适用:需要一个 refspec,并且没有 remote 来获得默认的 refspec,因此您必须至少提供一个;如果您确实提供了一个或多个,remote.<em>remote</em>.fetch 行将被忽略,因此我们无法获取它们这一事实是无关紧要的,除了上面的“大部分”注释。
在非常古老的 Git 中,遥控器(如 origin)不存在。每次都必须直接从 URL 中获取。这很痛苦,因此 Git 开发了几种不同的方法来处理它,最终导致了遥控器的发明,以及 git clone 为我们制造的标准第一个遥控器 origin。
但是,如果没有遥控器,就永远不可能有任何远程跟踪名称。如果我们没有origin,我们怎么会有origin/branch?答案是:我们没有。相反,git fetch 只是将其信息写入文件.git/FETCH_HEAD:
$ cat .git/FETCH_HEAD
222c4dd303570d096f0346c3cd1dff6ea2c84f83 branch 'branch' of <url>
进入这个文件的确切格式有点复杂,但是当git pull 是一个shell 脚本时,git pull 严重依赖它:git pull ran git fetch,然后使用哈希 ID(见左侧)、此处未包含的 not-for-merge 以在必要时跳过一些行,以及右侧的信息来构建 git merge 命令。所以这将允许git pull <em>url</em> branch 运行:
git merge -m "merge branch 'branch' of <url>" <hash>
(git pull 今天仍然如此,除了现在它不再是 shell 脚本并且不需要在运行 git fetch 和随后的 git merge 之间留下的 FETCH_HEAD 文件,因为构建了获取和合并步骤进入 C 程序)。
随着遥控器的发明,这可以简化,但很长一段时间都没有:git fetch 仍然写 .git/FETCH_HEAD 和 git pull 仍然是一个 shell 脚本,运行 git fetch 然后运行 @ 987654497@ 找出正确的行并构建命令行参数后。即使在今天,为了兼容性,git fetch 仍然会写入这个 FETCH_HEAD 文件。
因此,git fetch 可以使用仅包含 source 部分的 refspec 从 URL 或远程获取。在这种情况下,git fetch 要做的显而易见的事情是只写入 FETCH_HEAD 文件:如果我们需要,信息就在那里。
但是……这不方便。因此,随着遥控器的发明和remote.origin.fetch 配置行,git fetch 被告知阅读这些行并默认遵守这些参考规范。这将创建更方便的远程跟踪名称:您只需运行git fetch 或git fetch origin,现在您就有了origin/branch。由于默认设置使用 force 标志,您的origin/branch总是更新以匹配他们的branch。5
所以git fetch 或git fetch origin 做了很棒的事情:它完全更新了我们所有的远程跟踪名称,根据他们当前的分支,出现在另一个 Git 存储库中的任何新提交6 我们现在知道所有关于其存储库状态的信息,至少在我们的fetch 运行的纳秒内。 (到现在为止,秒可能已经过去了,事情可能会有很大的不同,这取决于他们的存储库的活跃程度。)
但是如果我们运行git fetch origin master 或git fetch origin main 会怎样?现在我们只要求更新 refspec master。在git push 中,master 是master:master 的缩写,它变成了refs/heads/master:refs/heads/master。但是对于git fetch,master 是master: 或refs/heads/master: 的缩写。这会写入.git/FETCH_HEAD,然后停止。
嗯,在 Git 版本 1.8.4 之前它就是这样做的。在那之前,git fetch origin master 没有更新我们的远程跟踪名称 origin/master。但这……不是最理想的?恶心?一方面,我觉得这很烦人,显然 Git 维护人员也这样做了。他们添加了他们所谓的机会性更新。
如果我们刚刚从origin 获取master,并且如果origin 具有默认 remote.origin.fetch refspecs 包括refs/heads/master:refs/remotes/origin/master——带有或不带有强制标志,以及之后如果合适,在任何 refspecs 中扩展 * - 然后 git fetch,无论如何,因为 1.8.4,将继续更新 refs/remotes/origin/master 现在。
这些机会性更新可以随时工作:git push origin master 检查您的推送是否成功,如果成功,则适当更新您的origin/master。 (这是在 Git 早于 1.8.4,这就是为什么不在 fetch 上这样做是如此不一致。)同样,git fetch origin master,它“意味着”git fetch origin refs/heads/master:,因此不更新目标 ref,仍然机会性地根据默认的 fetch refspec 更新远程跟踪名称。
这一切都要求您使用名称origin,以便Git 可以查找remote.origin.fetch。这就是为什么使用origin 代表的URL 会导致机会更新的缺乏。该 URL 不是远程的,Git 找不到 remote.origin.fetch 设置,无法应用机会更新规则。
5强制标志意味着即使不是快进也要执行此更新,这意味着你需要搜索什么快进 em> 在 Git 中的意思。我会在这里参考another answer I wrote about git fetch。此描述适用于获取和推送,但git push 现在有一个更高级的版本--force-if-lease,而git fetch 缺少。
6除非您将fetch.prune 设置为true 或使用各种修剪选项,否则git fetch 仍然会留下陈旧 远程跟踪名称。这里我将忽略这个问题。
这对你意味着什么,作为一种有用的特殊效果
由于git fetch 和git push 采用refspecs,并且无论如何都获取机会性更新,并且refspecs 遵循快进 规则,你可以运行:
git fetch origin refs/heads/br1:br1 refs/heads/br2:br2
只要你现在没有在任何一个分支。你的 Git 会在 origin 调用 Git,查找它们的分支名称,然后:
- 更新您的远程跟踪
origin/br1 和origin/br2,使用强制,机会主义;还有
- 创建或更新您的
refs/heads/br1,如果您的br1 领先于他们,拒绝(作为非快进)更新,同样适用于您的分支br2。
(我自己从来没有真正这样做过,如果您将 fetch.prune 设置为 true 并使用 * 进行一些通配符,您可以这样开枪打死自己。我在其中一个中破坏了一些裁判我在写这篇文章时使用的垃圾库。所以在这里避免使用通配符,特别是如果你打开修剪。)