#!/usr/bin/env python

# ====================================================================
#
# svn-tweak-author.py
#
# This script allows for server-side tweaks of the svn:author property
# in two modes:  single-revision and search-and-replace.  Both modes
# bypass the repository hook subsystem.
#
# ====================================================================
# Copyright (c) 2007 CollabNet.  All rights reserved.
#
# This software is licensed as described in the file COPYING, which
# you should have received as part of this distribution.  The terms
# are also available at http://subversion.tigris.org/license-1.html.
# If newer versions of this license are posted there, you may use a
# newer version instead, at your option.
#
# This software consists of voluntary contributions made by many
# individuals.  For exact contribution history, see the revision
# history and logs, available at http://subversion.tigris.org/.
# ====================================================================

import sys
import os
from svn import repos, fs, core

def error_and_exit(errmsg):
    """Print ERRMSG as an error, and exit with a non-zero error code."""
    sys.stderr.write("\nERROR: %s\n" % (errmsg))
    sys.exit(1)

def usage_and_exit(errmsg=None):
    """Print the usage message, to stderr if ERRMSG is provided, to
    stdout otherwise.  If ERRMSG is provided, print it as an error,
    too."""
    cmd = os.path.basename(sys.argv[0])
    stream = errmsg and sys.stderr or sys.stdout
    stream.write("""Usage: 1. %s REPOS-PATH replace OLDAUTHOR NEWAUTHOR
       2. %s REPOS-PATH revision REV NEWAUTHOR

Change the svn:author property for one or more revisions of the
repository located at REPOS-PATH.  If in "replace" mode, any instance
of an author named OLDAUTHOR is changed to NEWAUTHOR.  If in "revision"
mode, simply change the author of the single revision REV to NEWAUTHOR.

WARNING: Changing revision properties is not a versioned event, and
         this script will bypass the repository's hook subsystem (so
         you won't see notification emails and such from any changes
         made with this script).
""" % (cmd, cmd))
    if errmsg:
        error_and_exit(errmsg)
    else:
        sys.exit(0)

def fetch_rev_author(fs_obj, revision):
    """Return the value of the svn:author property for REVISION in
    repository filesystem FS_OBJ."""
    return fs.svn_fs_revision_prop(fs_obj, revision,
                                   core.SVN_PROP_REVISION_AUTHOR)

def tweak_rev_author(fs_obj, revision, author):
    """Change the value of the svn:author property for REVISION in
    repository filesystem FS_OBJ in AUTHOR."""
    print "Tweaking author for revision %d..." % (revision),
    try:
        fs.svn_fs_change_rev_prop(fs_obj, revision,
                                  core.SVN_PROP_REVISION_AUTHOR, author)
    except:
        print ""
        raise
    print "done."

def get_fs_obj(repos_path):
    """Return a repository filesystem object for the repository
    located at REPOS_PATH (which must obey the Subversion path
    canonicalization rules)."""
    return repos.svn_repos_fs(repos.svn_repos_open(repos_path))

def main():
    argc = len(sys.argv)
    if argc != 5:
        usage_and_exit("Not enough arguments provided.")
    try:
        repos_path = core.svn_path_canonicalize(sys.argv[1])
    except AttributeError:
        repos_path = os.path.normpath(sys.argv[1])
        if repos_path[-1] == '/' and len(repos_path) > 1:
            repos_path = repos_path[:-1]
    author = sys.argv[4]
    mode = sys.argv[2]
    try:
        if mode == "replace":
            old_author = sys.argv[3]
            fs_obj = get_fs_obj(repos_path)
            for revision in range(fs.svn_fs_youngest_rev(fs_obj) + 1):
                if fetch_rev_author(fs_obj, revision) == old_author:
                    tweak_rev_author(fs_obj, revision, author)
        elif mode == "revision":
            try:
                revision = int(sys.argv[3])
            except ValueError:
                usage_and_exit("Invalid revision number (%s) provided."
                               % (sys.argv[3]))
            tweak_rev_author(get_fs_obj(repos_path), revision, author)
        else:
            usage_and_exit("Invalid mode (%s) provided." % (mode))
    except SystemExit:
        raise
    except Exception, e:
        error_and_exit(str(e))

if __name__ == "__main__":
    main()

@@ -1557,16 +1557,17 @@
 DAUTHOR 
+%5B
 NEWAUTHO
@@ -1567,16 +1567,17 @@
 EWAUTHOR
+%5D
 %0A       
@@ -1606,16 +1606,17 @@
 ion REV 
+%5B
 NEWAUTHO
@@ -1616,16 +1616,17 @@
 EWAUTHOR
+%5D
 %0A%0AChange
@@ -1896,16 +1896,99 @@
 WAUTHOR.
+%0AIn either mode, if NEWAUTHOR is not provided, the existing author will%0Abe deleted.
 %0A%0AWARNIN
@@ -2737,24 +2737,126 @@
  AUTHOR.%22%22%22%0A
+    if author is None:%0A        print %22Deleting author for revision %25d...%22 %25 (revision),%0A    else:%0A    
     print %22T
@@ -2900,24 +2900,24 @@
 (revision),%0A
-
     try:%0A   
@@ -3405,10 +3405,21 @@
 rgc 
-!=
+%3C 4 or argc %3E
  5:%0A
@@ -3722,16 +3722,29 @@
 th%5B:-1%5D%0A
+    try:%0A    
     auth
@@ -3760,16 +3760,61 @@
 argv%5B4%5D%0A
+    except IndexError:%0A        author = None%0A
     mode

Make the NEWAUTHOR argument to 'svn-tweak-author.py replace' optional, indicating that you'd like to delete instances of particular author.