blob: 04605abc7d449d6396b76aa50b7b5dd1ac107541 [file] [log] [blame]
#!/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'])
sys.stdout.write("Fetch HEAD revision... ")
head_revision = get_head_revision(path, args)
print("done.")
print("Updating to revision %d" % (head_revision))
sys.stdout.write("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 as e:
print_error(str(e))
sys.exit(1)