when I did git status, there are both tracked and untracked files. Early the day, I just learned that git stash --include-untracked would stash the untracked files. It worked for me at that time. So I thought git stash --include-untracked would save both tracked and untracked files' change. But when I git stash apply, there is only untracked files' change left. The tracked files' change are lost.
Lost tracked files when doing git stash --include-untracked
1.7k Views Asked by ychz AtThere are 2 best solutions below
On
I found this question because I found myself in the same situation - I had copy and pasted git stash -u from another SO answer as a way to supposedly stash my untracked files, then was horrified when I did git stash apply and they weren't restored.
@torek has two excellent and very detailed answers which explain what git is doing when you git stash -u and why the files are not restored. With their help I think I have pieced together the operations needed to restore them.
This will rely on you not having stashed again or done other git stuff in the meantime, if you have then some of below may need adjusting.
If you read any of @torek's answers you will know that git stash -u actually made three commits - one of them has your untracked files in it, but it's not the one that gets applied by git stash apply and won't be shown in git stash list.
You should be able to find it by:
git show stash^3
If that looks like your untracked files, you're in luck!
Now just:
git cherry-pick --no-commit $(git rev-parse stash^3)
This will restore the untracked files... albeit staged as "Changes to be committed"
(git reset to un-stage them)
There's something suspicious here, but it's probably not the stash itself
git stash --include-untracked, which can be spelledgit stash -ufor short, makes three commits for the stash.The first two are the same two as usual: one to hold whatever was in the index at the time you ran
git stash, and the other to hold whatever was in the work-tree—but tracked files only—at that time. In other words, theicommit holding the index holds the result ofgit write-tree, and thewcommit holds the result of (the equivalent of)git add -u && git write-tree(although the stash code does this the hard way, or did in the old days of shell script stash).That's all that the stash would have if you ran
git stashwithout--allor--include-untracked: it would have the two commits fori(index state) andw(work-tree state), both of which have the current commitCas their first parent. Commitwhasias its second parent:If you do add
-uor-a, however, you get a three-commit stash: commitwacquires a third parent, a commit we can callu, that holds the untracked files. This third parent has no parent of its own (is an orphan / root-commit), so the drawing is now:The interesting thing about this new commit, and its effect in the work-tree as well, is this: *Commit
ucontains only untracked files.**Remember that a commit is a full and complete snapshot of all (tracked) files. Commit
uis made by—in a temporary index—discarding all tracked files and instead, tracking some or all untracked files. This step either adds only the untracked-but-not-ignored files (git stash -u), or all files (git stash -a). Then Git writes commitu, usinggit write-treeto turn the temporary index into a tree to put into commitu, so that commitucontains only the selected files.Now that these selected files are in commit
u,git stashremoves them from the work-tree. In practice, it used to just rungit cleanwith appropriate options. The new fancier C-codedgit stashstill does the equivalent (but, one might hope, with fewer bugs; see below).This is similar to what it does for the files in
iand/orw: it effectively does agit reset --hard, so that the work-tree's tracked files match theHEADcommit. (That is, it does this unless you use--keep-index, in which case it resets the files to match theicommit.) Thegit resetat this point has no effect on untracked files, which are outside the scope ofgit reset, and no effect on the current branch since the reset deliberately keeps that at theHEAD.Having stashed some untracked files in commit
u, though,git stashthen removes those files from the work-tree. That's quite important later (and maybe also immediately).Note: there was a bug in combining
git stash pushwith pathspecs, that potentially affects everything, but especially affects the stash variants made with-uor-a, where some versions of Git remove too many files. That is, you mightgit stashjust some subset of your files, but then Git wouldgit reset --hardorgit cleanall files, or too many files. (I believe these are all fixed today, but in general, I don't recommend usinggit stashat all, and especially not the fancy pathspec variants. Removing untracked files that weren't actually stashed is particularly egregious behavior, and some versions of Git do that!)You describe an
apply-time problem, but maybe not the usual oneHere's what you said:
As always, Git doesn't save changes, it saves snapshots.
Applying a normal (no-untracked-files) stash is done in one of two ways, depending on whether you use the
--indexflag. The variant without--indexis easier to explain, since it literally just ignores theicommit. (The variant with the--indexflag first usesgit apply --indexon a diff, and if that fails, suggests that you try without--index. If you want the effect of--index, this is terrible advice and you should ignore it. For this answer, though, let's ignore the--indexoption entirely.)Note: this is not the
--keep-indexflag, but rather the--indexflag. The--keep-indexflag applies only when creating a stash. The--indexflag applies when applying a stash.To apply the
wcommit, Git runsgit merge-recursivedirectly. This is not something you should ever do as a user, and whengit stashdoes it, that's not really all that wise either, but that's what it does. The effect is a lot like runninggit merge, except that if you have uncommitted changes in your index and/or work-tree, it may become impossible to return to this state in any sort of automated way.If you start with a "clean" index and work-tree, though—that is, if
git statussaysnothing to commit, working tree clean—this merge operation is almost exactly the same as a regulargit mergeorgit cherry-pick, in many ways. (Note that bothgit mergeandgit cherry-pickrequire that things be clean, at least by default.) The merge operation runs with the merge base set to the parent of commitw, the current or--ourscommit being the current commit as usual, and the other or--theirscommit being commitw.That is, suppose that your commit graph now looks like this:
so that you are on commit
B. The merge operation to apply the stash does a three-way merge withCas the merge base andwas the--theirscommit, and the current commit/work-tree as the--ourscommit. Git diffsCvsBto see what we changed, andCvswto see what they changed, and combines the two sets of differences.This is how the merge into
Bwill run, provided that Git can first un-stash commitu. The usual problem at this point is that Git can't un-stashu.Remember that commit
ucontains exactly (and only) the untracked files that were present when you made the stash, and that Git then removed withgit clean(and appropriate options). These files must still be absent from the work-tree. If they are not absent,git stash applywill be unable to extract the files fromuand will not proceed.Since the untracked files are untracked, it's hard to know if they changed
You talk about changes in untracked files.
Git of course doesn't store changes, so you can't find them that way. And if the files are untracked, they're not in the index right now either. So: how do you know they're changed? You need some other set of files to which to compare them.
The step that extracts commit
uis supposed to be all-or-nothing: it should either extract allufiles, or not. If it does extract allufiles,git stash applyshould go on to attempt to merge, somewhat as if bygit cherry-pick -n(except that cherry-pick writes to the index too), commitwin the stash. That should leave you with extractedufiles and mergedw-vs-Cchanges, in your work-tree.If there are conflicts between
C-vs-work-tree vsC-vs-w, you should have the conflict markers present in the work-tree, and your index should have been expanded as usual for a conflicted merge.If you can make a reproducer for your problem, that would probably provide huge amounts of clarity here.