Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Git stash removes added changes

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

  1. Why doesn't git stash them the way they were previously?
  2. How do I stash my changes in one go, so that on doing a stash pop, I get the older state, and not the newer one?

Currently, I manually do

git stash --keep-index
git stash
git stash pop
git add <stashed_files>
git stash pop

Problem with this is

  1. It takes 5 steps to stash & pop and I am looking for 2
  2. I sometimes may not remember there were 2 back to back stashes, which slightly complicates the task.

Edit - I would prefer a command line solution, since I work in a similar fashion on test servers.

like image 320
Anshul Goyal Avatar asked Oct 10 '13 04:10

Anshul Goyal


2 Answers

use --index option.

git stash
git stash pop --index
like image 31
ton Avatar answered Oct 09 '22 07:10

ton


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:

  • the current index
  • with -u or -a, untracked and even ignored files (and it also uses git clean to ditch them from the working directory)
  • the working directory, based on a delta between the current working directory and the 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:

  • the HEAD commit, as stash^ (first parent)
  • the index commit, as stash^2 (second parent)
  • the untracked/ignored commit, as 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. :-) )

like image 183
torek Avatar answered Oct 09 '22 09:10

torek