2 Replicate (replay) changesets from one SVN repository to another. 
   5 from svn2svn 
import base_version
, full_version
 
   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
 
  22 from datetime 
import datetime
 
  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 
  35 def parse_svn_commit_rev(output
): 
  37     Parse the revision number from the output of "svn commit". 
  39     output_lines 
= output
.strip("\n").split("\n") 
  41     for line 
in output_lines
: 
  42         if line
[0:19] == 'Committed revision ': 
  43             rev_num 
= line
[19:].rstrip('.') 
  45     assert rev_num 
is not None 
  48 def commit_from_svn_log_entry(log_entry
, commit_paths
=None, target_revprops
=None): 
  50     Given an SVN log entry and an optional list of changed paths, do an svn commit. 
  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'] 
  65         message 
+= "\nDate: " + svn_date
 
  66     if options
.log_author
: 
  67         message 
+= "\nAuthor: " + log_entry
['author'] 
  68     args 
+= ["-m", message
] 
  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'] 
  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'] 
  80             args 
+= ["--with-revprop", "%s=%s" % (key
, str(revprops
[key
]))] 
  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
)] 
  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. 
  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']) 
 106         # Check if the user tried to press Ctrl-C 
 108             raise KeyboardInterrupt 
 111 def verify_commit(source_rev
, target_rev
, log_entry
=None): 
 113     Compare the ancestry/content/properties between source_url vs target_url 
 114     for a given revision. 
 117     # Gather the offsets in the source repo to check 
 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']: 
 125             if not is_child_path(path
, source_base
): 
 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') 
 138                     ui
.status("  "+"verify_commit [mode=changed]: check_paths.append('%s')", path_offset
, level
=ui
.DEBUG
, color
='GREEN') 
 139                     check_paths
.append(path_offset
) 
 141                     if not d
['action'] in 'AR': 
 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
: 
 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
: 
 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
) 
 164     # If there were any paths deleted in the last revision (options.verify=1 mode), 
 165     # check that they were correctly deleted. 
 167         count_total 
= len(remove_paths
) 
 169         for path_offset 
in remove_paths
: 
 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') 
 176                 ui
.status(" (%s/%s) Verify remove: OK: %s", str(count
).rjust(len(str(count_total
))), count_total
, path_offset
, level
=ui
.EXTRA
) 
 178     # Compare each of the check_path entries between source vs. target 
 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
) 
 184         for path_offset 
in check_paths
: 
 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
 
 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 
 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']: 
 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
 
 215                 for v 
in changed_paths
: 
 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']) 
 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). 
 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". 
 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 
 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'] 
 255                                 if prop 
not in props2 
or \
 
 256                                         props1
[prop
] != props2
[prop
]: 
 260                                 if prop 
not in props1 
or \
 
 261                                         props1
[prop
] != props2
[prop
]: 
 265                             ui
.status("  verify_commit: skip %s@%s", working_path
, source_rev_tmp
, level
=ui
.DEBUG
, color
='GREEN_B', bold
=True) 
 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 
 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') 
 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))) 
 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') 
 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') 
 316                 ui
.status(" (%s/%s) Verify path: OK: %s", str(count
).rjust(len(str(count_total
))), count_total
, path_offset
, level
=ui
.EXTRA
) 
 318     # Ensure there are no "extra" files in the target side 
 319     if options
.verify 
== 2: 
 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
) 
 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') 
 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') 
 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") 
 341 def full_svn_revert(): 
 343     Do an "svn revert" and proactively remove any extra files in the working copy. 
 345     run_svn(["revert", "--recursive", "."]) 
 346     output 
= run_svn(["status"]) 
 348         output_lines 
= output
.strip("\n").split("\n") 
 349         for line 
in output_lines
: 
 351                 path 
= line
[4:].strip(" ") 
 352                 if os
.path
.isfile(path
): 
 354                 if os
.path
.isdir(path
): 
 357 def gen_tracking_revprops(source_rev
): 
 359     Build an array of svn2svn-specific source-tracking revprops. 
 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}
] 
 366 def sync_svn_props(source_url
, source_rev
, path_offset
): 
 368     Carry-forward any unversioned properties from the source repo to the 
 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
)]) 
 387 def get_rev_map(source_rev
, prefix
): 
 389     Find the equivalent rev # in the target repo for the given rev # from the source repo. 
 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') 
 397             return int(rev_map
[rev
]) 
 398     # Else, we fell off the bottom of the rev_map. Ruh-roh... 
 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') 
 404     rev_map
[int(source_rev
)]=int(target_rev
) 
 406 def build_rev_map(target_url
, target_end_rev
, source_info
): 
 408     Check for any already-replayed history from source_url (source_info) and 
 409     build the mapping-table of source_rev -> target_rev. 
 413     ui
.status("Rebuilding target_rev -> source_rev rev_map...", level
=ui
.VERBOSE
) 
 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']: 
 419             for v 
in log_entry
['revprops']: 
 420                 if v
['name'].startswith('svn2svn:'): 
 421                     revprops
[v
['name']] = v
['value'] 
 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
) 
 429                 if proc_count 
% 500 == 0: 
 430                     ui
.status("...processed %s (%s of %s)..." % (proc_count
, target_rev
, target_end_rev
), level
=ui
.VERBOSE
) 
 432 def path_in_list(paths
, path
): 
 434         if is_child_path(path
, p
): 
 438 def add_path(paths
, path
): 
 439     if not path_in_list(paths
, path
): 
 442 def in_ancestors(ancestors
, ancestor
): 
 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']) 
 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 
= ""): 
 454     Given the add'd source path, replay the "svn add/copy" commands to correctly 
 455     track renames across copy-from's. 
 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 
 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). 
 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
): 
 487     copyfrom_path 
= ancestor
['copyfrom_path'] if ancestor 
else "" 
 488     copyfrom_rev  
= ancestor
['copyfrom_rev']  if ancestor 
else "" 
 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') 
 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') 
 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') 
 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
) 
 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
)]) 
 534                     # Export the final verison of all files in this folder. 
 535                     add_path(export_paths
, path_offset
) 
 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
) 
 542             ui
.status(prefix 
+ ">> do_svn_add: Skipped 'svn copy': %s", path_offset
, level
=ui
.DEBUG
, color
='GREEN') 
 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
+"  "): 
 554                 # Export the final verison of all files in this folder. 
 555                 add_path(export_paths
, path_offset
) 
 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
) 
 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
+"  ") 
 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. 
 600 def process_svn_log_entry(log_entry
, ancestors
, commit_paths
, prefix 
= ""): 
 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". 
 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' 
 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') 
 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 
 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 "", 
 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
) 
 645         # Special-handling for replace's 
 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. 
 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) 
 664         # Handle all the various action-types 
 665         # (Handle "add" first, for "svn copy/move" support) 
 667             # Determine where to export from. 
 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'] 
 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. 
 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. 
 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
) 
 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
) 
 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) 
 724                 svnclient
.export(join_path(source_url
, path_offset
), source_rev
, path_offset
, force
=True, non_recursive
=True) 
 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
) 
 736             raise InternalError("Internal Error: process_svn_log_entry: Unhandled 'action' value: '%s'" 
 739     # Export the final version of all add'd paths from source_url 
 741         for path_offset 
in export_paths
: 
 742             svnclient
.export(join_path(source_url
, path_offset
), source_rev
, path_offset
, force
=True) 
 744 def keep_revnum(source_rev
, target_rev_last
, wc_target_tmp
): 
 746     Add "padding" target revisions as needed to keep source and target 
 747     revision #'s identical. 
 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". 
 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
) 
 769             # Check if the user tried to press Ctrl-C 
 771                 raise KeyboardInterrupt 
 772             target_rev_last 
= rev_num
 
 773         shell
.rmtree(wc_target_tmp
) 
 774     return target_rev_last
 
 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'], 
 781         str(datetime
.fromtimestamp(int(log_entry
['date'])).isoformat(' ')), level
=ui
.VERBOSE
) 
 782     ui
.status(log_entry
['message'], level
=ui
.VERBOSE
) 
 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') 
 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']) 
 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' 
 809     # Init start and end revision 
 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
) 
 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
) 
 820     ui
.status("Using source revision range %s:%s", source_start_rev
, source_end_rev
, level
=ui
.VERBOSE
) 
 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? 
 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') 
 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
) 
 840         ui
.status("Checking-out _wc_target...", level
=ui
.VERBOSE
) 
 841         svnclient
.svn_checkout(target_url
, wc_target
) 
 844         # If using an existing WC, make sure it's clean ("svn revert") 
 845         ui
.status("Cleaning-up _wc_target...", level
=ui
.VERBOSE
) 
 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") 
 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." 
 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
 
 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
)) 
 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
) 
 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
) 
 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
, "") 
 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
) 
 907             # Update rev_map, mapping table of source-repo rev # -> target-repo rev # 
 908             set_rev_map(source_start_rev
, target_rev
) 
 910             target_rev_last 
= target_rev
 
 912                 verify_commit(source_rev
, target_rev_last
) 
 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
) 
 917             print "Error: Called with continue-mode, but no already-replayed source history found in target_url." 
 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
) 
 924     svn_vers_t 
= svnclient
.version() 
 925     svn_vers 
= float(".".join(map(str, svn_vers_t
[0:2]))) 
 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
 
 934         for log_entry 
in it_log_entries
: 
 935             if options
.entries_proc_limit
: 
 936                 if num_entries_proc 
>= options
.entries_proc_limit
: 
 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
) 
 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 
 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
 
 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
 
 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): 
 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). 
 972                 verify_commit(source_rev_last
, target_rev_last
) 
 974     except KeyboardInterrupt: 
 976         print "\nStopped by user." 
 977         print "\nCleaning-up..." 
 982         print "\nCommand failed with following error:\n" 
 983         traceback
.print_exc() 
 984         print "\nCleaning-up..." 
 986         print run_svn(["status"]) 
 989         print "\nFinished at source revision %s%s." % (source_rev_last
, " (dry-run)" if options
.dry_run 
else "") 
 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" 
 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. 
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. 
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 
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)") 
1070     options
, args 
= parser
.parse_args() 
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 
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/\\ :-]+\}' 
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 
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 
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
) 
1100 if __name__ 
== "__main__": 
1101     sys
.exit(main() or 0)