繁体   English   中英

结帐<commit-hash> vs git checkout 分支

[英]git checkout <commit-hash> vs git checkout branch

我在玩 git 并在这里感到困惑。

develop分支的HEAD位于
235a6d8

当我做:

git checkout 235a6d8

从任何其他分支或从develop分支,这让我处于超然状态。
我不知道为什么当我检查这个分支上的最新提交时会发生这种情况。

当我做:

git checkout develop

我可以切换到正确的开发分支。

我没有得到git checkout <commit-has>git checkout branchname
它们有何不同?

一个git checkout <commit-hash> ,准备在<commit>之上工作,通过在它上面分离 HEAD(参见“ DETACHED HEAD”部分),并更新索引和工作树中的文件。

git checkout <branch>进行切换时:它准备在<branch>上工作,通过更新索引和工作树中的文件以及将 HEAD 指向分支来切换到它。

这令人困惑。

Mark Longair在“为什么切换分支的 git 命令名为“ git checkout ”?

他还在 2012 年 5 月写道:“ 最令人困惑的 git 术语”:

在 CVS 和 Subversion 中,“checkout”创建链接到该存储库的源代码的新本地副本。
Git 中最接近的命令是“ git clone ”。
然而,在 git 中,“ git checkout ”用于完全不同的东西。
事实上,它有两种截然不同的操作模式:

  • 要切换 HEAD 以指向新的分支或提交,请使用 git checkout <branch> 如果<branch>是真正的本地分支,这将切换到该分支(即 HEAD 将指向引用名称),或者如果它以其他方式解析为提交将分离 HEAD 并将其直接指向提交的对象名称。
  • 用来自特定提交或索引的内容替换工作副本和索引中的一个或多个文件。
    这在用法中可以看到: git checkout -- (update from the index)git checkout <tree-ish> -- (其中<tree-ish>通常是提交)。

在我的理想世界中,这两种操作方式会有不同的动词,它们都不是“ checkout

嗯......这就是为什么 Git 2.23(2019 年第三季度)将结帐拆分为:

  • git restore更新工作树(可能还有索引)
  • git switch可以切换分支,或者在需要时分离分支,以便将所有新提交添加到该分支的尖端。

除了 VonC 的回答(以及即将在 Git 2.23 中进行的更改)之外,还有几项值得注意。

因为git checkout做了多种不同的事情,所以它本质上是令人困惑的。

  • git checkout的工作之一是根据目标提交填充索引和工作树。 只要允许和必要,它就会这样做。

  • 另一种方法是改变记录在分支名称HEAD ,或设置HEAD作为一个分离的头在指定的提交。 它会在必要时执行此操作(前提是第一部分允许结帐操作)。

对于git checkout ,它将根据您提供的分支名称或提交说明符参数执行第二个操作。 也就是说,假设我们有一些 shell 变量$var设置为一些非空但合理的词:它可能设置为master ,或者master^{commit}a23456forigin/develop或类似的东西。 无论如何,我们现在运行:

git checkout $var

什么名称或哈希 ID进入HEAD 好吧,这是git checkout决定的方式:

  • 首先, git checkout尝试解析我们刚刚作为分支名称提供的字符串。 假设我们给它masterdevelop 这是一个有效的现有分支吗? 如果是这样,这就是应该进入HEAD的名称。 如果结帐成功,我们会将分支切换到该分支。

  • 否则,我们刚刚给它的字符串毕竟不是分支名称(即使它以 1 开头,例如在master~1中)。 Git 会尝试——尝试——将其解析为提交哈希 ID,就像通过git rev-parse 例如, a23456f确实看起来像一个缩写的哈希 ID。 如果它一个——如果 Git 的数据库中有一个对象的 ID 以a23456f ——那么 Git 会确保这个 ID 命名一个commit ,而不是某个其他对象。 1如果它是一个提交哈希 ID,那么它应该作为一个分离的 HEAD 进入HEAD的哈希 ID。 如果结帐成功,我们现在将在给定的提交处处于分离的 HEAD 模式。

  • 如果这两种尝试都不起作用, git checkout接下来会猜测$var可能是一个文件名,并尝试解决这个问题。 2但我们将在这里忽略这个特殊情况。

许多不是分支名称的名称在这里都可以正常工作。 例如, origin/master极有可能被解析为提交哈希 ID。 如果v2.1是有效标签,则v2.1可以解析为提交哈希 ID。 在所有这些情况下——只要$var结果不是分支名称,但可以解析为提交哈希 ID—— git checkout将尝试对该提交哈希执行分离的 HEAD 签出。

一旦git checkout决定您要求检查某个特定的提交,要么作为分支名称粘贴到附加的 HEAD 中,要么作为提交哈希 ID 粘贴到分离的 HEAD 中,然后Git 开始确定是否允许这样做. 这会变得非常复杂! 结帐时,有对当前分支未提交更改另一个分支关于是否以及何时它允许详细的注释,并记住--force告诉Git的,它应该做的结账无论如何,即使这些规则不会允许它。

但是,TL;DR 是原始哈希 ID始终是进入分离 HEAD 状态的请求。 是否导致分离的 HEAD 取决于复杂的“是否允许结帐”测试。

请注意,如果您创建一个名称可以是哈希 ID 的分支(例如cafedad有时事情会cafedad有点奇怪。 任何尝试使用它作为分支名称的Git 命令都会成功,因为它是一个。 任何尝试将其用作短哈希 ID 的Git 命令都可能会成功,因为它可能是一个有效的短哈希 ID!

除非你创建愚蠢的令人困惑的分支名称,否则这种特殊情况很少会成为问题,因为所有编写良好的 Git 命令都会在短哈希 ID 之前尝试分支名称。 为了说明起见,我故意使用通过git log找到的现有哈希的前六个字母创建了一个愚蠢的分支名称:

$ git branch f9089e 8dca754b1e874719a732bc9ab7b0e14b21b1bc10
$ git rev-parse f9089e
warning: refname 'f9089e' is ambiguous.
8dca754b1e874719a732bc9ab7b0e14b21b1bc10
$ git branch -d f9089e
Deleted branch f9089e (was 8dca754b1e).

请注意警告: f9089e被视为分支名称,因为它解析为8dca754b1e874719a732bc9ab7b0e14b21b1bc10 删除愚蠢的分支名称后,短散列再次解析为完整散列:

$ git rev-parse f9089e
f9089e8491fdf50d941f071552872e7cca0e2e04

如果您创建的分支名称意外地用作短哈希(例如babedecadecafedadcafedad您可能只在表示分支时输入短名称babecafedad 如果您指的是提交,您可能会用鼠标或其他方式剪切并粘贴完整的哈希 ID。

当您创建具有相同名称的分支和标记时,这里会发生真正的危险。 大多数Git 命令倾向于使用tag ,但git checkout更喜欢branch 这是一个非常混乱的情况。 幸运的是,它很容易修复:只需重命名两个实体之一,这样您的分支名称和标签名称就不会冲突。

(你也可以通过创建一个与某些现有完整哈希 ID 完全相同的分支名称来惹恼自己。这个特别讨厌,因为完整哈希 ID 往往优先于分支名称,但同样, git checkout是一个例外这条规则。幸运的是git branch -d也是如此。)


1任何 Git 存储库中都有四种类型的对象: commitstreeblobannotated tags 提交对象存储提交。 树和 blob 对象主要供 Git 内部使用,以某种类似于目录的方式存储文件名并存储文件数据。 带注释的标签对象是最棘手的:它们存储另一个对象的哈希 ID。 可以指示 Git 获取这样的标签并找到该标签连接到的提交。 作为一种特殊的复杂情况,带注释的标签最终会导致树或 blob 对象,因此某些标签可能根本不会命名提交——但通常,大多数标签最终都会命名提交。

如果您使用git rev-parse命令,您可以使用^{commit}后缀技巧告诉 Git:确保最终对象的类型为 commit。 如果直接对象具有 annotated-tag 类型,Git 将“剥离”(跟随其目的地)标签以查找其提交。 如果它没有找到提交——如果它找到了一棵树或 blob—— git rev-parse将吐出一条错误消息并使解析失败。 如果您正在编写自己的花哨脚本来对提交做一些有用的事情,那么这一切都被设计为正是所需要的。

(如果需要,这个“剥皮”过程会重复,因为一个带注释的标签的目标可以是另一个带注释的标签。这里的动词peel是为了提醒一个人剥洋葱:如果你发现另一层洋葱,再剥。最终你会找出洋葱的中心是什么。:-))

2请注意,从$var到任何$var设置的扩展是由shell (例如,通过 bash)完成的,而不是由 Git 完成的。 这在这里并不重要,因为我对$var内容施加了限制,但在更复杂的情况下,它确实如此。

这是一个简单的解释:

HEAD 是一个位于.git/HEAD的文件,它让 Git 了解在新提交出现时应该推进的分支。

当一个分支,如main被检出时,它包含:

ref: refs/heads/main

对于每个分支,Git 还在refs/heads目录中保存一个文件,例如main分支的文件refs/heads/main将在那里。

该文件包含该分支上最后一次提交的哈希值,即分支的tip of the branch

到目前为止,这两个文件告知 Git 应该推进哪个分支以及该分支上的最后一次提交是什么。

通过运行git checkout <branch name> ,HEAD 文件更新为包含该分支的名称。

因此运行git checkout 235a6d8 ,使 HEAD 指向特定的提交而不是指向特定的分支,这意味着 Git HEAD 是detached

为了再次附加 HEAD,只需运行git checkout <branch name>这将使事情恢复正常行为。

分离的头部状态对于检查特定时间点的项目状态、测试和错误发现/修复非常有用。

您可以在分离的 HEAD 状态下执行更多操作,您可以在文档中看到更多相关信息。

您可能还会发现在gitglossary 中检查术语headsHEAD很有趣。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM