Git推送: HEAD:refs/heads/<branch>和<branch>有什么区别?

19

命令1和命令2之间有什么不同?

1. git push <projectpath> HEAD:refs/heads/<branch>
2. git push <projectpath> <branch>
"HEAD:refs/heads/" 的含义是什么?"

2
@axiac 我有(https://git-scm.com/docs/git-push,https://git-scm.com/book/en/v2/Git-Internals-The-Refspec)。但是这些都没有清楚地解决分离 HEAD 的情况,或者强调 HEAD 的确切性质(当前提交到处都是)。因此,我的答案更好地阐述了这一点。 - VonC
3个回答

28

VonC's answer是正确的(并且被点赞),但我认为另一种看待这个问题的方式可能更有意义。

请注意,所有这些都是基于使用git push的四个单词形式,即git push 远程 refspec。 这里的远程部分通常只是名称origin。 我们将在稍后更好地定义refspec

git push执行的操作

git push需要做的(并且因此做了)是在另一台机器上调用另一个Git实例,1然后给那个Git实例一组引用(通常是分支名称,有时是标签名称)以进行更新。 引用只是一个名称,例如masterv1.2,理想情况下应该是完全限定的(refs/heads/masterrefs/tags/v1.2),以便我们可以确定它是什么类型的引用-分支、标签或其他内容。
为了让另一个 Git 更新你的 Git 提供的引用,你的 Git 必须同时提供其中一些大而丑陋的 SHA-1 哈希值:每个引用一个。换句话说,你的 Git 将要求他们的 Git 将他们的 refs/heads/master 设置为,比如,ed4f38babf3d81693a68d06cd0f5872093c009f6。(此时 - 实际上是在这之前一点点 - 你的 Git 和他们的 Git 会就你想要发送给他们哪些对象以及他们已经拥有哪些对象进行对话,所有这些都通过这些大而丑陋的哈希 ID 来完成。一旦两个 Git 就将要发送的内容达成一致,你的 Git 开始执行"计算对象"和"压缩对象",然后将它们发送给他们。"现在,请设置一些名称"部分几乎是最后发生的。)
获取名称和哈希部分
请注意,您的Git请求有两个部分:(1)完全限定的引用,以及(2)大而丑的哈希值。(实际上,还有第三部分,即--force标志,但该部分很容易忽略。)但是,你的 Git 从哪里获取这些内容呢?
如果你写入:
git push origin somename

您已经为您的Git提供了两个信息:名称为origin,它用于查找URL,以及名称somename。您的Git使用此信息来确定完整名称。 somename是标签吗?如果是,则完整名称为refs/tags/somenamesomename是分支吗?如果是,则完整名称为refs/heads/somename。两种方式都可以。当然,您也可以自己编写完整的名称-如果名称既是分支又是标记,则可能要这样做,而不是让Git为您选择一种。2 那么,您的Git从哪里获取大而丑的哈希值?答案是:从同一个名称中获取。无论是分支还是标记,名称somename只是命名某个特定的Git对象。如果您想随时查看哈希,请随时这样做:
git rev-parse somename

我会向你展示它。事实上,这就是我得到ed4f38babf3d81693a68d06cd0f5872093c009f6的方法:我去了Git存储库并执行了git rev-parse v2.1.1,它打印出了该哈希值,因为自从版本2.1.1发布以来,在任何完整的Git存储库中,v2.1.1都是一个有效的标签。

请注意,当您使用这个形式——这个git push remote name形式时,Git会查找您的仓库中的name参数以完成两个目的:找到它的全名,并获取它的哈希值。不管您的HEAD在哪里,只要该全名指向的位置正确即可。

但Git不必使用您的分支(或标签)ID

git push的第四个参数称为refspec,其语法实际上允许两个由冒号分隔的部分:

 git push origin src:dst

在这种情况下,dst 部分提供了名称,但 src 部分提供了哈希值。Git 将 src 部分通过 git rev-parse 运行,并生成哈希值。因此,您可以:
git push origin mybranch:refs/tags/v42

使用分支mybranch所标识的提交哈希,在其他Git存储库中创建标签v42

通常,HEAD包含一个分支名称

在Git中,HEAD始终命名当前提交。通常它通过命名一个分支并让分支命名提交来实现。因此,通常HEAD包含类似于master的分支名称,并且分支名称始终可以让您获得该分支的tip commit(这是Git定义“tip commit”的方式,请参阅Git词汇表中的分支定义)。但HEAD始终可以转换为提交:

$ git rev-parse HEAD
2b9288cc90175557766ef33e350e0514470b6ad4

因为HEAD要么是一个分支名称(然后是最新的提交),要么你有一个“分离的 HEAD”,在这种情况下,Git 直接将当前提交 ID 存储在HEAD中。

当 HEAD 分离时推送

请记住,为了推送,Git 需要获取这两个信息:哈希值和(完整的)名称。当HEAD不是“分离的”时,Git 可以从中获取两者: HEAD 有一个分支名称 - 实际上是完整的名称形式,而且分支名称有哈希值。但是当您处于“分离的 HEAD”模式时,HEAD只有一个哈希值。Git 无法在HEAD中找到分支名称。可能没有一个:您可能已经通过 ID 检出了提交,或者可能已经按标签名称检出,例如:

$ git checkout v2.1.1

这会让你进入“分离 HEAD”模式。

在这种情况下,Git 要求你提供源哈希 src(你仍然可以使用名称 HEAD 来获取它)和目标名称 dst。如果你使用 HEAD 作为源,Git 真的需要你拼出完整的目标,因为此时 Git 无法确定它应该是一个分支(refs/heads/dst)还是一个标签(refs/tags/dst)。4

git push 的其他形式

你可以使用较少的参数运行 git push,例如:

git push origin

甚至只是:

git push

没有refspec时,Git首先查看您的push.default设置。通常这是simple(自Git 2.0版本以来的默认设置)。在这种情况下,Git仅使用HEAD来确定要推送的内容 - 当然,这只适用于HEAD未分离的情况。这就是我们上面描述的内容。
(另外三个设置也使用HEAD。其中一个 - 在Git 2.0版本之前的默认设置不使用,但是该特定设置证明容易出错,这就是为什么默认设置更改的原因。除非你是Git大师,否则你可能不应该使用它。)
(如果省略remote,Git还将使用HEAD来确定要推送到哪里,并在需要时默认为origin。)
您还可以推送多个refspecs:
git push origin branch1 branch2 tag1 HEAD:refs/tags/tag2

在这种情况下,每个refspec都按照通常的方式处理:如果需要,获取其完全限定名称,以便您的Git每次都可以为其Git提供完全限定名称;如果您没有使用src:dst表单(或者如果您使用了src:dst表单,则查找src的ID),则查找其哈希ID。
您可以在refspecs中使用通配符:
git push origin 'refs/heads/*:refs/heads/*'

(some shells will eat, mangle, fold, spindle, or mutilate the *s so you may need to use quotes, as in this example; other shells won't—or at least usually won't—but it doesn't hurt to quote). 这会推送所有你的分支,或者至少尝试推送。这往往过于热情,会推送所有临时工作和实验分支,这可能不是你想要的,但这是Git在2.0版本之前默认做的。
而且,你可以使用一个空的src:
git push origin :refs/heads/deleteme

这是一种特殊情况的语法,意思是“让我的 Git 请求他们的 Git 删除 那个引用”(要删除标签,请拼出标签)。与分离 HEAD 一样,在的一侧缺少完全限定名称意味着你应该为他们的一侧提供完全限定名称。(请再次参见脚注4。)

强制标志

如果您在git push命令中添加--force,则您的 Git 将此标志传递给他们的 Git。而不是一个礼貌的请求——“请问先生,您是否想将您的refs/heads/master设置为ed4f38babf3d81693a68d06cd0f5872093c009f6?”——您的 Git 将其发送为相当坚定的要求。他们的 Git 仍然可以拒绝任何一种方式,但默认情况下,即使这并不明智,他们的 Git 也会这样做。
Refspecs允许您更严格地控制此标志。单个refspec中的force标志为前导加号+。例如,假设您有masterdevelop分支的新提交,以及一个新的rebased提交集合experiment,其他人都同意您可以force-push。
您可以这样做:
git push origin develop master; git push -f origin experiment

但是您可以将所有内容组合成一个大推送:
git push origin develop +experiment master

The leading "+" on "experiment" makes that one a command ("update experiment!") while leaving the others as polite requests ("please, sir, if you like, update develop and master").
(This is all a bit esoteric for push, but is actually something you use regularly every day with git fetch, which uses refspecs with + flags to create and update your remote-tracking branches.)
1如果“其他仓库”在您的同一台机器上,并且您正在使用基于file://或本地路径的URL,则这并不完全正确,但原则相同,操作方式相同。

2更好的方法是,首先不要让自己陷入这种情况。拥有一个既是分支名称又是标签名称的名称非常令人困惑。(由于Git的缩写习惯,避免使用类似远程名称的名称来命名分支也会出现类似的令人困惑的情况。Git会很好地处理它们,但是您可能不会。:-))

3实际上,有一个例外,大多数人永远不会注意到:当HEAD指向“未命名分支”时。这主要发生在一个没有任何提交的新存储库中。显然,如果没有提交,就没有HEAD可以命名的提交ID。当您使用git checkout --orphan创建一个新的孤立分支时,它也会发生。

如果您使用未经过资格认证的名称,他们的Git会查找名称以使其合格。这意味着您可能不知道自己正在尝试更新或删除哪种名称。无论如何,这通常都不是一个好主意。

"HEAD总是指向当前提交的版本。很多人都没有注意到这一点,例如在https://dev59.com/ulsW5IYBdhLWcg3w2qNM#34519716中。" - VonC
关于您最后的编辑(脚注3,未出现的分支),git cherry-pick 现在已经完全可以在未出现的分支上工作了!https://dev59.com/mF0a5IYBdhLWcg3wdYmO#38285663(git 2.9.1) - VonC
@VonC:有趣。一般来说,未出现的分支会带来挑战,因为我们大多数人(我也包括在内)倾向于不考虑“HEAD无法解析”的情况。如果Git能够将HEAD^{tree}解析为空树,那么这种情况实际上可以让许多脚本正常工作。 - torek
@VonC:是的,就是那个。 :-) - torek

6

区别:

  • git push <projectpath> HEAD:refs/heads/<branch>: 本地分支提交可能与远程分支提交不同,因为“HEAD”可以分离(未链接到任何分支)
  • git push <projectpath> <branch>: 本地分支提交将始终与远程分支提交相同。

详细信息:

一个合适的分支(意思是不是分离的HEAD)是一个存储在由HEAD直接引用的refname中的提交。
这意味着HEAD是一个符号引用refname(其中包含实际提交),或者直接到提交(分离的HEAD)。还请参见“HEAD: current commit”。

HEAD:refs/heads/<branch>是一个refspec,有<src>:<dst>,其中<src>通常是要推送的分支的名称,但它可以是任何任意的“SHA-1表达式”,例如master~4HEAD,而<dst>告诉哪个远程引用更新了此推送。

如果您有以下状态:

   git checkout abranch

   --x--Y--W--Y--Z (HEAD abranch)
     |
  (origin/abranch)

git push origin aBranch 将推送一个分支的HEAD(这里是Z)或者origin,结果为:

   --x--Y--W--Y--Z (FEAD, abranch, origin/abranch)

但是如果您直接检出abranch的一个新提交:
git checkout abranch~2

         (HEAD)
           |
   --x--Y--W--Y--Z (abranch)
     |
  (origin/abranch)

然后使用第二个语法推送,您将仅更新远程跟踪分支到该 W 提交:

git push origin HEAD:refs/heads/abranch

         (HEAD)
           |
   --x--Y--W--Y--Z (abranch)
           |
  (origin/abranch)

git push origin aBranch仍会将整个分支推送,即使HEAD没有引用aBranch


小问题:对于 push 命令,<src> 是你的(本地)端,而 <dst> 是他们的(远程)端。你目前列出的顺序适用于 fetch 命令。 - torek
@Razzle 推送一个分离的 HEAD 会改变远程跟踪分支的提交,而你的本地分支可能仍然停留在原始提交上。如果你推送分支(而不是分离的 HEAD),那么本地分支提交和远程分支提交都保持不变。 - VonC

2
第一条命令明确地推送给定名称的分支。如果您有一个与其中一个分支同名的标签,第二个命令将会有歧义。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接