1

After editing my code, git checkout -- . works for undoing my edits. The same command, however, doesn't work for undoing the changes made by git checkout [revision] . - I need git reset --hard; in that case instead.

Can anyone explain why the inconsistency?

Community
  • 1
  • 1
Stefan Monov
  • 11,332
  • 10
  • 63
  • 120

2 Answers2

1

This all has to do with Git's index.

The index has several functions in Git, but the main one is that it's where you build the next commit. Other version control systems don't have a separate index (or completely hide it from you): they have just the current commit, which is the obvious thing and which Git calls HEAD, and the work-tree, which is where you can see and edit your files. But Git has this extra, third thing.

This third thing, this index, is why you must git add changes you make. Until you git add your new version of some work-tree file, the index version still matches the one in the HEAD commit. So the next commit you will make won't have the new version of the file in it yet.

The command:

git checkout -- .

copies files from the index, to the work-tree. If you have not git added the updated versions, the index versions match the HEAD commit, so this copies the same version as the HEAD commit, to the work-tree.

The command:

git checkout <revision> -- .

however, does something very different: it copies everything from the specified revision into the index, and then copies those files from the index into the work-tree. Now that the index version of each file is changed:

git checkout -- .

just gets those same index versions into your work-tree again. They're all still ready to commit in a new commit, too.

You can:

git checkout HEAD -- .

which will copy all the files from the HEAD commit into the index and on into the work-tree. This will make HEAD, index, and work-tree all match again.

Meanwhile, the command:

git reset --hard HEAD

is a little bit different, but has the same effect in the end: first, it moves the current branch from HEAD to HEAD. Since this doesn't move it anywhere, that has no effect at all. Next, it copies the new HEAD's files into the index, and last, it copies those files from the index to the work-tree (this is the --hard step).

There are some additional differences that you could see if you had new files in the index, that were not in the HEAD commit. Using git checkout HEAD -- . would leave them in there, while git reset --hard HEAD would remove them (from index and work-tree both).

torek
  • 448,244
  • 59
  • 642
  • 775
  • Thanks. I just don't get why `git checkout` does two completely different things in two very similar cases, without even a difference of an extra CLI flag or something. – Stefan Monov Jun 28 '17 at 19:12
  • @StefanMonov: Personally I think this was a mistake: Git should have one command that means "switch branches" and a *different* command that means "extract files from some branch, possibly the current branch, without switching branches". Both might even be implemented with the same source file, the way `git log` and `git rev-list` are, but they should be different user-facing commands. But I did not design Git. – torek Jun 28 '17 at 19:15
  • Yeah. I'm not even talking about switching branches though. I'm talking about one command for "copy files from revision to index & work-tree" and another for "copy files from index to work-tree". – Stefan Monov Jun 28 '17 at 19:23
  • @StefanMonov: well, one can argue that the presence of the index itself is a mistake. Mercurial gets away without one for instance. But the index is very useful for delayed decisions during merges; Mercurial requires that you make certain merge decisions early, precisely because it does not have the index in the first place. The benefit (in Mercurial) is that, with some pretty clear and obvious exceptions, "what's in the work-tree now" is "what goes into the next commit" and there's no weird partial staging to deal with. – torek Jun 28 '17 at 20:19
  • Meanwhile, note that there *is* a separate command, `git checkout-index`, that copies files from the index to the work-tree (only). It's just that you can *also* do this with the Swiss Army Chainsaw `git checkout` command. – torek Jun 28 '17 at 20:22
0

git checkout -- . will discard changes in working directory and keep all files’ version as the last commit (as you said undo edits).

git checkout <commit> . will change all files’ version as specified. There have two situations:

  • git checkout <last commit> ., this will work same as git checkout -- . because git changes all the files’ version as the last commit.
  • git checkout <not last version> ., this will checkout all files’ version as you specified version, and since files are different from the last commit, so there has local changes need to be committed.

So in a word, for different situations:

Undo edits (not committed): git checkout -- .

Need to develop based on an earlier commit: git checkout <commit> . and git commit

Discard some committed changes: git reset --hard HEAD~n

Marina Liu
  • 36,876
  • 5
  • 61
  • 74