]> Tony Duckles's Git Repositories (git.nynim.org) - svn2svn.git/blob - svn2svn.py
Major rewrite for find_svn_ancestors() support
[svn2svn.git] / svn2svn.py
1 #!/usr/bin/env python
2 """
3 svn2svn.py
4
5 Replicate changesets from one SVN repository to another,
6 includes diffs, comments, and Dates of each revision.
7 It's also possible to retain the Author info if the Target SVN URL
8 is in a local filesystem (ie, running svn2svn.py on Target SVN server),
9 or if Target SVN URL is managed through ssh tunnel.
10 In later case, please run 'ssh-add' (adds RSA or DSA identities to
11 the authentication agent) before invoking svn2svn.py.
12
13 For example (in Unix environment):
14 $ exec /usr/bin/ssh-agent $SHELL
15 $ /usr/bin/ssh-add
16 Enter passphrase for /home/user/.ssh/id_dsa:
17 Identity added: /home/user/.ssh/id_dsa (/home/user/.ssh/id_dsa)
18 $ python ./svn2svn.py -a SOURCE TARGET
19
20 Written and used on Ubuntu 7.04 (Feisty Fawn).
21 Provided as-is and absolutely no warranty - aka Don't bet your life on it.
22
23 This tool re-used some modules from svnclient.py on project hgsvn
24 (a tool can create Mercurial repository from SVN repository):
25 http://cheeseshop.python.org/pypi/hgsvn
26
27 License: GPLv2, the same as hgsvn.
28
29 version 0.1.1; Jul 31, 2007; simford dot dong at gmail dot com
30 """
31
32 import os
33 import sys
34 import time
35 import locale
36 import shutil
37 import select
38 import calendar
39 import traceback
40
41 from optparse import OptionParser
42 from subprocess import Popen, PIPE
43 from datetime import datetime
44
45 try:
46 from xml.etree import cElementTree as ET
47 except ImportError:
48 try:
49 from xml.etree import ElementTree as ET
50 except ImportError:
51 try:
52 import cElementTree as ET
53 except ImportError:
54 from elementtree import ElementTree as ET
55
56 svn_log_args = ['log', '--xml', '-v']
57 svn_info_args = ['info', '--xml']
58 svn_checkout_args = ['checkout', '-q']
59 svn_status_args = ['status', '--xml', '-v', '--ignore-externals']
60 debug = True
61 runsvn_verbose = True
62
63 # define exception class
64 class ExternalCommandFailed(RuntimeError):
65 """
66 An external command failed.
67 """
68
69 def display_error(message, raise_exception = True):
70 """
71 Display error message, then terminate.
72 """
73 print "Error:", message
74 print
75 if raise_exception:
76 raise ExternalCommandFailed
77 else:
78 sys.exit(1)
79
80 # Windows compatibility code by Bill Baxter
81 if os.name == "nt":
82 def find_program(name):
83 """
84 Find the name of the program for Popen.
85 Windows is finnicky about having the complete file name. Popen
86 won't search the %PATH% for you automatically.
87 (Adapted from ctypes.find_library)
88 """
89 # See MSDN for the REAL search order.
90 base, ext = os.path.splitext(name)
91 if ext:
92 exts = [ext]
93 else:
94 exts = ['.bat', '.exe']
95 for directory in os.environ['PATH'].split(os.pathsep):
96 for e in exts:
97 fname = os.path.join(directory, base + e)
98 if os.path.exists(fname):
99 return fname
100 return None
101 else:
102 def find_program(name):
103 """
104 Find the name of the program for Popen.
105 On Unix, popen isn't picky about having absolute paths.
106 """
107 return name
108
109 def shell_quote(s):
110 if os.name == "nt":
111 q = '"'
112 else:
113 q = "'"
114 return q + s.replace('\\', '\\\\').replace("'", "'\"'\"'") + q
115
116 locale_encoding = locale.getpreferredencoding()
117
118 def run_svn(args, fail_if_stderr=False, encoding="utf-8"):
119 """
120 Run svn cmd in PIPE
121 exit if svn cmd failed
122 """
123 def _transform_arg(a):
124 if isinstance(a, unicode):
125 a = a.encode(encoding or locale_encoding)
126 elif not isinstance(a, str):
127 a = str(a)
128 return a
129 t_args = map(_transform_arg, args)
130
131 cmd = find_program("svn")
132 cmd_string = str(" ".join(map(shell_quote, [cmd] + t_args)))
133 if runsvn_verbose:
134 print "$", cmd_string
135 pipe = Popen([cmd] + t_args, executable=cmd, stdout=PIPE, stderr=PIPE)
136 out, err = pipe.communicate()
137 if pipe.returncode != 0 or (fail_if_stderr and err.strip()):
138 display_error("External program failed (return code %d): %s\n%s"
139 % (pipe.returncode, cmd_string, err))
140 return out
141
142 def svn_date_to_timestamp(svn_date):
143 """
144 Parse an SVN date as read from the XML output and
145 return the corresponding timestamp.
146 """
147 # Strip microseconds and timezone (always UTC, hopefully)
148 # XXX there are various ISO datetime parsing routines out there,
149 # cf. http://seehuhn.de/comp/pdate
150 date = svn_date.split('.', 2)[0]
151 time_tuple = time.strptime(date, "%Y-%m-%dT%H:%M:%S")
152 return calendar.timegm(time_tuple)
153
154 def parse_svn_info_xml(xml_string):
155 """
156 Parse the XML output from an "svn info" command and extract
157 useful information as a dict.
158 """
159 d = {}
160 tree = ET.fromstring(xml_string)
161 entry = tree.find('.//entry')
162 if entry:
163 d['url'] = entry.find('url').text
164 d['revision'] = int(entry.get('revision'))
165 d['repos_url'] = tree.find('.//repository/root').text
166 d['last_changed_rev'] = int(tree.find('.//commit').get('revision'))
167 d['kind'] = entry.get('kind')
168 return d
169
170 def parse_svn_log_xml(xml_string):
171 """
172 Parse the XML output from an "svn log" command and extract
173 useful information as a list of dicts (one per log changeset).
174 """
175 l = []
176 tree = ET.fromstring(xml_string)
177 for entry in tree.findall('logentry'):
178 d = {}
179 d['revision'] = int(entry.get('revision'))
180 # Some revisions don't have authors, most notably
181 # the first revision in a repository.
182 author = entry.find('author')
183 d['author'] = author is not None and author.text or None
184 d['date'] = svn_date_to_timestamp(entry.find('date').text)
185 # Some revisions may have empty commit message
186 message = entry.find('msg')
187 message = message is not None and message.text is not None \
188 and message.text.strip() or ""
189 # Replace DOS return '\r\n' and MacOS return '\r' with unix return '\n'
190 d['message'] = message.replace('\r\n', '\n').replace('\n\r', '\n'). \
191 replace('\r', '\n')
192 paths = d['changed_paths'] = []
193 for path in entry.findall('.//path'):
194 copyfrom_rev = path.get('copyfrom-rev')
195 if copyfrom_rev:
196 copyfrom_rev = int(copyfrom_rev)
197 paths.append({
198 'path': path.text,
199 'action': path.get('action'),
200 'copyfrom_path': path.get('copyfrom-path'),
201 'copyfrom_revision': copyfrom_rev,
202 })
203 l.append(d)
204 return l
205
206 def parse_svn_status_xml(xml_string, base_dir=None):
207 """
208 Parse the XML output from an "svn status" command and extract
209 useful info as a list of dicts (one per status entry).
210 """
211 l = []
212 tree = ET.fromstring(xml_string)
213 for entry in tree.findall('.//entry'):
214 d = {}
215 path = entry.get('path')
216 if base_dir is not None:
217 assert path.startswith(base_dir)
218 path = path[len(base_dir):].lstrip('/\\')
219 d['path'] = path
220 wc_status = entry.find('wc-status')
221 if wc_status.get('item') == 'external':
222 d['type'] = 'external'
223 elif wc_status.get('revision') is not None:
224 d['type'] = 'normal'
225 else:
226 d['type'] = 'unversioned'
227 l.append(d)
228 return l
229
230 def get_svn_info(svn_url_or_wc, rev_number=None):
231 """
232 Get SVN information for the given URL or working copy,
233 with an optionally specified revision number.
234 Returns a dict as created by parse_svn_info_xml().
235 """
236 if rev_number is not None:
237 args = [svn_url_or_wc + "@" + str(rev_number)]
238 else:
239 args = [svn_url_or_wc]
240 xml_string = run_svn(svn_info_args + args,
241 fail_if_stderr=True)
242 return parse_svn_info_xml(xml_string)
243
244 def svn_checkout(svn_url, checkout_dir, rev_number=None):
245 """
246 Checkout the given URL at an optional revision number.
247 """
248 args = []
249 if rev_number is not None:
250 args += ['-r', rev_number]
251 args += [svn_url, checkout_dir]
252 return run_svn(svn_checkout_args + args)
253
254 def run_svn_log(svn_url_or_wc, rev_start, rev_end, limit, stop_on_copy=False):
255 """
256 Fetch up to 'limit' SVN log entries between the given revisions.
257 """
258 if stop_on_copy:
259 args = ['--stop-on-copy']
260 else:
261 args = []
262 if rev_start != 'HEAD' and rev_end != 'HEAD':
263 args += ['-r', '%s:%s' % (rev_start, rev_end)]
264 args += ['--limit', str(limit), svn_url_or_wc]
265 xml_string = run_svn(svn_log_args + args)
266 return parse_svn_log_xml(xml_string)
267
268 def get_svn_status(svn_wc, flags=None):
269 """
270 Get SVN status information about the given working copy.
271 """
272 # Ensure proper stripping by canonicalizing the path
273 svn_wc = os.path.abspath(svn_wc)
274 args = []
275 if flags:
276 args += [flags]
277 args += [svn_wc]
278 xml_string = run_svn(svn_status_args + args)
279 return parse_svn_status_xml(xml_string, svn_wc)
280
281 def get_one_svn_log_entry(svn_url, rev_start, rev_end, stop_on_copy=False):
282 """
283 Get the first SVN log entry in the requested revision range.
284 """
285 entries = run_svn_log(svn_url, rev_start, rev_end, 1, stop_on_copy)
286 if not entries:
287 display_error("No SVN log for %s between revisions %s and %s" %
288 (svn_url, rev_start, rev_end))
289
290 return entries[0]
291
292 def get_first_svn_log_entry(svn_url, rev_start, rev_end):
293 """
294 Get the first log entry after/at the given revision number in an SVN branch.
295 By default the revision number is set to 0, which will give you the log
296 entry corresponding to the branch creaction.
297
298 NOTE: to know whether the branch creation corresponds to an SVN import or
299 a copy from another branch, inspect elements of the 'changed_paths' entry
300 in the returned dictionary.
301 """
302 return get_one_svn_log_entry(svn_url, rev_start, rev_end, stop_on_copy=True)
303
304 def get_last_svn_log_entry(svn_url, rev_start, rev_end):
305 """
306 Get the last log entry before/at the given revision number in an SVN branch.
307 By default the revision number is set to HEAD, which will give you the log
308 entry corresponding to the latest commit in branch.
309 """
310 return get_one_svn_log_entry(svn_url, rev_end, rev_start, stop_on_copy=True)
311
312
313 log_duration_threshold = 10.0
314 log_min_chunk_length = 10
315
316 def iter_svn_log_entries(svn_url, first_rev, last_rev):
317 """
318 Iterate over SVN log entries between first_rev and last_rev.
319
320 This function features chunked log fetching so that it isn't too nasty
321 to the SVN server if many entries are requested.
322 """
323 cur_rev = first_rev
324 chunk_length = log_min_chunk_length
325 chunk_interval_factor = 1.0
326 while last_rev == "HEAD" or cur_rev <= last_rev:
327 start_t = time.time()
328 stop_rev = min(last_rev, cur_rev + int(chunk_length * chunk_interval_factor))
329 entries = run_svn_log(svn_url, cur_rev, stop_rev, chunk_length)
330 duration = time.time() - start_t
331 if not entries:
332 if stop_rev == last_rev:
333 break
334 cur_rev = stop_rev + 1
335 chunk_interval_factor *= 2.0
336 continue
337 for e in entries:
338 yield e
339 cur_rev = e['revision'] + 1
340 # Adapt chunk length based on measured request duration
341 if duration < log_duration_threshold:
342 chunk_length = int(chunk_length * 2.0)
343 elif duration > log_duration_threshold * 2:
344 chunk_length = max(log_min_chunk_length, int(chunk_length / 2.0))
345
346 def commit_from_svn_log_entry(entry, files=None, keep_author=False):
347 """
348 Given an SVN log entry and an optional sequence of files, do an svn commit.
349 """
350 # This will use the local timezone for displaying commit times
351 timestamp = int(entry['date'])
352 svn_date = str(datetime.fromtimestamp(timestamp))
353 # Uncomment this one one if you prefer UTC commit times
354 #svn_date = "%d 0" % timestamp
355 if keep_author:
356 options = ["ci", "--force-log", "-m", entry['message'] + "\nDate: " + svn_date, "--username", entry['author']]
357 else:
358 options = ["ci", "--force-log", "-m", entry['message'] + "\nDate: " + svn_date + "\nAuthor: " + entry['author']]
359 if files:
360 options += list(files)
361 run_svn(options)
362 print ""
363
364 def in_svn(p):
365 """
366 Check if a given file/folder is under Subversion control.
367 Prior to SVN 1.6, we could "cheat" and look for the existence of ".svn" directories.
368 With SVN 1.7 and beyond, WC-NG means only a single top-level ".svn" at the root of the working-copy.
369 Use "svn status" to check the status of the file/folder.
370 TODO: Is there a better way to do this?
371 """
372 entries = get_svn_status(p)
373 if not entries:
374 return False
375 d = entries[0]
376 return (d['type'] == 'normal')
377
378 def svn_add_dir(p):
379 # set p = "." when p = ""
380 #p = p.strip() or "."
381 if p.strip() and not in_svn(p):
382 # Make sure our parent is under source-control before we try to "svn add" this folder.
383 svn_add_dir(os.path.dirname(p))
384 if not os.path.exists(p):
385 os.makedirs(p)
386 run_svn(["add", p])
387
388 def find_svn_parent(source_repos_url, source_path, branch_path, branch_rev):
389 """
390 Given a copy-from path (branch_path), walk the SVN history backwards to inspect
391 the ancestory of that path. If we find a "copyfrom_path" which source_path is a
392 substring match of, then return that source path. Otherwise, branch_path has no
393 ancestory compared to source_path. This is useful when comparing "trunk" vs. "branch"
394 paths, to handle cases where a file/folder was renamed in a branch and then that
395 branch was merged back to trunk.
396 * source_repos_url = Full URL to root of repository, e.g. 'file:///path/to/repos'
397 * source_path = e.g. 'trunk/projectA/file1.txt'
398 * branch_path = e.g. 'branch/bug123/projectA/file1.txt'
399 """
400
401 done = False
402 ancestor = { 'path': branch_path, 'revision': branch_rev }
403 ancestors = []
404 ancestors.append({'path': ancestor['path'], 'revision': ancestor['revision']})
405 rev = branch_rev
406 path = branch_path
407 while not done:
408 # Get "svn log" entry for path just before (or at) @rev
409 if debug:
410 print ">> find_svn_parent: " + source_repos_url + " " + path + "@" + str(rev)
411 log_entry = get_last_svn_log_entry(source_repos_url + path + "@" + str(rev), 1, str(rev))
412 if not log_entry:
413 done = True
414 # Update rev so that we go back in time during the next loop
415 rev = log_entry['revision']-1
416 # Check if our target path was changed in this revision
417 for d in log_entry['changed_paths']:
418 p = d['path']
419 if not p in path:
420 continue
421
422 # Check action-type for this file
423 action = d['action']
424 if action not in 'MARD':
425 display_error("In SVN rev. %d: action '%s' not supported. \
426 Please report a bug!" % (svn_rev, action))
427 if debug:
428 debug_desc = ": " + action + " " + p
429 if d['copyfrom_path']:
430 debug_desc += " (from " + d['copyfrom_path'] + "@" + str(d['copyfrom_revision']) + ")"
431 print debug_desc
432
433 if action == 'R':
434 # If file was replaced, it has no ancestor
435 return None
436 if action == 'D':
437 # If file was deleted, it has no ancestor
438 return None
439 if action == 'A':
440 # If file was added but not a copy, it has no ancestor
441 if not d['copyfrom_path']:
442 return None
443 p_old = d['copyfrom_path']
444 ancestor['path'] = ancestor['path'].replace(p, p_old)
445 ancestor['revision'] = d['copyfrom_revision']
446 ancestors.append({'path': ancestor['path'], 'revision': ancestor['revision']})
447 # If we found a copy-from case which matches our source_path, we're done
448 if (p_old == source_path) or (p_old.startswith(source_path + "/")):
449 return ancestors
450 # Else, follow the copy and keep on searching
451 rev = ancestor['revision']
452 path = ancestor['path']
453 if debug:
454 print ">> find_svn_parent: copy-from: " + path + "@" + str(rev) + " -- " + ancestor['path']
455 break
456 return None
457
458 def do_svn_copy(source_repos_url, source_path, dest_path, ancestors):
459 for ancestor in ancestors:
460 break
461 # TODO
462
463 def pull_svn_rev(log_entry, source_repos_url, source_url, target_url, source_path, original_wc, keep_author=False):
464 """
465 Pull SVN changes from the given log entry.
466 Returns the new SVN revision.
467 If an exception occurs, it will rollback to revision 'svn_rev - 1'.
468 """
469 svn_rev = log_entry['revision']
470 run_svn(["up", "--ignore-externals", "-r", svn_rev, original_wc])
471
472 removed_paths = []
473 modified_paths = []
474 unrelated_paths = []
475 commit_paths = []
476 for d in log_entry['changed_paths']:
477 # Get the full path for this changed_path
478 # e.g. u'/branches/xmpp/twisted/words/test/test.py'
479 p = d['path']
480 if not p.startswith(source_path + "/"):
481 # Ignore changed files that are not part of this subdir
482 if p != source_path:
483 unrelated_paths.append(p)
484 continue
485 # Calculate the relative path (based on source_path) for this changed_path
486 # e.g. u'twisted/words/test/test.py'
487 p = p[len(source_path):].strip("/")
488 # Record for commit
489 action = d['action']
490
491 if debug:
492 debug_desc = " " + action + " " + source_path + "/" + p
493 if d['copyfrom_path']:
494 debug_desc += " (from " + d['copyfrom_path'] + "@" + str(d['copyfrom_revision']) + ")"
495 print debug_desc
496
497 if action not in 'MARD':
498 display_error("In SVN rev. %d: action '%s' not supported. \
499 Please report a bug!" % (svn_rev, action))
500
501 # Try to be efficient and keep track of an explicit list of paths in the
502 # working copy that changed. If we commit from the root of the working copy,
503 # then SVN needs to crawl the entire working copy looking for pending changes.
504 # But, if we gather too many paths to commit, then we wipe commit_paths below
505 # and end-up doing a commit at the root of the working-copy.
506 if len (commit_paths) < 100:
507 commit_paths.append(p)
508
509 # Special-handling for replace's
510 if action == 'R':
511 # If file was "replaced" (deleted then re-added, all in same revision),
512 # then we need to run the "svn rm" first, then change action='A'. This
513 # lets the normal code below handle re-"svn add"'ing the files. This
514 # should replicate the "replace".
515 run_svn(["up", p])
516 run_svn(["remove", "--force", p])
517 action = 'A'
518
519 # Handle all the various action-types
520 # (Handle "add" first, for "svn copy/move" support)
521 if action == 'A':
522 # Determine where to export from
523 from_rev = svn_rev
524 from_path = source_path + "/" + p
525 svn_copy = False
526 # Handle cases where this "add" was a copy from another URL in the source repos
527 if d['copyfrom_revision']:
528 from_rev = d['copyfrom_revision']
529 from_path = d['copyfrom_path']
530 ancestors = find_svn_parent(source_repos_url, source_path, from_path, from_rev)
531 if ancestors:
532 parent = ancestors[len(ancestors)-1]
533 #from_rev = parent['revision']
534 #from_path = parent['path']
535 if debug:
536 print ">> find_svn_parent: FOUND PARENT: " + parent['path'] + "@" + str(parent['revision'])
537 print ancestors
538 # TODO: For copy-from's, need to re-walk the branch history to make sure we handle
539 # any renames correctly.
540 #from_path = from_path[len(source_path):].strip("/")
541 #svn_copy = True
542
543 if svn_copy:
544 #do_svn_copy(source_repos_url, source_path
545 run_svn(["copy", from_path, p])
546 else:
547 # Create (parent) directory if needed
548 if os.path.isdir(original_wc + os.sep + p):
549 p_path = p
550 else:
551 p_path = os.path.dirname(p).strip() or '.'
552 if not os.path.exists(p_path):
553 os.makedirs(p_path)
554
555 # Export the entire added tree. Can't use shutil.copytree() since that
556 # would copy ".svn" folders on SVN pre-1.7. Also, in cases where the copy-from
557 # is from some path in the source_repos _outside_ of our source_path, original_wc
558 # won't even have the source files we want to copy.
559 run_svn(["export", "--force", "-r", str(from_rev),
560 source_repos_url + from_path + "@" + str(from_rev), p])
561 # TODO: Need to copy SVN properties from source repos
562
563 if os.path.isdir(original_wc + os.sep + p):
564 svn_add_dir(p)
565 else:
566 p_path = os.path.dirname(p).strip() or '.'
567 svn_add_dir(p_path)
568 run_svn(["add", p])
569
570 elif action == 'D':
571 # Queue "svn remove" commands, to allow the action == 'A' handling the opportunity
572 # to do smart "svn copy" handling on copy/move/renames.
573 removed_paths.append(p)
574
575 elif action == 'R':
576 # TODO
577 display_error("Internal Error: Handling for action='R' not implemented yet.")
578
579 elif action == 'M':
580 modified_paths.append(p)
581
582 else:
583 display_error("Internal Error: pull_svn_rev: Unhandled 'action' value: '" + action + "'")
584
585 if removed_paths:
586 for r in removed_paths:
587 run_svn(["up", r])
588 run_svn(["remove", "--force", r])
589
590 if modified_paths:
591 for m in modified_paths:
592 run_svn(["up", m])
593 m_url = source_url + "/" + m
594 out = run_svn(["merge", "-c", str(svn_rev), "--non-recursive",
595 "--non-interactive", "--accept=theirs-full",
596 m_url+"@"+str(svn_rev), m])
597 # if conflicts, use the copy from original_wc
598 if out and out.split()[0] == 'C':
599 print "\n### Conflicts ignored: %s, in revision: %s\n" \
600 % (m, svn_rev)
601 run_svn(["revert", "--recursive", m])
602 if os.path.isfile(m):
603 shutil.copy(original_wc + os.sep + m, m)
604
605 if unrelated_paths:
606 print "Unrelated paths: "
607 print "*", unrelated_paths
608
609 # If we had too many individual paths to commit, wipe the list and just commit at
610 # the root of the working copy.
611 if len (commit_paths) > 99:
612 commit_paths = []
613
614 try:
615 commit_from_svn_log_entry(log_entry, commit_paths,
616 keep_author=keep_author)
617 except ExternalCommandFailed:
618 # try to ignore the Properties conflicts on files and dirs
619 # use the copy from original_wc
620 has_Conflict = False
621 for d in log_entry['changed_paths']:
622 p = d['path']
623 p = p[len(source_path):].strip("/")
624 if os.path.isfile(p):
625 if os.path.isfile(p + ".prej"):
626 has_Conflict = True
627 shutil.copy(original_wc + os.sep + p, p)
628 p2=os.sep + p.replace('_', '__').replace('/', '_') \
629 + ".prej-" + str(svn_rev)
630 shutil.move(p + ".prej", os.path.dirname(original_wc) + p2)
631 w="\n### Properties conflicts ignored:"
632 print "%s %s, in revision: %s\n" % (w, p, svn_rev)
633 elif os.path.isdir(p):
634 if os.path.isfile(p + os.sep + "dir_conflicts.prej"):
635 has_Conflict = True
636 p2=os.sep + p.replace('_', '__').replace('/', '_') \
637 + "_dir__conflicts.prej-" + str(svn_rev)
638 shutil.move(p + os.sep + "dir_conflicts.prej",
639 os.path.dirname(original_wc) + p2)
640 w="\n### Properties conflicts ignored:"
641 print "%s %s, in revision: %s\n" % (w, p, svn_rev)
642 out = run_svn(["propget", "svn:ignore",
643 original_wc + os.sep + p])
644 if out:
645 run_svn(["propset", "svn:ignore", out.strip(), p])
646 out = run_svn(["propget", "svn:externel",
647 original_wc + os.sep + p])
648 if out:
649 run_svn(["propset", "svn:external", out.strip(), p])
650 # try again
651 if has_Conflict:
652 commit_from_svn_log_entry(log_entry, commit_paths,
653 keep_author=keep_author)
654 else:
655 raise ExternalCommandFailed
656
657
658 def main():
659 usage = "Usage: %prog [-a] [-c] [-r SVN rev] <Source SVN URL> <Target SVN URL>"
660 parser = OptionParser(usage)
661 parser.add_option("-a", "--keep-author", action="store_true",
662 dest="keep_author", help="Keep revision Author or not")
663 parser.add_option("-c", "--continue-from-break", action="store_true",
664 dest="cont_from_break",
665 help="Continue from previous break")
666 parser.add_option("-r", "--svn-rev", type="int", dest="svn_rev",
667 help="SVN revision to checkout from")
668 (options, args) = parser.parse_args()
669 if len(args) != 2:
670 display_error("incorrect number of arguments\n\nTry: svn2svn.py --help",
671 False)
672
673 source_url = args.pop(0).rstrip("/")
674 target_url = args.pop(0).rstrip("/")
675 if options.keep_author:
676 keep_author = True
677 else:
678 keep_author = False
679
680 # Find the greatest_rev
681 # don't use 'svn info' to get greatest_rev, it doesn't work sometimes
682 svn_log = get_one_svn_log_entry(source_url, "HEAD", "HEAD")
683 greatest_rev = svn_log['revision']
684
685 original_wc = "_original_wc"
686 dup_wc = "_dup_wc"
687
688 ## old working copy does not exist, disable continue mode
689 if not os.path.exists(dup_wc):
690 options.cont_from_break = False
691
692 if not options.cont_from_break:
693 # Warn if Target SVN URL existed
694 cmd = find_program("svn")
695 pipe = Popen([cmd] + ["list"] + [target_url], executable=cmd,
696 stdout=PIPE, stderr=PIPE)
697 out, err = pipe.communicate()
698 if pipe.returncode == 0:
699 print "Target SVN URL: %s existed!" % target_url
700 if out:
701 print out
702 print "Press 'Enter' to Continue, 'Ctrl + C' to Cancel..."
703 print "(Timeout in 5 seconds)"
704 rfds, wfds, efds = select.select([sys.stdin], [], [], 5)
705
706 # Get log entry for the SVN revision we will check out
707 if options.svn_rev:
708 # If specify a rev, get log entry just before or at rev
709 svn_start_log = get_last_svn_log_entry(source_url, 1,
710 options.svn_rev)
711 else:
712 # Otherwise, get log entry of branch creation
713 svn_start_log = get_first_svn_log_entry(source_url, 1,
714 greatest_rev)
715
716 # This is the revision we will checkout from
717 svn_rev = svn_start_log['revision']
718
719 # Check out first revision (changeset) from Source SVN URL
720 if os.path.exists(original_wc):
721 shutil.rmtree(original_wc)
722 svn_checkout(source_url, original_wc, svn_rev)
723
724 # Import first revision (changeset) into Target SVN URL
725 timestamp = int(svn_start_log['date'])
726 svn_date = str(datetime.fromtimestamp(timestamp))
727 if keep_author:
728 run_svn(["import", original_wc, target_url, "-m",
729 svn_start_log['message'] + "\nDate: " + svn_date,
730 "--username", svn_start_log['author']])
731 else:
732 run_svn(["import", original_wc, target_url, "-m",
733 svn_start_log['message'] + "\nDate: " + svn_date +
734 "\nAuthor: " + svn_start_log['author']])
735
736 # Check out a working copy
737 if os.path.exists(dup_wc):
738 shutil.rmtree(dup_wc)
739 svn_checkout(target_url, dup_wc)
740
741 original_wc = os.path.abspath(original_wc)
742 dup_wc = os.path.abspath(dup_wc)
743 os.chdir(dup_wc)
744
745 # Get SVN info
746 svn_info = get_svn_info(original_wc)
747 # Get the base URL for the source repos
748 # e.g. u'svn://svn.twistedmatrix.com/svn/Twisted'
749 source_repos_url = svn_info['repos_url']
750 # Get the source URL for the source repos
751 # e.g. u'svn://svn.twistedmatrix.com/svn/Twisted/branches/xmpp'
752 source_url = svn_info['url']
753 assert source_url.startswith(source_repos_url)
754 # Get the relative offset of source_url based on source_repos_url
755 # e.g. u'/branches/xmpp'
756 source_path = source_url[len(source_repos_url):]
757
758 if options.cont_from_break:
759 svn_rev = svn_info['revision'] - 1
760 if svn_rev < 1:
761 svn_rev = 1
762
763 # Load SVN log starting from svn_rev + 1
764 it_log_entries = iter_svn_log_entries(source_url, svn_rev + 1, greatest_rev)
765
766 try:
767 for log_entry in it_log_entries:
768 pull_svn_rev(log_entry, source_repos_url, source_url, target_url, source_path,
769 original_wc, keep_author)
770
771 except KeyboardInterrupt:
772 print "\nStopped by user."
773 run_svn(["cleanup"])
774 run_svn(["revert", "--recursive", "."])
775 except:
776 print "\nCommand failed with following error:\n"
777 traceback.print_exc()
778 run_svn(["cleanup"])
779 run_svn(["revert", "--recursive", "."])
780 finally:
781 run_svn(["up"])
782 print "\nFinished!"
783
784
785 if __name__ == "__main__":
786 main()
787
788 # vim: shiftwidth=4 softtabstop=4