So I had some unstaged changes and some staged ones. I issued
Welcome to Git (version 1.8.3-preview20130601)
$ git stash save --keep-index
Saved working directory and index state WIP on master: ab0d18d Setup of alarms f
or network service + Logging exceptions + long arithmetic
HEAD is now at ab0d18d Setup of alarms for network service + Logging exceptions
+ long arithmetic
$ git stash
Saved working directory and index state WIP on master: ab0d18d Setup of alarms f
or network service + Logging exceptions + long arithmetic
HEAD is now at ab0d18d Setup of alarms for network service + Logging exceptions
+ long arithmetic
Then I hit amend last commit in the gui to split the ab0d18d commit into smaller ones. I unstaged some of the files and I hit
$ git stash save --keep-index
Saved working directory and index state WIP on master: ab0d18d Setup of alarms f
or network service + Logging exceptions + long arithmetic
HEAD is now at ab0d18d Setup of alarms for network service + Logging exceptions
+ long arithmetic
Repeated the above procedure :
$ git stash save --keep-index
Saved working directory and index state WIP on master: ab0d18d Setup of alarms f
or network service + Logging exceptions + long arithmetic
HEAD is now at ab0d18d Setup of alarms for network service + Logging exceptions
+ long arithmetic
Then I edited the commit message and I committed. Then I issued git stash pop
to start getting back my stashes and committing them one by one.
$ git stash pop
# On branch master
# Untracked files:
# (use "git add <file>..." to include in what will be committed)
#
# TODO.txt
nothing added to commit but untracked files present (use "git add" to track)
Dropped refs/stash@{0} (43facd88ea3548071b196324523bd017d680d6dd)
Disaster !
I had backups happily (long live dropbox)
2 questions :
- what did I do wrong ?
- how should one recover from such a scenario ?
EDIT : gitk after I restored the changes (the amended commit is the SETUP ALARMS one)
EDIT
Found a way to reproduce the problem - will be amending it (probably only the last part is needed - was trying to reproduce my original scenario exactly) but for starters :
mkdir test_stash; cd test_stash;git init
echo f1 >> f1 ; echo f2 >> f2 ; echo f3 >> f3 ; echo f4 >> f4 ; echo f5 >> f5
git add f1 f2 f3 f4 f5
git commit -m base123
echo change f1 to be amended >> f1
echo change f2 to be amended >> f2
echo change f3 to be amended >> f3
git add .
git commit -m tobeamended123
echo change f4 >> f4; git add f4
echo change f5 >> f5
git stash save --keep-index
git stash
git gui &
Now hit amend commit in the gui. Not sure to which command it corresponds to but git commit --amend
does not do the trick.
While in the amend state unstage file f3 in the gui again (click on it so it moves on the unstaged area - would be git reset HEAD f3
but this does not work either) then
git stash save --keep-index
git commit -m amended # not really amended - a new commit altogether
git stash pop
Getting :
# # On branch master
nothing to commit, working directory clean
Dropped refs/stash@{0} (898687d73b65ccc9e10cd826bc12fda1a4759651)
Expecting : the f3 modifications to show up
/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.
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 apop
. It doesn't! Bothgit stash apply
andgit stash pop
mix together the separated index change by default. Adding--keep-index
does not change this. Only the--index
argument toapply
andpop
tries to avoid mixing them.The "work directory" that
git stash
saves amounts, in effect, to the change from the currentHEAD
. This means that if the index has a change fromHEAD
, 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 ingit 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 withgit stash branch
, it drops working directory state. And it's causing your problem here.)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 status
—pop
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.
来源:https://stackoverflow.com/questions/18662740/git-stash-while-amending-last-commit-in-gui-pop-pops-nothing