Follow-up to r29036: Now that the "mergeinfo" transaction file is no
[svn.git] / contrib / client-side / svnmerge / svnmerge-migrate-history.py
blob273d39bec16cfe183fc19c99414cd45eef95f6e8
1 #!/usr/bin/env python
3 # svnmerge-migrate-history.py: Migrate merge history from svnmerge.py's
4 # format to Subversion 1.5's format.
6 # ====================================================================
7 # Copyright (c) 2007 CollabNet. All rights reserved.
9 # This software is licensed as described in the file COPYING, which
10 # you should have received as part of this distribution. The terms
11 # are also available at http://subversion.tigris.org/license-1.html.
12 # If newer versions of this license are posted there, you may use a
13 # newer version instead, at your option.
15 # This software consists of voluntary contributions made by many
16 # individuals. For exact contribution history, see the revision
17 # history and logs, available at http://subversion.tigris.org/.
18 # ====================================================================
20 import sys
21 import os
22 import sre
23 import getopt
24 try:
25 my_getopt = getopt.gnu_getopt
26 except AttributeError:
27 my_getopt = getopt.getopt
29 try:
30 import svn.core
31 import svn.fs
32 import svn.repos
33 except ImportError, e:
34 print >> sys.stderr, \
35 "ERROR: Unable to import Subversion's Python bindings: '%s'\n" \
36 "Hint: Set your PYTHONPATH environment variable, or adjust your " \
37 "PYTHONSTARTUP\nfile to point to your Subversion install " \
38 "location's svn-python directory." % e
39 sys.exit(1)
41 # Pretend we have boolean data types for older Python versions.
42 try:
43 True
44 False
45 except:
46 True = 1
47 False = 0
49 def usage_and_exit(error_msg=None):
50 """Write usage information and exit. If ERROR_MSG is provide, that
51 error message is printed first (to stderr), the usage info goes to
52 stderr, and the script exits with a non-zero status. Otherwise,
53 usage info goes to stdout and the script exits with a zero status."""
54 progname = os.path.basename(sys.argv[0])
56 stream = error_msg and sys.stderr or sys.stdout
57 if error_msg:
58 print >> stream, "ERROR: %s\n" % error_msg
59 print >> stream, """usage: %s REPOS_PATH [PATH_PREFIX...] [--verbose]
60 %s --help
62 Migrate merge history from svnmerge.py's format to Subversion 1.5's
63 format, stopping as soon as merge history is encountered for a
64 directory tree.
66 PATH_PREFIX defines the repository paths to examine for merge history
67 to migrate. If none are listed, the repository's root is examined.
69 Example: %s /path/to/repos trunk branches tags
70 """ % (progname, progname, progname)
71 sys.exit(error_msg and 1 or 0)
73 class Migrator:
74 "Migrates merge history."
76 repos_path = None
77 path_prefixes = None
78 verbose = False
79 fs = None
81 def run(self):
82 self.fs = svn.repos.fs(svn.repos.open(self.repos_path))
84 revnum = svn.fs.youngest_rev(self.fs)
85 root = svn.fs.revision_root(self.fs, revnum)
87 # Validate path prefixes, retaining path calculations performed in
88 # the process.
89 leading_paths = []
90 for path_prefix in self.path_prefixes:
91 path = "/".join(path_prefix[:-1])
92 leading_paths.append(path)
93 if svn.fs.check_path(root, path) != svn.core.svn_node_dir:
94 raise Exception("Repository path '%s' is not a directory" % path)
96 for i in range(0, len(self.path_prefixes)):
97 prefix = self.path_prefixes[i]
98 self.process_dir(root, revnum, leading_paths[i],
99 prefix[len(prefix) - 1] + ".*")
101 def process_dir(self, root, revnum, dir_path, pattern=None):
102 "Recursively process children of DIR_PATH."
103 dirents = svn.fs.dir_entries(root, dir_path)
104 for name in dirents.keys():
105 if not dirents[name].kind == svn.core.svn_node_dir:
106 continue
107 if pattern is None or sre.match(pattern, name):
108 if dir_path == "":
109 child_path = name
110 else:
111 child_path = "%s/%s" % (dir_path, name)
112 if self.verbose:
113 print "Examining path '%s' for conversion" % child_path
114 if not self.convert_path_history(root, revnum, child_path):
115 self.process_dir(root, revnum, child_path)
117 def convert_path_history(self, root, revnum, path):
118 "Migrate the merge history for PATH at ROOT at REVNUM."
120 ### Bother to handle any pre-existing, inherited svn:mergeinfo?
122 # Retrieve svnmerge.py's merge history meta data, and roll it into
123 # Subversion 1.5 mergeinfo.
124 mergeinfo_prop_val = svn.fs.node_prop(root, path,
125 svn.core.SVN_PROP_MERGE_INFO)
126 integrated_prop_val = svn.fs.node_prop(root, path, "svnmerge-integrated")
127 if self.verbose:
128 print "Discovered pre-existing Subversion mergeinfo of '%s'" % \
129 mergeinfo_prop_val
130 print "Discovered svnmerge.py mergeinfo of '%s'" % integrated_prop_val
131 mergeinfo_prop_val = self.add_to_mergeinfo(integrated_prop_val,
132 mergeinfo_prop_val)
133 ### LATER: We handle svnmerge-blocked by converting it into
134 ### svn:mergeinfo, until revision blocking becomes available in
135 ### Subversion's core.
136 blocked_prop_val = svn.fs.node_prop(root, path, "svnmerge-blocked")
137 if self.verbose:
138 print "Discovered svnmerge.py blocked revisions of '%s'" % \
139 blocked_prop_val
140 mergeinfo_prop_val = self.add_to_mergeinfo(blocked_prop_val,
141 mergeinfo_prop_val)
143 if mergeinfo_prop_val is not None:
144 # Begin a transaction in which we'll manipulate merge-related
145 # properties. Open the transaction root.
146 txn = svn.fs.begin_txn2(self.fs, revnum, 0)
147 root = svn.fs.txn_root(txn)
149 # Manipulate the merge history.
150 if self.verbose:
151 print "Queuing change of %s to '%s'" % \
152 (svn.core.SVN_PROP_MERGE_INFO, mergeinfo_prop_val)
153 svn.fs.change_node_prop(root, path, svn.core.SVN_PROP_MERGE_INFO,
154 mergeinfo_prop_val)
156 # Remove old property values.
157 if integrated_prop_val is not None:
158 if self.verbose:
159 print "Queuing removal of svnmerge-integrated"
160 svn.fs.change_node_prop(root, path, "svnmerge-integrated", None)
161 if blocked_prop_val is not None:
162 if self.verbose:
163 print "Queuing removal of svnmerge-blocked"
164 svn.fs.change_node_prop(root, path, "svnmerge-blocked", None)
166 # Commit the transaction containing our property manipulation.
167 if self.verbose:
168 print "Committing the transaction containing the above changes"
169 conflict, new_revnum = svn.fs.commit_txn(txn)
170 if conflict:
171 ### TODO: Do something more intelligent with the possible conflict.
172 raise Exception("Conflict encountered (%s)" % conflict)
173 print "Migrated merge history on '%s' in r%d" % (path, new_revnum)
174 return True
175 else:
176 # No merge history to manipulate.
177 if self.verbose:
178 print "No merge history on '%s'" % path
179 return False
181 def add_to_mergeinfo(self, svnmerge_prop_val, mergeinfo_prop_val):
182 if svnmerge_prop_val is not None:
183 if mergeinfo_prop_val:
184 mergeinfo = svn.core.svn_mergeinfo_parse(mergeinfo_prop_val)
185 to_migrate = svn.core.svn_mergeinfo_parse(svnmerge_prop_val)
186 mergeinfo = svn.core.svn_mergeinfo_merge(mergeinfo, to_migrate)
187 mergeinfo_prop_val = svn.core.svn_mergeinfo_to_stringbuf(mergeinfo)
188 else:
189 mergeinfo_prop_val = svnmerge_prop_val
191 return mergeinfo_prop_val
193 def set_path_prefixes(self, prefixes):
194 "Decompose path prefixes into something meaningful for comparision."
195 self.path_prefixes = []
196 for prefix in prefixes:
197 prefix_components = []
198 parts = prefix.split("/")
199 for i in range(0, len(parts)):
200 prefix_components.append(parts[i])
201 self.path_prefixes.append(prefix_components)
203 def main():
204 try:
205 opts, args = my_getopt(sys.argv[1:], "vh?",
206 ["from-paths=", "verbose", "help"])
207 except:
208 usage_and_exit("Unable to process arguments/options")
210 migrator = Migrator()
212 # Process arguments.
213 if len(args) >= 1:
214 migrator.repos_path = svn.core.svn_path_canonicalize(args[0])
215 if len(args) >= 2:
216 path_prefixes = args[1:]
217 else:
218 # Default to the root of the repository.
219 path_prefixes = [ "" ]
220 else:
221 usage_and_exit("REPOS_PATH argument required")
223 # Process options.
224 for opt, value in opts:
225 if opt == "--help" or opt in ("-h", "-?"):
226 usage_and_exit()
227 elif opt == "--verbose" or opt == "-v":
228 migrator.verbose = True
229 else:
230 usage_and_exit("Unknown option '%s'" % opt)
232 migrator.set_path_prefixes(path_prefixes)
233 migrator.run()
235 if __name__ == "__main__":
236 main()