SVN pre-commit hook

某团队希望做到Continuous Code Review, 想在每次check-in 到SVN之前,先判断特定用户群体否在commit log里面包含了”Review By: xxx”的字样。

记得以前NSN里面有人用过这个法子,记不太清了。

于是研究了一下脚本,其实SVN/GIT都提供了类似的hook, 在<your repository>/hooks 目录下,都是shell或cmd脚本(要看服务器的操作系统了),会在不同的事件时触发。

为了实现更复杂的功能或者需要跨平台,那不妨用shell或cmd去调用Python脚本咯。

SVN的基本原理就是用”svnlook”命令来查看服务器的元数据,比如”svnlook info REPOS_PATH –revision 23“(见http://i18n-zh.googlecode.com/svn/www/svnbook-1.4/svn.ref.svnlook.c.info.html

其中 REPOS_PATH是服务器上的绝对路径。–revision后面跟已有版本号码,而–transaction后面跟提交事务ID,也就是我们要利用的。

 

shell脚本如下,其中路径根据需要自行修改:

# pre-commit.sh
# %1 is REPOS_PATH passed in by svn server
# %2 is transaction ID passed in by svn server
python ./pre-commit.py %1 %2

 

Python脚本根据网上的例子修改,除了检查commit log,还可以通过”svnlook changed“命令来检查改动文件的内容。

#!/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 txn_log(look_cmd):
 """ Print the author, datestamp, log message size (in bytes), 
 and log message, followed by a newline character.
"svnlook info" gives output like:
 sally
 2003-02-22 17:44:49 -0600 (Sat, 22 Feb 2003)
 16
 Rearrange lunch.
 """
 print look_cmd
 lines = command_output(look_cmd % "info").splitlines()
 return [lines[0], ''.join(lines[3:])]
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 check_changed_file():
 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
def check_txn_log():
 usage = """usage: %prog REPOS
Run pre-commit options on a repository last log."""
 from optparse import OptionParser
 parser = OptionParser(usage=usage)
 errors = 0
 try:
 (opts, (repos)) = parser.parse_args()
 print parser.parse_args()
 look_cmd = "svnlook %s -t %s %s " % (
 "%s", repos[1], repos[0])
 (user, message) = txn_log(look_cmd)
 print user
 print message
user_list = ['jacky', 'jimmy']
 if not 'review by' in message.lower() and user in user_list:
 errors = 33
 except:
 parser.print_help()
 errors = 1
 return errors
if __name__ == "__main__":
 import sys
 sys.exit(check_txn_log())