| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206 |
- #!/bin/sh
- # Tidy staged Perl, judge it against .perlcriticrc, then run the tests, before
- # any of it lands.
- #
- # There is a .perltidyrc, and a tree stays tidy to it only if something makes it.
- # Left to people, it does not: tidying a file you are changing three lines of
- # buries the change in a hundred lines of reformatting, so the reasonable thing
- # is to skip it, and the tree drifts until a mass tidy is the only way back.
- # One commit's worth at a time is small enough that nobody has to weigh it up.
- #
- # perlcritic is here for the other half of that. Nothing else in tCMS runs the
- # profile, so this is where an objection is about the diff in front of you.
- #
- # There is one profile, .perlcriticrc, and which one that is was decided when
- # the floor was chosen: the packaging-perl skill asks which perl you
- # are targeting and copies the matching profile to that name. So the hook needs
- # no opinion about versions -- the file it reads is already the right one, and
- # changing your floor means replacing that file rather than editing this.
- #
- # Install it: cp git-hooks/pre-commit .git/hooks/
- #
- # A file qualifies three ways:
- # - a .pm, .pl or .t extension
- # - a .PL extension, which is how a Makefile.PL gets covered
- # - executable with perl on its shebang line, which is how the extensionless
- # scripts in bin/ get covered
- #
- # .t is in there because test files are often not executable, and the shebang
- # rule alone would then tidy whichever ones happen to be and let the rest drift.
- #
- # Both passes read the working tree rather than the index, so a file staged in
- # part is judged whole. That is deliberate: perltidy rewrites the file on disk
- # and the hook stages what it wrote, so the working copy is what lands anyway,
- # and two passes disagreeing about which bytes they looked at would be worse to
- # own than this.
- #
- # Both run from the top of the working tree, which is where git runs a hook and
- # the only place they work at all: the profile names .preferred_modules.ini and
- # .pod_stopwords by relative path.
- # Redirect output to stderr.
- exec 1>&2
- # ACMR so that a file deleted in this commit is not handed to perltidy, which
- # would fail on a path that is no longer there.
- staged=$(git diff --cached --name-only --diff-filter=ACMR)
- lib_files=''
- test_files=''
- for file in $staged
- do
- test -f "$file" || continue
- is_perl=''
- case "$file" in
- *.pm|*.pl|*.PL|*.t)
- is_perl=1
- ;;
- esac
- if [ -z "$is_perl" ] && test -x "$file" && head -n 1 "$file" | grep -Eq '^#!.*perl'
- then
- is_perl=1
- fi
- test -n "$is_perl" || continue
- # Whether a file is perl at all, and then which list it goes in. One case
- # statement doing both would file t/00-load.t under whichever arm it reached
- # first, and the .t rule is above the path rule.
- case "$file" in
- t/*) test_files="$test_files $file" ;;
- *) lib_files="$lib_files $file" ;;
- esac
- done
- # For the tidy pass, which does not care which list a file came from. The
- # emptiness tests below concatenate the two without a space instead: with one,
- # "no perl staged at all" would be a string holding a space, which is not empty.
- all_files="$lib_files $test_files"
- # Quoted here, because a bare [ $all_files ] is a syntax error the moment there
- # is more than one file -- which would stop the hook working in exactly the
- # commits that need it most. Unquoted below, because there the word splitting is
- # what turns one string into an argument list.
- if [ -z "$lib_files$test_files" ]
- then
- :
- elif command -v perltidy >/dev/null 2>&1
- then
- echo "Auto-tidying perl changes..."
- # -bext='/' makes perltidy delete the backup it would otherwise leave
- # sitting next to the file as <name>.bak.
- perltidy -b -bext='/' $all_files || {
- echo "pre-commit: perltidy failed, leaving the commit alone."
- exit 1
- }
- git update-index --add $all_files
- echo "Done."
- else
- echo "pre-commit: perltidy is not on PATH, skipping the tidy pass."
- fi
- # After the tidy pass and never before. The tidy pass rewrites these files and
- # stages what it wrote, so what critic has to judge is the bytes that will land
- # rather than the ones you saved -- and if you ever turn [CodeLayout::
- # RequireTidyCode] back on, running it first would fail the commit over the one
- # thing the hook had already set out to fix by itself.
- #
- # Three branches rather than two conditions and one else. Staging nothing the
- # profile judges -- a commit of documentation, say -- is not the same thing as
- # having no perlcritic to judge it with, and one else says the second whenever
- # the first is true.
- if [ -z "$lib_files$test_files" ]
- then
- echo "Nothing staged that the profile judges."
- elif command -v perlcritic >/dev/null 2>&1
- then
- echo "Running perlcritic..."
- # Both passes run even when the first has already failed, so one commit
- # shows every objection rather than one list's worth at a time.
- critic_failed=''
- if [ -n "$lib_files" ]
- then
- perlcritic --profile .perlcriticrc $lib_files || critic_failed=1
- fi
- # The same profile for t/, less one policy. A test is not a module: nobody
- # runs perldoc on it, so requiring POD of it asks for a page no reader will
- # ever open. Everything else in the profile still judges it, which is more
- # than a release does -- all_critic_ok() reads lib/ and not t/, so this hook
- # is the only thing that looks at a test file at all.
- if [ -n "$test_files" ]
- then
- perlcritic --profile .perlcriticrc --exclude Documentation::RequirePod $test_files || critic_failed=1
- fi
- if [ -n "$critic_failed" ]
- then
- echo
- echo "pre-commit: perlcritic refused the above. Either fix them, or say"
- echo "why with a '## no critic (Policy::Name)' and the reason beside it."
- echo "To commit without these checks at all: git commit --no-verify"
- exit 1
- fi
- echo "Done."
- else
- # Skipped rather than fatal, for the same reason the tidy pass is: a machine
- # without perlcritic can still commit.
- echo "pre-commit: perlcritic is not on PATH, skipping the critic pass."
- fi
- # Last, because the tests read the files that the tidy pass wrote. Whatever is
- # staged, and not only Perl: a change to a share/ file or a fixture breaks a
- # test as surely as a change to a module does. -l for lib/.
- #
- # Without the GIT_ variables that git sets for this hook. It sets
- # GIT_INDEX_FILE, and GIT_DIR too in a worktree, and git obeys them over -C. So
- # a test that runs git in a repository of its own would add, commit and write
- # config in this one. In a subshell, because the git commands of this hook need
- # them: under git commit -a, GIT_INDEX_FILE names the index being committed.
- run_tests() {
- (
- unset $(env | sed -n 's/^\(GIT_[A-Za-z0-9_]*\)=.*/\1/p')
- prove -lm -j8 "$@"
- ) || {
- echo "pre-commit: the tests above failed. To see why, run each failing file"
- echo "on its own: prove -lv t/<file>.t"
- echo "To commit without these checks at all: git commit --no-verify"
- exit 1
- }
- }
- test -d t || exit 0
- # Without tests-covering, every test, because nothing here knows which tests a
- # change can reach.
- if ! command -v tests-covering >/dev/null 2>&1
- then
- echo "Running every test, because tests-covering is not on PATH; install Perl::Tests::Covering..."
- run_tests t/
- exit 0
- fi
- # tests-covering chooses the tests that ran a line the diff changes, from the
- # records that git-hooks/post-commit keeps up to date. It asks
- # .tests-covering-map.pl about each file that no test loads, and the map names
- # the files that no test reads, such as dist.ini. --unexplained all runs every
- # test for a file that neither can place, such as a fixture. --full-index,
- # because it matches each file to its records by the blob id on the index line
- # of the diff, and an abbreviated id matches nothing.
- tests=$(git diff --cached --full-index | tests-covering --diff --unexplained all) || exit 1
- if [ -z "$tests" ]
- then
- echo "No test ran a line that this commit changes."
- exit 0
- fi
- echo "Running the tests that ran what this commit changes..."
- run_tests $tests
|