You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
188 lines
6.0 KiB
Python
188 lines
6.0 KiB
Python
#!/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:
|