Like you, I run
git stash --keep-index --include-untracked
I can then run tests and so on.
The next part is tricky. These are some things I tried:
git stash pop can fail with conflicts, which is unacceptable. git stash pop --index can fail with conflicts, which is unacceptable. git checkout stash -- . applies all tracked changes (good), but also stages them (unacceptable), and does not restore untracked files from the stash (unacceptable). The stash remains (fine -- I can git stash drop). git merge --squash --strategy-option=theirs stash can fail with conflicts, which is unacceptable, and even when it doesn't conflict it does not restore untracked files from the stash (unacceptable). git stash && git stash pop stash@{1} && git stash pop (trying to apply the changesets in reverse order) can fail with conflicts, which is unacceptable.
But I found a set of commands which does what we want:
# Stash what we actually want to commit git stash # Unstash the original dirty tree including any untracked files git stash pop stash@{1} # Replace the current index with that from the stash which contains only what we want to commit git read-tree stash # Drop the temporary stash of what we want to commit (we have it all in working tree now) git stash drop
For less output, and condensed into one line:
git stash --quiet && git stash pop --quiet stash@{1} && git read-tree stash && git stash drop --quiet
As far as I'm aware, the only thing this doesn't restore is files which were added in the index and then deleted from the working tree (they'll end up added and present) and files which were renamed in the index and then deleted from the working tree (same outcome). For this reason we need to look for files which match these two cases with a line like git status -z | egrep -z '^[AR]D' | cut -z -c 4- | tr '\0' '\n' before the initial stash, and then loop through and delete them after restoring.
Obviously you should only be running the initial git stash --keep-index --include-untracked if the working tree has any untracked files or unstaged changes. To check for that you can use the test git status --porcelain | egrep --silent '^(\?\?|.[DM])' in your script.
I believe this is better than the existing answers -- it doesn't need any intermediate variables (other than whether the tree was dirty or not, and a record of which files need to be deleted after restoring the stash), has fewer commands and doesn't require garbage collection to be switched off for safety. There are intermediate stashes, but I'd argue this this exactly the kind of thing they're for.
Here's my current pre-commit hook, which does everything mentioned:
#!/bin/sh # Do we need to tidy up the working tree before tests? # A --quiet option here doesn't actually suppress the output, hence redirection. git commit --dry-run >/dev/null ret=$? if [ $ret -ne 0 ]; then # Nothing to commit, perhaps. Bail with success. exit 0 elif git status --porcelain | egrep --silent '^(\?\?|.[DM])'; then # There are unstaged changes or untracked files dirty=true # Remember files which were added or renamed and then deleted, since the # stash and read-tree won't restore these # # We're using -z here to get around the difficulty of parsing # - renames (-> appears in the string) # - files with spaces or doublequotes (which are doublequoted, but not when # untracked for unknown reasons) # We're not trying to store the string with NULs in it in a variable, # because you can't do that in a shell script. todelete="$(git status -z | egrep -z '^[AR]D' | cut -z -c 4- | tr '\0' '\n')" else dirty=false fi if $dirty; then # Tidy up the working tree git stash --quiet --keep-index --include-untracked ret=$? # Abort if this failed if [ $ret -ne 0 ]; then exit $ret fi fi # Run tests, remember outcome make precommit ret=$? if $dirty; then # Restore the working tree and index git stash --quiet && git stash pop --quiet stash@{1} && git read-tree stash && git stash drop --quiet restore_ret=$? # Delete any files which had unstaged deletions if [ -n "$todelete" ]; then echo "$todelete" | while read file; do rm "$file" done # Abort if this failed if [ $restore_ret -ne 0 ]; then exit $restore_ret fi fi fi # Exit with the exit status of the tests exit $ret
Any improvements welcome.
git stash --include-untracked --keep-indexdoesn't actually do anything andgit stash popis popping off some other previously stashed patch? The problem is thatgit stashwill happily exit with a status of 0, even if nothing had been stashed. You need to look out for the "No local changes to save" message coming fromgit stash. It's a bit fragile, but I think it's the best you can do short of getting a change introduced to Git to makegit stashbehave a little differently.git stash --keep-indexstill stashes the index even if it doesn't remove the changes from the working copy (going by similar questions). Although the problem with git stash not reporting that the stash wasn't created (or the fact that it doesn't have an option to force a stash to be created even if empty) is another annoyance.