Subversion/pre-commit
Jump to navigation
Jump to search
#!/bin/sh
#
# The pre-commit hook is invoked before a Subversion txn is
# committed. Subversion runs this hook by invoking a program
# (script, executable, binary, etc.) named 'pre-commit' (this file),
# with the following ordered arguments:
#
# [1] REPOS-PATH (the path to this repository)
# [2] TXN-NAME (the name of the txn about to be committed)
#
# Note that a transaction is ephemeral and once committed, it receives a revision number.
# So pre-commit hooks work on a "transaction id" while post-commit hooks work on a "revision id"
# Revisions don't have a transaction id, and vice-versa.
#
# [STDIN] LOCK-TOKENS ** the lock tokens are passed via STDIN.
#
# If STDIN contains the line "LOCK-TOKENS:\n" (the "\n" denotes a
# single newline), the lines following it are the lock tokens for
# this commit. The end of the list is marked by a line containing
# only a newline character.
#
# Each lock token line consists of a URI-escaped path, followed
# by the separator character '|', followed by the lock token string,
# followed by a newline.
#
# This commit hook doesn't deal with lock tokens
#
# The default working directory for the invocation is undefined, so
# the program should set one explicitly if it cares.
#
# If the hook program exits with success, the txn is committed; but
# if it exits with failure (non-zero), the txn is aborted, no commit
# takes place, and STDERR is returned to the client. The hook
# program can use the 'svnlook' utility to help it examine the txn.
#
# On a Unix system, the normal procedure is to have 'pre-commit'
# invoke other programs to do the real work, though it may do the
# work itself too.
#
# *** NOTE: THE HOOK PROGRAM MUST NOT MODIFY THE TXN, EXCEPT ***
# *** FOR REVISION PROPERTIES (like svn:log or svn:author). ***
#
# This is why we recommend using the read-only 'svnlook' utility.
# In the future, Subversion may enforce the rule that pre-commit
# hooks should not modify the versioned data in txns, or else come
# up with a mechanism to make it safe to do so (by informing the
# committing client of the changes). However, right now neither
# mechanism is implemented, so hook writers just have to be careful.
#
# Note that 'pre-commit' must be executable by the user(s) who will
# invoke it (typically the user httpd runs as), and that user must
# have filesystem-level permission to access the repository.
#
# The hook program typically does not inherit the environment of
# its parent process. For example, a common problem is for the
# PATH environment variable to not be set to its usual value, so
# that subprograms fail to launch unless invoked via absolute path.
# If you're having unexpected problems with a hook program, the
# culprit may be unusual (or missing) environment variables.
#
# This is a simple inline script using the /bin/sh interpreter.
# For more examples and pre-written hooks, see those in
# the Subversion repository at
# http://svn.apache.org/repos/asf/subversion/trunk/tools/hook-scripts/ and
# http://svn.apache.org/repos/asf/subversion/trunk/contrib/hook-scripts/
#
#################################################################################
## Block commits that do not reference a JIRA issue number in the commit msg ##
## Greg Rundlett = eQuality Technology = info@eQuality-Tech.com ##
## Based in part on https://github.com/qazwart/SVN-Precommit-Kitchen-Sink-Hook ##
#################################################################################
# The commit message is the svn:log revision property
# We want a pre-commit hook that
# ensures there is a commit message of a minimum length
# ensures that the commit message contains a reference to a JIRA issue ID (eg. SEF-123)
# each JIRA issue ID referenced is a valid JIRA issue ID
# enforcement is only applied to specific branches (e.g. benu-2.1.0-31 and trunk)
REPOS="$1"
ID="$2"
# normally the second argument to a pre-commit hook is called the TXN for transaction id
# but in this script we call it ID
# because since svnlook changes the option based on the context in which
# you are using svnlook
# we call it ID so we can easily test this script against existing commits by using -r instead of -t
################################################################################
## CONFIGURATION ##
## ##
JIRA_URL="http://jira:8080"
JIRA_DASHBOARD="$JIRA_URL/secure/Dashboard.jspa"
COOKIE_FILE=/tmp/jira.cookie
username=bob
password="who's your uncle"
# define what a JIRA issue key looks like, using bash extended regular expression syntax
PATTERN="[A-Za-z]{3,4}-[0-9]+"
# set the minimum allowed length of a comment (characters)
MINLEN=1
# setup paths because we don't have an environment
SVNLOOK=/usr/bin/svnlook
WC=/usr/bin/wc
CURL=/usr/bin/curl
# also define built-in binaries to be sure what we're using
GREP=/bin/grep
CAT=/bin/cat
ECHO=/bin/echo
RM=/bin/rm
# For testing purposes, you can mock the arguments
# Just set TEST to true specify the ID and REPOS path manually
TEST=false
# TEST=true
# use a transaction id for the commit hook, but a revision id for testing
if [[ $TEST = true ]]; then
TYPE="-r"
else
TYPE="-t"
fi
# REPOS='/home_local/csvn/data/repositories/test/'
# ID=206 # has a JIRA key
# ID=207 # has a quote in the JIRA summary
# Enforce only on certain paths
# http://www.math.utah.edu/docs/info/gawk_5.html
RULE1=$($SVNLOOK changed "$TYPE" "$ID" "$REPOS" | awk '$2 ~ /trunk\//')
RULE2=$($SVNLOOK changed "$TYPE" "$ID" "$REPOS" | awk '$2 ~ /branches\/benu-2.1.0-31\//')
if [ -n "$RULE1" -o -n "$RULE2" ]; then
ENFORCE=true
echo "This path is under more stringent rules for commit messages." 1>&2
else
echo "Not enforcing any checks on this commit" 1>&2
:
fi
## ##
## ##
## END CONFIGURATION ##
################################################################################
# capture the commit message for easier usage
MSG=$($SVNLOOK log "$TYPE" "$ID" "$REPOS")
# get the length of the comment
MSGLEN=$($ECHO $MSG | $WC -c)
# Make sure that the log message contains some text (MINLEN or more characters).
$SVNLOOK log "$TYPE" "$ID" "$REPOS" | $GREP -E "[a-zA-Z0-9]{$MINLEN,}" > /dev/null || \
read -d '' ERRORMSG <<HERE
[ERROR]: Commit rejected, message too short
A commit message of more than $MINLEN characters is required
HERE
# Make sure the log message contains a JIRA issue key
#
#
# Format for check in comment:
# SEFP-1234: This is my comment
# SEFP-1345, SEFP-3424, SEFP-1231: This is my comment
#
# find them in the commit message and put them into an array
JIRA_KEYS=( $($SVNLOOK log "$TYPE" "$ID" "$REPOS" | $GREP --extended-regexp --only-matching $PATTERN) ) > /dev/null || \
read -d '' ERRORMSG2 <<HERE
[ERROR]: Commit rejected, no JIRA issue ID
The commit message must start with or contain at least one JIRA issue ID.
HERE
## A function to authenticate to the JIRA server
## We could also create a "trusted application" link in JIRA so that authentication would not be required
## This authentication uses basic auth and cookies
## The cookie file is cleaned up after validation is complete and no connection is needed
connect(){
$CURL -s -u $username:$password --cookie-jar $COOKIE_FILE --output /dev/null $JIRA_DASHBOARD || \
echo "Failed to Authenticate against $JIRA_URL"
if [ ! -f $COOKIE_FILE ]; then
echo "Could not create the cookie file $COOKIE_FILE" 1>&2
exit 1
fi
}
## A function to query the REST API of JIRA for the existence of the JIRA ISSUE KEY
## The query_jira command expects a JIRA KEY as an argument
query_jira(){
# if the argument is zero-length or not passed
if [ -z "$1" ]; then
echo "Can not query JIRA without an issue key; no key passed to query_jira()"
echo ""
exit 2
fi
JSON_URL="$JIRA_URL/rest/api/latest/issue/$1.json"
JSON_FILE="/tmp/$1.json"
$CURL -s --cookie $COOKIE_FILE $JSON_URL -o $JSON_FILE || \
echo "Failed to Collect $1 from $JIRA_URL" 1>&2
if [[ $TEST = true ]]; then
echo "Query complete."
#echo "showing $JSON_FILE"
#$CAT $JSON_FILE
#echo ""
fi
# $RM $JSON_FILE
}
## A function to get the 'summary' field from the JSON file
## The regex is using look-behind and look-ahead assertions to be able to isolate the
## key we are looking for ("summary"), and the ? modifier after the .* makes the match
## non-greedy so we get only the contents up to the next quote
get_jira_summary(){
SUMMARY=$($GREP --perl-regexp --only-matching '(?<=\"summary\":\").*?(?=\",")' $JSON_FILE)
# the JSON string has any double quotes escaped, so unescape them globally throughout the string
SUMMARY=$( echo $SUMMARY | sed 's/\\\"/"/g')
if [[ $TEST = true ]]; then
echo "Summary: $SUMMARY"
# exit 6
fi
}
## A function to set the SVN:LOG property of the transaction
## using Python because you can only do this using the Python bindings to SVN
set_log_message(){
$( /home/bob/pre-commit.py "$REPOS" "$ID" "$(echo $SUMMARY)" )
if [ $? != 0 ]; then
echo "Unable to set_log_message()"
exit 7
fi
}
## inspect the contents of the JSON to see that it contains the key e.g. "key":"SEFP-223"
## some other failure may have occured (auth).
## There isn't (at least in version 5.0) a specific method to just verify the ID
## https://developer.atlassian.com/static/rest/jira/5.0.html#id199677
## We could just retrieve the key field, or additionally, we could work with other fields and even
## use the API to add the JIRA summary to the svn comment
validate_jira_key(){
# if the argument is zero-length or not passed
if [ -z "$1" ]; then
echo "Can not validate without an issue key; no key passed to validate()" 1>&2
echo ""
exit 3
else
# uppercase whatever is found in the comment because from JIRA it will always be uppercase
JIRA_KEY="$(echo $1 | tr [a-z] [A-Z] )"
echo "Validating JIRA_KEY $JIRA_KEY in the commit by searching for it in the JSON file" 1>&2
fi
# extended syntax like this requires more processing because it spits out matches as "key":"SEF-3660"
# KEY=$( $GREP --extended-regexp --only-matching "\"key\":\"$PATTERN\"" $JSON_FILE )
# pcre allows us to use look-behind and look-ahead assertions
KEY=$( $GREP --perl-regexp --only-matching "(?<=\"key\":\")$PATTERN(?=\")" $JSON_FILE )
# echo "Comparing KEY $KEY with JIRA_KEY $JIRA_KEY" 1>&2
if [ -n $KEY ]; then echo "key not found in the JSON file" 1>&2
fi
if [ "$KEY" != "$JIRA_KEY" ]; then
echo "The JIRA issue ID $JIRA_KEY does not appear to be valid; or some other failure (I/O, perms?) occured."
exit 4
fi
# For debugging purposes, just exit this routine with non-zero to see the output of STDERR
# exit 10
}
# If we have an error message, then display it and exit non-zero so that the commit is aborted.
if [ -n "$ERRORMSG" -o -n "$ERRORMSG2" ] && [ -n "$ENFORCE" ]; then
echo -e "$ERRORMSG" 1>&2
echo -e "$ERRORMSG2" 1>&2
#echo "the commit message was"
#echo ""
#echo -e "$MSG" 1>&2
exit 5
else
# All preliminary checks passed, so validate the JIRA issue ids
connect
# loop through all the JIRA keys found in the commit message
for index in ${!JIRA_KEYS[*]}
do
if [[ $TEST = true ]]; then
printf "%4d: %s\n" $index "${JIRA_KEYS[$index]}"
fi
query_jira "${JIRA_KEYS[$index]}"
validate_jira_key "${JIRA_KEYS[$index]}"
if [ $index = 0 ]; then
get_jira_summary
SUMMARY="${JIRA_KEYS[$index]} $SUMMARY"
elif [ $index = 1 ]; then
SUMMARY="$SUMMARY See also: ${JIRA_KEYS[$index]}"
else
SUMMARY="$SUMMARY, ${JIRA_KEYS[$index]}"
fi
$RM -f $JSON_FILE
done
set_log_message
$RM -f $COOKIE_FILE
exit 0
fi