Home > Net >  How can I create a (temporary) clean working tree for running unit tests in a git pre-commit hook?
How can I create a (temporary) clean working tree for running unit tests in a git pre-commit hook?

Time:08-01

If I forget to stage a modification/a file that is neccesary for a unit test, I want that that unit test to fail (when running inside of a pre-commit hook). The exception to this is gitignored files/folders (like the node_modules-folder when dealing with Node.js).

It seems like git stash might allow me to create such a temporary state. It is also very important that I'm able to restore the state as if nothing happened after running the tests.

To describe the exact behaviour I want, I have created a bash script below:

#!/bin/bash
mkdir temporary-repo && cd temporary-repo || exit 1
git init > /dev/null 2>&1
printf '%s' "0" > ignored
printf '%s' "ignored" > .gitignore
printf '%s' "0" > modified-staged
printf '%s' "0" > modified-unstaged
printf '%s' "0" > modified-unstaged-undo
printf '%s' "0" > modified-unstaged-update
git add . && git commit -m "Initial commit"  > /dev/null 2>&1
printf '%s' "1" > new-staged
printf '%s' "1" > modified-staged
printf '%s' "1" > new-unstaged-undo
printf '%s' "1" > new-unstaged-update
printf '%s' "1" > modified-unstaged-undo
printf '%s' "1" > modified-unstaged-update
git add .
printf '%s' "0" > new-unstaged-undo
printf '%s' "0" > modified-unstaged-undo
printf '%s' "1" > new-unstaged
printf '%s' "1" > modified-unstaged
printf '%s' "2" > new-unstaged-update
printf '%s' "2" > modified-unstaged-update
before="$(git status)"

#####################
# CREATE STASH HERE #
#####################

echo "--- Created stash ---"
test "1" = "$(cat new-unstaged-undo)" || echo "ERR: new-unstaged-undo != 1"
test "1" = "$(cat modified-unstaged-undo)" || echo "ERR: modified-unstaged-undo != 1"
test "1" = "$(cat new-staged)" || echo "ERR: new-staged != 1"
test "1" = "$(cat modified-staged)" || echo "ERR: modified-staged != 1"
test -f new-unstaged && echo "ERR: new-unstaged should not exist"
test "0" = "$(cat modified-unstaged)" || echo "ERR: modified-unstaged != 0"
test "1" = "$(cat new-unstaged-update)" || echo "ERR: new-unstaged-update != 1"
test "1" = "$(cat modified-unstaged-update)" || echo "ERR: modified-unstaged-update != 1"
test "0" = "$(cat ignored)" || echo "ERR: ignored != 0"

##################
# POP STASH HERE #
##################

echo "--- Popped stash ---"
test "0" = "$(cat new-unstaged-undo)" || echo "ERR: new-unstaged-undo != 0"
test "0" = "$(cat modified-unstaged-undo)" || echo "ERR: modified-unstaged-undo != 0"
test "1" = "$(cat new-staged)" || echo "ERR: new-staged != 1"
test "1" = "$(cat modified-staged)" || echo "ERR: modified-staged != 1"
test "1" = "$(cat new-unstaged)" || echo "ERR: new-unstaged != 1"
test "1" = "$(cat modified-unstaged)" || echo "ERR: modified-unstaged != 1"
test "2" = "$(cat new-unstaged-update)" || echo "ERR: new-unstaged-update != 2"
test "2" = "$(cat modified-unstaged-update)" || echo "ERR: modified-unstaged-update != 2"
test "0" = "$(cat ignored)" || echo "ERR: ignored != 0"

after="$(git status)"
diff -y <(echo "$before") <(echo "$after")
cd .. && rm -rf temporary-repo

Attempt 1

# Create stash
git stash -k -u

# Pop stash
git stash pop

Failing test output:

--- Created stash ---
--- Popped stash ---
ERR: new-unstaged-undo != 0
ERR: modified-unstaged-undo != 0
ERR: new-unstaged-update != 2
ERR: modified-unstaged-update != 2
On branch master                                                On branch master
Changes to be committed:                                        Changes to be committed:
  (use "git restore --staged <file>..." to unstage)               (use "git restore --staged <file>..." to unstage)
        modified:   modified-staged                                     modified:   modified-staged
                                                              >         modified:   modified-unstaged
        modified:   modified-unstaged-undo                              modified:   modified-unstaged-undo
        modified:   modified-unstaged-update                  <
        new file:   new-staged                                          new file:   new-staged
        new file:   new-unstaged-undo                         <
        new file:   new-unstaged-update                       <

Changes not staged for commit:                                | Unmerged paths:
  (use "git add <file>..." to update what will be committed)  |   (use "git restore --staged <file>..." to unstage)
  (use "git restore <file>..." to discard changes in working  |   (use "git add <file>..." to mark resolution)
        modified:   modified-unstaged                         |         both modified:   modified-unstaged-update
        modified:   modified-unstaged-undo                    |         both added:      new-unstaged-undo
        modified:   modified-unstaged-update                  |         both added:      new-unstaged-update
        modified:   new-unstaged-undo                         <
        modified:   new-unstaged-update                       <

Untracked files:                                                Untracked files:
  (use "git add <file>..." to include in what will be committ     (use "git add <file>..." to include in what will be committ
        new-unstaged                                                    new-unstaged

Attempt 2

# Create stash
stash=$(git stash create -q)
git stash store "$stash"
git stash show -p | git apply --reverse
git diff --cached | git apply

# Pop stash
git reset --hard -q
git stash apply --index -q
git stash drop -q

Failing test output:

--- Created stash ---
ERR: new-unstaged should not exist
--- Popped stash ---
On branch master                                                On branch master
Changes to be committed:                                        Changes to be committed:
  (use "git restore --staged <file>..." to unstage)               (use "git restore --staged <file>..." to unstage)
        modified:   modified-staged                                     modified:   modified-staged
        modified:   modified-unstaged-undo                              modified:   modified-unstaged-undo
        modified:   modified-unstaged-update                            modified:   modified-unstaged-update
        new file:   new-staged                                          new file:   new-staged
        new file:   new-unstaged-undo                                   new file:   new-unstaged-undo
        new file:   new-unstaged-update                                 new file:   new-unstaged-update

Changes not staged for commit:                                  Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)      (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working      (use "git restore <file>..." to discard changes in working 
        modified:   modified-unstaged                                   modified:   modified-unstaged
        modified:   modified-unstaged-undo                              modified:   modified-unstaged-undo
        modified:   modified-unstaged-update                            modified:   modified-unstaged-update
        modified:   new-unstaged-undo                                   modified:   new-unstaged-undo
        modified:   new-unstaged-update                                 modified:   new-unstaged-update

Untracked files:                                                Untracked files:
  (use "git add <file>..." to include in what will be committ     (use "git add <file>..." to include in what will be committ
        new-unstaged                                                    new-unstaged

Attempt 1 creates the desired state for running unit tests, but fails to restore the original state. Attempt 2 fails to stash/remove new-unstaged, but successfully restores the original state.

Is there any way to get my desired behavior with git?

CodePudding user response:

Turns out a solution was staring me in the face the entire time.

Combining the two solutions seem to make my tests pass, and give me my desired behaviour:

# Create stash
git stash -k -u

# Pop stash
git reset --hard -q
git stash apply --index -q
git stash drop -q

Output:

--- Created stash ---
--- Popped stash ---
On branch master                                                On branch master
Changes to be committed:                                        Changes to be committed:
  (use "git restore --staged <file>..." to unstage)               (use "git restore --staged <file>..." to unstage)
        modified:   modified-staged                                     modified:   modified-staged
        modified:   modified-unstaged-undo                              modified:   modified-unstaged-undo
        modified:   modified-unstaged-update                            modified:   modified-unstaged-update
        new file:   new-staged                                          new file:   new-staged
        new file:   new-unstaged-undo                                   new file:   new-unstaged-undo
        new file:   new-unstaged-update                                 new file:   new-unstaged-update

Changes not staged for commit:                                  Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)      (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working      (use "git restore <file>..." to discard changes in working 
        modified:   modified-unstaged                                   modified:   modified-unstaged
        modified:   modified-unstaged-undo                              modified:   modified-unstaged-undo
        modified:   modified-unstaged-update                            modified:   modified-unstaged-update
        modified:   new-unstaged-undo                                   modified:   new-unstaged-undo
        modified:   new-unstaged-update                                 modified:   new-unstaged-update

Untracked files:                                                Untracked files:
  (use "git add <file>..." to include in what will be committ     (use "git add <file>..." to include in what will be committ
        new-unstaged                                                    new-unstaged
  • Related