简体   繁体   English

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

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

I was playing around with git and got confused here.我在玩 git 并在这里感到困惑。

The HEAD of develop branch is at develop分支的HEAD位于
235a6d8 235a6d8

When I do:当我做:

git checkout 235a6d8

from any other branch or from develop branch , this leaves me in detached head.从任何其他分支或从develop分支,这让我处于超然状态。
I am not sure why does this happen when I am checking out to the latest commit on this branch.我不知道为什么当我检查这个分支上的最新提交时会发生这种情况。

When I do:当我做:

git checkout develop

I can switch to develop branch correctly.我可以切换到正确的开发分支。

I am not getting the difference between git checkout <commit-has> and git checkout branchname .我没有得到git checkout <commit-has>git checkout branchname
How they are different ?它们有何不同?

A git checkout <commit-hash> , Prepare to work on top of <commit> , by detaching HEAD at it (see " DETACHED HEAD" section ), and updating the index and the files in the working tree.一个git checkout <commit-hash> ,准备在<commit>之上工作,通过在它上面分离 HEAD(参见“ DETACHED HEAD”部分),并更新索引和工作树中的文件。

While a git checkout <branch> does a switch: it prepares for working on <branch> , switch to it by updating the index and the files in the working tree, and by pointing HEAD at the branch.git checkout <branch>进行切换时:它准备在<branch>上工作,通过更新索引和工作树中的文件以及将 HEAD 指向分支来切换到它。

This is confusing.这令人困惑。

Mark Longair documented that confusion in " Why is the git command to switch branches named “ git checkout ”? " Mark Longair在“为什么切换分支的 git 命令名为“ git checkout ”?

He also wrote in May 2012: " The most confusing git terminology ":他还在 2012 年 5 月写道:“ 最令人困惑的 git 术语”:

In CVS and Subversion “checkout” creates a new local copy of the source code that is linked to that repository.在 CVS 和 Subversion 中,“checkout”创建链接到该存储库的源代码的新本地副本。
The closest command in Git is “ git clone ”. Git 中最接近的命令是“ git clone ”。
However, in git, “ git checkout ” is used for something completely distinct.然而,在 git 中,“ git checkout ”用于完全不同的东西。
In fact, it has two largely distinct modes of operation:事实上,它有两种截然不同的操作模式:

  • To switch HEAD to point to a new branch or commit, in the usage git checkout <branch> .要切换 HEAD 以指向新的分支或提交,请使用 git checkout <branch> If <branch> is genuinely a local branch, this will switch to that branch (ie HEAD will point to the ref name) or if it otherwise resolves to a commit will detach HEAD and point it directly to the commit's object name.如果<branch>是真正的本地分支,这将切换到该分支(即 HEAD 将指向引用名称),或者如果它以其他方式解析为提交将分离 HEAD 并将其直接指向提交的对象名称。
  • To replace a file or multiple files in the working copy and the index with their content from a particular commit or the index.用来自特定提交或索引的内容替换工作副本和索引中的一个或多个文件。
    This is seen in the usages: git checkout -- (update from the index) and git checkout <tree-ish> -- (where <tree-ish> is typically a commit).这在用法中可以看到: git checkout -- (update from the index)git checkout <tree-ish> -- (其中<tree-ish>通常是提交)。

In my ideal world, these two modes of operation would have different verbs, and neither of them would be “ checkout .在我的理想世界中,这两种操作方式会有不同的动词,它们都不是“ checkout

Well... That is why Git 2.23 (Q3 2019) will split checkout into:嗯......这就是为什么 Git 2.23(2019 年第三季度)将结帐拆分为:

  • git restore which updates the working tree (and possibly the index) git restore更新工作树(可能还有索引)
  • git switch which can switch branches, or detach one if requested, in order for all new commits to be added to the tip of this branch. git switch可以切换分支,或者在需要时分离分支,以便将所有新提交添加到该分支的尖端。

Besides VonC's answer (and the upcoming change in Git 2.23), it's worth noting a few more items.除了 VonC 的回答(以及即将在 Git 2.23 中进行的更改)之外,还有几项值得注意。

Because git checkout does multiple different things, it's inherently confusing.因为git checkout做了多种不同的事情,所以它本质上是令人困惑的。

  • One of git checkout 's jobs is to populate the index and work-tree based on the target commit. git checkout的工作之一是根据目标提交填充索引和工作树。 It will do this whenever it is allowed and necessary.只要允许和必要,它就会这样做。

  • Another is to change the branch name recorded in HEAD , or set up HEAD as a detached HEAD at the specified commit.另一种方法是改变记录在分支名称HEAD ,或设置HEAD作为一个分离的头在指定的提交。 It will do this whenever necessary (provided the first part allows the checkout operation).它会在必要时执行此操作(前提是第一部分允许结帐操作)。

For git checkout , it will do the second operation based on the branch name or commit specifier argument you give it.对于git checkout ,它将根据您提供的分支名称或提交说明符参数执行第二个操作。 That is, suppose we have some shell variable $var set to some non-empty but sensible word: it might be set to master , or maybe master^{commit} or a23456f or origin/develop or something along these lines.也就是说,假设我们有一些 shell 变量$var设置为一些非空但合理的词:它可能设置为master ,或者master^{commit}a23456forigin/develop或类似的东西。 In any case, we now run:无论如何,我们现在运行:

git checkout $var

What name or hash ID goes into HEAD ?什么名称或哈希 ID进入HEAD Well, here's how git checkout decides:好吧,这是git checkout决定的方式:

  • First, git checkout tries to resolve the string we just gave it as a branch name.首先, git checkout尝试解析我们刚刚作为分支名称提供的字符串。 Suppose we gave it master or develop .假设我们给它masterdevelop Is that a valid, existing branch?这是一个有效的现有分支吗? If so, that's the name that should go into HEAD .如果是这样,这就是应该进入HEAD的名称。 If the checkout succeeds, we will have switched branches, to that branch.如果结帐成功,我们会将分支切换到该分支。

  • Otherwise, the string we just gave it isn't a branch name after all (even if it starts with one, as in master~1 for instance).否则,我们刚刚给它的字符串毕竟不是分支名称(即使它以 1 开头,例如在master~1中)。 Git will try—attempt—to resolve it into a commit hash ID, as if by git rev-parse . Git 会尝试——尝试——将其解析为提交哈希 ID,就像通过git rev-parse For instance, a23456f sure looks like an abbreviated hash ID.例如, a23456f确实看起来像一个缩写的哈希 ID。 If it is one—if there's an object in Git's database with an ID starting with a23456f —then Git makes sure that this ID names a commit , rather than some other object.如果它一个——如果 Git 的数据库中有一个对象的 ID 以a23456f ——那么 Git 会确保这个 ID 命名一个commit ,而不是某个其他对象。 1 If it's a commit hash ID, that's the hash ID that should go into HEAD , as a detached HEAD. 1如果它是一个提交哈希 ID,那么它应该作为一个分离的 HEAD 进入HEAD的哈希 ID。 If the checkout succeeds, we will now be in detached HEAD mode, at the given commit.如果结帐成功,我们现在将在给定的提交处处于分离的 HEAD 模式。

  • If neither attempt works, git checkout will next guess that maybe, $var was meant to be a file name , and try to work that out.如果这两种尝试都不起作用, git checkout接下来会猜测$var可能是一个文件名,并尝试解决这个问题。 2 But we'll ignore this particular case here. 2但我们将在这里忽略这个特殊情况。

Many names that aren't branch names work fine here.许多不是分支名称的名称在这里都可以正常工作。 For instance, origin/master is extremely likely to be resolvable to a commit hash ID.例如, origin/master极有可能被解析为提交哈希 ID。 If v2.1 is a valid tag, v2.1 can be resolved to a commit hash ID.如果v2.1是有效标签,则v2.1可以解析为提交哈希 ID。 In all of these cases—whenever the $var result isn't a branch name already, but can be resolved into a commit hash ID— git checkout will attempt to do a detached-HEAD checkout of that commit hash.在所有这些情况下——只要$var结果不是分支名称,但可以解析为提交哈希 ID—— git checkout将尝试对该提交哈希执行分离的 HEAD 签出。

Once git checkout has decided that you have asked to check out some particular commit, either as a branch name to stick into an attached HEAD, or as a commit hash ID to stick into a detached HEAD, then Git goes about determining whether this is allowed.一旦git checkout决定您要求检查某个特定的提交,要么作为分支名称粘贴到附加的 HEAD 中,要么作为提交哈希 ID 粘贴到分离的 HEAD 中,然后Git 开始确定是否允许这样做. This can get very complicated!这会变得非常复杂! See Checkout another branch when there are uncommitted changes on the current branch for detailed notes about whether and when it's allowed, and remember that --force tells Git that it should do the checkout anyway, even if these rules wouldn't allow it.结帐时,有对当前分支未提交更改另一个分支关于是否以及何时它允许详细的注释,并记住--force告诉Git的,它应该做的结账无论如何,即使这些规则不会允许它。

The TL;DR, though, is that a raw hash ID is always a request to go into detached HEAD state.但是,TL;DR 是原始哈希 ID始终是进入分离 HEAD 状态的请求。 Whether it will result in a detached HEAD depends on that complicated "is the checkout allowed" test.是否导致分离的 HEAD 取决于复杂的“是否允许结帐”测试。

Note, too, that if you create a branch whose name could be a hash ID—such as cafedad —things get a little weird sometimes.请注意,如果您创建一个名称可以是哈希 ID 的分支(例如cafedad有时事情会cafedad有点奇怪。 Any Git command that tries to use it as a branch name will succeed, because it is one.任何尝试使用它作为分支名称的Git 命令都会成功,因为它是一个。 Any Git command that tries to use it as a short hash ID might succeed, because it might be a valid short hash ID!任何尝试将其用作短哈希 ID 的Git 命令都可能会成功,因为它可能是一个有效的短哈希 ID!

Unless you create stupidly confusing branch names, this particular case is rarely a problem, because all well-written Git commands try branch-name before short-hash-ID.除非你创建愚蠢的令人困惑的分支名称,否则这种特殊情况很少会成为问题,因为所有编写良好的 Git 命令都会在短哈希 ID 之前尝试分支名称。 For illustration, I've created a deliberately stupid branch name using the first six letters of an existing hash that I found via git log :为了说明起见,我故意使用通过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).

Note the warning: f9089e was treated as a branch name, as it parsed to 8dca754b1e874719a732bc9ab7b0e14b21b1bc10 .请注意警告: f9089e被视为分支名称,因为它解析为8dca754b1e874719a732bc9ab7b0e14b21b1bc10 After deleting the stupid branch name, the short hash parses to the full hash again:删除愚蠢的分支名称后,短散列再次解析为完整散列:

$ git rev-parse f9089e
f9089e8491fdf50d941f071552872e7cca0e2e04

If you made a branch name that accidentally works as a short hash—such as babe , decade , or cafedad —you probably only type in the short name babe or cafedad when you mean the branch.如果您创建的分支名称意外地用作短哈希(例如babedecadecafedadcafedad您可能只在表示分支时输入短名称babecafedad If you mean the commit, you probably cut-and-paste the full hash ID with your mouse, or whatever.如果您指的是提交,您可能会用鼠标或其他方式剪切并粘贴完整的哈希 ID。

The real danger here occurs when you create a branch and tag with the same name.当您创建具有相同名称的分支和标记时,这里会发生真正的危险。 Most Git commands tend to prefer the tag , but git checkout prefers the branch .大多数Git 命令倾向于使用tag ,但git checkout更喜欢branch This is a very confusing situation.这是一个非常混乱的情况。 Fortunately, it's easy to fix: just rename one of the two entities, so that your branch and tag names don't collide.幸运的是,它很容易修复:只需重命名两个实体之一,这样您的分支名称和标签名称就不会冲突。

(You can also mess with yourself by creating a branch name that is exactly the same as some existing full hash ID. This one is especially nasty as full hash IDs tend to take precedence over branch names, but again, git checkout is an exception to this rule. So is git branch -d , fortunately.) (你也可以通过创建一个与某些现有完整哈希 ID 完全相同的分支名称来惹恼自己。这个特别讨厌,因为完整哈希 ID 往往优先于分支名称,但同样, git checkout是一个例外这条规则。幸运的是git branch -d也是如此。)


1 There are four types of objects in any Git repository: commits , trees , blobs , and annotated tags . 1任何 Git 存储库中都有四种类型的对象: commitstreeblobannotated tags Commit objects store commits.提交对象存储提交。 Tree and blob objects are mainly for Git's internal use, to store file names in a somewhat-directory-ish fashion and to store file data.树和 blob 对象主要供 Git 内部使用,以某种类似于目录的方式存储文件名并存储文件数据。 Annotated tag objects are the trickiest: they store the hash ID of another object.带注释的标签对象是最棘手的:它们存储另一个对象的哈希 ID。 Git can be directed to take such a tag and find the commit that the tag connects to.可以指示 Git 获取这样的标签并找到该标签连接到的提交。 As a special complication, an annotated tag can instead ultimately lead to a tree or blob object, so some tags might not name commits after all—but typically, most tags end up naming a commit anyway.作为一种特殊的复杂情况,带注释的标签最终会导致树或 blob 对象,因此某些标签可能根本不会命名提交——但通常,大多数标签最终都会命名提交。

If you use the git rev-parse command, you can use that ^{commit} suffix trick to tell Git: make sure the final object has type commit.如果您使用git rev-parse命令,您可以使用^{commit}后缀技巧告诉 Git:确保最终对象的类型为 commit。 If the immediate object has type annotated-tag, Git will "peel off" (follow to its destination) the tag to find its commit.如果直接对象具有 annotated-tag 类型,Git 将“剥离”(跟随其目的地)标签以查找其提交。 If it doesn't find a commit—if it finds a tree or blob instead— git rev-parse will spit out an error message and fail the parse.如果它没有找到提交——如果它找到了一棵树或 blob—— git rev-parse将吐出一条错误消息并使解析失败。 This is all designed to be exactly what's needed if you are writing your own fancy script to do something useful with commits.如果您正在编写自己的花哨脚本来对提交做一些有用的事情,那么这一切都被设计为正是所需要的。

(This "peeling" process repeats if needed, because the target of an annotated tag can be another annotated tag. The verb peel here is meant to remind one of peeling an onion: if you find another layer of onion, peel again. Eventually you'll find out what's at the center of the onion. :-) ) (如果需要,这个“剥皮”过程会重复,因为一个带注释的标签的目标可以是另一个带注释的标签。这里的动词peel是为了提醒一个人剥洋葱:如果你发现另一层洋葱,再剥。最终你会找出洋葱的中心是什么。:-))

2 Note that the expansion from $var to whatever $var was set-to is done by the shell (eg, by bash), not by Git. 2请注意,从$var到任何$var设置的扩展是由shell (例如,通过 bash)完成的,而不是由 Git 完成的。 That doesn't matter right here because of the constraints I placed on what can be in $var , but in more complicated cases, it does.这在这里并不重要,因为我对$var内容施加了限制,但在更复杂的情况下,它确实如此。

Here is a simple explanation:这是一个简单的解释:

HEAD is a file located at .git/HEAD which keeps Git informed about the branch that should be advanced in the advent of new commits. HEAD 是一个位于.git/HEAD的文件,它让 Git 了解在新提交出现时应该推进的分支。

When a branch, like main is checked out it contains:当一个分支,如main被检出时,它包含:

ref: refs/heads/main

For each branch Git also keeps a file in the refs/heads directory, for instance the file refs/heads/main for the main branch will be there.对于每个分支,Git 还在refs/heads目录中保存一个文件,例如main分支的文件refs/heads/main将在那里。

This file contains the hash of the last commit on that branch, meaning the tip of the branch .该文件包含该分支上最后一次提交的哈希值,即分支的tip of the branch

These two files so far inform Git on which branch should be advanced and what is the last commit on that branch.到目前为止,这两个文件告知 Git 应该推进哪个分支以及该分支上的最后一次提交是什么。

By running git checkout <branch name> , the HEAD file is updated to contain that branch's name.通过运行git checkout <branch name> ,HEAD 文件更新为包含该分支的名称。

Thus running git checkout 235a6d8 , makes the HEAD point to a specific commit instead of pointing to a specific branch, meaning that Git HEAD is detached .因此运行git checkout 235a6d8 ,使 HEAD 指向特定的提交而不是指向特定的分支,这意味着 Git HEAD 是detached

In order to attach the HEAD again simply run git checkout <branch name> which will bring things back to the normal behavior.为了再次附加 HEAD,只需运行git checkout <branch name>这将使事情恢复正常行为。

The detached head state is useful in order to inspect the state of a project at specific points in time, for testing and bug finding/fixing.分离的头部状态对于检查特定时间点的项目状态、测试和错误发现/修复非常有用。

There is more to what you can do in a detached HEAD state and you can see more about that in the docs .您可以在分离的 HEAD 状态下执行更多操作,您可以在文档中看到更多相关信息。

You may also find it interesting to check terms heads and HEAD in the gitglossary .您可能还会发现在gitglossary 中检查术语headsHEAD很有趣。

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

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