【问题标题】:Why does git checkout with explicit refs/heads/branch give detached HEAD?为什么带有显式 refs/heads/branch 的 git checkout 会给出分离的 HEAD?
【发布时间】:2014-01-10 20:48:38
【问题描述】:

如果我只使用分支名称签出一个分支,HEAD 会更新为指向该分支。

$ git checkout branch
Switched to branch 'branch'

如果我使用refs/heads/branchheads/branch 签出一个分支,HEAD 就会分离。

$ git checkout refs/heads/branch
Note: checking out 'refs/heads/branch'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.

$ git checkout "refs/heads/branch"
Same result

$ git checkout heads/branch
Same result

为什么?如果它的版本依赖,我在 Ubuntu 12.04.3 上有 git 1.7.9.5。

【问题讨论】:

    标签: git git-checkout


    【解决方案1】:

    checkout 命令区分两种情况(嗯,实际上是“很多”,但让我们从这两种情况开始 :-)):

    • “我想‘上一个分支’;这是一个分支名称”:例如,git checkout branch
    • “我想查看某个特定的修订版本,并关闭任何分支;这是一个不是分支名称的分支标识符”:例如,git checkout 6240c5c

    (我个人认为这些应该使用不同的命令名称,但这只是我。另一方面,它会消除下面描述的所有奇怪之处。编辑,2020 年 6 月:在 Git 2.23 或稍后,这些 现在 在一个单独的命令中:git switch 是分支转换器,它需要 --detach 去一个分离的 HEAD;git restore 实现后面提到的文件恢复器这个帖子。不过,您仍然可以像在 2.23 之前的 Git 中一样使用现有的 git checkout 命令。)

    现在,假设您想要前者。只需写出分支名称,不带refs/heads/ 部分,这是最容易编写的。

    如果您想要后者,您可以通过gitrevisions 中列出的任何方法指定修订版,除了任何导致“进入分支”的方法。

    无论出于何种原因,此处选择的算法(记录在manual page 中的&lt;branch&gt; 下)是这样的:如果您写了一个名称,在向其添加refs/heads/ 时,名称一个分支,git checkout 将把你放在“那个分支上”。如果您指定@{-<em>N</em>}-,它将在HEAD reflog 中查找第N 个较旧的分支(- 表示@{-1})。否则,它会选择第二种方法,为您提供“分离的 HEAD”。 即使名称是 gitrevisions 中为避免歧义而建议的名称,也是如此,即 heads/xyz 当有另一个 xyz 时也是如此。 (但是:您可以添加--detach 以避免“进入分支”的情况,即使它会进入分支。)

    这也与 gitrevisions 文档中列出的解析规则相矛盾。为了证明这一点(虽然很难看到),我制作了一个标签和一个同名的分支,derp2

    $ git checkout derp2
    warning: refname 'derp2' is ambiguous.
    Previous HEAD position was ...
    Switched to branch 'derp2'
    

    这将我放在了分支上,而不是分离并转到标记的修订版。

    $ git show derp2
    warning: refname 'derp2' is ambiguous.
    ...
    

    这向我展示了标记版本,gitrevisions 说它应该这样做。


    附注:“进入分支”实际上意味着“将对分支名称的符号引用放入 git 目录中名为 HEAD 的文件中”。符号引用是文字文本ref: (带有尾随空格),后跟完整的分支名称,例如refs/heads/derp2git checkout 要求没有 refs/heads/ 部分的名称以添加 ref: refs/heads/ 部分似乎有点不一致,但那是你的 git。 :-) 这可能有一些历史原因:最初,作为符号引用,HEAD 文件实际上是分支文件的符号链接,它始终是一个文件。这些天来,部分是因为 Windows,部分是因为代码演变,它有 ref: 字符串,并且引用可能会被“打包”,因此无论如何都不能作为单独的文件使用。

    相反,“分离的 HEAD”实际上意味着“将原始 SHA-1 放入 HEAD 文件”。除了在这个文件中有一个数值之外,git 的行为方式与“在分支上”时的行为方式相同:添加新提交仍然有效,新提交的父级是当前提交。合并仍然可以完成,合并提交的父母是当前和待合并的提交。 HEAD 文件会在每次新提交发生时更新。1 在任何时候,您都可以创建指向当前提交的新分支或标签标签,以使新的提交链成为即使您关闭“分离的 HEAD”,也可以防止将来的垃圾收集;或者您可以简单地切换并让新的提交(如果有)被通常的垃圾收集取出。 (注意HEAD reflog 会在一段时间内阻止这种情况,我认为默认为 30 天。)

    [1如果你在“一个分支上”,同样的自动更新也会发生,它只是发生在HEAD所指的分支上。也就是说,如果你在分支 B 上并且你添加了一个新的提交,HEAD 仍然是 ref: refs/heads/B,但现在你通过 git rev-parse <em>B</em> 获得的提交 ID 是您刚刚添加的新提交。这就是分支“增长”的方式:“在分支上”添加的新提交会导致分支引用自动向前移动。同样,当处于这种“分离的 HEAD”状态时,添加的新提交会导致 HEAD 自动前进。]


    为了完整起见,这里列出了 git checkout 可以做的其他事情,如果我有这样的权力,我可能会输入各种单独的命令:

    • 查看某些路径的特定版本,通过索引写入:git checkout revspec -- path ...
    • 创建一个新分支:git checkout -b newbranch(加上git branch 的选项)
    • 创建一个新分支,当你对其进行提交时,它将成为根提交:git checkout --orphan(这会将你置于一个尚不存在的“分支”上,即将ref: refs/heads/<em>branch-name</em> 写入HEAD 但不创建分支branch-name;这也是master 是新存储库中未诞生分支的原因)
    • 创建或重新创建合并或合并冲突:git checkout -m ...
    • 通过选择合并的一侧或另一侧来解决合并冲突:git checkout --oursgit checkout --theirs
    • 在存储库对象和工作树文件之间交互式选择补丁,类似于git add --patch:git checkout --patch

    【讨论】:

    • 哇!感谢您的详细回复。当您在其末尾有更多问题时,您知道这是一个很好的答案;-) 为什么 git 认为在 refs/heads/ 中指定文件意味着我不想“进入分支”?此外,为什么当我指定一个带有 SHA-1 的任意文件时,git 不会对refs/heads/ 中的文件做同样的事情?它似乎不一致......
    • 一个分支是一个本地提交引用,当HEAD 附加到它时git commit 会更新,仅此而已。分支机构位于refs/headsgit checkout 仅将 HEAD 附加到那些,但您并不总是想这样做。拼写 refs/heads/master 是否应该被视为(非分支)任意提交结帐,因为如果您想要分支结帐,您刚刚说过 master,或者应该被视为分支结帐,因为如果您想要一个非分行结帐,你会说master^0 是一个判断电话,我认为双向都有很好的论据。
    • 我同意 jthill。有些东西真的需要“分离”,另一些需要“分支”,还有一些,这只是某人随意插入的中断:“A 表示在分支上,B 表示从分支分离”。同样,如果这些是不同的命令(“git switchtobranch”、“git detachcheckout”或其他任何命令),你就不需要随意了,你要么因为有可能就去做,要么因为不可能而拒绝。
    • @BreakingBenjamin:这些是单独的问题,真的。让我们先来看看git branchgit branch 显示了各种名称,并去掉了一些refs/ 前缀。 origin1/NewBranch 的实际名称是 refs/remotes/origin1/NewBranch,但您可以从中删除 refs/ 甚至 refs/origin/。当你运行 git branch -r 时,Git 选择剥离 refs/origin/,但是当你运行 git branch -a 时,Git 选择只剥离 refs/。为什么?问谁写了git branch;它并不一致,没有明显的理由选择其中一个。
    • 同时,我在上面的答案 (kernel.org/pub/software/scm/git/docs/gitrevisions.html) 中链接到的 gitrevisions 文档中指出了您可以使用 remotes/origin1/NewBranch 的原因。如果你给 Git 一个名字来翻译成哈希 ID,Git 会尝试一个六步的过程来进行翻译;六个步骤之一是添加refs/,结果是refs/remotes/origin1/NewBranch,这是一个存在的有效引用并映射到一个有效的哈希ID,所以git checkout可以检查出那个哈希ID。
    【解决方案2】:

    你不是在检查一个分支;您只是在检查恰好是分支负责人的提交。分支是单向指针:给定一个分支,您可以确定作为该分支头的确切提交,但您不能进行任意提交并确定它是哪个分支的头。因此,如果您要进行新的提交,Git 将不知道要更新哪个分支(如果有)。

    【讨论】:

    • 如此明确地告诉 checkout 使用文件(通过给它路径)查看文件中的 SHA,如果存在,检查该 SHA?如果我创建一个包含哈希的文件,那将不起作用,那么refs/heads 中的文件有什么特别之处?
    • 我真的不觉得这回答了关于为什么命令行将git checkout foo 解析为签出分支和git checkout refs/heads/foo 解析为签出分支foo 指向的提交的问题.
    • 因为符号提交很有用。我希望能够以分离模式检出分支的头部,因此它们提供了这样做的语法。为什么你希望这两种语法做同样的事情?似乎是多余的。
    • @BnWasteland 我同意,但是有--detach 选项可以为您提供分离的HEAD。在我看来,让显式文件变成提交文件似乎违反了最小惊讶原则,尤其是当树中其他任何地方的重复文件不会变成提交文件时。
    • @BnWasteland 我也希望能够检出对分离的 HEAD 的提交,但我更希望有一个合理的 UI 能够做到这一点。相反,我有git-checkout
    猜你喜欢
    • 1970-01-01
    • 2017-12-07
    • 1970-01-01
    • 2019-10-26
    • 2011-12-20
    • 2020-03-05
    • 2018-06-25
    • 2010-09-26
    • 2020-10-22
    相关资源
    最近更新 更多