Subversion Commit Hooks for PHP
Submitted by Matthew Turland on Fri, 11/21/2008 - 14:27When I recently began working for Blue Parabola, one of my first tasks involved helping to get our internal infrastructure set up. More specifically, I had to write and set up pre-commit and post-commit repository hooks for our Subversion repository.
First, the pre-commit hook. Its purpose is to perform a PHP lint check (as in the -l flag when using PHP from the command line) to ensure that PHP files being committed contain no syntax errors. When I received this task, I actually already had a copy of a pre-commit script to do this that Elizabeth Smith graciously provided to me ages ago. The script is written in bash and my skill level with the associated shell scripting language is admittedly rather low, as is my experience in creating and setting up Subversion hook scripts in general.
Two particular resources proved to be quite useful in helping me through this situation. First, the Advanced Bash-Scripting Guide helped me to understand the existing script from Elizabeth and make small modifications to it to suit our server environment. Second, the Implementing Repository Hooks section of the online book Version Control with Subversion was invaluable in pointing out potential gotchas in the process.
So, here's the pre-commit script.
#!/bin/bash
REPOS="$1"
TXN="$2"
PHP="/usr/local/php5/bin/php"
SVNLOOK="/usr/bin/svnlook"
AWK="/usr/bin/awk"
GREP="/bin/egrep"
SED="/bin/sed"
CHANGED=`$SVNLOOK changed -t "$TXN" "$REPOS" | $GREP "^[U|A]" | $AWK '{print $2}' | $GREP \\.php$`
for FILE in $CHANGED
do
MESSAGE=`$SVNLOOK cat -t "$TXN" "$REPOS" "$FILE" | $PHP -l`
if [ $? -ne 0 ]
then
echo 1>&2
echo "***********************************" 1>&2
echo "PHP error in: $FILE:" 1>&2
echo `echo "$MESSAGE" | $SED "s| -| $FILE|g"` 1>&2
echo "***********************************" 1>&2
exit 1
fi
done
Let's look at this piece by piece.
-
#!/bin/bash
Commonly called the sha-bang line, this indicates that the bash shell should be used to execute the script. Note that this can point to other things, like the PHP interpreter executable -
REPOS="$1"
This line assigns the values of command line arguments (referred to as a positional parameter in bash) to the variables REPOS and TXN. Yes, there is a good reason for why the position parameter references are surrounded by quotes. According to the pre-commit documentation, the values of these parameters will be the path to the Subversion repository receiving the commit (REPOS) and the commit transaction name (TXN) used in place of a revision number since one has not been assigned yet.
TXN="$2" -
PHP="/usr/local/php5/bin/php"
This block assigns the absolute paths of utilities to variables for later reference. The reason absolute paths are used is because Subversion normally executes hook programs with an empty environment. That is, depending on the server environment configuration, environmental variables like $PATH may not be set, which removes the ability to refer to external executables without an absolute path. As such, it's generally safest to use absolute paths so that hook script works consistently regardless of the environment assuming the paths are correct. Storing these in variables allows them to be changed easily later if the script is moved to a server with a different configuration.
SVNLOOK="/usr/bin/svnlook"
AWK="/usr/bin/awk"
GREP="/bin/egrep"
SED="/bin/sed" -
CHANGED=`$SVNLOOK changed -t "$TXN" "$REPOS" | $GREP "^[U|A]" | $AWK '{print $2}' | $GREP \\.php$`This statement uses command substitution (denoted by the backquotes or backticks) to execute a command and assign the resulting output to the variable CHANGED. The command in this case is comprised of calls to several utilities using I/O redirection to pass the output of each to the input of the next. The svnlook call returns a list of added or modified files in the current commit and their associated status markers. awk is then used to strip out the status markers so that only the file paths remain. Finally, grep filters out any files in the list that do not have a .php file extension. -
for FILE in $CHANGED ... do ... done
This for loop iterates over each item in the list stored in the CHANGED variable. Note that the use of whitespace as shown in the full code sample shown above is intentional, else the command separator (a semicolon) must be used to separate the end of the list ($CHANGED) from the do keyword. -
MESSAGE=`$SVNLOOK cat -t "$TXN" "$REPOS" "$FILE" | $PHP -l`
This statement uses command substitution again. svnlook is called to output the contents of the current file, which is passed as input to the PHP interpreter to perform the lint check. The resulting output is assigned to the MESSAGE variable. -
if [ $? -ne 0 ] ... then ... fi
This if block tests to see if the exit status of the last executed command (0 if the current file passes the lint check, a non-0 integer otherwise) indicates failure (i.e. is not equal to 0, hence -ne 0). -
echo 1>&2 ...
This sequence of echo statements output a message citing the relevant file and the location and description of the error. echo normally sends output to stdin, but 1>&2 indicates that I/O redirection should send output to stderr instead. An echo call without the specification of a value to output simply results in a newline being output. -
echo `echo "$MESSAGE" | $SED "s| -| $FILE|g"` 1>&2
This statement uses command substitution in conjunction with the text manipulation utility sed. Because input to the PHP interpreter is being sent via stdin, error messages will reference that (denoted by a hyphen) instead of the original file. To correct this, sed is used to substitute the filename in the appropriate location within $MESSAGE. -
exit 1
This statement forces the script to terminate and returns an exit status value of 1 to indicate failure. Subversion will take this to mean that the pre-commit hook operation failed and that it should abort the commit transaction. If this line is never executed, an exit status of 0 will be assumed and Subversion will proceed with the commit transaction.
The post-commit script was a slightly easier matter. The purpose of this one is to use the PHP_CodeSniffer PEAR package on the PHP files being committed and to inform the committer if any of them do not meet a specified coding standard. Luckily, a commit script is also provided with the package to do exactly this. Note that this is not included when installing the package via the PEAR installer; you have to download the latest tarball of the package to get it.
Being hosted on DreamHost, we followed their instructions to set up a local PEAR installation. After that, installing CodeSniffer is as simple as pear install PHP_CodeSniffer. phpcs, the CodeSniffer CLI utility, only has options to analyze a specified file or recursively analyze files in a directory and currently cannot take its input from stdin. As such, for the commit script to work, it must be configured with paths to the local PHP interpreter executable (to be executed at all) and svnlook utility (to read the contents of files in the commit, as in the pre-commit script described earlier).
And that's it. Before I found out about the commit script provided with CodeSniffer in PEAR, I actually started to write my own bash script. The only way to have it work correctly, however, was to point it at the base directory containing the code being committed, for reasons mentioned earlier in regard to phpcs. For larger repositories, this can obviously become a problem. So, I highly advise using the provided commit script where possible. All in all, learning enough bash to read the pre-commit script was the most difficult part of the task. Hope my experiences prove useful to someone out there.


nice post but..
for FILE in $CHANGEDor replace each occurrence of $FILE with $LINE :) Martin MartinovSmall error in your script
Oops!
Martin and Arjen, Indeed, you two were both correct. The offending line was a typo on my part and I've corrected the error you cited. Thank you both for taking the time to comment about it! Best Regards, Matthew Turland
useful feature
CHANGED=`$SVNLOOK changed -t "$TXN" "$REPOS" | $AWK '$2 ~ /\.ph(p|tml)$/ {print $2}'`I removed the need to use egrep, we're using AWK, so why not stick with it? Also, it allows the script to lint both .php and .phtml files both of which are common in many framework based PHP applications (e.g. Zend Framework). Also:EXITCODE=0 for FILE in $CHANGED ... echo "****************************************" 1>&2 EXITCODE=1 fi done exit $EXITCODEThis allows more than 1 error to be printed out in cases where multiple syntax errors occur (rare, but useful). Thanks again. -DaveGreat change
$AWK '$2 ~ /\.(php|phtml|inc|module)$/ {print $2}'`When setting $CHANGED and it works great.One more nit
You need to change your CHANGED line to:
CHANGED=`$SVNLOOK changed --"$TYPE" "$TXN" "$REPOS" | $GREP "^[U|A]" | $AWK '{print $2}' | $GREP \\.php$`Thanks!
That's a good point, ashton. I've corrected the line in the entry. Thanks for your comment!
Thankyou
This script will definitely save me some time in testing, thanks! I only had to change one thing, the php path on my system (running Ubuntu) was /usr/bin/php5 rather than /usr/local/php5/bin/php
Did you try to replace the
Did you try to replace the line PHP="/usr/local/php5/bin/php" with PHP=`which php` ? It can be easier that way.
If `which php` worked you
If `which php` worked you could just call php instead of using $PHP. However that won't work in most cases as $PATH is not (correctly) set in the context of the svn user.
nice script, good comments
thanks... :-)
Hook script in PHP