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