Search code examples
gitgithubversion-controlbitbucket

Unable to view or access stashed changes suddenly


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?


Solution

  • TL;DR

    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.

    Long

    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 commits 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 commits 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.