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