A Subversion Pre-Commit Hook
Subversion’s hook scripts provide a powerful and flexible way to associate actions with repository events. For example, the pre-commit hook allows you to check — and possibly abort — a transaction before it actually gets committed. This entry describes how to install and test a simple Python hook script to prohibit tabs from C++ files.
Creating and Installing a Hook Script
Your Subversion repository already has some template hook scripts. For
example, the pre-commit template is in
PATH_TO_REPOS/hooks/pre-commit.tmpl
. These templates contain
instructions on what the hook script does and what parameters it can expect.
Here, then, is the most direct route to creating and activating a pre-commit hook:
su - svn # As user svn cd PATH_TO_REPOS/hooks # Change to the hooks directory cp pre-commit.tmpl pre-commit # Create a pre-commit script emacs pre-commit # Edit to taste chmod u+x pre-commit # It needs to be executable
And that’s it.
You haven’t actually tested your new hook script so it probably doesn’t do what you meant it to. Either you can sit back and wait for users to complain or you can think of a way to test your hook before you install it.
System Testing a Hook Script
I’ve used a couple of strategies:
- Set up a throw away temporary repository and use this as a sandbox in which to develop your hook script.
- Find a way to test your hook script on the live repository before you actually install it.
In the case of a pre-commit hook the second strategy is quite attractive. Pre-commit hooks are typically used to enforce checks on a transaction before it gets committed and becomes a new repository revision. They are often introduced when a faulty revision gets into the repository and someone says: “Couldn’t Subversion have stopped this from happening?”
With a little ingenuity you can test how the pre-commit hook would have responded to such a faulty transaction which, in future, we would like to prohibit.
Svnlook
Svnlook
is the Subversion administrator’s friend. It can be used
on the Subversion server to examine the repository without changing
it. A hook script typically uses svnlook
to examine a repository
event and take appropriate action. Thus, for example, to see what
files were changed by revision 1234, we run:
svnlook changed PATH_TO_REPOS --revision 1234
To find what files might be changed by transaction 1234-1 (if nothing goes wrong) the command is similar:
svnlook changed PATH_TO_REPOS --transaction 1234-1
Examining transactions using svnlook
is tricky since transactions
are transient. When a transaction becomes a revision you can no longer
look at it.
The Pre-commit Hook
The pre-commit
hook gives you an opportunity to catch the
transaction just before it becomes a revision. Subversion
passes this hook two parameters:
- the path to the root of the repository
- the transaction identifier
The pre-commit can fail the transaction by printing an informative message to standard error and returning non-zero. A return code of zero allows the transaction to complete successfully.
For testing we can add an extra switch, --revision
, to our test
pre-commit hook. This switch is to indicate that the second parameter
is in fact a revision number. Now we can system-test our hook script
on exisiting repository revisions, and confirm that it does indeed
return non-zero for the bad ones and zero for the good ones.
test-pre-commit PATH_TO_REPOS --revision 1234
An Example Pre-commit Hook
Here, then, is a pre-commit hook to ban the TAB character from C++
source files. Test it using the --revision
option. Command line
help is available using --help
.
It uses two different flavours of svnlook
to examine the repository:
svnlook changed
— to find which files were changed by a transaction/revisionsvnlook cat
— to find the contents of a file in a transaction/revision
#!/bin/env python " Example Subversion pre-commit hook. " def command_output(cmd): " Capture a command's standard output. " import subprocess return subprocess.Popen( cmd.split(), stdout=subprocess.PIPE).communicate()[0] def files_changed(look_cmd): """ List the files added or updated by this transaction. "svnlook changed" gives output like: U trunk/file1.cpp A trunk/file2.cpp """ def filename(line): return line[4:] def added_or_updated(line): return line and line[0] in ("A", "U") return [ filename(line) for line in command_output(look_cmd % "changed").split("\n") if added_or_updated(line)] def file_contents(filename, look_cmd): " Return a file's contents for this transaction. " return command_output( "%s %s" % (look_cmd % "cat", filename)) def contains_tabs(filename, look_cmd): " Return True if this version of the file contains tabs. " return "\t" in file_contents(filename, look_cmd) def check_cpp_files_for_tabs(look_cmd): " Check C++ files in this transaction are tab-free. " def is_cpp_file(fname): import os return os.path.splitext(fname)[1] in ".cpp .cxx .h".split() cpp_files_with_tabs = [ ff for ff in files_changed(look_cmd) if is_cpp_file(ff) and contains_tabs(ff, look_cmd)] if len(cpp_files_with_tabs) > 0: sys.stderr.write("The following files contain tabs:\n%s\n" % "\n".join(cpp_files_with_tabs)) return len(cpp_files_with_tabs) def main(): usage = """usage: %prog REPOS TXN Run pre-commit options on a repository transaction.""" from optparse import OptionParser parser = OptionParser(usage=usage) parser.add_option("-r", "--revision", help="Test mode. TXN actually refers to a revision.", action="store_true", default=False) errors = 0 try: (opts, (repos, txn_or_rvn)) = parser.parse_args() look_opt = ("--transaction", "--revision")[opts.revision] look_cmd = "svnlook %s %s %s %s" % ( "%s", repos, look_opt, txn_or_rvn) errors += check_cpp_files_for_tabs(look_cmd) except: parser.print_help() errors += 1 return errors if __name__ == "__main__": import sys sys.exit(main())
Feedback
-
In which languages can you write a hook? thank you very much for the tutorial!
-
You can write the hook in whatever language you choose. Subversion will execute it provided 1) it is executable and 2) it has the name "pre-commit". If you use a scripting language (e.g. Python, Perl, Bash) then make sure the shebang line will locate the relevant executable.
-
If writing hooks on a windows system your actual hook file, e.g "pre-commit" has to be either a .exe or a .bat
I personally write most of my scripts in Perl and call them from the corresponding .bat file, remembering to pass the arguments to the script.
BEWARE: When Subversion calls the .bat file it does so with no environmental variables set, you will therefore have to manually set PATH= so that you do not receive "XYZ is not recognised as an internal or external command" errors.
-
I write them in Haskell
-
I write them in Haskell
-
Thanks for the tips Mounty. I have set up a Subversion server on Windows before and it worked just fine, but I admit I'm more comfortable using a Unix-based server.
-
Cheers Tony. I've not used Haskell myself. I'm experimenting with Scheme in my spare time, and often adopt a functional style of programming in other languages.
-
I just wanted to thank you - I've been trying to setup SVN commit hooks and nowhere did I find that your hook filenames should be "pre-commit" w/o an extension. Your example helped me to get my hooks working!
-
nice article, thank you for this!
-
BEWARE: When Subversion calls the .bat file it does so with no environmental variables set, you will therefore have to manually set PATH= so that you do not receive "XYZ is not recognised as an internal or external command" errors.
:(((
-
nice article, thank you for this!