I have opened up VS code to find that I can not see the stashed changes that I previously had the last time I opened the directory.
The stash contains a few week's worth of work so I am desperate to recover it somehow. I have tried viewing dangling commits but have not been able to find the lost work.
Is there a way to recover these stashed changes? Could it be an issue with the .git file?
See the recipe at the end of the git stash
documentation:
git fsck --unreachable |
grep commit | cut -d\ -f3 |
xargs git log --merges --no-walk --grep=WIP
You may need to adapt this for Windows if you don't have all the standard Linux tools used here.
In a comment, you added this:
git stash list
does not result in anything.
This means that the stash you made either was not a git stash
in the first place (I don't know if VSCode might have its own non-Git stashes), or was dropped. Assuming that it was a git stash
style stash, there are a few important things to know about stashes:
They consist of commits. This means that even once they're no longer find-able, they are usually still in the repository, at least for some time. But:
They are on no branch. You cannot find them using a branch, and because of the way git stash
uses (abuses?) its own reflog, you cannot find a dropped one in git reflog refs/stash
output.
For Git to find, and thus use, a commit, we must deliver the commit's hash ID to Git. If we literally can't find the hash ID, we equally can't find the commit. Each git stash
we make consists of two, or sometimes three, commits, and we must find the hash ID of the commit—or rather, of the "main" commit of the two-or-three that make up the stash.
Normally, we don't bother with hash IDs at all. We just use things like branch and tag names, or—for git stash
—a stash reference like stash@{2}
, or nothing at all, which means "the top of the stash stack". But these names are just ways that we can tell Git: Use the name, which you've stored in your databases somewhere, to find the hash ID.
This sort of thing—the fact that names turn into hash IDs—is why git log
shows the commit hash IDs. Those are what Git really needs, and since each commit has a unique hash ID—one never used before, for any commit anywhere in any Git repository, and never to be used again, for any commit anywhere in any Git repository—if we can find that hash ID, we can ask Git to fish that commit out of its databases.
The git stash
command makes these slightly-special commits:
$ git stash
Saved working directory and index state WIP on master: 30cc8d0f14 A regression fix for 2.37
$ git stash list
stash@{0}: WIP on master: 30cc8d0f14 A regression fix for 2.37
To find the actual commit hash ID, I can simply run git rev-parse refs/stash
, which is the special name by which Git finds the special commits:
$ git rev-parse refs/stash
0b8dfaeb5bc84d3560a9e84f24b059b00291c1d7
Unfortunately, once I drop the stash, there is no Git command to find that hash ID:
$ git stash drop
Dropped refs/stash@{0} (0b8dfaeb5bc84d3560a9e84f24b059b00291c1d7)
It's there on my screen, but there's no git xxx
command, for any xxx
, that will print that one hash ID.
If you have the hash ID on your screen somewhere, you can save it. Use your mouse to cut-and-paste it into a new branch or tag name, for instance:
$ git tag temp-save 0b8dfaeb5bc84d3560a9e84f24b059b00291c1d7
$ git stash show temp-save
Makefile | 1 +
1 file changed, 1 insertion(+)
(this particular stash is a kind of pointless one I made just for the purpose of this answer). Now that you have a branch or tag name—I made a tag name—you can use git stash
with that name to manipulate the saved stash.
This only works if you can see the hash ID on your screen somewhere. If not, you somehow have to find the hash ID. That's hard. It may be impossible, because a commit that literally can't be found—as is the case for a dropped stash when we haven't made another name for it like the tag trick above—is eligible for what Git calls garbage collection. The git gc
command, which Git will occasionally run automatically as a sort of background task, is allowed to completely eject this commit from Git's databases, after which it's no longer in your repository at all.
Fortunately, this sort of thing (a) doesn't happen all day every day: git gc
is a slow and compute-intensive task so Git doesn't just run it lightly, and (b) has a lot of built-in safety mechanisms: for instance, all objects get a 14-day grace period after they're created, during which git gc
won't remove them.
But that still leaves us with the task of somehow finding some random-looking hash ID that is the hash ID for the "main" commit of the two or three commits that git stash
made, that something (you or VSCode or whatever) dropped.
git fsck
The main trick for finding such a commit is to use git fsck
. This is a maintenance command that checks the health of the entire set of databases and ancillary files in a Git repository. It's a noisy command: it prints a lot of stuff that it finds, even if that's not actually a problem, so don't worry if git fsck
prints a lot of stuff. It's just trying (maybe not very successfully) to be helpful here.
I'll delete my temp-save
tag now and run a git fsck
(without --lost-found
initially), to show the kind of things it says, and to highlight which one is important:
$ git fsck
Checking object directories: 100% (256/256), done.
warning in tag d6602ec5194c87b0fc87103ca4d67251c76f233a: missingTaggerEntry: invalid format - expected 'tagger' line
Checking objects: 100% (345993/345993), done.
Checking connectivity: 338422, done.
dangling tag 3a1c74404d544d20c0d1f0b9c072b471a20b04c0
dangling blob 8b8fc5a6d6f02446a7bf9f306f0ea549b1599e98
dangling commit 0b8dfaeb5bc84d3560a9e84f24b059b00291c1d7
dangling tag 2b92bdd78a61718279a75ce722b14979efcc40a4
$
This process took about a minute. We saw a complaint about a malformed tag object (hash ID d6602ec5194c87b0fc87103ca4d67251c76f233a
). This is for Git v0.99, and I have no idea why there's a bad tag object here, but it's here. We can ignore it: you probably won't have any bad tags. (Some ancient Git repositories have objects that are no longer allowed in newer repositories—Git won't ever make these again—but are allowed to continue to exist so as to permit the old repository to keep functioning.)
Then git fsck
griped about four dangling objects:
dangling tag 3a1c74404d544d20c0d1f0b9c072b471a20b04c0
dangling blob 8b8fc5a6d6f02446a7bf9f306f0ea549b1599e98
dangling commit 0b8dfaeb5bc84d3560a9e84f24b059b00291c1d7
dangling tag 2b92bdd78a61718279a75ce722b14979efcc40a4
The dangling commit
s are the ones of interest here. Note that in this case, we got exactly one such commit, 0b8dfaeb5bc84d3560a9e84f24b059b00291c1d7
. That's the stash we're looking for!
(The rest of these are boring: the first dangling tag
is something Junio Hamano leaves in the repository on purpose so that you can get his GPG public key. It's been mis-diagnosed here, probably a small bug in git fsck
in this particular Git version. The second dangling tag
is a tag I made and deleted a while back: it's a leftover that git gc
will remove whenever it gets around to it. The dangling blob
object is, similarly, a leftover from a temporary thing I made.)
If we run git fsck
with the --lost-found
option, Git will create, in .git/lost-found
, two directories (folders), one named commit
and one named other
. It also finds a lot more dangling commit
s this time, which seems a bit odd: I don't know off-hand why this is. But the side effect is that we end up with these .git/lost-found/commit/*
files, which give us the hash ID of all the dangling commits. These are our suspected stashes.
We now want to have Git examine each of these commits. To be the commit we want to find, this commit must:
be a merge commit (have two or more parents): this is a technical weirdness of stash commits, that they are merge commits even though they were not made by git merge
and must not be treated as merge commits (this is one of a number of reasons that I recommend avoiding git stash
: the usual Git tools don't work with them!);
have, as its subject line, the subject you used when you made the stash: the default is WIP on branch: abbreviated hash ...
Using the recipe from the documentation, I find just the one commit, 0b8dfaeb5bc84d3560a9e84f24b059b00291c1d7
, that I found with a plain git fsck
as a "dangling commit".
All of this means that 0b8dfaeb5bc84d3560a9e84f24b059b00291c1d7
is the hash ID I am looking for. I can now make a temporary tag for it, as I did when I had the hash ID available for cut-and-paste in the first place, and then I can use git stash apply temp-save
to apply it, for instance.
Note that if you did use git fsck --unreachable
, you should probably enter .git/lost-found
and clean up the junk:
$ cd .git/lost-found
$ ls
commit other
$ rm -rf commit other
$ ls
$
If you use the documentation recipe, you won't need to do this cleanup step, but if it produces a whole lot of things to look at, you might want to use the --lost-found
option to obtain a list of potential-stash hash IDs for a multi-hour effort if necessary.