pre-commit 8.3 KB

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