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.
13 For example (in Unix environment):
14 $ exec /usr/bin/ssh-agent $SHELL
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
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.
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
27 License: GPLv2, the same as hgsvn.
29 version 0.1.1; Jul 31, 2007; simford dot dong at gmail dot com
41 from optparse
import OptionParser
42 from subprocess
import Popen
, PIPE
43 from datetime
import datetime
46 from xml
.etree
import cElementTree
as ET
49 from xml
.etree
import ElementTree
as ET
52 import cElementTree
as ET
54 from elementtree
import ElementTree
as ET
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']
63 # define exception class
64 class ExternalCommandFailed(RuntimeError):
66 An external command failed.
69 def display_error(message
, raise_exception
= True):
71 Display error message, then terminate.
73 print "Error:", message
76 raise ExternalCommandFailed
80 # Windows compatibility code by Bill Baxter
82 def find_program(name
):
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)
89 # See MSDN for the REAL search order.
90 base
, ext
= os
.path
.splitext(name
)
94 exts
= ['.bat', '.exe']
95 for directory
in os
.environ
['PATH'].split(os
.pathsep
):
97 fname
= os
.path
.join(directory
, base
+ e
)
98 if os
.path
.exists(fname
):
102 def find_program(name
):
104 Find the name of the program for Popen.
105 On Unix, popen isn't picky about having absolute paths.
114 return q
+ s
.replace('\\', '\\\\').replace("'", "'\"'\"'") + q
116 locale_encoding
= locale
.getpreferredencoding()
118 def run_svn(args
, fail_if_stderr
=False, encoding
="utf-8"):
121 exit if svn cmd failed
123 def _transform_arg(a
):
124 if isinstance(a
, unicode):
125 a
= a
.encode(encoding
or locale_encoding
)
126 elif not isinstance(a
, str):
129 t_args
= map(_transform_arg
, args
)
131 cmd
= find_program("svn")
132 cmd_string
= str(" ".join(map(shell_quote
, [cmd
] + t_args
)))
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
))
142 def svn_date_to_timestamp(svn_date
):
144 Parse an SVN date as read from the XML output and
145 return the corresponding timestamp.
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
)
154 def parse_svn_info_xml(xml_string
):
156 Parse the XML output from an "svn info" command and extract
157 useful information as a dict.
160 tree
= ET
.fromstring(xml_string
)
161 entry
= tree
.find('.//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')
170 def parse_svn_log_xml(xml_string
):
172 Parse the XML output from an "svn log" command and extract
173 useful information as a list of dicts (one per log changeset).
176 tree
= ET
.fromstring(xml_string
)
177 for entry
in tree
.findall('logentry'):
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'). \
192 paths
= d
['changed_paths'] = []
193 for path
in entry
.findall('.//path'):
194 copyfrom_rev
= path
.get('copyfrom-rev')
196 copyfrom_rev
= int(copyfrom_rev
)
199 'action': path
.get('action'),
200 'copyfrom_path': path
.get('copyfrom-path'),
201 'copyfrom_revision': copyfrom_rev
,
206 def parse_svn_status_xml(xml_string
, base_dir
=None):
208 Parse the XML output from an "svn status" command and extract
209 useful info as a list of dicts (one per status entry).
212 tree
= ET
.fromstring(xml_string
)
213 for entry
in tree
.findall('.//entry'):
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('/\\')
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:
226 d
['type'] = 'unversioned'
230 def get_svn_info(svn_url_or_wc
, rev_number
=None):
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().
236 if rev_number
is not None:
237 args
= [svn_url_or_wc
+ "@" + str(rev_number
)]
239 args
= [svn_url_or_wc
]
240 xml_string
= run_svn(svn_info_args
+ args
,
242 return parse_svn_info_xml(xml_string
)
244 def svn_checkout(svn_url
, checkout_dir
, rev_number
=None):
246 Checkout the given URL at an optional revision number.
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
)
254 def run_svn_log(svn_url_or_wc
, rev_start
, rev_end
, limit
, stop_on_copy
=False):
256 Fetch up to 'limit' SVN log entries between the given revisions.
259 args
= ['--stop-on-copy']
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
)
268 def get_svn_status(svn_wc
, flags
=None):
270 Get SVN status information about the given working copy.
272 # Ensure proper stripping by canonicalizing the path
273 svn_wc
= os
.path
.abspath(svn_wc
)
278 xml_string
= run_svn(svn_status_args
+ args
)
279 return parse_svn_status_xml(xml_string
, svn_wc
)
281 def get_one_svn_log_entry(svn_url
, rev_start
, rev_end
, stop_on_copy
=False):
283 Get the first SVN log entry in the requested revision range.
285 entries
= run_svn_log(svn_url
, rev_start
, rev_end
, 1, stop_on_copy
)
287 display_error("No SVN log for %s between revisions %s and %s" %
288 (svn_url
, rev_start
, rev_end
))
292 def get_first_svn_log_entry(svn_url
, rev_start
, rev_end
):
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.
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.
302 return get_one_svn_log_entry(svn_url
, rev_start
, rev_end
, stop_on_copy
=True)
304 def get_last_svn_log_entry(svn_url
, rev_start
, rev_end
):
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.
310 return get_one_svn_log_entry(svn_url
, rev_end
, rev_start
, stop_on_copy
=True)
313 log_duration_threshold
= 10.0
314 log_min_chunk_length
= 10
316 def iter_svn_log_entries(svn_url
, first_rev
, last_rev
):
318 Iterate over SVN log entries between first_rev and last_rev.
320 This function features chunked log fetching so that it isn't too nasty
321 to the SVN server if many entries are requested.
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
332 if stop_rev
== last_rev
:
334 cur_rev
= stop_rev
+ 1
335 chunk_interval_factor
*= 2.0
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))
346 def commit_from_svn_log_entry(entry
, files
=None, keep_author
=False):
348 Given an SVN log entry and an optional sequence of files, do an svn commit.
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
356 options
= ["ci", "--force-log", "-m", entry
['message'] + "\nDate: " + svn_date
, "--username", entry
['author']]
358 options
= ["ci", "--force-log", "-m", entry
['message'] + "\nDate: " + svn_date
+ "\nAuthor: " + entry
['author']]
360 options
+= list(files
)
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?
372 entries
= get_svn_status(p
)
376 return (d
['type'] == 'normal')
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
):
388 def find_svn_parent(source_repos_url
, source_path
, branch_path
, branch_rev
):
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'
402 ancestor
= { 'path': branch_path, 'revision': branch_rev }
404 ancestors
.append({'path': ancestor['path'], 'revision': ancestor['revision']}
)
408 # Get "svn log" entry for path just before (or at) @rev
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
))
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']:
422 # Check action-type for this file
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
))
428 debug_desc
= ": " + action
+ " " + p
429 if d
['copyfrom_path']:
430 debug_desc
+= " (from " + d
['copyfrom_path'] + "@" + str(d
['copyfrom_revision']) + ")"
434 # If file was replaced, it has no ancestor
437 # If file was deleted, it has no ancestor
440 # If file was added but not a copy, it has no ancestor
441 if not d
['copyfrom_path']:
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
+ "/")):
450 # Else, follow the copy and keep on searching
451 rev
= ancestor
['revision']
452 path
= ancestor
['path']
454 print ">> find_svn_parent: copy-from: " + path
+ "@" + str(rev
) + " -- " + ancestor
['path']
458 def do_svn_copy(source_repos_url
, source_path
, dest_path
, ancestors
):
459 for ancestor
in ancestors
:
463 def pull_svn_rev(log_entry
, source_repos_url
, source_url
, target_url
, source_path
, original_wc
, keep_author
=False):
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'.
469 svn_rev
= log_entry
['revision']
470 run_svn(["up", "--ignore-externals", "-r", svn_rev
, original_wc
])
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'
480 if not p
.startswith(source_path
+ "/"):
481 # Ignore changed files that are not part of this subdir
483 unrelated_paths
.append(p
)
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("/")
492 debug_desc
= " " + action
+ " " + source_path
+ "/" + p
493 if d
['copyfrom_path']:
494 debug_desc
+= " (from " + d
['copyfrom_path'] + "@" + str(d
['copyfrom_revision']) + ")"
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
))
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
)
509 # Special-handling for replace's
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".
516 run_svn(["remove", "--force", p
])
519 # Handle all the various action-types
520 # (Handle "add" first, for "svn copy/move" support)
522 # Determine where to export from
524 from_path
= source_path
+ "/" + p
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
)
532 parent
= ancestors
[len(ancestors
)-1]
533 #from_rev = parent['revision']
534 #from_path = parent['path']
536 print ">> find_svn_parent: FOUND PARENT: " + parent
['path'] + "@" + str(parent
['revision'])
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("/")
544 #do_svn_copy(source_repos_url, source_path
545 run_svn(["copy", from_path
, p
])
547 # Create (parent) directory if needed
548 if os
.path
.isdir(original_wc
+ os
.sep
+ p
):
551 p_path
= os
.path
.dirname(p
).strip() or '.'
552 if not os
.path
.exists(p_path
):
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
563 if os
.path
.isdir(original_wc
+ os
.sep
+ p
):
566 p_path
= os
.path
.dirname(p
).strip() or '.'
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
)
577 display_error("Internal Error: Handling for action='R' not implemented yet.")
580 modified_paths
.append(p
)
583 display_error("Internal Error: pull_svn_rev: Unhandled 'action' value: '" + action
+ "'")
586 for r
in removed_paths
:
588 run_svn(["remove", "--force", r
])
591 for m
in modified_paths
:
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" \
601 run_svn(["revert", "--recursive", m
])
602 if os
.path
.isfile(m
):
603 shutil
.copy(original_wc
+ os
.sep
+ m
, m
)
606 print "Unrelated paths: "
607 print "*", unrelated_paths
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:
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
621 for d
in log_entry
['changed_paths']:
623 p
= p
[len(source_path
):].strip("/")
624 if os
.path
.isfile(p
):
625 if os
.path
.isfile(p
+ ".prej"):
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"):
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
])
645 run_svn(["propset", "svn:ignore", out
.strip(), p
])
646 out
= run_svn(["propget", "svn:externel",
647 original_wc
+ os
.sep
+ p
])
649 run_svn(["propset", "svn:external", out
.strip(), p
])
652 commit_from_svn_log_entry(log_entry
, commit_paths
,
653 keep_author
=keep_author
)
655 raise ExternalCommandFailed
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()
670 display_error("incorrect number of arguments\n\nTry: svn2svn.py --help",
673 source_url
= args
.pop(0).rstrip("/")
674 target_url
= args
.pop(0).rstrip("/")
675 if options
.keep_author
:
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']
685 original_wc
= "_original_wc"
688 ## old working copy does not exist, disable continue mode
689 if not os
.path
.exists(dup_wc
):
690 options
.cont_from_break
= False
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
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)
706 # Get log entry for the SVN revision we will check out
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,
712 # Otherwise, get log entry of branch creation
713 svn_start_log
= get_first_svn_log_entry(source_url
, 1,
716 # This is the revision we will checkout from
717 svn_rev
= svn_start_log
['revision']
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
)
724 # Import first revision (changeset) into Target SVN URL
725 timestamp
= int(svn_start_log
['date'])
726 svn_date
= str(datetime
.fromtimestamp(timestamp
))
728 run_svn(["import", original_wc
, target_url
, "-m",
729 svn_start_log
['message'] + "\nDate: " + svn_date
,
730 "--username", svn_start_log
['author']])
732 run_svn(["import", original_wc
, target_url
, "-m",
733 svn_start_log
['message'] + "\nDate: " + svn_date
+
734 "\nAuthor: " + svn_start_log
['author']])
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
)
741 original_wc
= os
.path
.abspath(original_wc
)
742 dup_wc
= os
.path
.abspath(dup_wc
)
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
):]
758 if options
.cont_from_break
:
759 svn_rev
= svn_info
['revision'] - 1
763 # Load SVN log starting from svn_rev + 1
764 it_log_entries
= iter_svn_log_entries(source_url
, svn_rev
+ 1, greatest_rev
)
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
)
771 except KeyboardInterrupt:
772 print "\nStopped by user."
774 run_svn(["revert", "--recursive", "."])
776 print "\nCommand failed with following error:\n"
777 traceback
.print_exc()
779 run_svn(["revert", "--recursive", "."])
785 if __name__
== "__main__":
788 # vim: shiftwidth=4 softtabstop=4