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