blob: 7b5e036507c8bad08e6ae57ef8a44a5bbbbeaca9 [file] [log] [blame]
#!/usr/bin/env python
#
# svnmerge-migrate-history.py: Migrate merge history from svnmerge.py's
# format to Subversion 1.5's format.
#
# ====================================================================
# 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
import sre
import getopt
try:
my_getopt = getopt.gnu_getopt
except AttributeError:
my_getopt = getopt.getopt
try:
import svn.core
import svn.fs
import svn.repos
except ImportError, e:
print >> sys.stderr, \
"ERROR: Unable to import Subversion's Python bindings: '%s'\n" \
"Hint: Set your PYTHONPATH environment variable, or adjust your " \
"PYTHONSTARTUP\nfile to point to your Subversion install " \
"location's svn-python directory." % e
sys.exit(1)
# Pretend we have boolean data types for older Python versions.
try:
True
False
except:
True = 1
False = 0
def usage_and_exit(error_msg=None):
"""Write usage information and exit. If ERROR_MSG is provide, that
error message is printed first (to stderr), the usage info goes to
stderr, and the script exits with a non-zero status. Otherwise,
usage info goes to stdout and the script exits with a zero status."""
progname = os.path.basename(sys.argv[0])
stream = error_msg and sys.stderr or sys.stdout
if error_msg:
print >> stream, "ERROR: %s\n" % error_msg
print >> stream, """usage: %s REPOS_PATH [PATH_PREFIX...] [--verbose]
%s --help
Migrate merge history from svnmerge.py's format to Subversion 1.5's
format, stopping as soon as merge history is encountered for a
directory tree.
PATH_PREFIX defines the repository paths to examine for merge history
to migrate. If none are listed, the repository's root is examined.
Example: %s /path/to/repos trunk branches tags
""" % (progname, progname, progname)
sys.exit(error_msg and 1 or 0)
class Migrator:
"Migrates merge history."
repos_path = None
path_prefixes = None
verbose = False
fs = None
def run(self):
self.fs = svn.repos.fs(svn.repos.open(self.repos_path))
revnum = svn.fs.youngest_rev(self.fs)
root = svn.fs.revision_root(self.fs, revnum)
# Validate path prefixes, retaining path calculations performed in
# the process.
leading_paths = []
for path_prefix in self.path_prefixes:
path = "/".join(path_prefix[:-1])
leading_paths.append(path)
if svn.fs.check_path(root, path) != svn.core.svn_node_dir:
raise Exception("Repository path '%s' is not a directory" % path)
for i in range(0, len(self.path_prefixes)):
prefix = self.path_prefixes[i]
self.process_dir(root, revnum, leading_paths[i],
prefix[len(prefix) - 1] + ".*")
def process_dir(self, root, revnum, dir_path, pattern=None):
"Recursively process children of DIR_PATH."
dirents = svn.fs.dir_entries(root, dir_path)
for name in dirents.keys():
if not dirents[name].kind == svn.core.svn_node_dir:
continue
if pattern is None or sre.match(pattern, name):
if dir_path == "":
child_path = name
else:
child_path = "%s/%s" % (dir_path, name)
if self.verbose:
print "Examining path '%s' for conversion" % child_path
if not self.convert_path_history(root, revnum, child_path):
self.process_dir(root, revnum, child_path)
def convert_path_history(self, root, revnum, path):
"Migrate the merge history for PATH at ROOT at REVNUM."
### Bother to handle any pre-existing, inherited svn:mergeinfo?
# Retrieve svnmerge.py's merge history meta data, and roll it into
# Subversion 1.5 mergeinfo.
mergeinfo_prop_val = svn.fs.node_prop(root, path,
svn.core.SVN_PROP_MERGE_INFO)
integrated_prop_val = svn.fs.node_prop(root, path, "svnmerge-integrated")
if self.verbose:
print "Discoverd pre-existing Subversion mergeinfo of '%s'" % \
mergeinfo_prop_val
print "Discoverd svnmerge.py mergeinfo of '%s'" % integrated_prop_val
mergeinfo_prop_val = self.add_to_mergeinfo(integrated_prop_val,
mergeinfo_prop_val)
### LATER: We handle svnmerge-blocked by converting it into
### svn:mergeinfo, until revision blocking becomes available in
### Subversion's core.
blocked_prop_val = svn.fs.node_prop(root, path, "svnmerge-blocked")
if self.verbose:
print "Discoverd svnmerge.py blocked revisions of '%s'" % \
integrated_prop_val
mergeinfo_prop_val = self.add_to_mergeinfo(blocked_prop_val,
mergeinfo_prop_val)
if mergeinfo_prop_val is not None:
# Begin a transaction in which we'll manipulate merge-related
# properties. Open the transaction root.
txn = svn.fs.begin_txn2(self.fs, revnum, 0)
root = svn.fs.txn_root(txn)
# Manipulate the merge history.
if self.verbose:
print "Queuing change of %s to '%s'" % \
(svn.core.SVN_PROP_MERGE_INFO, mergeinfo_prop_val)
svn.fs.change_node_prop(root, path, svn.core.SVN_PROP_MERGE_INFO,
mergeinfo_prop_val)
# Remove old property values.
if integrated_prop_val is not None:
if self.verbose:
print "Queuing removal of svnmerge-integrated"
svn.fs.change_node_prop(root, path, "svnmerge-integrated", None)
if blocked_prop_val is not None:
if self.verbose:
print "Queuing removal of svnmerge-blocked"
svn.fs.change_node_prop(root, path, "svnmerge-blocked", None)
# Commit the transaction containing our property manipulation.
if self.verbose:
print "Committing the transaction containing the above changes"
conflict, new_revnum = svn.fs.commit_txn(txn)
if conflict:
### TODO: Do something more intelligent with the possible conflict.
raise Exception("Conflict encountered (%s)" % conflict)
print "Migrated merge history on '%s' in r%d" % (path, new_revnum)
return True
else:
# No merge history to manipulate.
if self.verbose:
print "No merge history on '%s'" % path
return False
def add_to_mergeinfo(self, svnmerge_prop_val, mergeinfo_prop_val):
if svnmerge_prop_val is not None:
if mergeinfo_prop_val:
mergeinfo = svn.core.svn_mergeinfo_parse(mergeinfo_prop_val)
to_migrate = svn.core.svn_mergeinfo_parse(svnmerge_prop_val)
### FIXME: The SWIG bindings may not be giving us an API that
### both accepts two mergeinfos and returns the merged result.
mergeinfo = svn.core.svn_mergeinfo_merge(mergeinfo, to_migrate, True)
mergeinfo_prop_val = \
svn.core.svn_megeinfo_mergeinfo_to_stringbuf(mergeinfo).data
else:
mergeinfo_prop_val = svnmerge_prop_val
return mergeinfo_prop_val
def set_path_prefixes(self, prefixes):
"Decompose path prefixes into something meaningful for comparision."
self.path_prefixes = []
for prefix in prefixes:
prefix_components = []
parts = prefix.split("/")
for i in range(0, len(parts)):
prefix_components.append(parts[i])
self.path_prefixes.append(prefix_components)
def main():
try:
opts, args = my_getopt(sys.argv[1:], "vh?",
["from-paths=", "verbose", "help"])
except:
usage_and_exit("Unable to process arguments/options")
migrator = Migrator()
# Process arguments.
if len(args) >= 1:
migrator.repos_path = svn.core.svn_path_canonicalize(args[0])
if len(args) >= 2:
path_prefixes = args[1:]
else:
# Default to the root of the repository.
path_prefixes = [ "" ]
else:
usage_and_exit("REPOS_PATH argument required")
# Process options.
for opt, value in opts:
if opt == "--help" or opt in ("-h", "-?"):
usage_and_exit()
elif opt == "--verbose" or opt == "-v":
migrator.verbose = True
else:
usage_and_exit("Unknown option '%s'" % opt)
migrator.set_path_prefixes(path_prefixes)
migrator.run()
if __name__ == "__main__":
main()