2 Replicate (replay) changesets from one SVN repository to another.
5 from .. import base_version
, full_version
7 from .. import svnclient
8 from ..shell
import run_svn
9 from ..errors
import (ExternalCommandFailed
, UnsupportedSVNAction
, InternalError
, VerificationError
)
10 from parse
import HelpFormatter
20 from datetime
import datetime
22 _valid_svn_actions
= "MARD" # The list of known SVN action abbr's, from "svn log"
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
33 def parse_svn_commit_rev(output
):
35 Parse the revision number from the output of "svn commit".
37 output_lines
= output
.strip("\n").split("\n")
39 for line
in output_lines
:
40 if line
[0:19] == 'Committed revision ':
41 rev_num
= line
[19:].rstrip('.')
43 assert rev_num
is not None
46 def commit_from_svn_log_entry(log_entry
, commit_paths
=None, target_revprops
=None):
48 Given an SVN log entry and an optional list of changed paths, do an svn commit.
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']
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
]
70 if log_entry
['revprops']:
71 # Carry forward any revprop's from the source revision
72 for v
in log_entry
['revprops']:
73 revprops
[v
['name']] = v
['value']
75 # Add any extra revprop's we want to set for the target repo commits
76 for v
in target_revprops
:
77 revprops
[v
['name']] = v
['value']
80 args
+= ["--with-revprop", "%s=%s" % (key
, str(revprops
[key
]))]
82 if len(commit_paths
)<100:
83 # If we don't have an excessive amount of individual changed paths, pass
84 # those to the "svn commit" command. Else, pass nothing so we commit at
85 # the root of the working-copy.
86 args
+= list(commit_paths
)
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
)
95 run_svn(["propset", "--revprop", "-r", rev_num
, "svn:date", log_entry
['date_raw']])
98 def full_svn_revert():
100 Do an "svn revert" and proactively remove any extra files in the working copy.
102 run_svn(["revert", "--recursive", "."])
103 output
= run_svn(["status"])
105 output_lines
= output
.strip("\n").split("\n")
106 for line
in output_lines
:
108 path
= line
[4:].strip(" ")
109 if os
.path
.isfile(path
):
111 if os
.path
.isdir(path
):
114 def gen_tracking_revprops(source_rev
):
116 Build an array of svn2svn-specific source-tracking revprops.
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}
]
123 def sync_svn_props(source_url
, source_rev
, path_offset
):
125 Carry-forward any unversioned properties from the source repo to the
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
])
144 def in_svn(p
, require_in_repo
=False, prefix
=""):
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.
151 entries
= svnclient
.get_svn_status(p
, no_recursive
=True)
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.
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')
166 def is_child_path(path
, p_path
):
167 return True if (path
== p_path
) or (path
.startswith(p_path
+"/")) else False
169 def join_path(base
, child
):
171 return base
+"/"+child
if child
else base
173 def find_svn_ancestors(svn_repos_url
, start_path
, start_rev
, stop_base_path
=None, prefix
=""):
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.
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.
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.
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'.
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')
198 cur_path
= start_path
200 first_iter_done
= False
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
)
207 ui
.status(prefix
+ ">> find_svn_ancestors: Done: no log_entry", level
=ui
.DEBUG
, color
='YELLOW')
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')
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']:
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')
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
:
236 # Check action-type for this file
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')
245 # If file/folder was deleted, ancestry-chain stops here
248 ui
.status(prefix
+ ">> find_svn_ancestors: Done: deleted", level
=ui
.DEBUG
, color
='YELLOW')
252 # If file/folder was added/replaced but not a copy, ancestry-chain stops here
253 if not d
['copyfrom_path']:
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')
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
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.
278 if ui
.get_level() >= ui
.DEBUG
:
280 for idx
in range(len(ancestors
)):
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
)):
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')
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')
295 def get_rev_map(source_rev
, prefix
):
297 Find the equivalent rev # in the target repo for the given rev # from the source repo.
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')
305 return int(rev_map
[rev
])
306 # Else, we fell off the bottom of the rev_map. Ruh-roh...
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')
312 rev_map
[int(source_rev
)]=int(target_rev
)
314 def build_rev_map(target_url
, target_end_rev
, source_info
):
316 Check for any already-replayed history from source_url (source_info) and
317 build the mapping-table of source_rev -> target_rev.
321 ui
.status("Rebuilding target_rev -> source_rev rev_map...", level
=ui
.VERBOSE
)
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']:
327 for v
in log_entry
['revprops']:
328 if v
['name'].startswith('svn2svn:'):
329 revprops
[v
['name']] = v
['value']
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
)
337 if proc_count
% 500 == 0:
338 ui
.status("...processed %s (%s of %s)..." % (proc_count
, target_rev
, target_end_rev
), level
=ui
.VERBOSE
)
340 def get_svn_dirlist(svn_path
, rev_number
= ""):
342 Get a list of all the child contents (recusive) of the given folder path.
347 args
+= ["-r", rev_number
]
348 path
+= "@"+str(rev_number
)
350 paths
= run_svn(args
, no_fail
=True)
351 paths
= paths
.strip("\n").split("\n") if len(paths
)>1 else []
354 def path_in_list(paths
, path
):
356 if is_child_path(path
, p
):
360 def add_path(paths
, path
):
361 if not path_in_list(paths
, path
):
364 def in_ancestors(ancestors
, ancestor
):
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'])
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
= ""):
376 Given the add'd source path, replay the "svn add/copy" commands to correctly
377 track renames across copy-from's.
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
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).
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
):
409 copyfrom_path
= ancestor
['copyfrom_path'] if ancestor
else ""
410 copyfrom_rev
= ancestor
['copyfrom_rev'] if ancestor
else ""
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')
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')
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')
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
)
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
])
456 # Export the final verison of all files in this folder.
457 add_path(export_paths
, path_offset
)
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
)
465 ui
.status(prefix
+ ">> do_svn_add: Skipped 'svn copy': %s", path_offset
, level
=ui
.DEBUG
, color
='GREEN')
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
+" "):
477 # Export the final verison of all files in this folder.
478 add_path(export_paths
, path_offset
)
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
)
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
+" ")
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.
525 def process_svn_log_entry(log_entry
, ancestors
, commit_paths
, prefix
= ""):
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".
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'
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')
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
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 "",
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
)
570 # Special-handling for replace's
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.
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
])
589 # Handle all the various action-types
590 # (Handle "add" first, for "svn copy/move" support)
592 # Determine where to export from.
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']
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.
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.
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
)
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
)
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
])
650 run_svn(["export", "--force", "-N" , "-r", source_rev
,
651 join_path(source_url
, path_offset
)+"@"+str(source_rev
), path_offset
])
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
)
663 raise InternalError("Internal Error: process_svn_log_entry: Unhandled 'action' value: '%s'"
666 # Export the final version of all add'd paths from source_url
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
])
672 def keep_revnum(source_rev
, target_rev_last
, wc_target_tmp
):
674 Add "padding" target revisions as needed to keep source and target
675 revision #'s identical.
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
696 def disp_svn_log_summary(log_entry
):
697 ui
.status("------------------------------------------------------------------------")
698 ui
.status("r%s | %s | %s",
699 log_entry
['revision'],
701 str(datetime
.fromtimestamp(int(log_entry
['date'])).isoformat(' ')))
702 ui
.status(log_entry
['message'])
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')
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'])
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']
724 # Init start and end revision
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
))
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
)
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?
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')
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
)
753 ui
.status("Checking-out _wc_target...", level
=ui
.VERBOSE
)
754 svnclient
.svn_checkout(target_url
, wc_target
)
757 # If using an existing WC, make sure it's clean ("svn revert")
758 ui
.status("Cleaning-up _wc_target...", level
=ui
.VERBOSE
)
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
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
))
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
)
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
)
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...
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
, "")
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
)
823 # Update rev_map, mapping table of source-repo rev # -> target-repo rev #
824 set_rev_map(source_start_rev
, target_rev
)
826 target_rev_last
= target_rev
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
)
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
)
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")
841 svn_vers_t
= svnclient
.get_svn_client_version()
842 svn_vers
= float(".".join(map(str, svn_vers_t
[0:2])))
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 []
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.
853 for log_entry
in it_log_entries
:
854 if options
.entries_proc_limit
:
855 if num_entries_proc
>= options
.entries_proc_limit
:
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
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
)
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
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):
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
885 except KeyboardInterrupt:
886 print "\nStopped by user."
887 print "\nCleaning-up..."
891 print "\nCommand failed with following error:\n"
892 traceback
.print_exc()
893 print "\nCleaning-up..."
895 print run_svn(["status"])
898 print "\nFinished at source revision %s%s." % (source_rev
, " (dry-run)" if options
.dry_run
else "")
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"
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.
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.
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
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)")
971 options
, args
= parser
.parse_args()
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
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
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/\\ :-]+\}'
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
989 parser
.error("unexpected --revision argument format; see 'svn help log' for valid revision formats")
991 options
.rev_start
= rev
[0] if len(rev
)>0 else None
992 options
.rev_end
= rev
[1] if len(rev
)>1 else None
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
)
1001 if __name__
== "__main__":
1002 sys
.exit(main() or 0)