Search code examples
bashgitgreppre-commit-hook

Trying to make a pre-commit hook that checks if files in index are formatted correctly


I've been trying to make a pre-commit hook that checks if files in the index are formatted correctly. I've tried so many things already but I just can't get the grep to work correctly. This is my code right now:

for FILE in $(git diff --cached --name-only)
do
    if [[ "$FILE" =~ \.(c|h|cpp|cc)$ ]]; then
        exec C:/dev/uct/clang-format-15.0.3.exe --dry-run -Werror $FILE | grep -c "violations"
    fi
done

I expect it to print the number of files that have formatting issues (I actually want to search for the string -Wclang-format-violations but I simplified it for testing).

If I &> the clang-format output to a file the violations correctly printed to that file.

exec C:/dev/uct/clang-format-15.0.3.exe --dry-run -Werror $FILE &> temp.txt

If I then run grep from the command line on that file it works.

grep -c violations temp.txt

I also tried grep on the file from the script but for some reason that didn't produce any output either (though I would prefer to not have to create a file).

exec C:/dev/uct/clang-format-15.0.3.exe --dry-run -Werror $FILE &> temp.txt
grep -c violations temp.txt

What am I doing wrong? I thought I would get this to work in half an hour at most.


Solution

  • The problem with grep is that you're not redirecting stderr, only stdout. (In your redirection to a file, you are redirecting stderr as well, with >&.) To capture stderr in the pipe, use |&.


    However you have a mismatch between what's staged to be committed and what you're looking at. In git, a file isn't staged to be committed, but its contents are. That means that you can stage - for example - part of a files contents to be committed. The file's contents in the working directory thus would not match what is staged.

    Here's a concrete example:

    echo "hello" > hello_world.txt
    git add hello_world.txt
    echo "world" >> hello_world.txt
    

    At this point, the contents staged in the index (from the git add on line 2) are hello. But the file contains hello world on disk.

    git status will show the file both as staged and modified:

    Changes to be committed:
      (use "git rm --cached <file>..." to unstage)
        new file:   hello_world.txt
    
    Changes not staged for commit:
      (use "git add <file>..." to update what will be committed)
      (use "git restore <file>..." to discard changes in working directory)
        modified:   hello_world.txt
    

    If you were to run git commit now, only the hello would be committed, not the file as it exists on disk.


    That means that you want to look at the staged contents of the file -- which is what will actually be committed -- not just the file on disk. Otherwise you may get false positives or negatives.

    To handle this case, you can pass what's staged to clang-format using the git show command. (git show :filename will output the staged contents of filename.)

    Try this:

    for FILE in $(git diff --cached --name-only)
    do
        if [[ "$FILE" =~ \.(c|h|cpp|cc)$ ]]; then
            git cat-file --filters ":${FILE}" | C:/dev/uct/clang-format-15.0.3.exe --dry-run -Werror --assume-filename="${FILE}" |& grep -c grep -c "\-Wclang\-format\-violations"
        fi
    done