24

If I start from a clean working tree and run git checkout <branch> <file>, where <branch> has a different version of this file, I end up with a staged rather than an unstaged change.

What's the reason for this? Is this just for consistency with other commands like git mv, which you would expect to stage changes? Is it for convenience when using git checkout to resolve merge conflicts? Or is there some other rationale?

It seems mildly odd to me since just using git checkout <branch> <file> does not offer any indication of whether I plan to commit the change.

Zoe
  • 27,060
  • 21
  • 118
  • 148
Luke
  • 7,110
  • 6
  • 45
  • 74
  • In `git checkout `, the branch is resolved as a tree, the tree of the head commit of the branch. The command updates the index first for the given file and then update the file in the working tree to match the version in the index. So now the file in the working tree and in the index are updated to be the same. It's just as staged as it is by `git add `. `git checkout --help` for more information. – ElpieKay May 24 '17 at 17:01
  • Possible duplicate of [Why does checkout sometimes stage a file?](https://stackoverflow.com/questions/28837246/why-does-checkout-sometimes-stage-a-file) – Keith Pinson Nov 03 '17 at 13:40

2 Answers2

16

It's really an implementation detail that the Git authors chose to let show through.

Git cannot—or rather, at one point, could not—read files directly from the repository into the work-tree. It has (or had) to pass them through an intermediary first: it had to copy them, or at least their vital statistics,1 somewhere else. Only then could Git copy the data to a work-tree file.2 The "somewhere else" is an index entry. The index is also called the staging area.

When you git checkout an entire commit, this is what you want anyway. So the internal limitation, of copying to the index first and only then to the work-tree, was actually a plus. So this mechanism, of copying into the index first, and only then on into the work-tree, was embedded into the implementation. Then, eventually, the user-oriented git checkout front end gained the ability to check out one individual file, or some small subset of files ... and it continued to do so through the index. The implementation detail became a documented feature.

Note that sometimes, the index is in use as a helper area during a conflicted merge. In this case, for some file F, there are up to three entries, in numbered slots 1 (base), 2 (--ours), and 3 (--theirs), instead of just one entry in the normal slot-zero. When this is so, you can extract any of the three index slot entries to the work-tree without disturbing the index. But if you use git checkout to extract a file from some other commit-or-tree, Git copies the file into the index, writing it to slot zero. This has the side effect of removing the higher-numbered slots, resolving the merge conflict!


1The main one is the hash ID. As ElpieKay noted in a comment, Git has to resolve the commit hash to a tree hash, then search the various trees to find whichever file(s) is/are of interest, so that it can obtain the blob hash. The index entry itself has a bunch more data as well, though, including stat structure data for the work-tree file, to make Git go fast.

2You can still use this work-flow, by using git read-tree to copy a tree into the index, then using git checkout-index to copy the index to the work-tree. Originally, Git consisted of a bunch of shell scripts like git-checkout wrapped around some fundamental C-coded pieces like git-read-tree. (The names were all hyphenated like this and there was no front end git command.)

torek
  • 448,244
  • 59
  • 642
  • 775
4

The Short Answer

  1. git checkout always copies items out of the index into the worktree.

  2. If you specify a commit other than the one you're on (e.g. the HEAD of another branch), checkout will always first copy the items from that commit into the index.

  3. Anything in the index that differs from HEAD will show as a "staged change". This is by definition.

See also Git checkout file from branch without changing index

Inigo
  • 12,186
  • 5
  • 41
  • 70