【问题标题】:Force Push a Git Subtree强制推送 Git 子树
【发布时间】:2021-12-19 23:32:58
【问题描述】:

互联网上每一篇关于“强制推送 Git 子树” 的文章都使用了gh-pages:gh-pages 的例子,比如Git force push subtree: error: unknown option `force' 等。

但是,gh-pages:gh-pages 的意思是

强制将 gh-pages 分支推送到位于 origin 的远程 gh-pages 分支

我根本无法适用于我的情况。

我有一个 master 分支,我已经完成了

$ git subtree split --prefix=my/subtree -b for-force-push
Created branch 'for-force-push'
1044de7a3abdccd0992c6973d23e0faabcc0082c

下一行应该是:
git push -f origin master:for-force-push
对吧?

但是,我看到了:

. . .
remote: Create a pull request for 'for-force-push' on GitHub by visiting:
. . .
remote: Create pull request for for-force-push:
remote:   https://bitbucket.org/. . .
. . .

即,我的每个遥控器都告诉我创建拉取请求,除了我的子树,它没有看到任何更新。顺便说一句,这是一个要点回购。

我做错了什么还是不能强制推送 gist repo 的子树?

PS。

找到另一个“快捷方式”

git push origin `git subtree split --prefix=my/subtree master`:master --force

但这只是让我的 git 处于以下状态:

On branch master
Your branch and 'origin/master' have diverged,
and have 140 and 7 different commits each, respectively.

这是一场彻底的灾难:(。

更新:

我尝试创建最少的步骤来复制这种情况,并且我已经用尽了我能想到的所有可能,但仍然无法强制推送到我的 gist 子树。详情如下:

cd /tmp
mkdir sbtr
cd sbtr

git init
git commit --allow-empty -n -m "Initial commit."

git remote add -f gist-subtree git@gist.github.com:7...36a.git

git subtree add --prefix=sbtrpth gist-subtree master --squash
git subtree pull --prefix=sbtrpth gist-subtree master --squash

# Create a situation that Updates were rejected because the tip of your current branch is behind its remote counterpart. 
# Then, all the following attempts failed:

 git push gist-subtree `git subtree split --prefix=sbtrpth gist-subtree master`:master --force
 git push gist-subtree `git subtree split --prefix=sbtrpth master`:master --force
 git remote add origin git@gitlab.com:me/sbtr.git
 git push origin `git subtree split --prefix=sbtrpth master`:master --force

这里是上述错误的详细日志:

$ git push gist-subtree `git subtree split --prefix=sbtrpth master`:master --force
fatal: ambiguous argument 'gist-subtree': unknown revision or path not in the working tree.

$ git push gist-subtree `git subtree split --prefix=sbtrpth master`:master --force

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 'master' on the remote side.

- Checking if the <src> being pushed ('7...a28')

  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: '7...a28:refs/heads/master'?

error: failed to push some refs to 'gist.github.com:7...36a.git'


$ git push origin `git subtree split --prefix=sbtrpth master`:master --force

Warning: Permanently added the ECDSA host key for IP address '172.65.251.78' to the list of known hosts.

error: The destination you provided is not a full refname (i.e.,

starting with "refs/"). We tried to guess what you meant by:

【问题讨论】:

  • 在 Internet 上发现另一个类似的抱怨 -- "如果在您的示例中本地和远程分支的名称不同,那就太好了 - 我永远记不起 gh-pages 的顺序:gh-pages 是 :)"

标签: git git-push git-subtree


【解决方案1】:

TL;DR

你的 refspec 是倒退的:你想要git push -f origin for-force-push:master。然而,这会让你得到与你所说的“彻底灾难”相同的东西。我不知道您在这里真正想要什么,因此无法就此提供任何进一步的建议。

git fetchgit push 都使用 refspec——嗯,一个或多个,但我们先描述一个。 refspec 的格式是 +&lt;src&gt;:&lt;dst&gt;+ 是可选的——它设置强制标志——而 src 和/或 dst 是可选的也是。如果您删除某些部分(源和/或目标),事情会变得有点混乱,如果您将不合格的引用用于源和/或目标,事情会变得有点混乱。 +git push --forcegit 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。所以,由于语法是&lt;src&gt;:&lt;dst&gt;,我们将使用:

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/masterrefs/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 也知道,因为它正在查看我们的存储库,是否有名为 br1master 的分支等等。对于所有匹配的分支名称,我们的 Git 会尝试 git push 那对名称。


3这适用于simple 的default-since-Git-2.0 push.default,也适用于currentupstream。它与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.defaultcurrent。请注意,这很容易意外地公开一个您本来不想推送的私有分支,因此,如果您这样做,请小心。 (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 fetchgit 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 名称的原因。他们的mastermain 成为我们的origin/masterorigin/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 行将被忽略,因此我们无法获取它们这一事实是无关紧要的,除了上面的“大部分”注释。


历史悠久的葡萄干(又名歇斯底里的葡萄干或hysterical reasons

在非常古老的 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_HEADgit pull 仍然是一个 shell 脚本,运行 git fetch 然后运行 ​​@ 987654497@ 找出正确的行并构建命令行参数后。即使在今天,为了兼容性,git fetch 仍然会写入这个 FETCH_HEAD 文件。

因此,git fetch 可以使用仅包含 source 部分的 refspec 从 URL 或远程获取。在这种情况下,git fetch 要做的显而易见的事情是只写入 FETCH_HEAD 文件:如果我们需要,信息就在那里。

但是……这不方便。因此,随着遥控器的发明和remote.origin.fetch 配置行,git fetch 被告知阅读这些行并默认遵守这些参考规范。这将创建更方便的远程跟踪名称:您只需运行git fetchgit fetch origin,现在您就有了origin/branch。由于默认设置使用 force 标志,您的origin/branch总是更新以匹配他们的branch5

所以git fetchgit fetch origin 做了很棒的事情:它完全更新了我们所有的远程跟踪名称,根据他们当前的分支,出现在另一个 Git 存储库中的任何新提交6 我们现在知道所有关于其存储库状态的信息,至少在我们的fetch 运行的纳秒内。 (到现在为止,可能已经过去了,事情可能会有很大的不同,这取决于他们的存储库的活跃程度。)

但是如果我们运行git fetch origin mastergit fetch origin main 会怎样?现在我们只要求更新 refspec master。在git push 中,mastermaster:master 的缩写,它变成了refs/heads/master:refs/heads/master。但是对于git fetchmastermaster: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 fetchgit push 采用refspecs,并且无论如何都获取机会性更新,并且refspecs 遵循快进 规则,你可以运行:

git fetch origin refs/heads/br1:br1 refs/heads/br2:br2

只要你现在没有任何一个分支。你的 Git 会在 origin 调用 Git,查找它们的分支名称,然后:

  • 更新您的远程跟踪origin/br1origin/br2,使用强制,机会主义;还有
  • 创建或更新您的refs/heads/br1,如果您的br1 领先于他们,拒绝(作为非快进)更新,同样适用于您的分支br2

(我自己从来没有真正这样做过,如果您将 fetch.prune 设置为 true 并使用 * 进行一些通配符,您可以这样开枪打死自己。我在其中一个中破坏了一些裁判我在写这篇文章时使用的垃圾库。所以在这里避免使用通配符,特别是如果你打开修剪。)

【讨论】:

  • 感谢您的详细解释 torek。你会在你的答案中总结一下,人们应该怎么做才能强制推送一个 Git 子树?更好地解释 3 步法和 1 步法。我包括了我的“彻底灾难”来说明/解释我试图尽可能多地理解有点解释的命令的情况,但仍然把自己搞砸了,因为我缺乏理解我应该做什么/common 情况,而不是那个特定的情况。谢谢。
  • 在 OP 中更新,“我试图创建最少的步骤来复制这种情况,我已经用尽了我能想到的一切,但仍然无法发挥作用推送到我的 gist 子树。”
  • 还有一件事情。在我看来,您错过了我所说的关键点,因为我在您的回答中没有找到对“子树”的单一引用。我能理解它们在你眼里都是一样的,但请理解在我眼里它们是完全不同的野兽。所以,请给出人们可以强制推送 Git 子树的最简单方法。
  • 好吧,我自己不使用git subtree(没有维护,据说有各种bug),但是如果强制推送的结果是彻底的灾难,我不知道你想要的。我只回答了强制推送问题:您的最终命令达到了与git push --force origin for_force_push:master 相同的结果(除了 3-command 变体在您身边保留了一个分支名称,而不仅仅是远程-跟踪名称origin/master——我不知道这对你是否有任何用途)。
  • 我想要的东西已经在多个地方多次强调过——人们强制推送 Git 子树的最简单方法。就是这样。
猜你喜欢
  • 2015-10-16
  • 2020-03-04
  • 1970-01-01
  • 2013-01-26
  • 2020-07-23
  • 2015-10-02
  • 2021-11-05
  • 1970-01-01
相关资源
最近更新 更多