Git hook to reject commits where files contain a specific string
Asked Answered
git
P

3

18

I am using Guard with Rspec; I use focus: true to force it run only tests I am working on. But sometimes I forget to remove focus: true and it is causing distraction for my future self and people I work with.

I want to make a git hook that would check the spec folder to make sure there is no focus: true in test files apart from spec/rails_helper.rb and keep it in Repository.

I have read this answer Putting git hooks into repository, guess it has to be a bit awkward.

How are hooks used to prevent a commit based on the contents of files?

Update

Here is what I have now but it doesn't work, even if there is no match, git refuses to commit.

FILES_PATTERN='\.rb(\..+)?$'
FORBIDDEN="(\, focus: true|binding\.pry)"
git diff --cached --name-only | egrep "$FILES_PATTERN" | xargs egrep --with-filename -n "$FORBIDDEN" && echo "Commit reject, found $FORBIDDEN reference, please remove" && exit 1
exit 0
Pocketful answered 10/11, 2014 at 2:52 Comment(1)
Have you tried with a very simple pattern? It could be that one of your patterns is matching too much. You could try debugging; logging the file name and the pattern that matches it. The approach is correct, it's just the script that's not quite right.Caucasoid
C
12

A good source of information is the book at git-scm.

You want the pre-commit hook. To return a non-zero value (and thus abort the commit), you'd want something along these lines:

FILES_PATTERN='\.rb(\..+)?$'
FORBIDDEN='focus: true'
git diff --cached --name-only | \
  grep -spec/ | \
  grep -E $FILES_PATTERN | \
  xargs grep --with-filename -n $FORBIDDEN && echo "COMMIT REJECTED Found '$FORBIDDEN' references. Please remove them before commiting" && exit 1

That's lifted from this rather good tips site. I haven't tested the tweaks I made.

Caucasoid answered 10/11, 2014 at 3:32 Comment(7)
Before they've closed the question. I have the book, didn't manage to finish it yet. I'll dig into hooks and amend your example. Thanks.Pocketful
No one's closing the question. Though I'll edit it a bit to make it more generically useful. Maybe the downvote will go away.Caucasoid
Here is what I have now (after reading article and comments). FILES_PATTERN='\.rb(\..+)?$' FORBIDDEN=', focus: true' git diff --cached --name-only | egrep "$FILES_PATTERN" | xargs egrep --with-filename -n "$FORBIDDEN" && echo "Commit reject, found $FORBIDDEN reference, please remove" && exit 1And it works sort of, but the problem is now I can't commit even without focus: true. I do git commit -m "..." and nothing is happening, no errors or anything.Pocketful
Do you have "exit 0" as the last line in your pre-commit hook?Caucasoid
Sorry I can't make it work, with exit 0 and everything. Added to the question.Pocketful
Not sure if you still have the problem, but I needed to remove the -v from grep -v spec/ as well as adding the exit 0. I also swapped the single and double quotes in the last line so that $FORBIDDEN would actually get interpolated. Full version in this GistMarrissa
@JohnYeates That's why people should test things before posting them... I've edited the post to fix those.Abruzzi
S
5

Building off the existing answers, here is the version I am using with support for multiple strings.

#!/bin/sh

declare -a arr=("This is the first string." "This is the second string.")

for i in "${arr[@]}"
do
    git diff --cached --name-only | xargs grep --with-filename -n $i && echo "COMMIT REJECTED! Found '$i' references. Please remove them before commiting." && exit 1
done

exit 0
Skillless answered 21/1, 2019 at 7:55 Comment(2)
thanks, that's a neat and easy solution :) any idea if we could also make it case-independant? so not only "This is the first string." would be matched, but also "this is the first string."? (apart from adding all variations to the array, for sure)Archil
@Archil I believe you can add --ignore-case after "grep" to do that.Skillless
C
0

Server-Side Git Hooks

I came up with this example for the use case of code formatting, but pretty much if you just replace the usage of Prettier with grep -r 'bad-thing' it should work as well:

ref_name=$1
new_rev=$3

# only check branches, not tags or bare commits
if [ -z $(echo $ref_name | grep "refs/heads/") ]; then
  exit 0
fi

# don't check empty branches
if [ "$(expr "${new_rev}" : '0*$')" -ne 0 ]; then
  exit 0
fi

# Checkout a copy of the branch (but also changes HEAD)
my_work_tree=$(mktemp -d -t git-work-tree.XXXXXXXX) 2>/dev/null
git --work-tree="${my_work_tree}" --git-dir="." checkout $new_rev -f >/dev/null

# Do the formatter check
echo "Checking code formatting..."
pushd ${my_work_tree} >/dev/null
prettier './**/*.{js,css,html,json,md}' --list-different
my_status=$?
popd >/dev/null

# reset HEAD to master, and cleanup
git --work-tree="${my_work_tree}" --git-dir="." checkout master -f >/dev/null
rm -rf "${my_work_tree}"

# handle error, if any
if [ "0" != "$my_status" ]; then
  echo "Please format the files listed above and re-commit."
  echo "(and don't forget your .prettierrc, if you have one)"
  exit 1
fi

There are some limitations to this, so I'd recommend taking a look at the "more complete" version as well:

The Gist of it...

If you want to do this server-side you'll be working with a bare repository (most likely), so you have to do a little bit of extra work to create a temporary place to check things out (as shown above).

Basically, this is the process:

  • use a server-side update hook (similar to pre-receive)
  • inspect the branch and commit
  • checkout the bare repo to a temp folder
  • run the check for specific string (grep -r 'bad-thing' .)
  • exit non-zero if there are offending files

And I would have adjusted the script to do the greping, but it's late (or rather very early) and I don't trust myself to not make a typo that breaks everything in trying to make a "simple" change. I know that the above works (because I've used it), so I'll leave at that.

HTH (not the OP per se, but others in the future - and perhaps myself again)

Crider answered 31/5, 2019 at 10:50 Comment(2)
when you say above in the script # reset HEAD to master, and cleanup, does this mean it will only work when committing to the master branch?Local
@Local no, this is just to get it back to the clean state so that when the next PR comes in and the process starts over, there's nothing leftover.Crider

© 2022 - 2024 — McMap. All rights reserved.