]> Tony Duckles's Git Repositories (git.nynim.org) - svn2svn.git/blob - svn2svn/run/svnreplay.py
Use proper exit-codes
[svn2svn.git] / svn2svn / run / svnreplay.py
1 """
2 Replicate (replay) changesets from one SVN repository to another.
3 """
4
5 from svn2svn import base_version, full_version
6 from svn2svn import ui
7 from svn2svn import shell
8 from svn2svn import svnclient
9 from svn2svn.shell import run_svn,run_shell_command
10 from svn2svn.errors import ExternalCommandFailed, UnsupportedSVNAction, InternalError, VerificationError
11 from svn2svn.run.common import in_svn, is_child_path, join_path, find_svn_ancestors
12 from parse import HelpFormatter
13 from breakhandler import BreakHandler
14
15 import sys
16 import os
17 import traceback
18 import operator
19 import optparse
20 import re
21 import urllib
22 from datetime import datetime
23
24 # Module-level variables/parameters
25 source_url = "" # URL to source path in source SVN repo, e.g. 'http://server/svn/source/trunk'
26 source_repos_url = "" # URL to root of source SVN repo, e.g. 'http://server/svn/source'
27 source_base = "" # Relative path of source_url in source SVN repo, e.g. '/trunk'
28 source_repos_uuid = "" # UUID of source SVN repo
29 target_url ="" # URL to target path in target SVN repo, e.g. 'file:///svn/repo_target/trunk'
30 target_repos_url = "" # URL to root of target SVN repo, e.g. 'http://server/svn/target'
31 target_base = "" # Relative path of target_url in target SVN repo, e.g. '/trunk'
32 rev_map = {} # The running mapping-table dictionary for source_url rev #'s -> target_url rev #'s
33 options = None # optparser options
34
35 def parse_svn_commit_rev(output):
36 """
37 Parse the revision number from the output of "svn commit".
38 """
39 output_lines = output.strip("\n").split("\n")
40 rev_num = None
41 for line in output_lines:
42 if line[0:19] == 'Committed revision ':
43 rev_num = line[19:].rstrip('.')
44 break
45 assert rev_num is not None
46 return int(rev_num)
47
48 def commit_from_svn_log_entry(log_entry, commit_paths=None, target_revprops=None):
49 """
50 Given an SVN log entry and an optional list of changed paths, do an svn commit.
51 """
52 # TODO: Run optional external shell hook here, for doing pre-commit filtering
53 # Display the _wc_target "svn status" info if running in -vv (or higher) mode
54 if ui.get_level() >= ui.EXTRA:
55 ui.status(">> commit_from_svn_log_entry: Pre-commit _wc_target status:", level=ui.EXTRA, color='CYAN')
56 ui.status(run_svn(["status"]), level=ui.EXTRA, color='CYAN')
57 # This will use the local timezone for displaying commit times
58 timestamp = int(log_entry['date'])
59 svn_date = str(datetime.fromtimestamp(timestamp))
60 # Uncomment this one one if you prefer UTC commit times
61 #svn_date = "%d 0" % timestamp
62 args = ["commit", "--force-log"]
63 message = log_entry['message']
64 if options.log_date:
65 message += "\nDate: " + svn_date
66 if options.log_author:
67 message += "\nAuthor: " + log_entry['author']
68 args += ["-m", message]
69 revprops = {}
70 if log_entry['revprops']:
71 # Carry forward any revprop's from the source revision
72 for v in log_entry['revprops']:
73 revprops[v['name']] = v['value']
74 if target_revprops:
75 # Add any extra revprop's we want to set for the target repo commits
76 for v in target_revprops:
77 revprops[v['name']] = v['value']
78 if revprops:
79 for key in revprops:
80 args += ["--with-revprop", "%s=%s" % (key, str(revprops[key]))]
81 if commit_paths:
82 if len(commit_paths)<100:
83 # If we don't have an excessive amount of individual changed paths, pass
84 # those to the "svn commit" command. Else, pass nothing so we commit at
85 # the root of the working-copy.
86 for c_path in commit_paths:
87 args += [svnclient.safe_path(c_path)]
88 rev_num = None
89 if not options.dry_run:
90 # Use BreakHandler class to temporarily redirect SIGINT handler, so that
91 # "svn commit" + post-commit rev-prop updating is a quasi-atomic unit.
92 # If user presses Ctrl-C during this, wait until after this full action
93 # has finished raising the KeyboardInterrupt exception.
94 bh = BreakHandler()
95 bh.enable()
96 # Run the "svn commit" command, and screen-scrape the target_rev value (if any)
97 output = run_svn(args)
98 rev_num = parse_svn_commit_rev(output) if output else None
99 if rev_num is not None:
100 if options.keep_date:
101 run_svn(["propset", "--revprop", "-r", rev_num, "svn:date", log_entry['date_raw']])
102 if options.keep_author:
103 run_svn(["propset", "--revprop", "-r", rev_num, "svn:author", log_entry['author']])
104 ui.status("Committed revision %s (source r%s).", rev_num, log_entry['revision'])
105 bh.disable()
106 # Check if the user tried to press Ctrl-C
107 if bh.trapped:
108 raise KeyboardInterrupt
109 return rev_num
110
111 def verify_commit(source_rev, target_rev, log_entry=None):
112 """
113 Compare the ancestry/content/properties between source_url vs target_url
114 for a given revision.
115 """
116 error_cnt = 0
117 # Gather the offsets in the source repo to check
118 check_paths = []
119 remove_paths = []
120 # TODO: Need to make this ancestry aware
121 if options.verify == 1 and log_entry is not None: # Changed only
122 ui.status("Verifying source revision %s (only-changed)...", source_rev, level=ui.VERBOSE)
123 for d in log_entry['changed_paths']:
124 path = d['path']
125 if not is_child_path(path, source_base):
126 continue
127 if d['kind'] == "":
128 d['kind'] = svnclient.get_kind(source_repos_url, path, source_rev, d['action'], log_entry['changed_paths'])
129 assert (d['kind'] == 'file') or (d['kind'] == 'dir')
130 path_is_dir = True if d['kind'] == 'dir' else False
131 path_is_file = True if d['kind'] == 'file' else False
132 path_offset = path[len(source_base):].strip("/")
133 if d['action'] == 'D':
134 remove_paths.append(path_offset)
135 elif not path_offset in check_paths:
136 ui.status("verify_commit: path [mode=changed]: kind=%s: %s", d['kind'], path, level=ui.DEBUG, color='YELLOW')
137 if path_is_file:
138 ui.status(" "+"verify_commit [mode=changed]: check_paths.append('%s')", path_offset, level=ui.DEBUG, color='GREEN')
139 check_paths.append(path_offset)
140 if path_is_dir:
141 if not d['action'] in 'AR':
142 continue
143 child_paths = svnclient.list(source_url.rstrip("/")+"/"+path_offset, source_rev, recursive=True)
144 for p in child_paths:
145 child_path_is_dir = True if p['kind'] == 'dir' else False
146 child_path_offset = p['path']
147 if not child_path_is_dir:
148 # Only check files
149 working_path = (path_offset+"/" if path_offset else "") + child_path_offset
150 if not working_path in check_paths:
151 ui.status(" "+"verify_commit [mode=changed]: check_paths.append('%s'+'/'+'%s')", path_offset, child_path_offset, level=ui.DEBUG, color='GREEN')
152 check_paths.append(working_path)
153 if options.verify == 2: # All paths
154 ui.status("Verifying source revision %s (all)...", source_rev, level=ui.VERBOSE)
155 child_paths = svnclient.list(source_url, source_rev, recursive=True)
156 for p in child_paths:
157 child_path_is_dir = True if p['kind'] == 'dir' else False
158 child_path_offset = p['path']
159 if not child_path_is_dir:
160 # Only check files
161 ui.status("verify_commit [mode=all]: check_paths.append('%s')", child_path_offset, level=ui.DEBUG, color='GREEN')
162 check_paths.append(child_path_offset)
163
164 # If there were any paths deleted in the last revision (options.verify=1 mode),
165 # check that they were correctly deleted.
166 if remove_paths:
167 count_total = len(remove_paths)
168 count = 0
169 for path_offset in remove_paths:
170 count += 1
171 if in_svn(path_offset):
172 ui.status(" (%s/%s) Verify path: FAIL: %s", str(count).rjust(len(str(count_total))), count_total, path_offset, level=ui.EXTRA, color='RED')
173 ui.status("VerificationError: Path removed in source rev r%s, but still exists in target WC: %s", source_rev, path_offset, color='RED')
174 error_cnt +=1
175 else:
176 ui.status(" (%s/%s) Verify remove: OK: %s", str(count).rjust(len(str(count_total))), count_total, path_offset, level=ui.EXTRA)
177
178 # Compare each of the check_path entries between source vs. target
179 if check_paths:
180 source_rev_first = int(min(rev_map, key=rev_map.get)) or 1 # The first source_rev we replayed into target
181 ui.status("verify_commit: source_rev_first:%s", source_rev_first, level=ui.DEBUG, color='YELLOW')
182 count_total = len(check_paths)
183 count = 0
184 for path_offset in check_paths:
185 count += 1
186 if count % 500 == 0:
187 ui.status("...processed %s (%s of %s)..." % (count, count, count_total), level=ui.VERBOSE)
188 ui.status("verify_commit: path_offset:%s", path_offset, level=ui.DEBUG, color='YELLOW')
189 source_log_entries = svnclient.run_svn_log(source_url.rstrip("/")+"/"+path_offset, source_rev, 1, source_rev-source_rev_first+1)
190 target_log_entries = svnclient.run_svn_log(target_url.rstrip("/")+"/"+path_offset, target_rev, 1, target_rev)
191 # Build a list of commits in source_log_entries which matches our
192 # target path_offset.
193 working_path = source_base+"/"+path_offset
194 source_revs = []
195 for log_entry in source_log_entries:
196 source_rev_tmp = log_entry['revision']
197 if source_rev_tmp < source_rev_first:
198 # Only process source revisions which have been replayed into target
199 break
200 #ui.status(" [verify_commit] source_rev_tmp:%s, working_path:%s\n%s", source_rev_tmp, working_path, pp.pformat(log_entry), level=ui.DEBUG, color='MAGENTA')
201 changed_paths_temp = []
202 for d in log_entry['changed_paths']:
203 path = d['path']
204 # Match working_path or any parents
205 if is_child_path(working_path, path):
206 ui.status(" verify_commit: changed_path: %s %s@%s (parent:%s)", d['action'], path, source_rev_tmp, working_path, level=ui.DEBUG, color='YELLOW')
207 changed_paths_temp.append({'path': path, 'data': d})
208 assert changed_paths_temp
209 # Reverse-sort any matches, so that we start with the most-granular (deepest in the tree) path.
210 changed_paths = sorted(changed_paths_temp, key=operator.itemgetter('path'), reverse=True)
211 # Find the action for our working_path in this revision. Use a loop to check in reverse order,
212 # so that if the target file/folder is "M" but has a parent folder with an "A" copy-from.
213 working_path_next = working_path
214 match_d = {}
215 for v in changed_paths:
216 d = v['data']
217 if not match_d:
218 match_d = d
219 path = d['path']
220 if d['action'] not in svnclient.valid_svn_actions:
221 raise UnsupportedSVNAction("In SVN rev. %d: action '%s' not supported. Please report a bug!"
222 % (log_entry['revision'], d['action']))
223 if d['action'] in 'AR' and d['copyfrom_revision']:
224 # If we found a copy-from action for a parent path, adjust our
225 # working_path to follow the rename/copy-from, just like find_svn_ancestors().
226 working_path_next = working_path.replace(d['path'], d['copyfrom_path'])
227 match_d = d
228 break
229 if is_child_path(working_path, source_base):
230 # Only add source_rev's where the path changed in this revision was a child
231 # of source_base, so that we silently ignore any history that happened on
232 # non-source_base paths (e.g. ignore branch history if we're only replaying trunk).
233 is_diff = False
234 d = match_d
235 if d['action'] == 'M':
236 # For action="M", we need to throw out cases where the only change was to
237 # a property which we ignore, e.g. "svn:mergeinfo".
238 if d['kind'] == "":
239 d['kind'] = svnclient.get_kind(source_repos_url, working_path, log_entry['revision'], d['action'], log_entry['changed_paths'])
240 assert (d['kind'] == 'file') or (d['kind'] == 'dir')
241 if d['kind'] == 'file':
242 # Check for file-content changes
243 # TODO: This should be made ancestor-aware, since the file won't always be at the same path in rev-1
244 sum1 = run_shell_command("svn cat -r %s '%s' | md5sum" % (source_rev_tmp, source_repos_url+working_path+"@"+str(source_rev_tmp)))
245 sum2 = run_shell_command("svn cat -r %s '%s' | md5sum" % (source_rev_tmp-1, source_repos_url+working_path_next+"@"+str(source_rev_tmp-1)))
246 is_diff = True if sum1 <> sum2 else False
247 if not is_diff:
248 # Check for property changes
249 props1 = svnclient.propget_all(source_repos_url+working_path, source_rev_tmp)
250 props2 = svnclient.propget_all(source_repos_url+working_path_next, source_rev_tmp-1)
251 # Ignore changes to "svn:mergeinfo", since we don't copy that
252 if 'svn:mergeinfo' in props1: del props1['svn:mergeinfo']
253 if 'svn:mergeinfo' in props2: del props2['svn:mergeinfo']
254 for prop in props1:
255 if prop not in props2 or \
256 props1[prop] != props2[prop]:
257 is_diff = True
258 break
259 for prop in props2:
260 if prop not in props1 or \
261 props1[prop] != props2[prop]:
262 is_diff = True
263 break
264 if not is_diff:
265 ui.status(" verify_commit: skip %s@%s", working_path, source_rev_tmp, level=ui.DEBUG, color='GREEN_B', bold=True)
266 else:
267 is_diff = True
268 if is_diff:
269 ui.status(" verify_commit: source_revs.append(%s), working_path:%s", source_rev_tmp, working_path, level=ui.DEBUG, color='GREEN_B')
270 source_revs.append({'path': working_path, 'revision': source_rev_tmp})
271 working_path = working_path_next
272 # Build a list of all the target commits "svn log" returned
273 target_revs = []
274 target_revs_rmndr = []
275 for log_entry in target_log_entries:
276 target_rev_tmp = log_entry['revision']
277 ui.status(" verify_commit: target_revs.append(%s)", target_rev_tmp, level=ui.DEBUG, color='GREEN_B')
278 target_revs.append(target_rev_tmp)
279 target_revs_rmndr.append(target_rev_tmp)
280 # Compare the two lists
281 for d in source_revs:
282 working_path = d['path']
283 source_rev_tmp = d['revision']
284 target_rev_tmp = get_rev_map(source_rev_tmp, " ")
285 working_offset = working_path[len(source_base):].strip("/")
286 sum1 = run_shell_command("svn cat -r %s '%s' | md5sum" % (source_rev_tmp, source_repos_url+working_path+"@"+str(source_rev_tmp)))
287 sum2 = run_shell_command("svn cat -r %s '%s' | md5sum" % (target_rev_tmp, target_url+"/"+working_offset+"@"+str(target_rev_tmp))) if target_rev_tmp is not None else ""
288 #print "source@%s: %s" % (str(source_rev_tmp).ljust(6), sum1)
289 #print "target@%s: %s" % (str(target_rev_tmp).ljust(6), sum2)
290 ui.status(" verify_commit: %s: source=%s target=%s", working_offset, source_rev_tmp, target_rev_tmp, level=ui.DEBUG, color='GREEN')
291 if not target_rev_tmp:
292 ui.status(" (%s/%s) Verify path: FAIL: %s", str(count).rjust(len(str(count_total))), count_total, path_offset, level=ui.EXTRA, color='RED')
293 ui.status("VerificationError: Unable to find corresponding target_rev for source_rev r%s in rev_map (path_offset='%s')", source_rev_tmp, path_offset, color='RED')
294 error_cnt +=1
295 continue
296 if target_rev_tmp not in target_revs:
297 # If found a source_rev with no equivalent target_rev in target_revs,
298 # check if the only difference in source_rev vs. source_rev-1 is the
299 # removal/addition of a trailing newline char, since this seems to get
300 # stripped-out sometimes during the replay (via "svn export"?).
301 # Strip any trailing \r\n from file-content (http://stackoverflow.com/a/1656218/346778)
302 sum1 = run_shell_command("svn cat -r %s '%s' | perl -i -p0777we's/\\r\\n\z//' | md5sum" % (source_rev_tmp, source_repos_url+working_path+"@"+str(source_rev_tmp)))
303 sum2 = run_shell_command("svn cat -r %s '%s' | perl -i -p0777we's/\\r\\n\z//' | md5sum" % (source_rev_tmp-1, source_repos_url+working_path+"@"+str(source_rev_tmp-1)))
304 if sum1 <> sum2:
305 ui.status(" (%s/%s) Verify path: FAIL: %s", str(count).rjust(len(str(count_total))), count_total, path_offset, level=ui.EXTRA, color='RED')
306 ui.status("VerificationError: Found source_rev (r%s) with no corresponding target_rev: path_offset='%s'", source_rev_tmp, path_offset, color='RED')
307 error_cnt +=1
308 continue
309 target_revs_rmndr.remove(target_rev_tmp)
310 if target_revs_rmndr:
311 rmndr_list = ", ".join(map(str, target_revs_rmndr))
312 ui.status(" (%s/%s) Verify path: FAIL: %s", str(count).rjust(len(str(count_total))), count_total, path_offset, level=ui.EXTRA, color='RED')
313 ui.status("VerificationError: Found one or more *extra* target_revs: path_offset='%s', target_revs='%s'", path_offset, rmndr_list, color='RED')
314 error_cnt +=1
315 else:
316 ui.status(" (%s/%s) Verify path: OK: %s", str(count).rjust(len(str(count_total))), count_total, path_offset, level=ui.EXTRA)
317
318 # Ensure there are no "extra" files in the target side
319 if options.verify == 2:
320 target_paths = []
321 child_paths = svnclient.list(target_url, target_rev, recursive=True)
322 for p in child_paths:
323 child_path_is_dir = True if p['kind'] == 'dir' else False
324 child_path_offset = p['path']
325 if not child_path_is_dir:
326 target_paths.append(child_path_offset)
327 # Compare
328 for path_offset in target_paths:
329 if not path_offset in check_paths:
330 ui.status("VerificationError: Path exists in target (@%s) but not source (@%s): %s", target_rev, source_rev, path_offset, color='RED')
331 error_cnt += 1
332 for path_offset in check_paths:
333 if not path_offset in target_paths:
334 ui.status("VerificationError: Path exists in source (@%s) but not target (@%s): %s", source_rev, target_rev, path_offset, color='RED')
335 error_cnt += 1
336
337 if error_cnt > 0:
338 raise VerificationError("Found %s verification errors" % (error_cnt))
339 ui.status("Verified revision %s (%s).", target_rev, "all" if options.verify == 2 else "only-changed")
340
341 def full_svn_revert():
342 """
343 Do an "svn revert" and proactively remove any extra files in the working copy.
344 """
345 run_svn(["revert", "--recursive", "."])
346 output = run_svn(["status"])
347 if output:
348 output_lines = output.strip("\n").split("\n")
349 for line in output_lines:
350 if line[0] == "?":
351 path = line[4:].strip(" ")
352 if os.path.isfile(path):
353 os.remove(path)
354 if os.path.isdir(path):
355 shell.rmtree(path)
356
357 def gen_tracking_revprops(source_rev):
358 """
359 Build an array of svn2svn-specific source-tracking revprops.
360 """
361 revprops = [{'name':'svn2svn:source_uuid', 'value':source_repos_uuid},
362 {'name':'svn2svn:source_url', 'value':urllib.quote(source_url, ":/")},
363 {'name':'svn2svn:source_rev', 'value':source_rev}]
364 return revprops
365
366 def sync_svn_props(source_url, source_rev, path_offset):
367 """
368 Carry-forward any unversioned properties from the source repo to the
369 target WC.
370 """
371 source_props = svnclient.propget_all(join_path(source_url, path_offset), source_rev)
372 target_props = svnclient.propget_all(path_offset)
373 if 'svn:mergeinfo' in source_props:
374 # Never carry-forward "svn:mergeinfo"
375 del source_props['svn:mergeinfo']
376 for prop in target_props:
377 if prop not in source_props:
378 # Remove any properties which exist in target but not source
379 run_svn(["propdel", prop, svnclient.safe_path(path_offset)])
380 for prop in source_props:
381 if prop not in target_props or \
382 source_props[prop] != target_props[prop]:
383 # Set/update any properties which exist in source but not target or
384 # whose value differs between source vs. target.
385 run_svn(["propset", prop, source_props[prop], svnclient.safe_path(path_offset)])
386
387 def get_rev_map(source_rev, prefix):
388 """
389 Find the equivalent rev # in the target repo for the given rev # from the source repo.
390 """
391 ui.status(prefix + ">> get_rev_map(%s)", source_rev, level=ui.DEBUG, color='GREEN')
392 # Find the highest entry less-than-or-equal-to source_rev
393 for rev in range(int(source_rev), 0, -1):
394 in_rev_map = True if rev in rev_map else False
395 ui.status(prefix + ">> get_rev_map: rev=%s in_rev_map=%s", rev, str(in_rev_map), level=ui.DEBUG, color='BLACK_B')
396 if in_rev_map:
397 return int(rev_map[rev])
398 # Else, we fell off the bottom of the rev_map. Ruh-roh...
399 return None
400
401 def set_rev_map(source_rev, target_rev):
402 #ui.status(">> set_rev_map: source_rev=%s target_rev=%s", source_rev, target_rev, level=ui.DEBUG, color='GREEN')
403 global rev_map
404 rev_map[int(source_rev)]=int(target_rev)
405
406 def build_rev_map(target_url, target_end_rev, source_info):
407 """
408 Check for any already-replayed history from source_url (source_info) and
409 build the mapping-table of source_rev -> target_rev.
410 """
411 global rev_map
412 rev_map = {}
413 ui.status("Rebuilding target_rev -> source_rev rev_map...", level=ui.VERBOSE)
414 proc_count = 0
415 it_log_entries = svnclient.iter_svn_log_entries(target_url, 1, target_end_rev, get_changed_paths=False, get_revprops=True)
416 for log_entry in it_log_entries:
417 if log_entry['revprops']:
418 revprops = {}
419 for v in log_entry['revprops']:
420 if v['name'].startswith('svn2svn:'):
421 revprops[v['name']] = v['value']
422 if revprops and \
423 revprops['svn2svn:source_uuid'] == source_info['repos_uuid'] and \
424 revprops['svn2svn:source_url'] == urllib.quote(source_info['url'], ":/"):
425 source_rev = revprops['svn2svn:source_rev']
426 target_rev = log_entry['revision']
427 set_rev_map(source_rev, target_rev)
428 proc_count += 1
429 if proc_count % 500 == 0:
430 ui.status("...processed %s (%s of %s)..." % (proc_count, target_rev, target_end_rev), level=ui.VERBOSE)
431
432 def path_in_list(paths, path):
433 for p in paths:
434 if is_child_path(path, p):
435 return True
436 return False
437
438 def add_path(paths, path):
439 if not path_in_list(paths, path):
440 paths.append(path)
441
442 def in_ancestors(ancestors, ancestor):
443 match = True
444 for idx in range(len(ancestors)-1, 0, -1):
445 if int(ancestors[idx]['revision']) > ancestor['revision']:
446 match = is_child_path(ancestor['path'], ancestors[idx]['path'])
447 break
448 return match
449
450 def do_svn_add(source_url, path_offset, source_rev, source_ancestors, \
451 parent_copyfrom_path="", parent_copyfrom_rev="", \
452 export_paths={}, is_dir = False, skip_paths=[], prefix = ""):
453 """
454 Given the add'd source path, replay the "svn add/copy" commands to correctly
455 track renames across copy-from's.
456
457 For example, consider a sequence of events like this:
458 1. svn copy /trunk /branches/fix1
459 2. (Make some changes on /branches/fix1)
460 3. svn mv /branches/fix1/Proj1 /branches/fix1/Proj2 " Rename folder
461 4. svn mv /branches/fix1/Proj2/file1.txt /branches/fix1/Proj2/file2.txt " Rename file inside renamed folder
462 5. svn co /trunk && svn merge /branches/fix1
463 After the merge and commit, "svn log -v" with show a delete of /trunk/Proj1
464 and and add of /trunk/Proj2 copy-from /branches/fix1/Proj2. If we were just
465 to do a straight "svn export+add" based on the /branches/fix1/Proj2 folder,
466 we'd lose the logical history that Proj2/file2.txt is really a descendant
467 of Proj1/file1.txt.
468
469 'path_offset' is the offset from source_base to the file to check ancestry for,
470 e.g. 'projectA/file1.txt'. path = source_repos_url + source_base + path_offset.
471 'source_rev' is the revision ("svn log") that we're processing from the source repo.
472 'parent_copyfrom_path' and 'parent_copyfrom_rev' is the copy-from path of the parent
473 directory, when being called recursively by do_svn_add_dir().
474 'export_paths' is the list of path_offset's that we've deferred running "svn export" on.
475 'is_dir' is whether path_offset is a directory (rather than a file).
476 """
477 source_base = source_url[len(source_repos_url):] # e.g. '/trunk'
478 ui.status(prefix + ">> do_svn_add: %s %s", join_path(source_base, path_offset)+"@"+str(source_rev),
479 " (parent-copyfrom: "+parent_copyfrom_path+"@"+str(parent_copyfrom_rev)+")" if parent_copyfrom_path else "",
480 level=ui.DEBUG, color='GREEN')
481 # Check if the given path has ancestors which chain back to the current source_base
482 found_ancestor = False
483 ancestors = find_svn_ancestors(source_repos_url, join_path(source_base, path_offset), source_rev, stop_base_path=source_base, prefix=prefix+" ")
484 ancestor = ancestors[len(ancestors)-1] if ancestors else None # Choose the eldest ancestor, i.e. where we reached stop_base_path=source_base
485 if ancestor and not in_ancestors(source_ancestors, ancestor):
486 ancestor = None
487 copyfrom_path = ancestor['copyfrom_path'] if ancestor else ""
488 copyfrom_rev = ancestor['copyfrom_rev'] if ancestor else ""
489 if ancestor:
490 # The copy-from path has ancestry back to source_url.
491 ui.status(prefix + ">> do_svn_add: Check copy-from: Found parent: %s", copyfrom_path+"@"+str(copyfrom_rev),
492 level=ui.DEBUG, color='GREEN', bold=True)
493 found_ancestor = True
494 # Map the copyfrom_rev (source repo) to the equivalent target repo rev #. This can
495 # return None in the case where copyfrom_rev is *before* our source_start_rev.
496 tgt_rev = get_rev_map(copyfrom_rev, prefix+" ")
497 ui.status(prefix + ">> do_svn_add: get_rev_map: %s (source) -> %s (target)", copyfrom_rev, tgt_rev, level=ui.DEBUG, color='GREEN')
498 else:
499 ui.status(prefix + ">> do_svn_add: Check copy-from: No ancestor chain found.", level=ui.DEBUG, color='GREEN')
500 found_ancestor = False
501 if found_ancestor and tgt_rev:
502 # Check if this path_offset in the target WC already has this ancestry, in which
503 # case there's no need to run the "svn copy" (again).
504 path_in_svn = in_svn(path_offset, prefix=prefix+" ")
505 log_entry = svnclient.get_last_svn_log_entry(path_offset, 1, 'HEAD', get_changed_paths=False) if in_svn(path_offset, require_in_repo=True, prefix=prefix+" ") else []
506 if (not log_entry or (log_entry['revision'] != tgt_rev)):
507 copyfrom_offset = copyfrom_path[len(source_base):].strip('/')
508 ui.status(prefix + ">> do_svn_add: svn_copy: Copy-from: %s", copyfrom_path+"@"+str(copyfrom_rev), level=ui.DEBUG, color='GREEN')
509 ui.status(prefix + " copyfrom: %s", copyfrom_path+"@"+str(copyfrom_rev), level=ui.DEBUG, color='GREEN')
510 ui.status(prefix + " p_copyfrom: %s", parent_copyfrom_path+"@"+str(parent_copyfrom_rev) if parent_copyfrom_path else "", level=ui.DEBUG, color='GREEN')
511 if path_in_svn and \
512 ((parent_copyfrom_path and is_child_path(copyfrom_path, parent_copyfrom_path)) and \
513 (parent_copyfrom_rev and copyfrom_rev == parent_copyfrom_rev)):
514 # When being called recursively, if this child entry has the same ancestor as the
515 # the parent, then no need to try to run another "svn copy".
516 ui.status(prefix + ">> do_svn_add: svn_copy: Same ancestry as parent: %s",
517 parent_copyfrom_path+"@"+str(parent_copyfrom_rev),level=ui.DEBUG, color='GREEN')
518 pass
519 else:
520 # Copy this path from the equivalent path+rev in the target repo, to create the
521 # equivalent history.
522 if parent_copyfrom_path:
523 # If we have a parent copy-from path, we mis-match that so display a status
524 # message describing the action we're mimic'ing. If path_in_svn, then this
525 # is logically a "replace" rather than an "add".
526 ui.status(" %s %s (from %s)", ('R' if path_in_svn else 'A'), join_path(source_base, path_offset), ancestors[0]['copyfrom_path']+"@"+str(copyfrom_rev), level=ui.VERBOSE)
527 if path_in_svn:
528 # If local file is already under version-control, then this is a replace.
529 ui.status(prefix + ">> do_svn_add: pre-copy: local path already exists: %s", path_offset, level=ui.DEBUG, color='GREEN')
530 svnclient.update(path_offset)
531 svnclient.remove(path_offset, force=True)
532 run_svn(["copy", "-r", tgt_rev, svnclient.safe_path(join_path(target_url, copyfrom_offset), tgt_rev), svnclient.safe_path(path_offset)])
533 if is_dir:
534 # Export the final verison of all files in this folder.
535 add_path(export_paths, path_offset)
536 else:
537 # Export the final verison of this file.
538 svnclient.export(source_repos_url+join_path(source_base, path_offset), source_rev, path_offset, force=True)
539 if options.keep_prop:
540 sync_svn_props(source_url, source_rev, path_offset)
541 else:
542 ui.status(prefix + ">> do_svn_add: Skipped 'svn copy': %s", path_offset, level=ui.DEBUG, color='GREEN')
543 else:
544 # Else, either this copy-from path has no ancestry back to source_url OR copyfrom_rev comes
545 # before our initial source_start_rev (i.e. tgt_rev == None), so can't do a "svn copy".
546 # Create (parent) directory if needed.
547 # TODO: This is (nearly) a duplicate of code in process_svn_log_entry(). Should this be
548 # split-out to a shared tag?
549 p_path = path_offset if is_dir else os.path.dirname(path_offset).strip() or None
550 if p_path and not os.path.exists(p_path):
551 run_svn(["mkdir", svnclient.safe_path(p_path)])
552 if not in_svn(path_offset, prefix=prefix+" "):
553 if is_dir:
554 # Export the final verison of all files in this folder.
555 add_path(export_paths, path_offset)
556 else:
557 # Export the final verison of this file. We *need* to do this before running
558 # the "svn add", even if we end-up re-exporting this file again via export_paths.
559 svnclient.export(source_repos_url+join_path(source_base, path_offset), source_rev, path_offset, force=True)
560 # If not already under version-control, then "svn add" this file/folder.
561 run_svn(["add", "--parents", svnclient.safe_path(path_offset)])
562 if options.keep_prop:
563 sync_svn_props(source_url, source_rev, path_offset)
564 if is_dir:
565 # For any folders that we process, process any child contents, so that we correctly
566 # replay copies/replaces/etc.
567 do_svn_add_dir(source_url, path_offset, source_rev, source_ancestors,
568 copyfrom_path, copyfrom_rev, export_paths, skip_paths, prefix+" ")
569
570 def do_svn_add_dir(source_url, path_offset, source_rev, source_ancestors, \
571 parent_copyfrom_path, parent_copyfrom_rev, \
572 export_paths, skip_paths, prefix=""):
573 source_base = source_url[len(source_repos_url):] # e.g. '/trunk'
574 # Get the directory contents, to compare between the local WC (target_url) vs. the remote repo (source_url)
575 # TODO: paths_local won't include add'd paths because "svn ls" lists the contents of the
576 # associated remote repo folder. (Is this a problem?)
577 paths_local = svnclient.list(path_offset)
578 paths_remote = svnclient.list(join_path(source_url, path_offset), source_rev)
579 ui.status(prefix + ">> do_svn_add_dir: paths_local: %s", str(paths_local), level=ui.DEBUG, color='GREEN')
580 ui.status(prefix + ">> do_svn_add_dir: paths_remote: %s", str(paths_remote), level=ui.DEBUG, color='GREEN')
581 # Update files/folders which exist in remote but not local
582 for p in paths_remote:
583 path_is_dir = True if p['kind'] == 'dir' else False
584 working_path = join_path(path_offset, p['path']).lstrip('/')
585 #print "working_path:%s = path_offset:%s + path:%s" % (working_path, path_offset, path)
586 if not working_path in skip_paths:
587 do_svn_add(source_url, working_path, source_rev, source_ancestors,
588 parent_copyfrom_path, parent_copyfrom_rev,
589 export_paths, path_is_dir, skip_paths, prefix+" ")
590 # Remove files/folders which exist in local but not remote
591 for p in paths_local:
592 if not p in paths_remote:
593 working_path = join_path(path_offset, p['path']).lstrip('/')
594 ui.status(" %s %s", 'D', join_path(source_base, working_path), level=ui.VERBOSE)
595 svnclient.update(working_path)
596 svnclient.remove(working_path, force=True)
597 # TODO: Does this handle deleted folders too? Wouldn't want to have a case
598 # where we only delete all files from folder but leave orphaned folder around.
599
600 def process_svn_log_entry(log_entry, ancestors, commit_paths, prefix = ""):
601 """
602 Process SVN changes from the given log entry. Build an array (commit_paths)
603 of the paths in the working-copy that were changed, i.e. the paths which
604 we'll pass to "svn commit".
605 """
606 export_paths = []
607 source_rev = log_entry['revision']
608 source_url = log_entry['url']
609 source_base = source_url[len(source_repos_url):] # e.g. '/trunk'
610 ui.status(prefix + ">> process_svn_log_entry: %s", source_url+"@"+str(source_rev), level=ui.DEBUG, color='GREEN')
611 for d in log_entry['changed_paths']:
612 # Get the full path for this changed_path
613 # e.g. '/branches/bug123/projectA/file1.txt'
614 path = d['path']
615 if not is_child_path(path, source_base):
616 # Ignore changed files that are not part of this subdir
617 ui.status(prefix + ">> process_svn_log_entry: Unrelated path: %s (base: %s)", path, source_base, level=ui.DEBUG, color='GREEN')
618 continue
619 if d['kind'] == "" or d['kind'] == 'none':
620 # The "kind" value was introduced in SVN 1.6, and "svn log --xml" won't return a "kind"
621 # value for commits made on a pre-1.6 repo, even if the server is now running 1.6.
622 # We need to use other methods to fetch the node-kind for these cases.
623 d['kind'] = svnclient.get_kind(source_repos_url, path, source_rev, d['action'], log_entry['changed_paths'])
624 assert (d['kind'] == 'file') or (d['kind'] == 'dir')
625 path_is_dir = True if d['kind'] == 'dir' else False
626 path_is_file = True if d['kind'] == 'file' else False
627 # Calculate the offset (based on source_base) for this changed_path
628 # e.g. 'projectA/file1.txt'
629 # (path = source_base + "/" + path_offset)
630 path_offset = path[len(source_base):].strip("/")
631 # Get the action for this path
632 action = d['action']
633 if action not in svnclient.valid_svn_actions:
634 raise UnsupportedSVNAction("In SVN rev. %d: action '%s' not supported. Please report a bug!"
635 % (source_rev, action))
636 ui.status(" %s %s%s", action, d['path'],
637 (" (from %s)" % (d['copyfrom_path']+"@"+str(d['copyfrom_revision']))) if d['copyfrom_path'] else "",
638 level=ui.VERBOSE)
639
640 # Try to be efficient and keep track of an explicit list of paths in the
641 # working copy that changed. If we commit from the root of the working copy,
642 # then SVN needs to crawl the entire working copy looking for pending changes.
643 commit_paths.append(path_offset)
644
645 # Special-handling for replace's
646 if action == 'R':
647 # If file was "replaced" (deleted then re-added, all in same revision),
648 # then we need to run the "svn rm" first, then change action='A'. This
649 # lets the normal code below handle re-"svn add"'ing the files. This
650 # should replicate the "replace".
651 if path_offset and in_svn(path_offset):
652 # Target path might not be under version-control yet, e.g. parent "add"
653 # was a copy-from a branch which had no ancestry back to trunk, and each
654 # child folder under that parent folder is a "replace" action on the final
655 # merge to trunk. Since the child folders will be in skip_paths, do_svn_add
656 # wouldn't have created them while processing the parent "add" path.
657 if path_is_dir:
658 # Need to "svn update" before "svn remove" in case child contents are at
659 # a higher rev than the (parent) path_offset.
660 svnclient.update(path_offset)
661 svnclient.remove(path_offset, force=True)
662 action = 'A'
663
664 # Handle all the various action-types
665 # (Handle "add" first, for "svn copy/move" support)
666 if action == 'A':
667 # Determine where to export from.
668 svn_copy = False
669 # Handle cases where this "add" was a copy from another URL in the source repo
670 if d['copyfrom_revision']:
671 copyfrom_path = d['copyfrom_path']
672 copyfrom_rev = d['copyfrom_revision']
673 skip_paths = []
674 for tmp_d in log_entry['changed_paths']:
675 tmp_path = tmp_d['path']
676 if is_child_path(tmp_path, path) and tmp_d['action'] in 'ARD':
677 # Build list of child entries which are also in the changed_paths list,
678 # so that do_svn_add() can skip processing these entries when recursing
679 # since we'll end-up processing them later. Don't include action="M" paths
680 # in this list because it's non-conclusive: it could just mean that the
681 # file was modified *after* the copy-from, so we still want do_svn_add()
682 # to re-create the correct ancestry.
683 tmp_path_offset = tmp_path[len(source_base):].strip("/")
684 skip_paths.append(tmp_path_offset)
685 do_svn_add(source_url, path_offset, source_rev, ancestors, "", "", export_paths, path_is_dir, skip_paths, prefix+" ")
686 # Else just "svn export" the files from the source repo and "svn add" them.
687 else:
688 # Create (parent) directory if needed
689 p_path = path_offset if path_is_dir else os.path.dirname(path_offset).strip() or None
690 if p_path and not os.path.exists(p_path):
691 run_svn(["mkdir", svnclient.safe_path(p_path)])
692 # Export the entire added tree.
693 if path_is_dir:
694 # For directories, defer the (recurisve) "svn export". Might have a
695 # situation in a branch merge where the entry in the svn-log is a
696 # non-copy-from'd "add" but there are child contents (that we haven't
697 # gotten to yet in log_entry) that are copy-from's. When we try do
698 # the "svn copy" later on in do_svn_add() for those copy-from'd paths,
699 # having pre-existing (svn-add'd) contents creates some trouble.
700 # Instead, just create the stub folders ("svn mkdir" above) and defer
701 # exporting the final file-state until the end.
702 add_path(export_paths, path_offset)
703 else:
704 # Export the final verison of this file. We *need* to do this before running
705 # the "svn add", even if we end-up re-exporting this file again via export_paths.
706 svnclient.export(join_path(source_url, path_offset), source_rev, path_offset, force=True)
707 if not in_svn(path_offset, prefix=prefix+" "):
708 # Need to use in_svn here to handle cases where client committed the parent
709 # folder and each indiv sub-folder.
710 run_svn(["add", "--parents", svnclient.safe_path(path_offset)])
711 if options.keep_prop:
712 sync_svn_props(source_url, source_rev, path_offset)
713
714 elif action == 'D':
715 if path_is_dir:
716 # For dirs, need to "svn update" before "svn remove" because the final
717 # "svn commit" will fail if the parent (path_offset) is at a lower rev
718 # than any of the child contents. This needs to be a recursive update.
719 svnclient.update(path_offset)
720 svnclient.remove(path_offset, force=True)
721
722 elif action == 'M':
723 if path_is_file:
724 svnclient.export(join_path(source_url, path_offset), source_rev, path_offset, force=True, non_recursive=True)
725 if path_is_dir:
726 # For dirs, need to "svn update" before export/prop-sync because the
727 # final "svn commit" will fail if the parent is at a lower rev than
728 # child contents. Just need to update the rev-state of the dir (d['path']),
729 # don't need to recursively update all child contents.
730 # (??? is this the right reason?)
731 svnclient.update(path_offset, non_recursive=True)
732 if options.keep_prop:
733 sync_svn_props(source_url, source_rev, path_offset)
734
735 else:
736 raise InternalError("Internal Error: process_svn_log_entry: Unhandled 'action' value: '%s'"
737 % action)
738
739 # Export the final version of all add'd paths from source_url
740 if export_paths:
741 for path_offset in export_paths:
742 svnclient.export(join_path(source_url, path_offset), source_rev, path_offset, force=True)
743
744 def keep_revnum(source_rev, target_rev_last, wc_target_tmp):
745 """
746 Add "padding" target revisions as needed to keep source and target
747 revision #'s identical.
748 """
749 bh = BreakHandler()
750 if int(source_rev) <= int(target_rev_last):
751 raise InternalError("keep-revnum mode is enabled, "
752 "but source revision (r%s) is less-than-or-equal last target revision (r%s)" % \
753 (source_rev, target_rev_last))
754 if int(target_rev_last) < int(source_rev)-1:
755 # Add "padding" target revisions to keep source and target rev #'s identical
756 if os.path.exists(wc_target_tmp):
757 shell.rmtree(wc_target_tmp)
758 run_svn(["checkout", "-r", "HEAD", "--depth=empty", svnclient.safe_path(target_repos_url, "HEAD"), svnclient.safe_path(wc_target_tmp)])
759 for rev_num in range(int(target_rev_last)+1, int(source_rev)):
760 run_svn(["propset", "svn2svn:keep-revnum", rev_num, svnclient.safe_path(wc_target_tmp)])
761 # Prevent Ctrl-C's during this inner part, so we'll always display
762 # the "Commit revision ..." message if we ran a "svn commit".
763 bh.enable()
764 output = run_svn(["commit", "-m", "", svnclient.safe_path(wc_target_tmp)])
765 rev_num_tmp = parse_svn_commit_rev(output) if output else None
766 assert rev_num == rev_num_tmp
767 ui.status("Committed revision %s (keep-revnum).", rev_num)
768 bh.disable()
769 # Check if the user tried to press Ctrl-C
770 if bh.trapped:
771 raise KeyboardInterrupt
772 target_rev_last = rev_num
773 shell.rmtree(wc_target_tmp)
774 return target_rev_last
775
776 def disp_svn_log_summary(log_entry):
777 ui.status("------------------------------------------------------------------------", level=ui.VERBOSE)
778 ui.status("r%s | %s | %s",
779 log_entry['revision'],
780 log_entry['author'],
781 str(datetime.fromtimestamp(int(log_entry['date'])).isoformat(' ')), level=ui.VERBOSE)
782 ui.status(log_entry['message'], level=ui.VERBOSE)
783
784 def real_main(args):
785 global source_url, target_url, rev_map
786 # Use urllib.unquote() to URL-decode source_url/target_url values.
787 # All URLs passed to run_svn() should go through svnclient.safe_path()
788 # and we don't want to end-up *double* urllib.quote'ing if the user-
789 # supplied source/target URL's are already URL-encoded.
790 source_url = urllib.unquote(args.pop(0).rstrip("/")) # e.g. 'http://server/svn/source/trunk'
791 target_url = urllib.unquote(args.pop(0).rstrip("/")) # e.g. 'file:///svn/target/trunk'
792 ui.status("options: %s", str(options), level=ui.DEBUG, color='GREEN')
793
794 # Make sure that both the source and target URL's are valid
795 source_info = svnclient.info(source_url)
796 assert is_child_path(source_url, source_info['repos_url'])
797 target_info = svnclient.info(target_url)
798 assert is_child_path(target_url, target_info['repos_url'])
799
800 # Init global vars
801 global source_repos_url,source_base,source_repos_uuid
802 source_repos_url = source_info['repos_url'] # e.g. 'http://server/svn/source'
803 source_base = source_url[len(source_repos_url):] # e.g. '/trunk'
804 source_repos_uuid = source_info['repos_uuid']
805 global target_repos_url,target_base
806 target_repos_url = target_info['repos_url'] # e.g. 'http://server/svn/target'
807 target_base = target_url[len(target_repos_url):] # e.g. '/trunk'
808
809 # Init start and end revision
810 try:
811 source_start_rev = svnclient.get_rev(source_repos_url, options.rev_start if options.rev_start else 1)
812 except ExternalCommandFailed:
813 print "Error: Invalid start source revision value: %s" % (options.rev_start)
814 return 1
815 try:
816 source_end_rev = svnclient.get_rev(source_repos_url, options.rev_end if options.rev_end else "HEAD")
817 except ExternalCommandFailed:
818 print "Error: Invalid end source revision value: %s" % (options.rev_end)
819 return 1
820 ui.status("Using source revision range %s:%s", source_start_rev, source_end_rev, level=ui.VERBOSE)
821
822 # TODO: If options.keep_date, should we try doing a "svn propset" on an *existing* revision
823 # as a sanity check, so we check if the pre-revprop-change hook script is correctly setup
824 # before doing first replay-commit?
825
826 target_rev_last = target_info['revision'] # Last revision # in the target repo
827 wc_target = os.path.abspath('_wc_target')
828 wc_target_tmp = os.path.abspath('_wc_target_tmp')
829 num_entries_proc = 0
830 commit_count = 0
831 source_rev = None
832 target_rev = None
833
834 # Check out a working copy of target_url if needed
835 wc_exists = os.path.exists(wc_target)
836 if wc_exists and not options.cont_from_break:
837 shell.rmtree(wc_target)
838 wc_exists = False
839 if not wc_exists:
840 ui.status("Checking-out _wc_target...", level=ui.VERBOSE)
841 svnclient.svn_checkout(target_url, wc_target)
842 os.chdir(wc_target)
843 if wc_exists:
844 # If using an existing WC, make sure it's clean ("svn revert")
845 ui.status("Cleaning-up _wc_target...", level=ui.VERBOSE)
846 run_svn(["cleanup"])
847 full_svn_revert()
848
849 if not options.cont_from_break:
850 # Warn user if trying to start (non-continue) into a non-empty target path
851 if not options.force_nocont:
852 top_paths = svnclient.list(target_url, "HEAD")
853 if len(top_paths)>0:
854 print "Error: Trying to replay (non-continue-mode) into a non-empty target_url location. " \
855 "Use --force if you're sure this is what you want."
856 return 1
857 # Get the first log entry at/after source_start_rev, which is where
858 # we'll do the initial import from.
859 source_ancestors = find_svn_ancestors(source_repos_url, source_base, source_end_rev, prefix=" ")
860 it_log_start = svnclient.iter_svn_log_entries(source_url, source_start_rev, source_end_rev, get_changed_paths=False, ancestors=source_ancestors)
861 source_start_log = None
862 for log_entry in it_log_start:
863 # Pick the first entry. Need to use a "for ..." loop since we're using an iterator.
864 source_start_log = log_entry
865 break
866 if not source_start_log:
867 raise InternalError("Unable to find any matching revisions between %s:%s in source_url: %s" % \
868 (source_start_rev, source_end_rev, source_url))
869
870 # This is the revision we will start from for source_url
871 source_start_rev = int(source_start_log['revision'])
872 ui.status("Starting at source revision %s.", source_start_rev, level=ui.VERBOSE)
873 ui.status("", level=ui.VERBOSE)
874 if options.keep_revnum and source_rev > target_rev_last:
875 target_rev_last = keep_revnum(source_rev, target_rev_last, wc_target_tmp)
876
877 # For the initial commit to the target URL, export all the contents from
878 # the source URL at the start-revision.
879 disp_svn_log_summary(svnclient.get_one_svn_log_entry(source_repos_url, source_start_rev, source_start_rev))
880 # Export and add file-contents from source_url@source_start_rev
881 source_start_url = source_url if not source_ancestors else source_repos_url+source_ancestors[len(source_ancestors)-1]['copyfrom_path']
882 top_paths = svnclient.list(source_start_url, source_start_rev)
883 for p in top_paths:
884 # For each top-level file/folder...
885 path_is_dir = True if p['kind'] == "dir" else False
886 path_offset = p['path']
887 if in_svn(path_offset, prefix=" "):
888 raise InternalError("Cannot replay history on top of pre-existing structure: %s" % join_path(source_start_url, path_offset))
889 if path_is_dir and not os.path.exists(path_offset):
890 os.makedirs(path_offset)
891 svnclient.export(join_path(source_start_url, path_offset), source_start_rev, path_offset, force=True)
892 run_svn(["add", svnclient.safe_path(path_offset)])
893 # Update any properties on the newly added content
894 paths = svnclient.list(source_start_url, source_start_rev, recursive=True)
895 if options.keep_prop:
896 sync_svn_props(source_start_url, source_start_rev, "")
897 for p in paths:
898 path_offset = p['path']
899 ui.status(" A %s", join_path(source_base, path_offset), level=ui.VERBOSE)
900 if options.keep_prop:
901 sync_svn_props(source_start_url, source_start_rev, path_offset)
902 # Commit the initial import
903 num_entries_proc += 1
904 target_revprops = gen_tracking_revprops(source_start_rev) # Build source-tracking revprop's
905 target_rev = commit_from_svn_log_entry(source_start_log, target_revprops=target_revprops)
906 if target_rev:
907 # Update rev_map, mapping table of source-repo rev # -> target-repo rev #
908 set_rev_map(source_start_rev, target_rev)
909 commit_count += 1
910 target_rev_last = target_rev
911 if options.verify:
912 verify_commit(source_rev, target_rev_last)
913 else:
914 # Re-build the rev_map based on any already-replayed history in target_url
915 build_rev_map(target_url, target_rev_last, source_info)
916 if not rev_map:
917 print "Error: Called with continue-mode, but no already-replayed source history found in target_url."
918 return 1
919 source_start_rev = int(max(rev_map, key=rev_map.get))
920 assert source_start_rev
921 ui.status("Continuing from source revision %s.", source_start_rev, level=ui.VERBOSE)
922 ui.status("", level=ui.VERBOSE)
923
924 svn_vers_t = svnclient.version()
925 svn_vers = float(".".join(map(str, svn_vers_t[0:2])))
926
927 # Load SVN log starting from source_start_rev + 1
928 source_ancestors = find_svn_ancestors(source_repos_url, source_base, source_end_rev, prefix=" ")
929 it_log_entries = svnclient.iter_svn_log_entries(source_url, source_start_rev+1, source_end_rev, get_revprops=True, ancestors=source_ancestors) if source_start_rev < source_end_rev else []
930 source_rev_last = source_start_rev
931 exit_code = 0
932
933 try:
934 for log_entry in it_log_entries:
935 if options.entries_proc_limit:
936 if num_entries_proc >= options.entries_proc_limit:
937 break
938 # Replay this revision from source_url into target_url
939 source_rev = log_entry['revision']
940 log_url = log_entry['url']
941 #print "source_url:%s log_url:%s" % (source_url, log_url)
942 if options.keep_revnum:
943 if source_rev < target_rev_last:
944 print "Error: Last target revision (r%s) is equal-or-higher than starting source revision (r%s). " \
945 "Cannot use --keep-revnum mode." % (target_rev_last, source_start_rev)
946 return 1
947 target_rev_last = keep_revnum(source_rev, target_rev_last, wc_target_tmp)
948 disp_svn_log_summary(log_entry)
949 # Process all the changed-paths in this log entry
950 commit_paths = []
951 process_svn_log_entry(log_entry, source_ancestors, commit_paths)
952 num_entries_proc += 1
953 # Commit any changes made to _wc_target
954 target_revprops = gen_tracking_revprops(source_rev) # Build source-tracking revprop's
955 target_rev = commit_from_svn_log_entry(log_entry, commit_paths, target_revprops=target_revprops)
956 source_rev_last = source_rev
957 if target_rev:
958 # Update rev_map, mapping table of source-repo rev # -> target-repo rev #
959 source_rev = log_entry['revision']
960 set_rev_map(source_rev, target_rev)
961 target_rev_last = target_rev
962 commit_count += 1
963 if options.verify:
964 verify_commit(source_rev, target_rev_last, log_entry)
965 # Run "svn cleanup" every 100 commits if SVN 1.7+, to clean-up orphaned ".svn/pristines/*"
966 if svn_vers >= 1.7 and (commit_count % 100 == 0):
967 run_svn(["cleanup"])
968 if source_rev_last == source_start_rev:
969 # If there were no new source_url revisions to process, still trigger
970 # "full-mode" verify check (if enabled).
971 if options.verify:
972 verify_commit(source_rev_last, target_rev_last)
973
974 except KeyboardInterrupt:
975 exit_code = 1
976 print "\nStopped by user."
977 print "\nCleaning-up..."
978 run_svn(["cleanup"])
979 full_svn_revert()
980 except:
981 exit_code = 1
982 print "\nCommand failed with following error:\n"
983 traceback.print_exc()
984 print "\nCleaning-up..."
985 run_svn(["cleanup"])
986 print run_svn(["status"])
987 full_svn_revert()
988 finally:
989 print "\nFinished at source revision %s%s." % (source_rev_last, " (dry-run)" if options.dry_run else "")
990
991 return exit_code
992
993 def main():
994 # Defined as entry point. Must be callable without arguments.
995 usage = "svn2svn, version %s\n" % str(full_version) + \
996 "<http://nynim.org/projects/svn2svn> <https://github.com/tonyduckles/svn2svn>\n\n" + \
997 "Usage: %prog [OPTIONS] source_url target_url\n"
998 description = """\
999 Replicate (replay) history from one SVN repository to another. Maintain
1000 logical ancestry wherever possible, so that 'svn log' on the replayed repo
1001 will correctly follow file/folder renames.
1002
1003 Examples:
1004 Create a copy of only /trunk from source repo, starting at r5000
1005 $ svnadmin create /svn/target
1006 $ svn mkdir -m 'Add trunk' file:///svn/target/trunk
1007 $ svnreplay -av -r 5000 http://server/source/trunk file:///svn/target/trunk
1008 1. The target_url will be checked-out to ./_wc_target
1009 2. The first commit to http://server/source/trunk at/after r5000 will be
1010 exported & added into _wc_target
1011 3. All revisions affecting http://server/source/trunk (starting at r5000)
1012 will be replayed to _wc_target. Any add/copy/move/replaces that are
1013 copy-from'd some path outside of /trunk (e.g. files renamed on a
1014 /branch and branch was merged into /trunk) will correctly maintain
1015 logical ancestry where possible.
1016
1017 Use continue-mode (-c) to pick-up where the last run left-off
1018 $ svnreplay -avc http://server/source/trunk file:///svn/target/trunk
1019 1. The target_url will be checked-out to ./_wc_target, if not already
1020 checked-out
1021 2. All new revisions affecting http://server/source/trunk starting from
1022 the last replayed revision to file:///svn/target/trunk (based on the
1023 svn2svn:* revprops) will be replayed to _wc_target, maintaining all
1024 logical ancestry where possible."""
1025 parser = optparse.OptionParser(usage, description=description,
1026 formatter=HelpFormatter(), version="%prog "+str(full_version))
1027 parser.add_option("-v", "--verbose", dest="verbosity", action="count", default=1,
1028 help="enable additional output (use -vv or -vvv for more)")
1029 parser.add_option("-a", "--archive", action="store_true", dest="archive", default=False,
1030 help="archive/mirror mode; same as -UDP (see REQUIRE's below)\n"
1031 "maintain same commit author, same commit time, and file/dir properties")
1032 parser.add_option("-U", "--keep-author", action="store_true", dest="keep_author", default=False,
1033 help="maintain same commit authors (svn:author) as source\n"
1034 "(REQUIRES 'pre-revprop-change' hook script to allow 'svn:author' changes)")
1035 parser.add_option("-D", "--keep-date", action="store_true", dest="keep_date", default=False,
1036 help="maintain same commit time (svn:date) as source\n"
1037 "(REQUIRES 'pre-revprop-change' hook script to allow 'svn:date' changes)")
1038 parser.add_option("-P", "--keep-prop", action="store_true", dest="keep_prop", default=False,
1039 help="maintain same file/dir SVN properties as source")
1040 parser.add_option("-R", "--keep-revnum", action="store_true", dest="keep_revnum", default=False,
1041 help="maintain same rev #'s as source. creates placeholder target "
1042 "revisions (by modifying a 'svn2svn:keep-revnum' property at the root of the target repo)")
1043 parser.add_option("-c", "--continue", action="store_true", dest="cont_from_break",
1044 help="continue from last source commit to target (based on svn2svn:* revprops)")
1045 parser.add_option("-f", "--force", action="store_true", dest="force_nocont",
1046 help="allow replaying into a non-empty target folder")
1047 parser.add_option("-r", "--revision", type="string", dest="revision", metavar="ARG",
1048 help="revision range to replay from source_url\n"
1049 "A revision argument can be one of:\n"
1050 " START start rev # (end will be 'HEAD')\n"
1051 " START:END start and ending rev #'s\n"
1052 "Any revision # formats which SVN understands are "
1053 "supported, e.g. 'HEAD', '{2010-01-31}', etc.")
1054 parser.add_option("-u", "--log-author", action="store_true", dest="log_author", default=False,
1055 help="append source commit author to replayed commit mesages")
1056 parser.add_option("-d", "--log-date", action="store_true", dest="log_date", default=False,
1057 help="append source commit time to replayed commit messages")
1058 parser.add_option("-l", "--limit", type="int", dest="entries_proc_limit", metavar="NUM",
1059 help="maximum number of source revisions to process")
1060 parser.add_option("-n", "--dry-run", action="store_true", dest="dry_run", default=False,
1061 help="process next source revision but don't commit changes to "
1062 "target working-copy (forces --limit=1)")
1063 parser.add_option("-x", "--verify", action="store_const", const=1, dest="verify",
1064 help="verify ancestry and content for changed paths in commit after every target commit or last target commit")
1065 parser.add_option("-X", "--verify-all", action="store_const", const=2, dest="verify",
1066 help="verify ancestry and content for entire target_url tree after every target commit or last target commit")
1067 parser.add_option("--debug", dest="verbosity", const=ui.DEBUG, action="store_const",
1068 help="enable debugging output (same as -vvv)")
1069 global options
1070 options, args = parser.parse_args()
1071 if len(args) != 2:
1072 parser.error("incorrect number of arguments")
1073 if options.verbosity < 10:
1074 # Expand multiple "-v" arguments to a real ui._level value
1075 options.verbosity *= 10
1076 if options.dry_run:
1077 # When in dry-run mode, only try to process the next log_entry
1078 options.entries_proc_limit = 1
1079 options.rev_start = None
1080 options.rev_end = None
1081 if options.revision:
1082 # Reg-ex for matching a revision arg (http://svnbook.red-bean.com/en/1.5/svn.tour.revs.specifiers.html#svn.tour.revs.dates)
1083 rev_patt = '[0-9A-Z]+|\{[0-9A-Za-z/\\ :-]+\}'
1084 rev = None
1085 match = re.match('^('+rev_patt+'):('+rev_patt+')$', options.revision) # First try start:end match
1086 if match is None: match = re.match('^('+rev_patt+')$', options.revision) # Next, try start match
1087 if match is None:
1088 parser.error("unexpected --revision argument format; see 'svn help log' for valid revision formats")
1089 rev = match.groups()
1090 options.rev_start = rev[0] if len(rev)>0 else None
1091 options.rev_end = rev[1] if len(rev)>1 else None
1092 if options.archive:
1093 options.keep_author = True
1094 options.keep_date = True
1095 options.keep_prop = True
1096 ui.update_config(options)
1097 return real_main(args)
1098
1099
1100 if __name__ == "__main__":
1101 sys.exit(main() or 0)