git checkout HEAD~2 (example) deletes untracked files if they were added with last commit

11,556

Solution 1

They should not [be deleted].

Yes, they should. Commits are historical states. You're checking out a state before they were committed, so they should not be there. Just imagine working on your project a year from now. Half the files have been renamed, there are dozens of new files, and for whatever reason you decide to check out today's version. Surely you wouldn't want all those dozens of files to just sit around and clutter it up! They don't belong there! (And of course "untracked files...added with last commit" doesn't make any sense - if you committed them, they're now tracked.)

If the files really should have been in the old commit, likely what you want to do is use an interactive rebase (git rebase -i <start commit> <branch>, further instructions provided by git)to squash a couple commits together. Perhaps you'll need to reorder the commits too, pushing the "add these files" commit back farther in the history where it belongs. Or, if you notice this right after you forget to add the files, simply add them and use commit --amend to amend to the previous commit instead of creating a separate one.

Finally, if you really do get this set in the history this way (others have pulled so you don't want to rebase/amend), you can check out the old commit, and then check out the files from the newer commit:

git checkout <old-commit>
git checkout <new-commit> file1 file2 dir1 dir2/file3 ...

Solution 2

Just checkout HEAD again and you get those files back. Git checkout HEAD~2 reverts the directory of your repository back to the tracked status you had two commits ago. This is totally expected behaviour.

Share:
11,556
Rob
Author by

Rob

Engineer in electronic systems and embedded stuff.

Updated on June 04, 2022

Comments

  • Rob
    Rob almost 2 years

    I add new files (which were present before as untracked files) and commit them. When I checkout to before this commit, these files are deleted. They should not.

    It does not matter whether .gitignore lists these files or not (which requires to do git add -f ...).

    • Mark Peters
      Mark Peters over 13 years
      Are you sure you're the authority to suggest what should happen? Seems perfectly logical to me; those files don't exist at the point of your checkout.
  • Andrew Aylett
    Andrew Aylett over 13 years
    +1: Git has no idea whether you added those newly-tracked objects just before the checkin where you committed them (usual, should therefore delete them) or whether you had them lying around in your repository before-hand (decidedly unusual, why didn't you add them earlier?).
  • Rob
    Rob over 13 years
    Why didn't I add them earlier? Because I'm a human..., and this hinders me in rebuilding former commits.
  • halfdan
    halfdan over 13 years
    Check git status before commiting and check if all required files are committed / tracked.
  • Rob
    Rob over 13 years
    Of course. When unaccidentally untracked files remain untracked, it is no problem to revisit an older commit. But I add them, and a revisit of an older commit shows that those files are deleted. I'd wish that checkout offered a flag to not delete files.
  • Rob
    Rob over 13 years
    Your conclusions are right, because finally clean. Indeed I don't want to modify the history - only the last proposal applies. Only goal of this action: compare target program behaviour because of a bug. Your solution with two checkouts provides the same effect as my solution with diff/checkout/apply
  • Rob
    Rob over 13 years
    To be precise: technically it is quite well possible not to delete files when returning to a former commit as in this case: Imagine a big leap backwards. While the "machine" is internally working through history, on every border between "tracked" and untracked", it could decide to leave that file(s) in the state just beyond that border. To my opinion, it wouldn't mess up any working directory content, rather restore even more precisely any commit state.
  • Cascabel
    Cascabel over 13 years
    @Rob: So you think that when you check out an old commit, you should end up with the union of the files existing from that commit through to the future? What if your code dynamically loads all available plugins, and there are plugins in the future version which won't work with the older version? What if there are two divergent branches which have the checked-out commit as an ancestor - how will the VCS know which branch's version to put in the tree? This is not at all a consistent idea. If you check out a state, the VCS must actually put things into that state.
  • Rob
    Rob over 13 years
    Jefromi, I'm not an expert, but when the git engine traverses each commit upon a checkout in history (condition: linear return to any parent level), I'd believe that perfect reversibility of each individual commit 'border' is possible, when a border is discovered with "file track" addition. Of course only, when a specific flag is used. If going in history before an "add" point, such files get unaltered state. This is a risk, because e.g. .config may very well have changed in untracked state as well. The user should be aware of this, comparable as awareness about history hacking.
  • Rob
    Rob over 13 years
    Amendment: while editing the above message, I accidentally removed a "real-world" note. One of the files I added during development was a kernel .config in a product specific branch. This should clarify the context.
  • Cascabel
    Cascabel over 13 years
    So you want the behavior of git checkout <tree-ish> to depend on the current HEAD. That's kind of scary - in my mind, it should only depend on the tree-ish you check out. And I understand what you're trying to do with the config files, I think, but asking for checkout to have different behavior isn't the solution. This is definitely a special case, and you have to handle it by manually checking out the needed files, or by doing temporary cherry-picks or merges.
  • Rob
    Rob over 13 years
    I needed some time to check out the history concept. As git points out to be (the only?) SCM that does not apply delta storage of commits, it needs no history traverse to checkout a certain commit. So, you win ;-) - thanks for your critical remarks.
  • Rob
    Rob over 13 years
    Experiment: I have created a test commit on top of the tip, where .config is git removed. When I checkout a former commit where .config is not tracked, too (beyond the bunch of commits where it is tracked), this file remains untouched. This proves that git does not traverse history while doing checkouts.