1

I'm studying Pro Git to understand how reset and checkout work. I understand now about the trees, and how each command affects each tree depending on the mode and if paths are specified. But one thing has me confused.

Pro Git specifies that when using git checkout with paths:

[git checkout] is just like git reset [branch] file in that it updates the index with that file at that commit, but it also overwrites the file in the working directory.”

However, in my experimentation I'm unable to reproduce this expected behavior.

If I'm on a topic branch with commits red, green, blue:

9c070df (HEAD -> colors) blue
28a97c1 green
5edafd9 red

with a single file whose patches are:

9c070df (HEAD -> colors) blue
diff --git a/colors.txt b/colors.txt
index 9d8beb6..ff67b54 100644
--- a/colors.txt
+++ b/colors.txt
@@ -1,2 +1,3 @@
 red
 green
+blue
28a97c1 green
diff --git a/colors.txt b/colors.txt
index a9d1386..9d8beb6 100644
--- a/colors.txt
+++ b/colors.txt
@@ -1 +1,2 @@
 red
+green
5edafd9 red
diff --git a/colors.txt b/colors.txt
new file mode 100644
index 0000000..a9d1386
--- /dev/null
+++ b/colors.txt
@@ -0,0 +1 @@
+red

If HEAD is on blue, and I git reset 5edafd9 -- colors.txt, I'll have

+ green
+ blue

on the working tree, and

- green
- blue

in the index, which is expected, since the only line red is applied to the index. So when working tree is diffed with the index, it looks like those lines are added, and when index is diffed with head, it looks like those lines are removed. That's expected and understood.

But when I git checkout -- colors.txt, only the working tree is affected, leaving the index intact.

Why is this?

Canucklesandwich
  • 673
  • 5
  • 13
  • 1
    Have you read the [documentation](https://git-scm.com/docs/git-checkout) already? – Micha Wiedenmann Aug 04 '19 at 07:23
  • Thanks for the reminder! A classic case of RTFM. Says it right there: including tree-ish will update working tree and index. I assumed not specifying HEAD meant it would just default to HEAD for tree-ish, but they are two entirely different commands. – Canucklesandwich Aug 05 '19 at 19:57

2 Answers2

2

The git checkout command is ... complicated :-)

  • git checkout -- colors.txt copies colors.txt from the index (:colors.txt) to the work-tree. (This is what you did.)

  • git checkout HEAD -- colors.txt copies colors from the HEAD commit (HEAD:colors.txt) to the index, then to the work-tree. (This is what you meant to do.)

In both cases, this may overwrite uncommitted data: in the index-to-work-tree case, it will overwrite any work-tree version that git status would call "not staged for commit", and in the HEAD-to-index-and-work-tree case it will also overwrite any index version that git status would call "staged for commit".

Meanwhile, of course:

  • git checkout branchname switches to the given branch name while also copying files from the tip commit of that branch to the index and work-tree—but this particular git checkout operation is nondestructive, failing if it would have to overwrite uncommitted data.

  • git checkout of a commit specifier that is not a branch name will switch to a detached HEAD at the given commit, also nondestructively.

(These can be forced to make them destructive.)

  • With the -m flag, git checkout will either reconstruct a conflicted merge (destructive) or attempt to merge one or many files (potentially somewhat destructive).

  • With --ours or --theirs, git checkout can extract index staging version 2 or 3 of a file. This is destructive of any unsaved work-tree data.

These options (when used with pathspecs in the -m case) are in some ways somewhat similar to the first two cases: the current branch does not change; only work-tree and perhaps index copies of files change.

  • With -b, -t / --track, or --no-track, git checkout can create a new branch. With -B, git checkout can either create a new branch or, in effect, git reset an existing one. With --orphan, git checkout can set you up in a situation similar to that in a new, totally-empty repository, where the next commit you make will create a branch that has no previous commits on it.

These actions are somewhat like the second two sets of git checkout behaviors, as you may be on a different branch if the git checkout succeeds. These are generally nondestructive as well. Note, however, that -B can move an existing branch name arbitrarily. If you don't have reflogs enabled, this may be difficult to recover from.

  • With -p, git checkout can interactively patch the difference between a commit (really, any "tree-ish", as Git puts it) and the work-tree, or the index and the work-tree.

There are a few more options you can sprinkle into the mix, but these five groups of really fairly-different behaviors are all shoved into one git checkout command. I've long held that this is too crowded, and as of Git 2.23, the Git folks finally seem to have come around to agreeing: Git will have two new commands, git switch and git restore, that only do some of what git checkout currently does. git checkout will still exist and still do what it always did, but if you want to switch from, say, master to develop, you can run git switch develop and not worry about what happens if you mistype develop. (Currently, if you mistype it as git checkout devop, you might clobber the uncommitted file named devop.)

torek
  • 330,127
  • 43
  • 437
  • 552
2

But when I git checkout -- colors.txt, only the working tree is affected, leaving the index intact

That is because this form of checkout (git checkout [<tree-ish>] [--] <pathspec>…​) is about overwriting paths in the working tree by replacing with the contents in the index or in the <tree-ish> (most often a commit).

That will be, with Git 2.23, what git restore will do:

'git restore' by default will only update worktree. (as opposed to 'checkout <tree> <paths>', which updates both worktree and index, like git restore --staged --worktree --source would).

If you want to switch branch, you will use git switch.

Git 2.23 will be released in a few days, August 2019.

VonC
  • 1,042,979
  • 435
  • 3,649
  • 4,283