Search code examples
bashgithusky

Husky pre commit hook and squashing commits


I am using "husky": "^7.0.4".

My team squashes their commits before opening a PR.

I have a pre-commit file to automate this workflow. Every other time I run the commit function, the pre-commit flow works perfectly. So the 1st, 3rd, 5th, etc. works. The 2nd, 4th, 6th, etc time prints this error

fatal: cannot lock ref 'HEAD': is at 766hdjoXXX but expected 766e11XXX

I thought it might be because I wasn't changing the file, however when I tried changing something, that didn't work either(it succeeds and fails every other time regardless). Any idea what's wrong?

Here is the pre-commit file:

read -n1 -p "Do you want to squash commits? [n/Y]" SHOULD_SQUASH < /dev/tty

case $SHOULD_SQUASH in  
  n|N) 
    echo
    echo Skipping squash, now linting files...
    ;;
  y|Y) 
    [ -z "$SQUASH_BRANCH" ] && SQUASH_BRANCH=develop
    branch=$(git symbolic-ref HEAD)
    echo
    echo Squashing all commits from $branch
    git reset $(git merge-base $SQUASH_BRANCH $branch)
    echo ------SUCCESS!------
    echo Commits successfully squashed.
    git add .
    echo Added all files successfully.
    ;;
  *) 
    echo
    echo Skipping squash, now linting files...
    ;;
esac

npx pretty-quick --staged
npm run lint

The squash function is from a custom function, that works with no problem, we created that lives in .zshrc.


Solution

  • Pre-commit files in general should not use git reset and git add. It is possible to make this work sometimes, but you get odd effects, including the one you're seeing (and sometimes worse ones). A pre-commit script should limit itself to testing whether the commit is OK, and if so, exiting zero; if not, the script should exit nonzero, without attempting to make any changes.1

    Instead of calling your script .git/pre-commit and invoking it with git commit, call it makecommit and invoke it as makecommit. Or, call it git-c and invoke it as git c. Have the script do its thing—including run npm lint—and if all looks good, have it run git commit. You won't need a pre-commit hook at all, but if you like, you can have one that reads as follows:

    [ "$RUN_FROM_OUR_SCRIPT" = yes ] && exit 0
    echo "don't run git commit directly, run our script instead"
    exit 1
    

    Then instead of just git commit, have your script do:

    RUN_FROM_OUR_SCRIPT=yes git commit
    

    which will set the variable that the pre-commit hook tests to make sure git commit was run from your script.

    Note that you will no longer need to redirect the read from /dev/tty. (You probably should also consider using git reset --soft, and/or verifying the that index content matches the working tree content.)


    1If you like to live dangerously, and really want to have a script that can update files, make sure that $GIT_INDEX_FILE is unset or set to .git/index, to make sure you have not been invoked as git commit --only.