During development, I routinely add working versions of files (but not commit them) to my git repo. I continue to work on those files, till they reach commitable stage, when I can commit them. So the repo looks like below
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# modified: testfile1
#
# 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: testfile1
# modified: testfile2
When I do a git stash
, and then do a git stash pop
, I get
# 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: testfile1
# modified: testfile2
Questions
Currently, I manually do
git stash --keep-index
git stash
git stash pop
git add <stashed_files>
git stash pop
Problem with this is
Edit - I would prefer a command line solution, since I work in a similar fashion on test servers.
use --index option.
git stash
git stash pop --index
I see this is already answered but let me add a bit more, and a warning: there's a buglet in git stash
.
When you run git stash
or git stash save
(the default is save
so these are the same thing) without using -p
, the stash
script—it lives in the git-core
directory, whose location varies depending on the git installation, it may be in /usr/libexec/git-core
or /usr/local/libexec/git-core
for instance—creates a commit with two (or sometimes three) parent commits. In order, these commits are:
-u
or -a
, untracked and even ignored files (and it also uses git clean
to ditch them from the working directory)HEAD
commit (this is the source of the buglet; see below).It then sets up refs/stash
to point to the last of these commits, the working-directory commit. This commit has as its parents:
stash^
(first parent)stash^2
(second parent)stash^3
(third parent), if it exists.This stash does in fact contain everything that was in place at the time of the stash, but the buglet shows up best when you use git stash pop --index
or git stash apply --index
to recover the "pre-stash state". (I'm going to use git stash apply
exclusively below, but pop
is just apply
followed by drop
.)
Now, if you just run git stash apply
, as you noted, it gives you a lot of changes not staged for commit
files even though you had carefully staged some things before you ran git stash save
. This is because it's a lot easier to merge together these changes like this, regardless of work directory state, including if you've checked out a different branch or whatever, and including if you stage some files before running git stash apply
. (In fact, git stash apply
uses git's merging code to bring in the work-directory changes.)
If you run git stash apply --index
, though, the stash
script first tries to add to the index exactly what you had in it at the time of the original save
. (If nothing is staged yet, this recovers your original state.) Assuming it can do that, it then tries to set up the working directory similarly (again using the merge mechanisms). If it can't set up the index properly, it does nothing to the index, and suggests you retry without --index
.
Here's where the buglet comes in. Suppose you start with a file, say basefile
, with no changes. You make a change and stage this:
$ cat basefile
base
$ git status --short
$ echo add to basefile >> basefile; git add basefile
but then you decide you want the working-directory copy to have no change from the HEAD
revision:
$ ed basefile
21
2d
w
5
q
$ git status --short
MM basefile
The tricky bit here is that basefile
is modified in the index, and then modified again in the work-dir, but the second change takes it back to what's in the HEAD
commit. When you run git stash save
, the stash script accidentally records the index version as if it were the work-in-progress version.
If you now do a git stash apply --index
and run git status --short
:
$ git stash save
Saved working directory and index state WIP on master: 94824e1 initial
HEAD is now at 94824e1 initial
stash created
$ git stash apply --index
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# modified: basefile
#
$ git status --short
M basefile
Here git has restored the index version into the index, and then set the working-directory version to the same thing that was in the index version:
$ cat basefile
base
add to basefile
The fix for the stash script is a one-word change but so far no one seems to like it. Perhaps the problem is that if you apply the stash without --index
, this effectively combines the index change (the extra line, add to basefile
) with nothing, so that the work-directory version has the extra line. However, that's not consistent with the way it behaves when both index and working-directory versions are different:
$ git stash drop
Dropped refs/stash@{0} (61c83c866bc522c58df62320b77e647ffd28aa95)
$ echo base > basefile
$ git status --short
$ echo add to basefile >> basefile
$ git add basefile
$ ed basefile
21
2c
different change
w
22
q
$ git status --short
MM basefile
$ git stash save
Saved working directory and index state WIP on master: 94824e1 initial
HEAD is now at 94824e1 initial
$ git stash apply
# 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: basefile
#
no changes added to commit (use "git add" and/or "git commit -a")
$ cat basefile
base
different change
Here, by "mushing together" the index and work tree changes, but applying without --index
, we recover only the work-tree changes.
(Fortunately since we're using apply
instead of pop
we can change our minds now:
$ git reset --hard HEAD
HEAD is now at 94824e1 initial
$ git stash apply -q --index
$ git status --short
MM basefile
and if we look at the index and work-dir versions, we can now see both versions of basefile
.)
(The one-word fix to the stash script is to change HEAD
to $i_tree
in the line reading:
git diff --name-only -z HEAD -- >"$TMP-stagenames" &&
around line 118. I posted this to the git mailing list and got ... crickets. :-) )
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With