I'm trying to do a pre-commit hook with a bare run of unit tests and I want to make sure my working directory is clean. Compiling takes a long time so I want to take advantage of reusing compiled binaries whenever possible. My script follows examples I've seen online:
# Stash changes
git stash -q --keep-index
# Run tests
...
# Restore changes
git stash pop -q
This causes problems though. Here's the repro:
- Add
// Step 1
toa.java
git add .
- Add
// Step 2
toa.java
git commit
git stash -q --keep-index
# Stash changes- Run tests
git stash pop -q
# Restore changes
At this point I hit the problem. The git stash pop -q
apparently has a conflict and in a.java
I have
// Step 1
<<<<<<< Updated upstream
=======
// Step 2
>>>>>>> Stashed changes
Is there a way to get this to pop cleanly?
There is—but let's get there in a slightly roundabout fashion. (Also, see warning below: there's a bug in the stash code which I thought was very rare, but apparently more people are running into. New warning, added in Dec 2021:
git stash
has been rewritten in C and has a whole new crop of bugs. I used to suggest mildly thatgit stash
be avoided; now I urge everyone to avoid it if at all possible.)git stash push
(the default action forgit stash
; note that this was spelledgit stash save
in 2015, when I wrote the first version of this answer) makes a commit that has at least two parents (see this answer to a more basic question about stashes). Thestash
commit is the work-tree state, and the second parent commitstash^2
is the index-state at the time of the stash.After the stash is made (and assuming no
-p
option), the script—git stash
is a shell script—usesgit reset --hard
to clean out the changes.When you use
--keep-index
, the script does not change the saved stash in any way. Instead, after thegit reset --hard
operation, the script uses an extragit read-tree --reset -u
to wipe out the work-directory changes, replacing them with the "index" part of the stash.In other words, it's almost like doing:
except that
git reset
would also move the branch—not at all what you want, hence theread-tree
method instead.This is where your code comes back in. You now
# Run tests
on the contents of the index commit.Assuming all goes well, I presume you want to get the index back into the state it had when you did the
git stash
, and get the work-tree back into its state as well.With
git stash apply
orgit stash pop
, the way to do that is to use--index
(not--keep-index
, that's just for stash-creation time, to tell the stash script "whack on the work directory").Just using
--index
will still fail though, because--keep-index
re-applied the index changes to the work directory. So you must first get rid of all of those changes ... and to do that, you simply need to (re)rungit reset --hard
, just like the stash script itself did earlier. (Probably you also want-q
.)So, this gives as the last
# Restore changes
step:(I'd separate them out as:
myself, just for clarity, but the
pop
will do the same thing).As noted in a comment below, the final
git stash pop --index -q
complains a bit (or, worse, restores an old stash) if the initialgit stash push
step finds no changes to save. You should therefore protect the "restore" step with a test to see if the "save" step actually stashed anything.The initial
git stash --keep-index -q
simply exits quietly (with status 0) when it does nothing, so we need to handle two cases: no stash exists either before or after the save; and, some stash existed before the save, and the save did nothing so the old existing stash is still the top of the stash stack.I think the simplest method is to use
git rev-parse
to find out whatrefs/stash
names, if anything. So we should have the script read something more like this:warning: small bug in git stash
(Note: I believe this bug was fixed in the conversion to C. Instead, there are numerous other bugs now. They will no doubt eventually be fixed, but depending on which version of Git you are using,
git stash
may have various bugs of varying seriousness.)There's a minor bug in the way
git stash
writes its "stash bag". The index-state stash is correct, but suppose you do something like this:When you run
git stash push
after this, the index-commit (refs/stash^2
) has the inserted text infoo.txt
. The work-tree commit (refs/stash
) should have the version offoo.txt
without the extra inserted stuff. If you look at it, though, you'll see it has the wrong (index-modified) version.The script above uses
--keep-index
to get the working tree set up as the index was, which is all perfectly fine and does the right thing for running the tests. After running the tests, it usesgit reset --hard
to go back to theHEAD
commit state (which is still perfectly fine) ... and then it usesgit stash apply --index
to restore the index (which works) and the work directory.This is where it goes wrong. The index is (correctly) restored from the stash index commit, but the work-directory is restored from the stash work-directory commit. This work-directory commit has the version of
foo.txt
that's in the index. In other words, that last step—cp /tmp/save foo.txt
—that undid the change, has been un-un-done!(The bug in the
stash
script occurs because the script compares the work-tree state against theHEAD
commit in order to compute the set of files to record in the special temporary index before making the special work-dir commit part of the stash-bag. Sincefoo.txt
is unchanged with respect toHEAD
, it fails togit add
it to the special temporary index. The special work-tree commit is then made with the index-commit's version offoo.txt
. The fix is very simple but no one has put it into official git [yet?].Not that I want to encourage people to modify their versions of git, but here's the fix.)