syntax-check.sh   [plain text]


#!/bin/bash
#
# SVN Syntax Check Hook Script
# Copyright (c) 2007, Lucas Nealan <lucas@sizzo.org>, Facebook Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  
# 02110-1301  USA
#
# --------------------------------------------------------------------
#
# This script provides language independant syntax checking 
# functionality intended to be invoked from a subversion pre-commit
# hook.
#
# Invocation: /path/to/syntax-check $1 $2
#         or: source syntax-check 
#
# Requires bash 3.x or higher.
#

FPATTERN="\.\(php\|phpt\)$"
FLANG="PHP" 
SYNTAX_CMD="php"
SYNTAX_ARGS="-l"
# address to email notifications of syntax errors
NOTIFY_SYNTAX="user@domain.tld"
# adderss to notify for syntax script failures
NOTIFY_ERROR="user@domain.tld" 
# log of syntax errors, must be writable by svn server users
SYNTAX_LOG="/tmp/syntax.log"
BYPASSPW="change_this_syntax_bypass_password_please"

[ -z "$REPOS" ] && REPOS="$1"
[ -z "$TXN" ]   && TXN="$2"
[ -z "$MODE" ]  && if [ -n "$3" ]; then MODE="$3"; else MODE="-t"; fi
[ -z "$SVNLOOK" ] && SVNLOOK=svnlook
[ -z "$LOG" ] && LOG=`$SVNLOOK log $MODE "$TXN" "$REPOS"`
[ -z "$DIFF" ] && DIFF=`$SVNLOOK diff $MODE "$TXN" "$REPOS"`
[ -z "$AUTHOR" ] && AUTHOR=`$SVNLOOK author $MODE "$TXN" "$REPOS"`
[ -z "$CHANGEDFILES" ] && CHANGEDFILES=`$SVNLOOK changed $MODE "$TXN" "$REPOS"`
[ -n "$SYNTAXENABLED" ] && SYNTAXENABLED="1"

syntaxclean() {
  [ -d $1 ] && rm -rf $1
}

syntaxexit() {
  echo $1 >> /dev/stderr
  echo $1 >> $SYNTAX_LOG

  # save working dir and debug files
  if [ -d $WORKING ]; then
    [ -n "$DIFF" ] && echo $"$DIFF" > $WORKING/patch
    [ -n "$LOG" ] && echo $"$LOG" > $WORKING/svnlog
    [ -n "$CHANGED" ] && echo $"$CHANGED" > $WORKING/changed
    mv $WORKING $WORKING.failed
  fi

  [ -n "$NOTIFY_ERROR" ] && echo "$1 ($WORKING.failed)" | mail -s "syntax commit failed" $NOTIFY_ERROR

  # clean working dir if specified
  [ -n $2 ] && syntaxclean $2
  exit 1
}

function strlpad() {
  STR="$1"
  CHAR=$2
  LEN=$3
  L=${#STR}
  D=$((LEN-L))
  echo "$STR""`printf '%'$D's'| tr \  \"$CHAR\"`"
}

function errormessage() {
  echo
  HEADER=`strlpad "Error" "-" 76`
  echo "|--$HEADER|" >&2
  SPACES="`strlpad '' ' ' 78`"
  echo "|$SPACES|" >&2
  ERROR=`strlpad "$1" " " 77`
  echo "| $ERROR|" >&2
  echo "|$SPACES|" >&2
  LINE=`strlpad "" "-" 78`
  echo "|$LINE|" >&2
  echo 
}

if [ "$SYNTAXENABLED" == "1" ]; then
  # allow selective bypass of syntax check for commits
  [[ "$LOG" =~ "$BYPASSPW" ]] && return;

  # get changed file list and count
  NUMMATCHCHANGED=0
  if [ -n "$CHANGEDFILES" ]; then
    MATCHCHANGED=`echo $"$CHANGEDFILES" | grep "$FPATTERN"`
    [ -n "$MATCHCHANGED" ] && NUMMATCHCHANGED=`echo $"$MATCHCHANGED" | wc -l`
  fi

  # make sure matched files were changed
  if [ $NUMMATCHCHANGED -gt 0 ]; then
    # create temporary working directory 
    WORKING=/tmp/$(basename $0).$$
    [ -d $WORKING ] && rm -rf $WORKING
    mkdir $WORKING || syntaxexit "failed to create temp dir for syntax check: $WORKING"
    cd $WORKING

    # export changed files (no dirs) from local repo (speed)
    IFS=$'\n'
    for LINE in $MATCHCHANGED; do
      IFS=' '
      WORDS=($LINE)
      FSTATUS=${WORDS[0]}
      FNAME=${WORDS[1]}

      # only export modified and deleted files. new files wont exist in repo yet
      if [ "$FSTATUS" == "U" ] || [ "$FSTATUS" == "UU" ] || [ "$FSTATUS" == "A" ]; then
        TMPFNAME=${FNAME//\//.}
        $SVNLOOK cat $MODE "$TXN" "$REPOS" $FNAME > $TMPFNAME

        file `which $SYNTAX_CMD` || syntaxexit "unablet to find systax command binary: $SYNTAX_CMD"
        SYNTAXERROR=`$SYNTAX_CMD $SYNTAX_ARGS $TMPFNAME 2> $WORKING/$TMPFNAME.STDERR`
        SYNTAXRETURN=$?
        [ -s "$WORKING/$TMPFNAME.STDERR" ] && SYNTAXWARNING=`cat $WORKING/$TMPFNAME.STDERR`

        if [ "$SYNTAXRETURN" -ne 0 ] || [ -n "$SYNTAXWARNING" ]; then
          [ -n "$SYNTAXWARNING" ] && SYNTAXERROR=$SYNTAXWARNING

          # cleanup HTML out of PHP parse error so a human can read it
          if [ "$FLANG" == "PHP" ]; then
            SYNTAXERROR=`echo $SYNTAXERROR | sed -e 's/<[^<]*>//g' | cut -d',' -f 2`
            SYNTAXERROR=`echo $SYNTAXERROR | sed -e 's/\(on line [0-9]* \)/\1\n/g'`
          fi
          
          # sloppy email notification
          ETMP=$WORKING/sloppy.txt
          echo "$FLANG Syntax Error: $SYNTAXERROR" > $ETMP
          echo >> $ETMP
          echo Log: $LOG >> $ETMP
          echo >> $ETMP
          echo $"$DIFF" >> $ETMP
          cat $ETMP | mail -s "SVN SYNTAX ERROR: $AUTHOR" $NOTIFY_SYNTAX
          rm -f $ETMP

          echo "$AUTHOR: $FLANG Syntax Error: $SYNTAXERROR" >> $SYNTAX_LOG
          errormessage "$FLANG Syntax Error: $SYNTAXERROR"
          syntaxclean $WORKING
          exit 1
        fi
      fi
    done
    # exit within a loop only sets the return value of the loop itself, check this to exit
    [ $? -ne 0 ] && exit 1
  fi 
  syntaxclean $WORKING
fi