First, let's answer the question you actually asked, because you need to know this eventually anyway:
How can I make it only check the code for the current commit that is supposed to be committed?
The contents of the commit-that-will-be-made is whatever is in the index.
This is every file, not just files you recently git add
ed. For instance, if your work-tree has with eight source-controlled *.py
Python files, and you modified and git add
-ed two of them, there are eight *.py
files that will be committed this time. Every commit has every file, and not a set of changes from some previous file(s).1
In order to check those eight files (and not the current contents of the work-tree), you need to extract the contents of the index somewhere.
Of course, this is not what you actually want:
it actually checks the whole branch and if it finds anything in branch that does not follow pep8, it will terminate (that repository is a bit old and some code did not follow pep8 from the start, so I know it could be refactored, but I don't need pre-hook to tell me that about already committed code)
Again, it does not actually check the branch; it checks the work tree. While this does not immediately help you get where you need to go, it is important.
What we need here is to extract, perhaps from the index, the files that are different from the current commit.
The way to do this is to make a new commit. Ideally, we might make this commit somewhere other than on the current branch, because if we make it on the branch, we have to un-make it again later.
There is a command that does this—that makes commits from the index, but not on a branch—and that command is git stash
. Unfortunately, there is a bug in git stash
when you go to use it this way. Rather than using the work-arounds I describe in that answer, we can do something different: we can make our own tree in a temporary directory, after comparing the current index to the HEAD
commit.
The script for this (actually tested, even!) is below.
1You might object here that git log -p
or git show
shows you the changes. You are correct that it shows you changes, but it does so by running git diff
against a previous commit, which also contains every file. It is by comparing the previous version of "everything" to the next version of "everything" that git discovers what changed.
#! /bin/sh
# run-checks: run some checking command(s) on a proposed commit.
#
# Optionally, run it only on files that differ from those in
# the current commit (added or modified, treating rename as
# modify), and/or do not run it at all if there are
# no such files (e.g., if the commit consists only of file
# removals).
usage()
{
echo "usage: $0 [-d] checkcmd [args ...]" 1>&2
exit 1
}
# probably should use git rev-parse feature now, oh well
diffmode=false
skipempty=true
while true; do
case "$1" in
-d|--diff) diffmode=true; shift;;
-z|--run-even-if-empty) skipempty=false; shift;;
-dz) diffmode=true; skipempty=false; shift;;
*) break;;
esac
done
case "$#" in
0) usage;;
esac
# from here on, exit on error
set -e
# get temporary directory and arrange to clean it up
tdir=$(mktemp -d -t run-checks)
trap "rm -rf $tdir" 0 1 2 3 15
# Get list of changed files (whether or not we are using
# only the changed files). This includes deleted files.
# For efficiency, we treat renames as delete/add pairs here.
# Require that new commit not match current commit.
if test $(git diff --cached --name-only --no-renames HEAD | wc -l) -eq 0; then
echo "no changes to test before committing"
exit 1
fi
# Populate work tree in temp dir. If we only want changed
# files, limit the checkout to files added or modified. Note
# that this list might be empty.
if $diffmode; then
git diff --cached --name-only --no-renames --diff-filter=AM -z HEAD |
xargs -0 git --work-tree=$tdir checkout -f --
else
git --work-tree=$tdir checkout -f -- .
fi
# Now run checker in temp work tree. Our exit status is
# its exit status. Do not use exec since we must still clean
# up the temp dir, and optionally skip checker if work tree is empty.
cd $tdir
if test $(ls -A | wc -l) -eq 0; then
is_empty=true
else
is_empty=false
fi
if $skipempty && $is_empty; then exit 0; fi
if $is_empty; then
$@
else
$@ *
fi