git stash while amending last commit (in gui) - pop pops nothing

不问归期 提交于 2019-12-07 08:48:30

/I'm keeping the first long reply below (might try to move it to a new question later), but now that there's a "reproduce example" I'll go through that. Let me make a list of points here though.

  1. git stash always stashes both the index and the work dir. One might think --keep-index makes it stash more, or changes the way the stashed value is handled on a pop. It doesn't! Both git stash apply and git stash pop mix together the separated index change by default. Adding --keep-index does not change this. Only the --index argument to apply and pop tries to avoid mixing them.

  2. The "work directory" that git stash saves amounts, in effect, to the change from the current HEAD. This means that if the index has a change from HEAD, but the current work directory does not, there's really no change saved in the "WIP on branch..." commit. (This is, I think, a bug in git stash. I have sent a test case and possible fix to the git mailing list. For "normal" cases it's fine, but if you've split out some parts and then want to recover your exact state later with git stash branch, it drops working directory state. And it's causing your problem here.)

  3. Applying a stash tries to make changes to the current state that mirror the changes in the stashed state. This can be complicated, because the current state is not necessarily anything like it was when you saved the stash.

Here's what git-gui is doing. At the time you fire it up you have this (actual commit numbers will of course vary). The unlabeled "WIP on master" is the "first" stash, now stash@{1}.

$ git stash list
stash@{0}: WIP on master: c93c8fe tobeamended123
stash@{1}: WIP on master: c93c8fe tobeamended123
$ git log --decorate --oneline --graph --all 'stash@{1}'
*   3d01942 (refs/stash) WIP on master: c93c8fe tobeamended123
|\  
| * 6be9135 index on master: c93c8fe tobeamended123
|/  
| *   de8038c WIP on master: c93c8fe tobeamended123
| |\  
|/ /  
| * 3db6cfc index on master: c93c8fe tobeamended123
|/  
* c93c8fe (HEAD, master) tobeamended123
* 828d5cf base123

Now in git gui, when you select "amend last commit", it finds the ref for the HEAD commit (c93c8fe, in my case). It does not actually do anything to it (yet). But as soon as you click on f3 to unstage it, it does something: it grabs the previous version of f3 (I'm not sure what the gui uses underneath, my guess would be HEAD^'s copy) and stuffs it into the index. If you examine f3 it still has the extra line in it, but if you git show :0:f3 to see the version in the index, it no longer has that line.

Note that no refs have changed due to gui-mouse-clicks, and there are no new commits. All the action has taken place inside the index.

Next, you went back to the command line and ran:

$ git stash save --keep-index

This made a third pair of commits, one with the index and one with the current directory. The index version has the extra line in f1 and f2 and lacks the extra line in f3. The current-directory version should (one would think) have the extra line in all three files—but, alas, it does not, because git stash save compares current dir vs HEAD commit, and the extra line is there in the HEAD commit, so it's not in the stashed version.

Unforunately, you used that --keep-index argument, so now the working directory version is the same as the stashed index version. File f3 no longer has the extra line.

From here on, the problem persists (the change is gone, --keep-index tossed it). You can of course recover it from the original commit ("tobeamended123"). But that's where things went wrong in this case: the command-line stash saved the index, and then compared the work directory against HEAD, which had not changed, so did not save the (non-change) to f3.


I don't see disaster, but I see something confusing, which I bet confused you. I don't know why you used --keep-index above. (In fact, I'm not sure what use-case --keep-index might be intended for1, and it seems to me that apply and pop should probably default to --index, but that's another matter entirely....) And, you made four total stash "pushes", and only "popped" one, leaving three to go.

[1I found the intended use-case, right there in the documentation: for testing what is currently in the index, before committing it. But wait, huhwha?, --keep-index does commit it, on the stash ref. You might as well just commit anyway, using git checkout -b test-stash to keep it safely segregated until you're happy with it. If you test it and it fails and you need to modify it, that stash is going to have conflicts. If you test and it works you can just pull / fast-forward-merge the commit that worked, into your earlier branch.]

The "tl;dr" short answer

Run git stash list. You'll see a list of:

stash@{0}: WIP on master: ab0d18d Setup of alarms ...
stash@{1}: WIP on master: ...

items. Use git stash apply --index 'stash@{n}' (the --index is optional) to try to apply each saved stash by name-and-number, without popping any of them. It's a stack, with stash@{0} the most recently pushed and (by this point) stash@{3} the first (longest-ago) pushed.

The apply-without-pop means you can git reset --hard to get back to master and ready to git stash apply a different stash. (Be sure you start the whole sequence with a clean work directory, perhaps by adding another git stash, although that could get confusing again. :-) )

If you've made a particularly big mess, you can use use git stash branch name 'stash@{n}'. This is a big, fast, effective hammer whose main drawback is that you have to invent a branch name. (You can git stash show the stashes to see what's in them, to help you come up with names.) Don't let this scare you, as you can always rename the branch or even delete it later. See the long description for exactly how this works.

When you're all done with all your stashes, use git stash clear to wipe them all out.

Regarding git commit --amend vs git stash

These are actually somewhat independent. The commit --amend works on a commit-chain based on whatever branch you're on. Let's say you're on master and the chain looks like this (in git log --graph --oneline --decorate, or gitk):

* 67dec43 (HEAD, master) "amendme" commit
* 9c37840 previous commit

You edit and git add some things—I will change file f3 and add it—and then run git commit --amend. This takes the index and makes a new commit, but the new commit's parent is one back from where master was, i.e., the previous commit above. Now the log output looks like this:

* 68c51f3 (HEAD, master) replacement for "amendme" commit
* 9c37840 previous commit

What you can't see (because there's no branch label on it) is that 67dec43 is still in there (until it expires and gets garbage collected), but if you tell git log to look there it will:

$ git log --graph --decorate --oneline master 67dec43
* 68c51f3 (HEAD, master) replacement for "amendme" commit
| * 67dec43 "amendme" commit
|/  
* 9c37840 previous commit

You have a branch coming off "previous commit", with the master label at the new replacement commit and the "amendme" commit on an unlabeled branch.

Let's do this again, with a stash in place this time. I start with a "known bad" file in f3 in the "amendme" commit. I then put in a second (but still not right) f3 and run git stash. Finally, I fix f3 "for real" and use --amend. The stash keeps a reference to the now-unlabeled branch, because a stash is a new commit (really, two). Here are the last few steps:

$ git log --graph --decorate --oneline
*   3c97241 (refs/stash) WIP on master: 67dec43 "amendme" commit
|\  
| * f3a50e9 index on master: 67dec43 "amendme" commit
|/  
* 67dec43 (HEAD, master) "amendme" commit
* 9c37840 previous commit
* 84408ef base
$ echo 'better changes for f3' > f3
$ git add f3
$ git commit --amend -m 'replacement for "amendme" commit'
$ git log --graph --decorate --oneline --all
* c1f1042 (HEAD, master) replacement for "amendme" commit
| *   3c97241 (refs/stash) WIP on master: 67dec43 "amendme" commit
| |\  
| | * f3a50e9 index on master: 67dec43 "amendme" commit
| |/  
| * 67dec43 "amendme" commit
|/  
* 9c37840 previous commit
* 84408ef base

If you try to apply the stash, there will be a conflict (because the stash changes file f3, with my intermediate, "not completely bad, but not better either" version):

$ git stash apply
git stash apply 
Auto-merging f3
CONFLICT (content): Merge conflict in f3
$ git reset --hard master
HEAD is now at c1f1042 replacement for "amendme" commit
$ git stash apply --index
Auto-merging f3
CONFLICT (content): Merge conflict in f3
Index was not unstashed.
$ git reset --hard master
HEAD is now at c1f1042 replacement for "amendme" commit

These are the same as any other conflict when bringing commits in, such as cherry-pick or merge, and you resolve them the same way.

If you like, you can stick a branch or tag label on the "amendme" commit:

$ git branch master-old 67dec43
$ git log --graph --oneline --decorate --all
* c1f1042 (HEAD, master) replacement for "amendme" commit
| *   3c97241 (refs/stash) WIP on master: 67dec43 "amendme" commit
| |\  
| | * f3a50e9 index on master: 67dec43 "amendme" commit
| |/  
| * 67dec43 (master-old) "amendme" commit
|/  
* 9c37840 previous commit
* 84408ef base

and now it's easily available for reference. You can then check it out and git stash pop --index that particular stash; this is guaranteed to work (hence the pop is safe, although you might want to apply anyway until you've done several of these). See also "Using git stash branch" below, which automates this.

How stash works, the long version

Let's step back a bit. I want to show a simplified example, with just three files.

Let's make a temp dir and git repo and commit a starting point, with three one-line-long files:

$ mkdir /tmp/tt; cd /tmp/tt; git init
... # create files f1, f2, f3; git add ...
$ git commit -m base
[master 84408ef] base
 3 files changed, 3 insertions(+)
 create mode 100644 f1
 create mode 100644 f2
 create mode 100644 f3
$ ls
f1      f2      f3
$ cat f1 f2 f3
this file stays the same
this file changes in the index
this file changes in the WIP

Now, let's make the changes happen:

$ echo more for f2 >> f2; git add f2
$ echo more for f3 >> f3

At this point, f2 is changed and staged:

$ git diff --cached
diff --git a/f2 b/f2
index 78991d3..3a2f199 100644
--- a/f2
+++ b/f2
@@ -1 +1,2 @@
 this file changes in the index
+more for f2

and f3 is changed but not staged:

$ git diff
diff --git a/f3 b/f3
index d5943ba..188fe9b 100644
--- a/f3
+++ b/f3
@@ -1 +1,2 @@
 this file changes in the WIP
+more for f3

Here diff --cached shows the staged stuff (in the index) and diff without --cached shows the unstaged stuff.

Now, let's git stash (the default op is to save). The stash will add two commits to the repo. The first one is just the stuff staged so far (if there's nothing staged, stash forces in a no-changes commit) and the second is a merge commit, of that-plus-work-dir. So:

$ git stash
Saved working directory and index state WIP on master: 84408ef base
HEAD is now at 84408ef base
$ git log --graph --oneline --decorate --all
*   753a6c8 (refs/stash) WIP on master: 84408ef base
|\  
| * 36b23f2 index on master: 84408ef base
|/  
* 84408ef (HEAD, master) base

That first one, index on master, has my change to f2:

$ git show 36b23f2
[snip]
diff --git a/f2 b/f2
index 78991d3..3a2f199 100644
--- a/f2
+++ b/f2
@@ -1 +1,2 @@
 this file changes in the index
+more for f2

The second has both changes (f2 and f3), but is a merge commit, so git show shows a combined diff, only showing f3:

$ git show 753a6c8
[snip]
diff --cc f3
index d5943ba,d5943ba..188fe9b
--- a/f3
+++ b/f3
@@@ -1,1 -1,1 +1,2 @@@
  this file changes in the WIP
++more for f3

(Aside: if you want to compare any merge M against each parent, use git show -m M. For instance, git show -m 753a6c8 first diffs 753a6c8^1-vs-753a6c8, then 753a6c8^2-vs-753a6c8.)

What's with this --keep-index thing?

Normally, after you do a git stash, you have a clean directory so there's nothing to "re-stash", as it were:

$ git status
# On branch master
nothing to commit, working directory clean
$ git stash
No local changes to save

But you asked stash to --keep-index. That still makes the usual stash entry, but then it extracts the contents of the index commit, putting that into both the working directory and the index. Let's pop off the current stash, look at the state (git statuspop does the status automatically—and then git log --graph --oneline --decorate --all), and see that we're back to the work in progress state but there's nothing staged this time:

$ git stash pop
...
$ git log --graph --oneline --decorate --all
* 84408ef (HEAD, master) base

Now let's re-stage f2 and re-do the stash save, but this time, with --keep-index:

$ git add f2
$ git stash save --keep-index
Saved working directory and index state WIP on master: 84408ef base
HEAD is now at 84408ef base

Looks the same as before ... but not quite:

$ git status
# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#   modified:   f2
#

Use git log --graph --oneline --decorate --all and you'll see basically the same thing (with different commit hashes) as before: stash committed the index, then committed a merge-commit of the work tree. But this time it also re-extracted the index, so now you have "changes to be committed". This is just f2, not f3.

This means you can (somewhat pointlessly) git stash save again. And you did! Let's use that log-graph-one-line-decorate thing (I use it a lot), both before and after:

$ git log --graph --oneline --decorate --all
*   7efe9a6 (refs/stash) WIP on master: 84408ef base
|\  
| * 76c840e index on master: 84408ef base
|/  
* 84408ef (HEAD, master) base
$ git stash save
Saved working directory and index state WIP on master: 84408ef base
HEAD is now at 84408ef base
$ git log --graph --oneline --decorate --all
$ git lola
*   eb383e0 (refs/stash) WIP on master: 84408ef base
|\  
| * aba15e6 index on master: 84408ef base
|/  
* 84408ef (HEAD, master) base

Looks the same before-and-after, at first blush. But look closely at the SHA-1 IDs. They changed! Before the second git stash save, refs/stash named commit 7efe9a6. Now it names eb383e0!

The reflog (or, pay attention, it's getting complicated)

OK, it's not that bad, but now you have to learn about the "reflog". Where did the other stash go? The answer is, it's been "pushed" and has disappeared into the reflog. There's a minor extra wrinkle, too: the "stash" is not a regular branch or tag. Instead, it's in refs/stash. So here's one way to see it, using git log -g, which means "look at reflogs":

$ git log -g --oneline refs/stash
eb383e0 refs/stash@{0}: WIP on master: 84408ef base
7efe9a6 refs/stash@{1}: WIP on master: 84408ef base

Aha, there they are, both 7efe9a6 and eb383e0. They have "user form full names" (refs/stash@{1} for instance) that are a bit of a pain to use. Fortunately stash works (unless you name a branch stash) to get the "top-most" {0} one, and you can write stash@{1} for the other. Or we can go for full-blown automation:

$ git log -g --pretty=format:%H refs/stash

This dumps out their full hashes, which we can use as arguments to git log --graph --decorate, to get this:

$ git log --graph --oneline --decorate $(git log -g --pretty=format:%H refs/stash)
*   eb383e0 (refs/stash) WIP on master: 84408ef base
|\  
| * aba15e6 index on master: 84408ef base
|/  
| *   7efe9a6 WIP on master: 84408ef base
| |\  
|/ /  
| * 76c840e index on master: 84408ef base
|/  
* 84408ef (HEAD, master) base

That's all just to see what's still "in there", in the repo.

(Or, of course, you can use gitk, as you did, to see them. gitk is smart enough to look for the stash reflogs.)

(Aside: you can also use git reflog show refs/stash. The reflog show sub-command just runs git log -g --oneline.)

Back to our problem, again

Now that we've done a first git stash save --keep-index and then a pointless git stash save, now what?

Well, we can git stash pop to get the most recent (top-most of stack) stash back:

$ git stash pop
# On branch master
# Changes not staged for commit:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#   modified:   f2
#
no changes added to commit (use "git add" and/or "git commit -a")
Dropped refs/stash@{0} (eb383e050d150a8ce5b69a3662849ffdd7070c89)

What happened to f3? As we noted earlier, the second git stash save saved only the "kept index", i.e., just the changed f2. What we need is to get back to the first stash.

$ git stash pop
error: Your local changes to the following files would be overwritten by merge:
    f2
Please, commit your changes or stash them before you can merge.
Aborting

That's not much help, is it? :-)

If you're not sure what you're doing, now is a good time to make a "save stuff" branch (you can always delete it later). Just git checkout -b help-me-spock or whatever, add, and commit. This stuff is now on a branch and easier to keep track of. But we know what we are doing, and that we have f2 in the other stash. So we can just wipe this out:

$ git reset --hard

Now we're back to the state we would have had, if we had done just one git stash save, without --keep-index: we're on master, with the working directory clean, and a single stash saved. We can git stash list it, git stash show it, and so on. So now:

$ git stash pop --index
# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#   modified:   f2
#
# Changes not staged for commit:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#   modified:   f3
#
Dropped refs/stash@{0} (7efe9a65c44156921bbbcb6a3df4edc5cb44492b)

and we have everything back. (Or, without --index, stash will just apply all the changes to the working directory, rather than restoring index and work-dir.)

Using git stash apply

The nice thing about git stash pop is that it applies and then drops the top-most stash entry. The annoying thing is, it applies and then drops the entry. If you use git stash apply instead, it hangs on to it.

Among other things, that's quite handy if you misspell --index as --keep-index (I did more than once, while typing this), or leave it out and later decide it would have been nice to use it. You can git reset --hard and re-do the apply.

If you're done with a stash entry, git stash drop entry will remove it from the reflog. For instance, suppose you do git stash apply --index 'stash@{1}' and then decide it's all good and want to add and/or commit it and then forget about that stash. You can then git stash drop 'stash@{1}'. The drawback is that this renumbers the rest: what was stash@{2} becomes stash@{1}, and so on. I find it's sometimes easier to keep them all around and use git stash clear to get rid of all of them at once, at the end.

Wait a minute, what's with these --index-es?

By default, git stash apply and git stash pop take the saved index ("changes staged for commit") and work-in-progress ("changes not staged for commit") and put them both into effect as work-in-progress only. Often that's fine, but if you've carefully staged some bits and left others unstaged, you might well want all that back. The --index argument to apply (and pop) tries to do that. Sometimes it turns out to be "too hard". In that case, you have two options: leave out --index, or use git stash branch.

Using git stash branch

I mentioned above, in the section on amended commits vs stashes, that you can add a new branch label to a commit that has a stash on it, and then apply or even pop the corresponding stash, with --index, and it will always work. The reason is simple: the stash is a merge commit of the index and WIP, corresponding to the commit they're on. If you check that commit out (as a "detached HEAD"), the index and WIP will apply cleanly.

So, suppose you add a new branch name at the commit in question, and get on the new branch (git checkout -b newname). Now apply (and pop-off) the stash, using --index: you're now in exactly the same state you were when you first ran git stash save, except that the branch has a different name. And that's what git stash branch does: you give it a new branch name and tell it which stash to use (the default is refs/stash, A.K.A. stash@{0}). It uses that stash entry to find the parent commit, attaches the branch name there, and then does a git stash pop --index.

At this point you can use git status, git diff --cached, git diff, etc., to see what's in the index and what's not, decide what else if anything to add, then git commit to add new stuff to the new branch you've created.

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!