Subversion/pre-commit

From Freephile Wiki
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