#!/usr/bin/python # svn-hook-postcommit-review # This script should be invoked from the subversion post-commit hook like this: # # REPOS="$1" # REV="$2" # /usr/bin/python /some/path/svn-hook-postcommit-review "$REPOS" "$REV" || exit 1 # # Searches the commit message for text in the form of: # publish review - publishes a review request # draft review - creates a draft review request # # The space before 'review' may be ommitted. # # The log message is interpreted for review request parameters: # summary = up to first period+space, first new-line, or 250 chars # description = entire log message # existing review updated if log message includes 'update review:[0-9]+' # bugs added to review if log message includes commands as defined in # supported_ticket_cmds # # By default, the review request is created out of a diff between the current # revision (M) and the previous revision (M-1). # # To create a diff that spans multiple revisions, include # 'after revision:[0-9]+' in the log message. # # To limit the diff to changes in a certain path (e.g. a branch), include # 'base path:""' in the log message. The path must be relative to # the root of the repository and be surrounded by single or double quotes. # # An example commit message is: # # Changed blah and foo to do this or that. Publish review ticket:1 # update review:2 after revision:3 base path:'internal/trunk/style'. # # This would update the existing review 2 with a diff of changes to files under # the style directory between this commit and revision 3. It would place # the entire log message in the review summary and description, and put # bug id 1 in the bugs field. # # This script may only be run from outside a working copy. # # # User configurable variables # # Path to post-review script POSTREVIEW_PATH = "" # Username and password for Review Board user that will be connecting # to create all review requests. This user must have 'submit as' # privileges, since it will submit requests in the name of svn committers. USERNAME = 'su_user' PASSWORD = 'TYxxcGm337FtubqN' # If true, runs post-review in debug mode and outputs its diff DEBUG = False # # end user configurable variables # import sys import os import subprocess import re import svn.fs import svn.core import svn.repos # list of trac commands from trac-post-commit-hook.py. # numbers following these commands will be added to the bugs # field of the review request. supported_ticket_cmds = {'review': '_cmdReview', 'publishreview': '_cmdReview', 'publish review': '_cmdReview', 'draftreview': '_cmdReview', 'draft review': '_cmdReview'} ticket_prefix = '(?:#|(?:ticket|issue|bug)[: ]?)' ticket_reference = ticket_prefix + '[0-9]+' ticket_command = (r'(?P[A-Za-z]*).?' '(?P%s(?:(?:[, &]*|[ ]?and[ ]?)%s)*)' % (ticket_reference, ticket_reference)) def execute(command, env=None, ignore_errors=False): """ Utility function to execute a command and return the output. Derived from Review Board's post-review script. """ if env: env.update(os.environ) else: env = os.environ p = subprocess.Popen(command, stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr = subprocess.STDOUT, shell = False, close_fds = sys.platform.startswith('win'), universal_newlines = True, env = env) data = p.stdout.read() rc = p.wait() if rc and not ignore_errors: sys.stderr.write('Failed to execute command: %s\n%s\n' % (command, data)) sys.exit(1) return data def main(): if len(sys.argv) != 3: sys.stderr.write('Usage: %s \n' % sys.argv[0]) sys.exit(1) repos = sys.argv[1] rev = sys.argv[2] # verify that rev parameter is an int try: int(rev) except ValueError: sys.stderr.write("Parameter must be an int, was given %s\n" % rev) sys.exit(1) # get the svn file system object fs_ptr = svn.repos.svn_repos_fs(svn.repos.svn_repos_open( svn.core.svn_path_canonicalize(repos))) # get the log message log = svn.fs.svn_fs_revision_prop(fs_ptr, int(rev), svn.core.SVN_PROP_REVISION_LOG) # error if log message is blank if len(log.strip()) < 1: sys.stderr.write("Log message is empty, no review request created\n") sys.exit(1) # get the author author = svn.fs.svn_fs_revision_prop(fs_ptr, int(rev), svn.core.SVN_PROP_REVISION_AUTHOR) # error if author is blank if len(author.strip()) < 1: sys.stderr.write("Author is blank, no review request created\n") sys.exit(1) # check whether to create a review, based on presence of word # 'review' with prefix review = r'(?:publish|draft)(?: )?review' if not re.search(review, log, re.M | re.I): print 'No review requested' sys.exit(0) # check for update to existing review m = re.search(r'update(?: )?review:([0-9]+)', log, re.M | re.I) if m: reviewid = '--review-request-id=' + m.group(1) else: reviewid = '' # check whether to publish or leave review as draft if re.search(r'draft(?: )?review', log, re.M | re.I): publish = '' else: publish = '-p' # get previous revision number -- either 1 prior, or # user-specified number m = re.search(r'after(?: )?revision:([0-9]+)', log, re.M | re.I) if m: prevrev = m.group(1) else: prevrev = int(rev) - 1 # check for an explicitly-provided base path (must be contained # within quotes) m = re.search(r'base ?path:[\'"]([^\'"]+)[\'"]', log, re.M | re.I) if m: base_path = m.group(1) else: base_path = '' # get bug numbers referenced in this log message ticket_command_re = re.compile(ticket_command) ticket_re = re.compile(ticket_prefix + '([0-9]+)') ticket_ids = [] ticket_cmd_groups = ticket_command_re.findall(log) for cmd, tkts in ticket_cmd_groups: funcname = supported_ticket_cmds.get(cmd.lower(), '') if funcname: for tkt_id in ticket_re.findall(tkts): ticket_ids.append(tkt_id) if ticket_ids: bugs = '--bugs-closed=' + ','.join(ticket_ids) else: bugs = '' # summary is log up to first period+space / first new line / first 250 chars # (whichever comes first) summary = '--summary=' + log[:250].splitlines().pop(0).split('. ').pop(0) # other parameters for postreview repository_url = '--repository-url=file://' + repos password = '--password=' + PASSWORD username = '--username=' + USERNAME description = "--description=(In [%s]) %s" % (rev, log) submitas = '--submit-as=' + author revision = '--revision-range=%s:%s' % (prevrev, rev) # common arguments args = [repository_url, username, password, publish, submitas, revision, base_path, reviewid] # filter out any potentially blank args, which will confuse post-review args = [i for i in args if len(i) > 1] # if not updating an existing review, add extra arguments if len(reviewid) == 0: args += [summary, description, bugs] if DEBUG: args += ['-d', '--output-diff'] print [os.path.join(POSTREVIEW_PATH, 'post-review')] + args # Run Review Board post-review script data = execute([os.path.join(POSTREVIEW_PATH, 'post-review')] + args, env = {'LANG': 'en_US.UTF-8'}) if DEBUG: print data if __name__ == '__main__': main()