git checkout <commit-hash>与git checkout分支

时间:2019-07-20 08:30:04

标签: git git-checkout

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

develop分支的 HEAD 位于
235a6d8

当我这样做时:

git checkout 235a6d8

来自任何其他分支或来自 develop分支,这让我陷入了孤立的头脑。
我不确定在签出该分支的最新提交时为什么会发生这种情况。

当我这样做时:

git checkout develop

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

我没有得到 git checkout <commit-has> git checkout branchname 之间的区别。
它们有何不同?

3 个答案:

答案 0 :(得分:5)

一个git checkout <commit-hash>,准备通过在其上分离HEAD(请参阅“ DETACHED HEAD" section)来在<commit>上工作,并更新工作树中的索引和文件。 / p>

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

这令人困惑。

Mark Longair记录了“ Why is the git command to switch branches named “git checkout”?”中的混乱情况

他还在2012年5月写道:“ The most confusing git terminology”:

  

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

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

在我理想的世界中,这两种操作方式将具有不同的动词,而且都不是“ checkout

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

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

答案 1 :(得分:2)

除了VonC的答案(以及即将发布的Git 2.23更改)之外,还需要注意其他一些内容。

由于git checkout会做多种不同的事情,因此它固有地令人困惑。

  • git checkout的工作之一是根据目标提交来填充索引和工作树。它将在allowed且必要时执行此操作。

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

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

origin/develop

git checkout $var 中使用了什么名称或哈希ID ?好吧,这是HEAD的决定方式:

  • 首先,git checkout尝试解析我们刚刚作为分支名称提供的字符串。假设我们给了它git checkoutmaster。那是一个有效的现有分支吗?如果是这样,应该是develop的名称。 如果检出成功,我们将把分支切换到该分支。

    < / li>
  • 否则,我们刚刚给它提供的字符串毕竟不是分支名称(即使它以1开头,例如HEAD)。 Git将尝试(尝试)将其解析为提交哈希ID,就像通过master~1一样。例如,git rev-parse肯定看起来像一个缩写的哈希ID。如果它是 -如果Git数据库中有一个对象的ID以a23456f开头,则Git确保此ID命名为 commit ,而不是 1 如果它是一个提交哈希ID,则应作为独立HEAD进入a23456f的哈希ID。 If < / em>检出成功,我们将在给定的提交下进入分离的HEAD模式。

  • 如果两种尝试均无效,那么HEAD可能会猜测git checkout可能是一个文件名,然后尝试解决这个问题。 sup> 2 但在这里我们将忽略这种特殊情况。

许多没有分支名称的名称在这里都可以正常工作。例如,$var很可能可以解析为提交哈希ID。如果origin/master是有效标签,则可以将v2.1解析为提交哈希ID。在所有这些情况下(无论v2.1结果还不是分支名称,但 都可以解析为提交哈希ID),$var会尝试执行分离操作-HEAD确认该提交哈希。

一旦git checkout决定您要求签出某些特定的提交,或者将其作为分支名称粘贴到附加的HEAD中,或者作为提交哈希ID粘贴到分离的HEAD中,然后 Git开始确定是否允许这样做。这会变得非常复杂!请参阅Checkout another branch when there are uncommitted changes on the current branch,以获取有关是否允许和何时使用的详细说明,并且请记住,git checkout告诉Git无论如何必须执行结帐,即使这些规则 t 允许。

但是,

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

也请注意,如果您创建的分支的名称​​可能是哈希ID(例如--force),则有时会有些奇怪。任何尝试将其用作分支名称 的Git命令都将成功,因为它是一个。任何尝试将其用作短哈希ID 可能的Git命令都会成功,因为它可能是有效的短哈希ID!

除非您创建令人迷惑的分支名称,否则这种情况很少出现问题,因为所有编写良好的Git命令都会在short-hash-ID之前尝试使用分支名称。为了说明,我使用通过cafedad找到的现有哈希的前六个字母创建了一个故意愚蠢的分支名称:

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 babedecade),则可能只输入表示分支时的简称cafedadbabe。如果您指的是提交,则可能会用鼠标或其他任何东西剪切并粘贴完整的哈希ID。

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

(您还可以通过创建与某些现有完整散列ID完全相同的分支名称来弄乱自己。这特别讨厌,因为 full 散列ID通常优先于分支名称,但同样,git checkout是该规则的例外。幸运的是git checkout也是如此。)


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

如果使用git branch -d命令,则可以使用后缀git rev-parse告诉Git:确保最终对象的类型为commit。如果直接对象的类型为commit键入annotated-tag,Git将“剥离”(遵循其目的地)标签以找到其提交。如果找不到提交,而是找到树或blob,^{commit}会吐出一条错误消息,并且使解析失败。如果您要编写自己的精美脚本来执行一些有用的提交操作,那么所有这些都完全可以满足您的需要。

(如果需要,此“去皮”过程会重复,因为带注释的标签的目标可以是另一个带注释的标签。动词 peel 此处的意思是提醒人们去皮洋葱:如果发现另一层洋葱,再去皮,最终您会发现洋葱的中心是什么。:-))

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

答案 2 :(得分:0)

这里是一个简单的解释:

HEAD只是一个指针,保留对分支和提交的引用。

每次创建新提交时,它都会自动移动以指向该特定分支中的新提交。

运行git checkout <branch name>时,HEAD指针将指向该分支以及该分支上的最后一次提交,即分支的尖端。

如果直接签出提交哈希,则使HEAD指向特定的提交,但没有特定的分支,这意味着它是detached

要再次附加HEAD,只需签出一个分支,然后将其附加到该分支并指向其最后一次提交,即可使一切恢复正常。

在分离的HEAD状态下,您可以做更多的事情,但是在docs中您可以看到更多的信息。