incremental-update.py   [plain text]


#!/usr/bin/env python

# ====================================================================
#
# incremental-update.py
#
# This script performs updates of a single working copy tree piece by
# piece, starting with deep subdirectores, and working its way up
# toward the root of the working copy.  Why?  Because for working
# copies that have significantly mixed revisions, the size and
# complexity of the report that Subversion has to transmit to the
# server can be prohibitive, even triggering server-configured limits
# for such things.  But doing an incremental update, you lessen the
# chance of hitting such a limit.
#
# ====================================================================
# 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/.
# ====================================================================

# --------------------------------------------------------------------
# Configuration (oooh... so complex...)
#

SVN_BINARY='svn'

#
# --------------------------------------------------------------------

import sys
import os
import re


def print_error(err):
    sys.stderr.write("ERROR: %s\n\n" % (err))

def usage_and_exit(err=None):
    if err:
        stream = sys.stderr
        print_error(err)
    else:
        stream = sys.stdout
    stream.write("""Usage: %s [OPTIONS] WC-DIR

Update WC-DIR in an incremental fashion, starting will smaller
subtrees of it, and working up toward WC-DIR itself.  SVN_UP_ARGS are
command-line parameters passed straight through to the Subversion
command-line client (svn) as parameters to its update command.

WARNING: Speed of operation is explicitly *NOT* of interest to this
script.  Use it only when a typical 'svn update' isn't working for you
due to the complexity of your working copy's mixed-revision state.

Options:

    --username USER      Specify the username used to connect to the repository
    --password PASS      Specify the PASSWORD used to connect to the repository

""" % (os.path.basename(sys.argv[0])))
    sys.exit(err and 1 or 0)


def get_head_revision(path, args):
    """Return the current HEAD revision for the repository associated
    with PATH.  ARGS are extra arguments to provide to the svn
    client."""

    lines = os.popen('%s status --show-updates --non-recursive %s %s'
                     % (SVN_BINARY, args, path)).readlines()
    if lines and lines[-1].startswith('Status against revision:'):
        return int(lines[-1][24:].strip())
    raise Exception, "Unable to fetch HEAD revision number."


def compare_paths(path1, path2):
    """This is a sort() helper function for two paths."""

    path1_len = len (path1);
    path2_len = len (path2);
    min_len = min(path1_len, path2_len)
    i = 0

    # Are the paths exactly the same?
    if path1 == path2:
      return 0

    # Skip past common prefix
    while (i < min_len) and (path1[i] == path2[i]):
      i = i + 1

    # Children of paths are greater than their parents, but less than
    # greater siblings of their parents
    char1 = '\0'
    char2 = '\0'
    if (i < path1_len):
      char1 = path1[i]
    if (i < path2_len):
      char2 = path2[i]

    if (char1 == '/') and (i == path2_len):
      return 1
    if (char2 == '/') and (i == path1_len):
      return -1
    if (i < path1_len) and (char1 == '/'):
      return -1
    if (i < path2_len) and (char2 == '/'):
      return 1

    # Common prefix was skipped above, next character is compared to
    # determine order
    return cmp(char1, char2)


def harvest_dirs(path):
    """Return a list of versioned directories under working copy
    directory PATH, inclusive."""

    # 'svn status' output line matcher, taken from the Subversion test suite
    rm = re.compile('^([!MACDRUG_ ][MACDRUG_ ])([L ])([+ ])([S ])([KOBT ]) ' \
                    '([* ])   [^0-9-]*(\d+|-|\?) +(\d|-|\?)+ +(\S+) +(.+)')
    dirs = []
    fp = os.popen('%s status --verbose %s' % (SVN_BINARY, path))
    while 1:
        line = fp.readline()
        if not line:
            break
        line = line.rstrip()
        if line.startswith('Performing'):
            break
        match = rm.search(line)
        if match:
            stpath = match.group(10)
            try:
                if os.path.isdir(stpath):
                    dirs.append(stpath)
            except:
                pass
    return dirs


def main():
    argc = len(sys.argv)
    if argc < 2:
        usage_and_exit("No working copy directory specified")
    if '--help' in sys.argv:
        usage_and_exit(None)
    path = sys.argv[-1]
    args = ' '.join(sys.argv[1:-1] + ['--non-interactive'])
    print "Fetch HEAD revision...",
    head_revision = get_head_revision(path, args)
    print "done."
    print "Updating to revision %d" % (head_revision)
    print "Harvesting the list of subdirectories...",
    dirs = harvest_dirs(path)
    print "done."
    dirs.sort(compare_paths)
    dirs.reverse()
    print "Update the tree, one subdirectory at a time.  This could take " \
          "a while."
    num_dirs = len(dirs)
    width = len(str(num_dirs))
    format_string = '[%%%dd/%%%dd] Updating %%s' % (width, width)
    current = 0
    for dir in dirs:
        current = current + 1
        print format_string % (current, num_dirs, dir)
        os.system('%s update --quiet --revision %d %s %s'
                  % (SVN_BINARY, head_revision, args, dir))


if __name__ == "__main__":
    try:
        main()
    except SystemExit:
        raise
    except Exception, e:
        print_error(str(e))
        sys.exit(1)