I have a pre-commit hook that runs a script on the whole code base as if the commit was completed. To accomplish this I first git stash -k -u
to stash my unstaged changes and untracked files. Then I run the check (which at this point only contains committed and staged code). Afterwards, I want to put the repo back in the same state it was before. This means the staged changes remain and the working tree is restored to as it was, including untracked files and unstaged changes.
I'm not sure exactly how to do the last part. First I tried git stash pop
, but this fails when files with staged changes also have unstaged changes. Next I tried git restore -s stash .
which fixes the previous problem, but doesn't restore the untracked changes. It's as if the .
in the command refers to the layout of the working tree rather than the layout of source tree. Is this a bug? Is there some other way to accomplish what I'm describing? I'm out of ideas.
CodePudding user response:
emphasized textI have a pre-commit hook that runs a script on the whole code base as if the commit [had been] completed. To accomplish this I first
git stash -k -u
to stash my unstaged changes and untracked files. Then I run the check (which at this point only contains committed and staged code). Afterwards, I want to put the repo back in the same state it was before.
The recommended method for this is:
git reset --hard
git stash pop --index
The pop
step here should always succeed, provided the git stash push
step did anything in the first place. That last "provided" means there's an important proviso: you should check to see whether git stash -k -u
actually made a stash, because under some conditions, it does nothing at all. In this case the git reset --hard
is unnecessary (but safe as long as your tests did not modify anything), and the git stash pop
must not be run since that would pop some other stash.
(How do you know whether git stash push
actually created a stash? I recommend a two-step git rev-parse
sequence, which works back into older version of git stash
where the push
verb doesn't even exist—where you have to use git stash save
instead—where you do:
old=$(git rev-parse -q --verify refs/stash || true)
git stash push -k -u
new=$(git rev-parse -q --verify refs/stash || true)
You can now test whether $current
and $new
match as strings:
test "$old" != "$new" && do_unstash=true || do_unstash=false
for instance. Now you can condition your final git stash pop
sequence with:
if $do_unstash; then git reset --hard && git stash pop --index; fi
If the push
step made a stash, the outputs of the two git rev-parse
commands differ: either $old
is empty and $new
is non-empty and is the hash ID of the stash, or $old
is non-empty and $new
is non-empty and they differ and $new
is again the hash ID of the stash. But if git stash push
decided to do nothing, then either $old
is empty and $new
is empty, or $old
contains the old top-of-stash-stack hash ID and $new
contains the same hash ID.
If you have the space for it, though, I'd suggest running the tests on a "clean" git checkout-index
run in an empty temporary directory, after which you can just remove the empty temporary directory. This does not require any fancy git stash
work, and hence functions correctly even when git stash
has been broken (as it has been in several recent Git versions where, apparently based on questions here on StackOverflow, sometimes git stash pop
silently fails to restore untracked files). This also neatly sidesteps all weird corner cases, such as intent-to-add index entries that never actually got added.
CodePudding user response:
Not sure this is a good answer, but there is git stash show --only-untracked-v
(no idea why the --only-untracked
switch even exists), which outputs the diff of only the untracked files in the stash. This diff can be piped into git apply -
to recreate the file.
In total...
git stash show --only-untracked -v | git apply -