Diwas
Diwas

Reputation: 167

All my modifications gone after git stash

I was working on the main branch, then I decided to create a new branch called test-branch. I tried git checkout feature first which warned me to stash or commit my changes. I stashed them using git stash save latest modification and then I went to test-branch using git checkout -b test-branch. I was trying to add all files (not ignoring any) and commit to the branch. So I deleted everyting from .gitignore. After running git add ., I came back to main without committing to test-branch. I deleted that branch using git branch -D test-branch. And then I used git stash apply in main. Now my code has gone to the last commit version and all the modifications I did after that commit exist no more. What do I do now ?

Before swtiching branches

PS C:\Users\Administrator\Desktop\projects\songs> git checkout feature
error: Your local changes to the following files would be overwritten by checkout:
        .gitignore
        app.py
        music.db
        static/css/styles.css
        templates/favorites.html
        templates/layout.html
Please commit your changes or stash them before you switch branches.

PS C:\Users\Administrator\Desktop\projects\songs> git stash save "latest modification"
Saved working directory and index state On main: latest modification
Unlink of file 'music.db' failed. Should I try again? (y/n) y
fatal: Could not reset index file to revision 'HEAD'.

After switching branches

PS C:\Users\Administrator\Desktop\projects\songs> git checkout -b test-branch
Switched to a new branch 'test-branch'

PS C:\Users\Administrator\Desktop\projects\songs> git st
 M music.db
?? static/scripts/downloader.js
?? test/

PS C:\Users\Administrator\Desktop\projects\songs> git add .

I then came back again to main

PS C:\Users\Administrator\Desktop\projects\songs> git checkout main
Switched to branch 'main'
M       .gitignore
M       music.db
A       static/scripts/downloader.js
A       test/a.exe
A       test/ipvalidator.exe
A       test/nextvalidator.c
A       test/nextvalidator.exe
Your branch is up to date with 'origin/main'.

PS C:\Users\Administrator\Desktop\projects\songs> git branch -D "test-branch"
Deleted branch test-branch (was 08f1d8e).

Then I applied the stash and my changes are gone


PS C:\Users\Administrator\Desktop\projects\songs> git stash apply
error: Your local changes to the following files would be overwritten by merge:
        .gitignore
Please commit your changes or stash them before you merge.
Aborting

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        modified:   music.db
        new file:   static/scripts/downloader.js
        new file:   test/a.exe
        new file:   test/ipvalidator.exe
        new file:   test/nextvalidator.c
        new file:   test/nextvalidator.exe

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   .gitignore

Untracked files:
        __pycache__/
        flask_session/
        improvements.txt
        
        run.ps1
        static/downloads/
        templates/test.html
        test.py
        venv/

Edit:

As per @ElderFuthark 's request, I ran git stash show --stat and got the following:

PS C:\Users\Administrator\Desktop\projects\songs>  git stash show --stat
 .gitignore                 |   4 ++--
 app.py                     |  11 +++++++----
 music.db                   | Bin 69632 -> 86016 bytes
 static/css/musicPlayer.css |   1 +
 static/css/styles.css      |   4 +++-
 templates/favorites.html   |   2 ++
 templates/layout.html      |   8 ++++++--
 templates/play.html        |  27 +++++++++++++++++++--------
 8 files changed, 40 insertions(+), 17 deletions(-)

Upvotes: 1

Views: 660

Answers (2)

torek
torek

Reputation: 487795

Long, so, part 1 of 2

(see part 2 for what you can do)

There are multiple things to learn here, and also multiple different steps to take to recover everything. There's overlap between the two but they're not exactly the same.

One thing I would suggest as a learning item is "don't use git stash" because it often makes a big mess. 😱 People do like it a lot, though.

Here are things you need to know before we get started on fixing things (you may well know some of these already, but I'll enumerate them here):

  • Git is mostly about commits. It's not really about files (though we store files in commits), and it's not really about branches (a poorly-defined word). We use branch names to help us (and Git) find commits, which then store the files.

  • Commits themselves have—or can have—parent/child relationships. These form things that Git calls branches, which are different from the branch names that Git also call branches.

  • Commits are numbered, with big ugly random-looking hash IDs or object IDs. These IDs are actually numbers expressed in hexadecimal. Every commit gets a unique ID, one that has never been used before for any commit anywhere ever, and one that will never be used again. So the ID works to find the commit. Git actually needs that ID, but they're so big and ugly and random-looking that humans are very bad at them, and we mostly don't use them: we mostly use branch names instead.

  • What's in a commit is a pair of things:

    • Each commit holds a full snapshot of every file (that it holds: some files get added or deleted over time).

    • Each commit also holds some metadata, or information about the commit itself: who made it and when, for instance. This metadata, for any given a commit, includes a list—usually just one element long—of the raw hash ID of the previous commit. Git calls this the parent of the commit, and it's how commits link together into "branches" (the group-of-commits kind of branch, not the name kind).

    All of these parts are completely read-only, and they last as long as the commit itself lasts.

  • Because a commit is read-only, you can never actually work on a commit directly. Instead, the act of "checking out" a commit (with git checkout or git switch) copies the files out of the commit into a usable work area. Git calls this your working tree or work-tree and it's quite simply where you do your work.

    Note that checking out some commit makes that commit the current commit. This current commit will, in the future, become the parent commit of your next new commit, unless of course you switch to another commit as your current commit.

  • For reasons known only to Git's inventor—though we can speculate all we want—Git doesn't just have two copies of each "active" file. There is indeed one saved permanently in the current commit, and a usable, edit-able copy in your working tree. Most version control systems do this, so that there are two copies. But Git keeps a third copy (or "copy" because it's in Git's internal pre-de-duplicated form) of each file that came out of a commit. This third copy is how Git makes the next commit, and it's stored in a very important, but poorly-named, place.

So, to review a bit:

  • We give a branch name to git checkout or git switch.

  • That name locates a commit by its hash ID. Git will, if this checkout or switch is successful, extract the files from that commit so that we can work on them. That commit—it hash ID—becomes the current commit, and that name, whatever it is, becomes the current branch name.

  • To check out that commit, Git must fill in both the working tree and this poorly-named third copy, from the files saved permanently in that commit (as permanent as the commit itself, which is "mostly permanent").

Git's index and your working tree

It's time to talk about that poorly-named area that holds the third copy of each file, and its relationship to your working tree. Git calls this thing by three different names:

  • It is the index. This name has no obvious meaning, which is both good and bad: good, because you probably won't bring a bunch of preconceived notions, and bad, because, well, the reaction to "index" is mostly "huh? what?"

  • It is also the staging area. This is often a better name because it talks about how we use it. We git add a file to "stage" it for commit. But it's a bit misleading, because that file is already in the staging area even before we git add it. Or rather, it's there if it was already in the current commit.

  • As a much older name, that's mostly going disused now, Git sometimes calls this the cache. You mostly see this in flags, as in git rm --cached or git diff --cached.

Anyway, what you need to know about this thing—which I'll call the index here—is that it holds your proposed next commit. That is, if you were to run git commit right now, whenever "right now" is, you'll either get a new commit or an error. If you don't get an error, the new commit you will get holds, as its full snapshot of every file, all of the files that are in the index, and no other files at all. The specific version of the file that will be in the new commit is the data that we find in the index copy of the file.

Because this index copy is a third copy (but pre-de-duplicated so that git commit goes fast), it's possible for it to be different from the current commit copy. It's also possible for it to be different from the working tree copy.

When we first check out some commit, Git will—usually; we'll see an exception in a little while—copy all of the files from that commit into its index / staging-area, right then. So the commit and the index will match. And, Git will usually copy all of the files from the commit/index (which match) to the working tree, right then, too. So the commit, the index, and the working tree all match.

When this is the case—when everything matches—git status is usually pretty quiet:

$ git status
On branch main

nothing to commit, working tree clean

for instance.

If you change a file in the working tree and run git status, you get:

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   Makefile

for instance. If you then git add that file, this changes yet again:

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        modified:   Makefile

What's going on here is simple: git add told Git: Read the working tree copy of the file. Compress it down into Git's internal de-duplicated storage format to make it ready for committing. Check to see if that's a duplicate of some existing file; if so, throw out the compressing work you did and re-use the duplicate, and if not, you now have the prepared compressed file ready for committing, and in either case, update your index so the next commit will use this updated version of the file.

In other words, git add copies the file into the index. The index was ready to go into the next commit. It's still ready to go into the next commit. But now it has a different version of the added file.

If the fie you git add is all-new—if it wasn't in the index before—Git still de-duplicates the content. This means that if you store ten files in a commit, but they're all identical, Git only stores one copy of it, just under ten names. Then Git updates the name-and-content data in the index / staging-area / cache, so that the next commit will have this file. If the file was in the index before, Git kicks out the old copy to make room for the new copy. In either case, the index continues to be ready to commit. The index always holds your proposed next snapshot.

The way you make commits, then, is:

  • extract some existing commit to index and working tree;
  • work on files (in your working tree);
  • use git add to update the proposed snapshot in the index; and
  • run git commit.

When you run git commit, Git:

  • packages up the files from the index / staging-area: this is the new snapshot;
  • adds the necessary metadata, including the current commit hash ID as the parent of the new commit;
  • writes all of this to the Git database (which holds the commits and other objects, indexed by their hash IDs) so as to obtain the new commit hash ID.

Git then stuffs the new commit's hash ID into the branch name.

A picture of making new commits

What this means is that we can draw a Git "branch" like this:

... <-F <-G <-H   <-- your-branch (HEAD)

Here, the branch's name is your-branch. You are "on" this branch because you used git switch or git checkout with the name your-branch. The name holds the hash ID of commit H—H stands in for some big ugly hash ID like 30cc8d0f147546d4dd77bf497f4dec51e7265bd8 here. So H is the current commit, and it's also the last commit "on" the branch.

The files you're working on / with came out of commit H. They are now in your working tree, and in Git's index / staging-area.

Commit H has a parent commit, which we're calling G, that also has a snapshot and metadata, and that commit has a parent commit that we're calling F, which also has a snapshot and metadata, and so on. But we're using commit H.

You now modify some files and git add them to update Git's index, and run git commit. Git saves every file—including all the de-duplicated, unchanged duplicates from commit H, which take no space since they're de-duplicated—into a new snapshot and makes a new commit, which gets a new unique hash ID, but we'll just call it I. Commit I has commit H's hash ID as its parent, so that I points backwards to H:

...--F--G--H   <-- your-branch (HEAD)
            \
             I

and now, as the final step of git commit, Git stuffs I's hash ID into the name your-branch, so that this name points to I instead of H:

...--F--G--H
            \
             I   <-- your-branch (HEAD)

(and now there's no reason to draw the commits with a bend in the line, but I left it in to make it obvious that it's the branch name that moved).

Creating a new branch name

Suppose that before we make commit I, we'd like to put it on a different branch. This is where that name HEAD, in parentheses, comes in. If we want to make a new branch name, we can use git branch to do that:

...--F--G--H   <-- your-branch

We run:

git branch new-branch

and get:

...--F--G--H   <-- new-branch, your-branch

We now need a way to say which branch name you're using, and that "way" is to attach the special name HEAD to just one branch name:

...--F--G--H   <-- new-branch, your-branch (HEAD)

This means that the branch you're "on" is your-branch, not new-branch. Both names select commit H, though, and you can now:

git checkout new-branch    # or git switch new-branch

to get:

...--F--G--H   <-- new-branch (HEAD), your-branch

Remember that I said, above, that

When we first check out some commit, Git will—usually; we'll see an exception in a little while—copy all of the files from that commit into its index / staging-area, right then.

We just hit our first exception. Git is deliberately a little bit sneaky. If we switch from one branch name to another, Git uses the fact that commits hold de-duplicated files to avoid doing work. For each file that's a duplicate across the two commits, Git does nothing. When you're moving from commit H to commit H, all the files are, by definition, duplicates, so Git doesn't have to do anything at all—and it doesn't!

So, whether or not you have modified any files in the index and/or working tree since the git checkout that copied commit H's files into the index and working tree, your switch from H to H does absolutely nothing at all other than attach HEAD to the other branch name.

This means that you can, at any time, create a new branch name and switch to it, and the only thing that actually changes is that the new name is now your current branch name. Your current commit and all the files in all three copies are all undisturbed.

You can combine the create-new-name and the switch-to-name with git checkout -b or git switch -c, as you did; this makes no real difference except that Git won't create the branch name unless the switch will work.

Tracked files, or, one last thing we need to know about the index

I saved this for a separate section because it gets a little complicated, and quite crucial for some cases, here.

As we've already seen, Git's index always holds your proposed next commit—or rather, its snapshot; Git doesn't assemble the metadata until you actually run git commit. As such, the index holds copies (or de-duplicated "copies", really) of the files that will be in the next commit.

You will also, at times, see Untracked files in the output of git status. But what, precisely, is an untracked file? Fortunately the answer is ridiculously simple: An untracked file, in Git, is simply any file that is not currently in Git's index. Note that it does not matter whether the file is in any commit. It just has to not be in Git's index right now. (It also has to exist in your working tree, of course. Otherwise we could say that the file rumplestiltskin.straw is untracked in almost every repository, since it isn't in the index, but it isn't in the working tree either. We don't usually remark on things that don't exist.)

By contrast, then, a tracked file is a file that is in Git's index. It does not have to be in any commit yet, and it does not even have to be in your working tree. So tracked files are simply those files in the proposed next commit (or its snapshot). Untracked files are those not in the proposed next commit. You can use git add to make an untracked file tracked, and you can use git rm—with or without --cached—to remove a tracked file from Git's index. Using git rm without --cached removes it from both Git's index and your working tree, so that we don't think about it much any more, but git rm --cached removes it from Git's index without removing it from your working tree, making it untracked.

Remember of course that Git's index gets filled in at times, so that the set of tracked files changes. And of course, you run git add and/or git rm and this can also change the set of tracked files. So "tracked" is not a permanent thing: it's always based on what is in Git's index right now.

When we run git status, it often whines about various files being untracked. The purpose of this complaint is to remind us to git add the files. But sometimes we don't want to git add those files. This is what .gitignore is about: it only affects untracked files, and it stops the whining.

It also affects any en-masse git add we run, though. To make things much more convenient to use, Git lets us run git add . or git add --all, and Git scans the working tree at this point to find files to git add. Git will:

  • add changed files, so that the index copy gets updated, and
  • add new-to-Git files, so that new untracked files become tracked.
  • As an odd special case, git add can even remove some files, though I won't cover that case here.

But adding new-to-Git untracked files would be wrong if those files are supposed to be, and stay, untracked. So listing an untracked file in a .gitignore tells Git not only to shut up about it, but also to not-add it with one of these "add all the files" operations.

Listing a tracked file in .gitignore has no effect because such a file is already in Git's index. That isn't a key item in your question here, but it's important to know in general. The name .gitignore is a bit misleading: it should perhaps be .git-do-not-complain-about-these-files-if-they-are-untracked-and-when-they-are-untracked-do-not-add-them-with-en-masse-git-add-commands-either-because-they-are-supposed-to-stay-untracked, or something like that. But who wants to type that in as a file name? So .gitignore it is.

Simple use of git stash

(Note: Since Git version 2.13, git stash save is deprecated in favor of git stash push. Both do the same thing when used for the simple case I'll be describing here, but git stash push has options to do fancier things.)

What git stash does can be described simply—perhaps too simply, in some cases, but we won't worry about those here—as: make commits that are on no branch at all and then use git reset --hard. That's what you didd here, with:

PS C:\Users\Administrator\Desktop\projects\songs> git stash save "latest modification"
Saved working directory and index state On main: latest modification

Something went slightly wrong right after this point but for now I'm going to ignore that.

The git stash save operation here made those two commits—a stash itself consists of either two or three commits, but we don't need to worry about the three-commit variety here—and those two commits saved:

  • the state of the index (i.e., the same snapshot git commit would have made), and
  • the state of your working tree's tracked files: i.e., the files you'd get in a new commit if you ran git add -u (update tracked files) and git commit.

The metadata in these funky git stash commits is a little weird, though if you use git stash save with a message, that sets the commit subject for the commit that git stash list will show.

These two new commits are not put on any branch at all, which means git log won't normally show them. (They are on refs/stash so git log --all does show them. They look weird because one of the commits is technically a merge commit, even though it's not the result of using git merge and should not be treated as a merge commit. This is one of the reasons you must use git stash to deal with these special commits, and is a reason I recommend avoiding git stash, as only git stash can deal with them properly. That makes a lot of the usual Git toolbox less useful.)

Let's look now at exactly what you did

Your computer-output starts here:

PS C:\Users\Administrator\Desktop\projects\songs> git checkout feature
error: Your local changes to the following files would be overwritten by checkout:
        .gitignore
        app.py
        music.db
        static/css/styles.css
        templates/favorites.html
        templates/layout.html
Please commit your changes or stash them before you switch branches.

This means your git checkout didn't actually do anything. It simply reported which files in your working tree have un-committed work (possibly git add-ed, possibly not git add-ed, but not git commit-ed) that, if Git were to switch to whichever commit is the last commit on branch name feature, Git would have to remove from Git's index and your working tree.

Git didn't switch, so the files are all still intact (in both Git's index and your working tree).

PS C:\Users\Administrator\Desktop\projects\songs> git stash save "latest modification"
Saved working directory and index state On main: latest modification

Here, git stash made the two commits that saved the current index state and the tracked files' state. Git then ran git reset --hard HEAD to attempt to:

  • make the index match the current commit, and
  • make the working tree match the index (except for untracked files: those remain untouched).

But something went wrong:

Unlink of file 'music.db' failed. Should I try again? (y/n) y

fatal: Could not reset index file to revision 'HEAD'.

Probably (since you're on Windows) some program had file music.db open, which prevented its removal. It's not clear what else might have gone wrong in terms of resetting the index file, but this alone sufficed to make git reset --hard HEAD fail.

PS C:\Users\Administrator\Desktop\projects\songs> git checkout -b test-branch
Switched to a new branch 'test-branch'

This created a new branch name pointing to the same commit you were already on. The drawing I make here will be wrong, but I can't make the right one; only you can do that.

          I--J   <-- feature
         /
...--G--H   <-- main
         \
          K   <-- somebranch, test-branch (HEAD)
PS C:\Users\Administrator\Desktop\projects\songs> git st
 M music.db
?? static/scripts/downloader.js
?? test/

Presumably git st is an alias for git status --short, so this git status tells us that:

  • music.db is tracked, and the committed and index versions match, but the index and working tree versions differ;
  • static/scripts/downloader.js is untracked; and
  • there are multiple untracked files summarized by test/ (so, somewhere within that folder).
PS C:\Users\Administrator\Desktop\projects\songs> git add .

This does an en-masse add of all modified files, i.e., music.db, and all untracked files that got whined-about, i.e., static/scripts/downloader.js and the files—we don't know their names—in test/.

(Side note: while Windows uses a backwards slash, e.g., test\, to separate the top level folder name from additional names in that folder, Git itself doesn't exactly believe in folders in the first place, and always uses a forward slash. Git's index can only store files, not folders, and it stores them under names like static/scripts/downloader.js, with forward slashes. Git knows how to break that up into folder-and-file pieces and interact with the OS, which may demand backward slashes instead of forward ones, but internally Git always uses the forward ones. Since the index itself only holds file names, complete with forward slashes, Git is unable to save an empty directory in a commit: OS-level folder creation is always just implied here. If the index could hold directory entries here, Git would be able to store empty folders.)

The add, of course, updates Git's index. So the index now matches the working tree, except for truly-ignored files (files that are both untracked in the working tree and also listed in .gitignore).

No commit happens yet, so that's all that happens. Next we have:

PS C:\Users\Administrator\Desktop\projects\songs> git checkout main
Switched to branch 'main'
M       .gitignore
M       music.db
A       static/scripts/downloader.js
A       test/a.exe
A       test/ipvalidator.exe
A       test/nextvalidator.c
A       test/nextvalidator.exe
Your branch is up to date with 'origin/main'.

Here, Git made use of that exceptional case I mentioned yet again. We did, however, have to switch commits. Unlike the H-to-H case I described, Git did have to switch from one commit to another. The (wrong) graph I drew above says that we switched from commit K to commit H:

          I--J   <-- feature
         /
...--G--H   <-- main (HEAD)
         \
          K   <-- somebranch, test-branch

Let me augment this drawing now, to show the two commits that git stash made, because they'll matter soon. Here they are:

          I--J   <-- feature
         /
...--G--H   <-- main (HEAD)
         \
          K   <-- somebranch, test-branch
          |\
          i-w   <-- refs/stash

Remember, your attempt to git checkout feature had failed, leaving you on branch somebranch. That's where you were when you ran git stash. The two commits that Git made "hang off of" the then-current commit, which I've drawn as commit K. Commit i holds the index state and commit w holds the git add -u work-tree state. The git reset --hard then tried to reset all tracked files back to their somebranch state, but failed for music.db at least.

Now, however, we're on commit H as our current commit. The git switch command was able to not erase music.db while switching commits, because the de-duplicated copy of music.db in commit K matches the de-duplicated copy of music.db in commit H. So, even though that file is "modified" in the working tree—due to the failure of the git reset --hard—Git was able to keep the modification in place in the working tree.

Having successfully done the git checkout file-updating, git checkout now runs a kind of git status --short, minus some information. The goal of this git status --short is to show you what's different in your index and/or working tree right now, vs what's in the commit that would have been written to your index, but didn't have to be:

M       .gitignore
M       music.db

These two files differ. That is, git checkout would have replaced both .gitignore and music.db with the copies from commit H, but it didn't have to do that, so it skipped that. (Where are they different? Is it the index, the working tree, or both? Unlike git status --short, this doesn't say.)

A       static/scripts/downloader.js
A       test/a.exe
A       test/ipvalidator.exe
A       test/nextvalidator.c
A       test/nextvalidator.exe

These five files differ in that they are in your index and working tree right now but are not in commit H.

PS C:\Users\Administrator\Desktop\projects\songs> git branch -D "test-branch"
Deleted branch test-branch (was 08f1d8e).

This got rid of the name test-branch, which used to find commit K. Fortunately:

  • You have the abbreviated hash ID on your screen: it's 08f1d8e. You can use this to find the commit.
  • You also know that whatever branch you were on before—the one I'm calling somebranch in my drawings above—this same name also points to commit 08f1d8e, so you can just use that name. (It's probably not somebranch.)

Next, you ran:

PS C:\Users\Administrator\Desktop\projects\songs> git stash apply
error: Your local changes to the following files would be overwritten by merge:
        .gitignore
Please commit your changes or stash them before you merge.
Aborting

By whatever luck (good or bad), git stash apply decided that it was unable to apply the stash. It therefore did nothing at all. Note the last line, Aborting.

You then have a full git status output (which seems a bit odd without a git status command, but maybe git stash ran that—I generally avoid git stash and it has evolved a few times and I'm not sure which versions have done what, at this point). This gives us a lot of information:

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        modified:   music.db
        new file:   static/scripts/downloader.js
        new file:   test/a.exe
        new file:   test/ipvalidator.exe
        new file:   test/nextvalidator.c
        new file:   test/nextvalidator.exe

This is the list of files that differ between HEAD (commit H in my drawing) and Git's index, as if we'd run git diff --staged --name-status, more or less. We see that music.db in the index differs from music.db in commit H, and that the five files added earlier are still there in Git's index. So these are changes that git checkout main was able to preserve in Git's index.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   .gitignore

This is a list of files that differ between Git's index and your working tree, as if we'd run git diff --name-status. There's just one file, .gitignore, listed here. So this must be the edit you did to allow the static/ and test/ files to become tracked, which git checkout was able to leave undisturbed when it made commit H the current commit. It's not different in Git's index though: the git add somehow skipped this one. It probably got restored by the git reset --hard earlier (unlike music.db where something went wrong). So that implies that the .gitignore in commit K matches the .gitignore here—though some of this is guesswork. (It all depends on exactly what happened during the git reset --hard failure.)

Untracked files:
        __pycache__/
        flask_session/
        improvements.txt
        
        run.ps1
        static/downloads/
        templates/test.html
        test.py
        venv/

This lists files that are in the working tree, that aren't in Git's index. I think they would have been added earlier by git add . so they must not have existed while you were on commit K. That seems ... odd, and casts some doubt on my analysis above. Again, given the failure of git reset --hard, I'm just not sure about any of this.

Upvotes: 2

torek
torek

Reputation: 487795

Long: part 2 of 2: what you can do now

(see also part 1)

The short version of this is:

  • what you have available through Git itself, to restore files, is whatever's in commits, plus anything you can commit right now from Git's index;
  • what you have available on your computer in general includes any untracked files and any backups you made with backup software (e.g., Time Machine on macOS).

These are the places to go to get files back.

Your git stash show --stat shows a diff from the commit I've been labeling K to the one in w:

git stash show --stat
 .gitignore                 |   4 ++--
 app.py                     |  11 +++++++----
 music.db                   | Bin 69632 -> 86016 bytes
 static/css/musicPlayer.css |   1 +
 static/css/styles.css      |   4 +++-
 templates/favorites.html   |   2 ++
 templates/layout.html      |   8 ++++++--
 templates/play.html        |  27 +++++++++++++++++++--------

You also have, now, in your index, a version of music.db (probably matching that in the w commit here), and the Added files. You can git commit this or git stash again if you like, to make more commits. You still have your existing stash.

You can turn any stash into a branch with git stash branch. If there is valuable data in the existing stash, that's my general recommendation: once it's a normal everyday branch, you can use all of Git's tools with it.

If there's nothing valuable in Git's index and your working tree right now, you can use git reset --hard HEAD to reset both of those.

Since there was a problem with music.db, you should find out why: there's probably a program that had, or still has, it open, preventing you from replacing the file. If you can terminate that program, or convince it to close the file, you can work on / with it again.

So, do each of these things—e.g., make a commit out of what you have now, if necessary and appropriate, fix whatever kept you from working with music.db, and/or turn the existing stash into a branch with git stash branch. If there's nothing valuable in the stash, drop the stash.

Using git stash branch

Before you can use git stash branch, you need to be in a "clean" state—that is, one where git status does not talk about changes that are to-be-committed and not-staged-for-commit. (Or, one where your git st short-status alias does not show any M, A, D, etc., files; ?? untracked files are sometimes OK.)

Then you can run:

git stash branch new-branch-for-stash

What this does is:

  • check out the parent of the i and w commits (that's commit K in my drawings here); and
  • use git stash apply --index to restore both the index and working tree states.

This almost always works (the exceptions include cases like "some program holds music.db open so that we can't patch it"; note that music.db is different between commit K and w here). It leaves you in a state where you can run git commit, or git add and git commit, or git commit followed by git add and git commit. Choose whichever you would like, to commit the changes you'd stashed, and get back to a "clean" git status (except, perhaps, for untracked files).

Untracked files are the usual problem here, especially when .gitignore has changed over time, because some files may actually be in some commits, when they shouldn't really be in those commits, and if you successfully check out such a commit, that file will now be in Git's index. Git will now want to remove the file if and when you switch from that commit to another commit that lacks the file.

The solution is usually the obvious trivial one: rename the untracked files out of the way, or entirely out of the working tree (which is also out of the way). That way, you still have the files and they are still not in the index. By using fresh names for them, they won't be clobbered by any commits where they were accidentally (or on purpose but incorrectly) stored.

For any files that aren't in Git, you'll need to restore them using some outside-Git mechanism.

Upvotes: 2

Related Questions