| #!/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) |