pre-commit 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. #!/bin/sh
  2. # Tidy staged Perl, judge it against .perlcriticrc, then run the tests, before
  3. # any of it lands.
  4. #
  5. # There is a .perltidyrc, and a tree stays tidy to it only if something makes it.
  6. # Left to people, it does not: tidying a file you are changing three lines of
  7. # buries the change in a hundred lines of reformatting, so the reasonable thing
  8. # is to skip it, and the tree drifts until a mass tidy is the only way back.
  9. # One commit's worth at a time is small enough that nobody has to weigh it up.
  10. #
  11. # perlcritic is here for the other half of that. `dzil test` runs the profile
  12. # too, through [@TestingMania], but that is the pass you run before a release --
  13. # by which point the objection is twenty commits old and the person who can
  14. # answer it has moved on. Here it is about the diff in front of you.
  15. #
  16. # There is one profile, .perlcriticrc, and which one that is was decided when
  17. # this distribution was scaffolded: the packaging-perl skill asks which perl you
  18. # are targeting and copies the matching profile to that name. So the hook needs
  19. # no opinion about versions -- the file it reads is already the right one, and
  20. # changing your floor means replacing that file rather than editing this.
  21. #
  22. # Install it: cp git-hooks/pre-commit .git/hooks/
  23. #
  24. # A file qualifies three ways:
  25. # - a .pm, .pl or .t extension
  26. # - a .PL extension, which is how a Makefile.PL gets covered
  27. # - executable with perl on its shebang line, which is how the extensionless
  28. # scripts in bin/ get covered
  29. #
  30. # .t is in there because test files are often not executable, and the shebang
  31. # rule alone would then tidy whichever ones happen to be and let the rest drift.
  32. #
  33. # Both passes read the working tree rather than the index, so a file staged in
  34. # part is judged whole. That is deliberate: perltidy rewrites the file on disk
  35. # and the hook stages what it wrote, so the working copy is what lands anyway,
  36. # and two passes disagreeing about which bytes they looked at would be worse to
  37. # own than this.
  38. #
  39. # Both run from the top of the working tree, which is where git runs a hook and
  40. # the only place they work at all: the profile names .preferred_modules.ini and
  41. # .pod_stopwords by relative path.
  42. # Redirect output to stderr.
  43. exec 1>&2
  44. # ACMR so that a file deleted in this commit is not handed to perltidy, which
  45. # would fail on a path that is no longer there.
  46. staged=$(git diff --cached --name-only --diff-filter=ACMR)
  47. lib_files=''
  48. test_files=''
  49. for file in $staged
  50. do
  51. test -f "$file" || continue
  52. is_perl=''
  53. case "$file" in
  54. *.pm|*.pl|*.PL|*.t)
  55. is_perl=1
  56. ;;
  57. esac
  58. if [ -z "$is_perl" ] && test -x "$file" && head -n 1 "$file" | grep -Eq '^#!.*perl'
  59. then
  60. is_perl=1
  61. fi
  62. test -n "$is_perl" || continue
  63. # Whether a file is perl at all, and then which list it goes in. One case
  64. # statement doing both would file t/00-load.t under whichever arm it reached
  65. # first, and the .t rule is above the path rule.
  66. case "$file" in
  67. t/*) test_files="$test_files $file" ;;
  68. *) lib_files="$lib_files $file" ;;
  69. esac
  70. done
  71. # For the tidy pass, which does not care which list a file came from. The
  72. # emptiness tests below concatenate the two without a space instead: with one,
  73. # "no perl staged at all" would be a string holding a space, which is not empty.
  74. all_files="$lib_files $test_files"
  75. # Quoted here, because a bare [ $all_files ] is a syntax error the moment there
  76. # is more than one file -- which would stop the hook working in exactly the
  77. # commits that need it most. Unquoted below, because there the word splitting is
  78. # what turns one string into an argument list.
  79. if [ -z "$lib_files$test_files" ]
  80. then
  81. :
  82. elif command -v perltidy >/dev/null 2>&1
  83. then
  84. echo "Auto-tidying perl changes..."
  85. # -bext='/' makes perltidy delete the backup it would otherwise leave
  86. # sitting next to the file as <name>.bak.
  87. perltidy -b -bext='/' $all_files || {
  88. echo "pre-commit: perltidy failed, leaving the commit alone."
  89. exit 1
  90. }
  91. git update-index --add $all_files
  92. echo "Done."
  93. else
  94. echo "pre-commit: perltidy is not on PATH, skipping the tidy pass."
  95. fi
  96. # After the tidy pass and never before. The tidy pass rewrites these files and
  97. # stages what it wrote, so what critic has to judge is the bytes that will land
  98. # rather than the ones you saved -- and if you ever turn [CodeLayout::
  99. # RequireTidyCode] back on, running it first would fail the commit over the one
  100. # thing the hook had already set out to fix by itself.
  101. #
  102. # Three branches rather than two conditions and one else. Staging nothing the
  103. # profile judges -- a commit of documentation, say -- is not the same thing as
  104. # having no perlcritic to judge it with, and one else says the second whenever
  105. # the first is true.
  106. if [ -z "$lib_files$test_files" ]
  107. then
  108. echo "Nothing staged that the profile judges."
  109. elif command -v perlcritic >/dev/null 2>&1
  110. then
  111. echo "Running perlcritic..."
  112. # Both passes run even when the first has already failed, so one commit
  113. # shows every objection rather than one list's worth at a time.
  114. critic_failed=''
  115. if [ -n "$lib_files" ]
  116. then
  117. perlcritic --profile .perlcriticrc $lib_files || critic_failed=1
  118. fi
  119. # The same profile for t/, less one policy. A test is not a module: nobody
  120. # runs perldoc on it, so requiring POD of it asks for a page no reader will
  121. # ever open. Everything else in the profile still judges it, which is more
  122. # than a release does -- all_critic_ok() reads lib/ and not t/, so this hook
  123. # is the only thing that looks at a test file at all.
  124. if [ -n "$test_files" ]
  125. then
  126. perlcritic --profile .perlcriticrc --exclude Documentation::RequirePod $test_files || critic_failed=1
  127. fi
  128. if [ -n "$critic_failed" ]
  129. then
  130. echo
  131. echo "pre-commit: perlcritic refused the above. Either fix them, or say"
  132. echo "why with a '## no critic (Policy::Name)' and the reason beside it."
  133. echo "To commit without these checks at all: git commit --no-verify"
  134. exit 1
  135. fi
  136. echo "Done."
  137. else
  138. # Skipped rather than fatal, for the same reason the tidy pass is: a machine
  139. # without perlcritic can still commit, and `dzil test` will not let the
  140. # distribution be released without one.
  141. echo "pre-commit: perlcritic is not on PATH, skipping the critic pass."
  142. fi
  143. # Last, because the tests read the files that the tidy pass wrote. Whatever is
  144. # staged, and not only Perl: a change to a share/ file or a fixture breaks a
  145. # test as surely as a change to a module does. -l for lib/, as `dzil test`
  146. # finds it.
  147. #
  148. # Without the GIT_ variables that git sets for this hook. It sets
  149. # GIT_INDEX_FILE, and GIT_DIR too in a worktree, and git obeys them over -C. So
  150. # a test that runs git in a repository of its own would add, commit and write
  151. # config in this one. In a subshell, because the git commands of this hook need
  152. # them: under git commit -a, GIT_INDEX_FILE names the index being committed.
  153. run_tests() {
  154. (
  155. unset $(env | sed -n 's/^\(GIT_[A-Za-z0-9_]*\)=.*/\1/p')
  156. prove -lm -j8 "$@"
  157. ) || {
  158. echo "pre-commit: the tests above failed. To see why, run each failing file"
  159. echo "on its own: prove -lv t/<file>.t"
  160. echo "To commit without these checks at all: git commit --no-verify"
  161. exit 1
  162. }
  163. }
  164. test -d t || exit 0
  165. # Without tests-covering, every test, because nothing here knows which tests a
  166. # change can reach.
  167. if ! command -v tests-covering >/dev/null 2>&1
  168. then
  169. echo "Running every test, because tests-covering is not on PATH; install Perl::Tests::Covering..."
  170. run_tests t/
  171. exit 0
  172. fi
  173. # tests-covering chooses the tests that ran a line the diff changes, from the
  174. # records that git-hooks/post-commit keeps up to date. It asks
  175. # .tests-covering-map.pl about each file that no test loads, and the map names
  176. # the files that no test reads, such as dist.ini. --unexplained all runs every
  177. # test for a file that neither can place, such as a fixture. --full-index,
  178. # because it matches each file to its records by the blob id on the index line
  179. # of the diff, and an abbreviated id matches nothing.
  180. tests=$(git diff --cached --full-index | tests-covering --diff --unexplained all) || exit 1
  181. if [ -z "$tests" ]
  182. then
  183. echo "No test ran a line that this commit changes."
  184. exit 0
  185. fi
  186. echo "Running the tests that ran what this commit changes..."
  187. run_tests $tests