Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Git Hook Fails Silently

I have a post-checkout and post-merge githook with these contents:

#!/bin/bash
# MIT © Sindre Sorhus - sindresorhus.com
set -eux  

changed_files="$(git diff-tree -r --name-only --no-commit-id HEAD@{1} HEAD)"

check_run() {
    echo "$changed_files" | grep --quiet "$1" && eval "$2"
}

echo ''
echo 'running git submodule update --init --recursive if .gitmodules has changed'
check_run .gitmodules "git submodule update --init --recursive"

echo ''
echo 'running npm install if package.json has changed'
check_run package.json "npm prune && npm install"

echo ''
echo 'running npm build:localhost'
npm run build:localhost 

Strangely if there are no changes to .gitmodules, the script ends rather than progress to check the package.json. (it doesn't even execute the echo lines after line 12)

removing the check_run invocations and putting just the straight commands instead seems to work fine.

removing check_run .gitmodules "git submodule update --init --recursive" does work as well. However the same behavior is exhibited in the next line: check_run package.json "npm prune && npm install" if package.json has not been changed

Is there something that I am missing that causes check_run to end the script on the first file change not being found?

Visual Proof: enter image description here

like image 539
jth41 Avatar asked May 09 '17 20:05

jth41


People also ask

How do you fix pre-commit hook failure?

Another Solution: Delete the . git/hook folder and then uninstall and reinstall husky. There are chances for conflicts with husky-generated files and .

How do you skip a husky pre-commit hook?

Use the --no-verify option to skip git commit hooks, e.g. git commit -m "commit message" --no-verify . When the --no-verify option is used, the pre-commit and commit-msg hooks are bypassed.

How do I ignore pre-commit?

Quick tip if you want to skip the pre-commit validations and quickly want to get a commit out there. To get your commit through without running that pre-commit hook, use the --no-verify option. Voila, without pre-commit hooks running!

What is git pre-commit hook?

The pre-commit hook is run first, before you even type in a commit message. It's used to inspect the snapshot that's about to be committed, to see if you've forgotten something, to make sure tests run, or to examine whatever you need to inspect in the code.


1 Answers

The set -e is the problem here: -e means "exit if something fails", where "fails" is defined as "exits nonzero".

We see in the output, as the last few lines:

+ check_run .gitmodules 'git submodule update --init --recursive'
+ echo ''
+ grep --quiet .gitmodules

(and then nothing else). The script exited after grep --quiet.

Here's the actual definition of check_run:

check_run() {
    echo "$changed_files" | grep --quiet "$1" && eval "$2"
}

This parses as left && right where left is the echo ... | grep ... and right is the eval "$2".

We see that the left part ran and the right part didn't. Here's where we need to know something about shells: even with -e set, they don't immediately exit if something fails, as long as that something is part of a test.1 So this isn't immediately the problem.

But it is still the problem, because left && right has, as its exit status, the exit status of the last thing it runs. The last thing it ran is the left pipeline, which was echo ... | grep .... The exit status of the pipeline is the exit status of its last component,2 i.e., grep. The grep exits zero if it finds the string (and with --quiet, suppresses its output as well), 1 if not, and 2 on generic errors, so the exit status was 1.

Therefore, the exit status of left && right was also 1.

Therefore, since -e was in effect, the shell exited!

The cure is either to avoid -e (but this means if something else fails unexpectedly, the shell plows on, so this might be a bit dangerous), or to make sure that the left && right does not make the shell exit.

There is one straightforward way to do the latter: replace left && right with if left; then right; fi:

if echo "$changed_files" | grep --quiet "$1"; then
    eval "$2"
fi

Note that if eval "$2" fails, the shell will still exit.

There is a different and slightly tricky way to do this with a different effect: replace left && right with left && right || true. The "and" expression binds more tightly, so this means:

  • Evaluate left
  • If that succeeds (exits zero), evaluate right
  • If the && fails (either left exits nonzero, or right is run and right exits nonzero), evaluate the || true part, which exits 0.

Hence:

echo "$changed_files" | grep --quiet "$1" && eval "$2" || true

always exits 0, eval-ing "$2" if and only if the left side fails (does not find the grepped-for expression).

If you want check_run to plow on even if eval "$2" fails, use the second (|| true) version. If you want check_run to stop (and have the whole shell exit) under -e if eval "$2" fails, use the first (if ...; then) version.


1Truly ancient 4BSD /bin/sh, long before it went open source, had a bug: -e would make the shell exit in these cases. (I think I fixed that one myself at one point, but when 4BSD went to a new sh, not based on Steve Bourne's original code, the bug just wasn't there at all.)

2In bash, you can control this in more detail. In particular you can get the status of every component of a pipeline in the $PIPESTATUS array variable.

like image 71
torek Avatar answered Nov 05 '22 18:11

torek