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 # ====================================================================
25 my_getopt
= getopt
.gnu_getopt
26 except AttributeError:
27 my_getopt
= getopt
.getopt
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
41 # Pretend we have boolean data types for older Python versions.
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
58 print >> stream
, "ERROR: %s\n" % error_msg
59 print >> stream
, """usage: %s REPOS_PATH [PATH_PREFIX...] [--verbose]
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
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)
74 "Migrates merge history."
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
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
:
107 if pattern
is None or sre
.match(pattern
, name
):
111 child_path
= "%s/%s" % (dir_path
, name
)
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")
128 print "Discovered pre-existing Subversion mergeinfo of '%s'" % \
130 print "Discovered svnmerge.py mergeinfo of '%s'" % integrated_prop_val
131 mergeinfo_prop_val
= self
.add_to_mergeinfo(integrated_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")
138 print "Discovered svnmerge.py blocked revisions of '%s'" % \
140 mergeinfo_prop_val
= self
.add_to_mergeinfo(blocked_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.
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
,
156 # Remove old property values.
157 if integrated_prop_val
is not None:
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:
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.
168 print "Committing the transaction containing the above changes"
169 conflict
, new_revnum
= svn
.fs
.commit_txn(txn
)
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
)
176 # No merge history to manipulate.
178 print "No merge history on '%s'" % path
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
)
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
)
205 opts
, args
= my_getopt(sys
.argv
[1:], "vh?",
206 ["from-paths=", "verbose", "help"])
208 usage_and_exit("Unable to process arguments/options")
210 migrator
= Migrator()
214 migrator
.repos_path
= svn
.core
.svn_path_canonicalize(args
[0])
216 path_prefixes
= args
[1:]
218 # Default to the root of the repository.
219 path_prefixes
= [ "" ]
221 usage_and_exit("REPOS_PATH argument required")
224 for opt
, value
in opts
:
225 if opt
== "--help" or opt
in ("-h", "-?"):
227 elif opt
== "--verbose" or opt
== "-v":
228 migrator
.verbose
= True
230 usage_and_exit("Unknown option '%s'" % opt
)
232 migrator
.set_path_prefixes(path_prefixes
)
235 if __name__
== "__main__":