A Subversion Pre-Commit Hook

Subversion allows you to run commands, called hook scripts, whenever actions take place in your repository. The most useful of these that I’ve found is the pre-commit hook, which allows you check a transaction for problems before it is committed.

Word Aligned has an excellent introduction on setting up pre-commit hooks and includes a small example. I use a more complex script on my own server, and thought it might be of use to somebody else. It verifies the following:

  • The commit message is at least 10 characters long
  • The commit message is different from the previous message (handy when hit the up arrow and accidentally try to commit again)
  • Tabs are used for indentation instead of spaces (sorry, I’m a tabs guy – you can change it if you need to)
  • PHP files don’t have syntax errors
  • C and C++ files don’t have any digraphs or trigraphs

#!/usr/bin/python
import sys

# These should point to the respective commands
SVNLOOK = '/usr/bin/svnlook'
PHP = '/usr/bin/php'
MIN_MESSAGE_LENGTH = 10

# Gets a command's output
def commandOutput(command):
	import subprocess
	process = subprocess.Popen(command.split(), stdout = subprocess.PIPE)
	return process.communicate()[0] # 0 is stdout


# Returns an array of the changed files' names
def getChangedFiles(svnPath, transaction):
	# Run svnlook to find the files that were changed
	output = commandOutput('%s changed %s --transaction %s' % (SVNLOOK, svnPath, transaction))

	# The output from svnlook looks like the following:
	# U   folder/file1.cpp
	# A   folder/file2.cpp
	# where U means updated and A means added
	def changed(fileName):
		return len(line) > 0 and line[0] in ('A', 'U')
	changedFiles = [line[4:] for line in output.split('\n') if changed(line)]

	# svnlook inserts an empty line, so output.split() will have an extra
	# line with nothing in it - ignore the last lines if they're empty
	while 0 == len(changedFiles[-1]):
		changedFiles = changedFiles[:-1]

	return changedFiles


# Checks that the message is a minimum length
def checkMessageLen(svnPath, transaction, minLength):
	# Run svnlook to get the message log
	output = commandOutput('%s log %s --transaction %s' % (SVNLOOK, svnPath, transaction))

	if len(output) < minLength:
		sys.stderr.write('Message must be at least %d characters\n' % minLength)
		return 1
	else:
		return 0


# Checks that the message is different from the last message
def checkRepeatMessage(svnPath, transaction):
	# Run svnlook to get the message log
	output = commandOutput('%s log %s --transaction %s' % (SVNLOOK, svnPath, transaction))

	currentMessage = output

	lastRevision = int(transaction.split('-')[0]) - 1

	# Empty repositories have no messages
	if lastRevision  0:
			sys.stderr.write('Regex \'%s\' matched \'%s\' on line(s): %s\n' % (regex, fileName, badLines))

	return matches


# Checks a PHP file for syntax errors
def checkPHPSyntax(svnPath, transaction, fileName):
	# Run svnlook to get the file contents
	output = commandOutput('%s cat %s %s --transaction %s' % (SVNLOOK, svnPath, fileName, transaction))

	# Run PHP syntax checking on the file contents
	command = (PHP, '-l')
	process = subprocess.Popen(command, stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr = subprocess.PIPE)
	output = process.communicate(fileContents)[0]
	process.wait()

	if 0 != process.returncode:
		sys.stderr.write('Found syntax errors in \'%s\':' % fileName)
		# Print the offending lines that were returned from php -l
		for line in output:
			sys.stderr.write(line)
		return 1 # Error found

	return 0 # No errors


svnPath = sys.argv[1]
transaction = sys.argv[2]
files = getChangedFiles(svnPath, transaction)

errorCount = checkMessageLen(svnPath, transaction, MIN_MESSAGE_LENGTH)
errorCount += checkRepeatMessage(svnPath, transaction)

for file in files:
	# Prefer tabs for indentation
	# Skip .txt files
	if not file.endswith(".txt") and not file.endswith(".sql"):
		errorCount = errorCount + stringsInFile(svnPath, transaction, [' {4}'], file)

	# Check PHP syntax
	if file.endswith('.php'):
		errorCount += checkPHPSyntax(svnPath, transaction, file)

	# C and C++ files should not use digraphs or trigraphs
	digraph = '%s|%s|%s|%s|%s' % ('<:', '', ':>', '%:')
	trigraph = '\?\?[=/\'()!-]'
	if file.endswith(('.c', '.cpp', '.cxx', '.cc', '.C', '.h', '.hpp')):
		errorCount += stringsInFile(svnPath, transaction, [digraph, trigraph], file)

sys.exit(errorCount)
Advertisements
This entry was posted in Uncategorized and tagged . Bookmark the permalink.

2 Responses to A Subversion Pre-Commit Hook

  1. HX_unbanned says:

    Damn. This one is good. I will try to upload a Delphi included check here. Maybe it will help others that use space-indentation …

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s