25

If I checkout a branch using just the branch name, HEAD is updated to point at that branch.

$ git checkout branch
Switched to branch 'branch'

If I checkout a branch by using refs/heads/branch or heads/branch, HEAD becomes detached.

$ 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

Why? If its version dependent, I have git 1.7.9.5 on Ubuntu 12.04.3.

darthbith
  • 18,484
  • 9
  • 60
  • 76

2 Answers2

22

The checkout command distinguishes between two cases (well, actually "many", but let's start with just the two :-) ):

  • "I want to ‘get on a branch’; here's a branch name": e.g., git checkout branch.
  • "I want to look at some particular revision, and get off any branch; here's a branch-identifier that is not a branch name": e.g., git checkout 6240c5c.

(Personally, I think these should use different command names, but that's just me. On the other hand, it would obviate all the weirdness described below. Edit, Jun 2020: In Git 2.23 or later, these are in a separate command now: git switch is the branch-changer, and it requires --detach to go to a detached HEAD; git restore implements the file-restorer mentioned later in this posting. You can still use the existing git checkout command the same way as in pre-2.23 Git, though.)

Now, let's say that you want the former. That's easiest to write by just writing out the branch name, without the refs/heads/ part.

If you want the latter, you can specify a revision by any of the methods listed in gitrevisions, except for any method that results in "getting on the branch".

For whatever reason, the algorithm chosen here—it is documented in the manual page, under <branch>—is this: If you've written a name that, when adding refs/heads/ to it, names a branch, git checkout will put you "on that branch". If you specify @{-N} or -, it will look up the N-th older branch in the HEAD reflog (with - meaning @{-1}). Otherwise it chooses the second method, giving you the "detached HEAD". This is true even if the name is the one suggested in gitrevisions for avoiding ambiguity, i.e., heads/xyz when there's another xyz. (But: you can add --detach to avoid the "get on a branch" case even if it would otherwise get on the branch.)

This also contradicts the resolving rules listed in the gitrevisions document. To demonstrate this (although it's hard to see), I made a tag and branch with the same name, derp2:

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

This put me on the branch, rather than detaching and going to the tagged revision.

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

This showed me the tagged version, the way gitrevisions says it should.


One side note: "getting on a branch" really means "putting a symbolic reference to a branch name into the file named HEAD in the git directory". The symbolic reference is the literal text ref: (with trailing space) followed by the full branch name, e.g., refs/heads/derp2. It seems kind of inconsistent that git checkout demands the name without the refs/heads/ part in order to add the ref: refs/heads/ part, but that's git for you. :-) There may be some historic reason for this: originally, to be a symbolic reference, the HEAD file was actually a symbolic link to the branch file, which was always a file. These days, in part because of Windows and in part just through code evolution, it has that literal ref: string, and references may become "packed" and hence not available as a separate file anyway.

Contrariwise, a "detached HEAD" really means "putting a raw SHA-1 into the HEAD file". Other than having a numeric value in this file, git continues to behave the same way as when "on a branch": adding a new commit still works, with the new commit's parent being the current commit. Merges can still be done as well, with the merge commit's parents being the current and to-be-merged commits. The HEAD file is updated with each new commit as it happens.1 At any point you can create a new branch or tag label pointing to the current commit, to cause the new chain of commits to be preserved against future garbage collection even after you switch off the "detached HEAD"; or you can simply switch away and let the new commits, if any, get taken out with the usual garbage-collection. (Note that the HEAD reflog will prevent this for some time, default 30 days I think.)

[1 If you're "on a branch", the same auto-update happens, it just happens to the branch that HEAD refers to. That is, if you're on branch B and you add a new commit, HEAD still says ref: refs/heads/B, but now the commit-ID that you get with git rev-parse B is the new commit you just added. This is how branches "grow": new commits added while "on the branch" cause the branch reference to move forward automatically. Likewise, when in this "detached HEAD" state, new commits added cause HEAD to move forward automatically.]


For completeness, here's a list of other things git checkout can do, that I might have put in various separate commands if I had such powers:

  • check out a specific version of some path(s), writing through the index: git checkout revspec -- path ...
  • create a new branch: git checkout -b newbranch (plus options for git branch)
  • create a new branch that, if and when you do a commit on it, will be a root commit: git checkout --orphan (this puts you "on a branch" that does not yet exist, i.e., writes ref: refs/heads/branch-name into HEAD but does not create the branch branch-name; this is also how master is an unborn branch in a new repository)
  • create or re-create a merge or merge conflict: git checkout -m ...
  • resolve a merge conflict by picking one or the other "side" of the merge: git checkout --ours, git checkout --theirs
  • interactively select patches between repository objects and work-tree files, similar to git add --patch: git checkout --patch
torek
  • 448,244
  • 59
  • 642
  • 775
  • Wow! Thanks for the detailed response. You know its a good answer when you have even more questions at the end of it ;-) Why does git think that specifying the file in `refs/heads/` means that I don't want to "get on a branch"? And furthermore, why, when I specify an arbitrary file with a SHA-1 in it, does git not do the same thing as for the files in `refs/heads/`? It just seems inconsistent... – darthbith Jan 10 '14 at 23:24
  • 3
    A branch is a local commit-reference that `git commit` updates when `HEAD` is attached to it, that's all. Branches live in `refs/heads`. `git checkout` only attaches `HEAD` to those, but you don't always want to do that. Whether the spelling `refs/heads/master` should be treated like a (non-branch) arbitrary-commit checkout because if you'd wanted the branch checkout you'd have just said `master`, or should be treated like a branch checkout because if you wanted a non-branch checkout you'd have said `master^0` is a judgement call, I think there's good arguments to be made both ways. – jthill Jan 11 '14 at 00:41
  • 1
    I agree with jthill. Some things really cry out for being "detach", others cry out for "branch", and some, it's just sort of an arbitrary break someone put in: "A means on-branch, B means detach-from-branch". Again, if these were different commands ("git switchtobranch", "git detachcheckout" or whatever) you wouldn't need to be arbitrary, you'd just either do it because it's possible, or refuse because it's not. – torek Jan 11 '14 at 00:56
  • @torek , How does checkout work with a remote tracking branch name. I mean `git checkout remotes/origin1/NewBranch` works and even `git checkout origin1/NewBranch` works . How ? And hence why `git branch -a` shows remote tracking branch names appended with `remotes/` in beginning ? – Number945 May 05 '18 at 15:29
  • 1
    @BreakingBenjamin: those are separate questions, really. Let's take on `git branch` first: `git branch` shows various names, and strips off some of the `refs/` prefixes. The actual name of `origin1/NewBranch` is `refs/remotes/origin1/NewBranch`, but you can remove `refs/` or even `refs/origin/` from this. When you run `git branch -r` Git chooses to strip off `refs/origin/` but when you run `git branch -a` Git chooses to strip only `refs/`. Why? Ask whoever wrote `git branch`; it's not consistent and there's no obvious reason to select one or the other. – torek May 05 '18 at 15:56
  • 1
    Meanwhile, the reason you can use `remotes/origin1/NewBranch` is called out in the gitrevisions documentation that I linked-to in the answer above (https://www.kernel.org/pub/software/scm/git/docs/gitrevisions.html). If you give Git a name to translate to a hash ID, Git tries a six-step process for doing the translation; one of the six steps is to add `refs/`, and the result is `refs/remotes/origin1/NewBranch`, which is a valid reference that exists and maps to a valid hash ID, so `git checkout` can check out that hash ID. – torek May 05 '18 at 15:58
  • Wow. Awesome response. One question: Given this behavior, how can I "get on the branch" through the `refs/heads` path? e.g. something like `git checkout --on-branch refs/heads/develop` – Max Coplan Jun 30 '20 at 18:48
  • @MaxCoplan: both `git checkout` and the new (2.23 or later) `git switch` will try whatever argument you give them as a branch name first, so if `refs/heads/develop` already exists as a branch name, `git checkout develop` or `git switch develop` will switch to it. There's a slight danger with the pre-2.23 `git checkout` that if `refs/heads/develop` does *not* exist as a branch name and cannot be resolved to a commit, it will be treated as a pathspec instead. This can destroy uncommitted work in the `develop` file or sub-tree! This is all better in 2.23+, though. – torek Jun 30 '20 at 20:42
2

You're not checking out a branch; you're merely checking out a commit that happens to be the head of a branch. Branches are one-way pointers: given a branch, you can determine the exact commit which is the head of that branch, but you cannot take an arbitrary commit and determine which branch(es) it is the head of. Thus, if you were to make a new commit, Git would not know which, if any, branch to update.

chepner
  • 497,756
  • 71
  • 530
  • 681
  • So explicitly telling checkout to use a file (by giving it the path) looks into the file for a SHA, and if present, checks out that SHA? That doesn't work if I create a file with a hash in it, so what is special about the files in `refs/heads`? – darthbith Jan 10 '14 at 21:20
  • 2
    I don't really feel like this answers the question as to why the command line parses `git checkout foo` as checking out a branch and `git checkout refs/heads/foo` as checking out the commitish that branch `foo` points to. – Edward Thomson Jan 10 '14 at 21:42
  • Because symbolic commitishes are useful. I WANT to be able to checkout the head of a branch in detached mode, and so they provide a syntax to do so. Why would you want the two syntaxes to do the same thing? Seems redundant. – BnWasteland Jan 10 '14 at 22:45
  • 1
    @BnWasteland I agree, but there is the `--detach` option to give you the detached `HEAD`. Having the explicit file turn into a commitish seems to me to violate the principle of least astonishment, especially when duplicate files anywhere else in the tree do not turn commitish. – darthbith Jan 10 '14 at 22:58
  • @BnWasteland I want to be able to checkout a commit to a detached HEAD as well, but I would prefer a sensible UI to be able to do it. Instead I have `git-checkout`. – Edward Thomson Jan 10 '14 at 23:11