OLD | NEW |
(Empty) | |
| 1 #!/usr/bin/env python |
| 2 # Copyright (c) 2011 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file at |
| 5 # http://src.chromium.org/viewvc/chrome/trunk/src/LICENSE |
| 6 |
| 7 """Commit bot fake author svn server hook. |
| 8 |
| 9 Looks for svn commit --withrevprop realauthor=foo, replaces svn:author with this |
| 10 author and sets the property commitbot to the commit bot credential to signify |
| 11 this revision was committed with the commit bot. |
| 12 |
| 13 It achieves its goal using an undocumented way. This script could use 'svnlook' |
| 14 to read revprop properties but the code would still be needed to overwrite the |
| 15 properties. |
| 16 |
| 17 http://svnbook.red-bean.com/nightly/en/svn.reposadmin.create.html#svn.reposadmin
.create.hooks |
| 18 strongly advise against modifying a transation in a commit because the svn |
| 19 client caches certain bits of repository data. Upon asking subversion devs, |
| 20 having the wrong svn:author cached on the commit checkout is the worst that can |
| 21 happen. |
| 22 |
| 23 This code doesn't care about this issue because only the commit bot will trigger |
| 24 this code, which runs in a controlled environment. |
| 25 |
| 26 The transaction file format is also extremely unlikely to change. If it does, |
| 27 the hook will throw an UnexpectedFileFormat exception which will be silently |
| 28 ignored. |
| 29 """ |
| 30 |
| 31 import os |
| 32 import re |
| 33 import sys |
| 34 |
| 35 |
| 36 class UnexpectedFileFormat(Exception): |
| 37 """The transaction file format is not the format expected.""" |
| 38 |
| 39 |
| 40 def read_svn_dump(filepath): |
| 41 """Returns list of (K, V) from a keyed svn file. |
| 42 |
| 43 Don't use a map so ordering is kept. |
| 44 |
| 45 raise UnexpectedFileFormat if the file cannot be understood. |
| 46 """ |
| 47 class InvalidHeaderLine(Exception): |
| 48 """Raised by read_entry when the line read is not the format expected. |
| 49 """ |
| 50 |
| 51 try: |
| 52 f = open(filepath, 'rb') |
| 53 except EnvironmentError: |
| 54 raise UnexpectedFileFormat('The transaction file cannot be opened') |
| 55 |
| 56 try: |
| 57 out = [] |
| 58 def read_entry(entrytype): |
| 59 header = f.readline() |
| 60 match = re.match(r'^' + entrytype + ' (\d+)$', header) |
| 61 if not match: |
| 62 raise InvalidHeaderLine(header) |
| 63 datalen = int(match.group(1)) |
| 64 data = f.read(datalen) |
| 65 if len(data) != datalen: |
| 66 raise UnpexpectedFileFormat( |
| 67 'Data value is not the expected length') |
| 68 # Reads and ignore \n |
| 69 if f.read(1) != '\n': |
| 70 raise UnpexpectedFileFormat('Data value doesn\'t end with \\n') |
| 71 return data |
| 72 |
| 73 while True: |
| 74 try: |
| 75 key = read_entry('K') |
| 76 except InvalidHeaderLine, e: |
| 77 # Check if it's the end of the file. |
| 78 if e.args[0] == 'END\n': |
| 79 break |
| 80 raise UnpexectedFileFormat('Failed to read a key: %s' % e) |
| 81 try: |
| 82 value = read_entry('V') |
| 83 except InvalidHeaderLine, e: |
| 84 raise UnpexectedFileFormat('Failed to read a value: %s' % e) |
| 85 out.append([key, value]) |
| 86 return out |
| 87 finally: |
| 88 f.close() |
| 89 |
| 90 |
| 91 def write_svn_dump(filepath, data): |
| 92 """Writes a svn keyed file with a list of (K, V).""" |
| 93 f = open(filepath, 'wb') |
| 94 try: |
| 95 def write_entry(entrytype, value): |
| 96 f.write('%s %d\n' % (entrytype, len(value))) |
| 97 f.write(value) |
| 98 f.write('\n') |
| 99 |
| 100 for k, v in data: |
| 101 write_entry('K', k) |
| 102 write_entry('V', v) |
| 103 f.write('END\n') |
| 104 finally: |
| 105 f.close() |
| 106 |
| 107 |
| 108 def find_key(data, key): |
| 109 """Finds the item in a list of tuple where item[0] == key. |
| 110 |
| 111 asserts if there is more than one item with the key. |
| 112 """ |
| 113 items = [i for i in data if i[0] == key] |
| 114 if not items: |
| 115 return None |
| 116 assert len(items) == 1 |
| 117 return items[0] |
| 118 |
| 119 |
| 120 def handle_commit_bot(repo_path, tx, commit_bot, admin_email): |
| 121 """Replaces svn:author with realauthor and sets commit-bot.""" |
| 122 # The file format is described there: |
| 123 # http://svn.apache.org/repos/asf/subversion/trunk/notes/dump-load-format.tx
t |
| 124 propfilepath = os.path.join( |
| 125 repo_path, 'db', 'transactions', tx + '.txn', 'props') |
| 126 |
| 127 # Do a lot of checks to make sure everything is in the expected format. |
| 128 try: |
| 129 data = read_svn_dump(propfilepath) |
| 130 except UnexpectedFileFormat: |
| 131 return ( |
| 132 'Failed to parse subversion server transaction format.\n' |
| 133 'Please contact %s ASAP with\n' |
| 134 'this error message.') % admin_email |
| 135 if not data: |
| 136 return ( |
| 137 'Failed to load subversion server transaction file.\n' |
| 138 'Please contact %s ASAP with\n' |
| 139 'this error message.') % admin_email |
| 140 |
| 141 realauthor = find_key(data, 'realauthor') |
| 142 if not realauthor: |
| 143 # That's fine, there is no author to fake. |
| 144 return |
| 145 |
| 146 author = find_key(data, 'svn:author') |
| 147 if not author or not author[1]: |
| 148 return ( |
| 149 'Failed to load svn:author from the transaction file.\n' |
| 150 'Please contact %s ASAP with\n' |
| 151 'this error message.') % admin_email |
| 152 |
| 153 if author[1] != commit_bot: |
| 154 # The author will not be changed and realauthor will be kept as a |
| 155 # revision property. |
| 156 return |
| 157 |
| 158 if len(realauthor[1]) > 50: |
| 159 return 'Fake author was rejected due to being too long.' |
| 160 |
| 161 if not re.match(r'^[a-zA-Z0-9\@\-\_\+\%\.]+$', realauthor[1]): |
| 162 return 'Fake author was rejected due to not passing regexp.' |
| 163 |
| 164 # Overwrite original author |
| 165 author[1] = realauthor[1] |
| 166 # Remove realauthor svn property |
| 167 data.remove(realauthor) |
| 168 # Add svn property commit-bot=<commit-bot username> |
| 169 data.append(('commit-bot', commit_bot)) |
| 170 write_svn_dump(propfilepath, data) |
| 171 |
| 172 |
| 173 def main(): |
| 174 # Replace with your commit-bot credential. |
| 175 commit_bot = 'user1@example.com' |
| 176 admin_email = 'dude@example.com' |
| 177 ret = handle_commit_bot(sys.argv[1], sys.argv[2], commit_bot, admin_email) |
| 178 if ret: |
| 179 print >> sys.stderr, ret |
| 180 return 1 |
| 181 return 0 |
| 182 |
| 183 |
| 184 if __name__ == '__main__': |
| 185 sys.exit(main()) |
| 186 |
| 187 # vim: ts=4:sw=4:tw=80:et: |
OLD | NEW |