Rufus
Rufus

Reputation: 5566

Why doesn't git checkout FILE reapply uncommitted changes?

git checkout COMMIT switches your current state to that pointed by COMMIT and reapplies any uncommitted changes (it even fails nicely if your uncommitted changes are incompatible with the COMMIT you want to switch to.

This functions in stark contrast to git checkout COMMIT FILE which just blindly discards any uncommitted changes (without any warning) of FILE and "forces" the state of FILE to that in COMMIT.

My question is, why the inconsistency? (perhaps with regards to historic / design decisions)

Surely it could have been made such that git checkout COMMIT FILE attempted to reapply uncommitted changes to FILE and failed gracefully if that failed. Especially since git checkout is often marketed as a "safe" operation. If anything, the current behavior of git checkout COMMIT FILE should be the behavior of git reset --hard COMMIT FILE. (On a side note, the behavior of git reset COMMIT and git reset FILE also leaves room for improvement in consistency...)

Upvotes: 2

Views: 119

Answers (2)

torek
torek

Reputation: 490068

git checkout commit switches your current state to that pointed by commit ...

Yes, sort of. It's actually substantially more complicated under the hood.

... and reapplies any uncommitted changes (it even fails nicely if your uncommitted changes are incompatible with the commit you want to switch to).

It really doesn't do that first part (but does do the second), and that's the complication: what git checkout is doing is a subset of what the git read-tree documentation calls a two tree merge. If you follow this link, you'll see that this has 21 separate cases listed (one, case #3, has two sub-cases, and I'm ignoring case #0 which cannot happen, so if you count the sub-cases and case #0 you get 23 different cases). I won't list them all here, but they kind of boil down to this idea: If Git doesn't have to touch the index and work-tree when switching commits, it doesn't. That's what keeps your uncommitted changes in place: Git is not re-applying anything, it's just not ripping out anything either. It just gently tiptoes around the uncommitted changes.

That, in turn, sort of (but not entirely) explains this radically different behavior, that some—including me—might go so far as to call evil:

This functions in stark contrast to git checkout commit file which just blindly discards any uncommitted changes (without any warning) of file and "forces" the state of file to that in commit.

Right, and this is because Git views your command as a request to replace the index copy of file with that from commit. The updated index copy is copied out to the work-tree as well.

I would argue (and have in the past) that this should be a separate Git command. It happens to be implemented by the same source code simply because that source code is full of "extract file from commit, write to index and work-tree" code. But so is git reset, and git reset is a separate command.1 So this is at best an excuse of the Hysterical Raisins form.

Surely it could have been made such that git checkout commit file attempted to reapply uncommitted changes to file and failed gracefully if that failed.

That, too, is available in git checkout, under the -m option ... sort of. However, it's not on a per file basis, but rather on a whole commit basis only. That is, you can git checkout -m commit, which merges your work-tree with the target commit (as a three-tree merge), but not git checkout -m commit file. To get that effect, extract the file from the commit, choose a merge base version to extract, and use git merge-file on the three files:

git show other:file > file.other
git show base:file > file.base
git merge-file file file.base file.other

1Git's reset also crams too many unrelated bits of functionality into one user-facing command, if you ask me, but you didn't.

(On a side note, the behavior of git reset commit and git reset file also leaves room for improvement in consistency...)

OK, maybe you did! :-)

Upvotes: 3

Mark Adelsberger
Mark Adelsberger

Reputation: 45819

When you check out a commit, git does not "reapply" changes. It leaves modified work tree and index copies unchanged if and only if the checkout would not affect the file - i.e. the version of the file in the commit you're checking out is the same as the version of the file in the pre-checkout HEAD commit. But if it has to touch a file at all, and the index or working tree copy of that file has modifications, then the checkout aborts.

When you checkout a specific path, that path is clobbered without warning because that is the definition and purpose of checking out a specific path. You say git checkout -- myfile.txt specifically because you want to revert myfile.txt, and in fact that's the command git status would recommend using if you want to revert myfile.txt. Keep in mind that for this to be consistent with the "checkout commit" behavior, it still wouldn't try to reapply (merge) the local changes; it would just refuse to do the checkout on any modified file. (And it's fair to say that wouldn't make much sense.)

The seeming inconsistency is one of several symptoms of the unfortunate choice to use the same keyword for two entirely different operations. While either of those operations could reasonably be named "checkout", it is one of the few decisions I really dislike about git that it uses that name for both.

Upvotes: 1

Related Questions