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