#!/usr/bin/env python # Copyright (c) 2011 The Chromium Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file at # http://src.chromium.org/viewvc/chrome/trunk/src/LICENSE """Commit bot fake author svn server hook. Looks for svn commit --withrevprop realauthor=foo, replaces svn:author with this author and sets the property commitbot to the commit bot credential to signify this revision was committed with the commit bot. It achieves its goal using an undocumented way. This script could use 'svnlook' to read revprop properties but the code would still be needed to overwrite the properties. http://svnbook.red-bean.com/nightly/en/svn.reposadmin.create.html#svn.reposadmin.create.hooks strongly advise against modifying a transation in a commit because the svn client caches certain bits of repository data. Upon asking subversion devs, having the wrong svn:author cached on the commit checkout is the worst that can happen. This code doesn't care about this issue because only the commit bot will trigger this code, which runs in a controlled environment. The transaction file format is also extremely unlikely to change. If it does, the hook will throw an UnexpectedFileFormat exception which will be silently ignored. """ import os import re import sys class UnexpectedFileFormat(Exception): """The transaction file format is not the format expected.""" def read_svn_dump(filepath): """Returns list of (K, V) from a keyed svn file. Don't use a map so ordering is kept. raise UnexpectedFileFormat if the file cannot be understood. """ class InvalidHeaderLine(Exception): """Raised by read_entry when the line read is not the format expected. """ try: f = open(filepath, 'rb') except EnvironmentError: raise UnexpectedFileFormat('The transaction file cannot be opened') try: out = [] def read_entry(entrytype): header = f.readline() match = re.match(r'^' + entrytype + ' (\d+)$', header) if not match: raise InvalidHeaderLine(header) datalen = int(match.group(1)) data = f.read(datalen) if len(data) != datalen: raise UnpexpectedFileFormat( 'Data value is not the expected length') # Reads and ignore \n if f.read(1) != '\n': raise UnpexpectedFileFormat('Data value doesn\'t end with \\n') return data while True: try: key = read_entry('K') except InvalidHeaderLine, e: # Check if it's the end of the file. if e.args[0] == 'END\n': break raise UnpexectedFileFormat('Failed to read a key: %s' % e) try: value = read_entry('V') except InvalidHeaderLine, e: raise UnpexectedFileFormat('Failed to read a value: %s' % e) out.append([key, value]) return out finally: f.close() def write_svn_dump(filepath, data): """Writes a svn keyed file with a list of (K, V).""" f = open(filepath, 'wb') try: def write_entry(entrytype, value): f.write('%s %d\n' % (entrytype, len(value))) f.write(value) f.write('\n') for k, v in data: write_entry('K', k) write_entry('V', v) f.write('END\n') finally: f.close() def find_key(data, key): """Finds the item in a list of tuple where item[0] == key. asserts if there is more than one item with the key. """ items = [i for i in data if i[0] == key] if not items: return None assert len(items) == 1 return items[0] def handle_commit_bot(repo_path, tx, commit_bot, admin_email): """Replaces svn:author with realauthor and sets commit-bot.""" # The file format is described there: # http://svn.apache.org/repos/asf/subversion/trunk/notes/dump-load-format.txt propfilepath = os.path.join( repo_path, 'db', 'transactions', tx + '.txn', 'props') # Do a lot of checks to make sure everything is in the expected format. try: data = read_svn_dump(propfilepath) except UnexpectedFileFormat: return ( 'Failed to parse subversion server transaction format.\n' 'Please contact %s ASAP with\n' 'this error message.') % admin_email if not data: return ( 'Failed to load subversion server transaction file.\n' 'Please contact %s ASAP with\n' 'this error message.') % admin_email realauthor = find_key(data, 'realauthor') if not realauthor: # That's fine, there is no author to fake. return author = find_key(data, 'svn:author') if not author or not author[1]: return ( 'Failed to load svn:author from the transaction file.\n' 'Please contact %s ASAP with\n' 'this error message.') % admin_email if author[1] != commit_bot: # The author will not be changed and realauthor will be kept as a # revision property. return if len(realauthor[1]) > 50: return 'Fake author was rejected due to being too long.' if not re.match(r'^[a-zA-Z0-9\@\-\_\+\%\.]+$', realauthor[1]): return 'Fake author was rejected due to not passing regexp.' # Overwrite original author author[1] = realauthor[1] # Remove realauthor svn property data.remove(realauthor) # Add svn property commit-bot=<commit-bot username> data.append(('commit-bot', commit_bot)) write_svn_dump(propfilepath, data) def main(): # Replace with your commit-bot credential. commit_bot = 'user1@example.com' admin_email = 'dude@example.com' ret = handle_commit_bot(sys.argv[1], sys.argv[2], commit_bot, admin_email) if ret: print >> sys.stderr, ret return 1 return 0 if __name__ == '__main__': sys.exit(main()) # vim: ts=4:sw=4:tw=80:et: