2 Replicate (replay) changesets from one SVN repository to another.
5 from svn2svn
import base_version
, full_version
7 from svn2svn
import shell
8 from svn2svn
import svnclient
9 from svn2svn
.shell
import run_svn
,run_shell_command
10 from svn2svn
.errors
import ExternalCommandFailed
, UnsupportedSVNAction
, InternalError
, VerificationError
11 from svn2svn
.run
.common
import in_svn
, is_child_path
, join_path
, find_svn_ancestors
12 from parse
import HelpFormatter
13 from breakhandler
import BreakHandler
22 from datetime
import datetime
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 target_repos_url
= "" # URL to root of target SVN repo, e.g. 'http://server/svn/target'
31 target_base
= "" # Relative path of target_url in target SVN repo, e.g. '/trunk'
32 rev_map
= {} # The running mapping-table dictionary for source_url rev #'s -> target_url rev #'s
33 options
= None # optparser options
35 def parse_svn_commit_rev(output
):
37 Parse the revision number from the output of "svn commit".
39 output_lines
= output
.strip(os
.linesep
).split(os
.linesep
)
41 for line
in output_lines
:
42 if line
[0:19] == 'Committed revision ':
43 rev_num
= line
[19:].rstrip('.')
45 assert rev_num
is not None
48 def commit_from_svn_log_entry(log_entry
, commit_paths
=None, target_revprops
=None):
50 Given an SVN log entry and an optional list of changed paths, do an svn commit.
52 if options
.beforecommit
:
53 # Run optional external shell hook here, for doing pre-commit filtering
54 # $1 = Path to working copy
55 # $2 = Source revision #
56 args
= [os
.getcwd(), log_entry
['revision']]
57 run_shell_command(options
.beforecommit
, args
=args
)
58 # Display the _wc_target "svn status" info if running in -vv (or higher) mode
59 if ui
.get_level() >= ui
.EXTRA
:
60 ui
.status(">> commit_from_svn_log_entry: Pre-commit wc_target status:", level
=ui
.EXTRA
, color
='CYAN')
61 ui
.status(run_svn(["status"]), level
=ui
.EXTRA
, color
='CYAN')
62 # This will use the local timezone for displaying commit times
63 timestamp
= int(log_entry
['date'])
64 svn_date
= str(datetime
.fromtimestamp(timestamp
))
65 # Uncomment this one one if you prefer UTC commit times
66 #svn_date = "%d 0" % timestamp
67 args
= ["commit", "--force-log"]
68 message
= log_entry
['message']
70 message
+= "\nDate: " + svn_date
71 if options
.log_author
:
72 message
+= "\nAuthor: " + log_entry
['author']
73 args
+= ["-m", message
]
75 if log_entry
['revprops']:
76 # Carry forward any revprop's from the source revision
77 for v
in log_entry
['revprops']:
78 revprops
[v
['name']] = v
['value']
80 # Add any extra revprop's we want to set for the target repo commits
81 for v
in target_revprops
:
82 revprops
[v
['name']] = v
['value']
85 args
+= ["--with-revprop", "%s=%s" % (key
, str(revprops
[key
]))]
87 if len(commit_paths
)<100:
88 # If we don't have an excessive amount of individual changed paths, pass
89 # those to the "svn commit" command. Else, pass nothing so we commit at
90 # the root of the working-copy.
91 for c_path
in commit_paths
:
92 args
+= [svnclient
.safe_path(c_path
)]
94 if not options
.dry_run
:
95 # Use BreakHandler class to temporarily redirect SIGINT handler, so that
96 # "svn commit" + post-commit rev-prop updating is a quasi-atomic unit.
97 # If user presses Ctrl-C during this, wait until after this full action
98 # has finished raising the KeyboardInterrupt exception.
101 # Run the "svn commit" command, and screen-scrape the target_rev value (if any)
102 output
= run_svn(args
)
103 rev_num
= parse_svn_commit_rev(output
) if output
else None
104 if rev_num
is not None:
105 if options
.keep_date
:
106 run_svn(["propset", "--revprop", "-r", rev_num
, "svn:date", log_entry
['date_raw']])
107 if options
.keep_author
:
108 run_svn(["propset", "--revprop", "-r", rev_num
, "svn:author", log_entry
['author']])
109 ui
.status("Committed revision %s (source r%s).", rev_num
, log_entry
['revision'])
111 # Check if the user tried to press Ctrl-C
113 raise KeyboardInterrupt
116 def verify_commit(source_rev
, target_rev
, log_entry
=None):
118 Compare the ancestry/content/properties between source_url vs target_url
119 for a given revision.
122 # Gather the offsets in the source repo to check
125 # TODO: Need to make this ancestry aware
126 if options
.verify
== 1 and log_entry
is not None: # Changed only
127 ui
.status("Verifying source revision %s (only-changed)...", source_rev
, level
=ui
.VERBOSE
)
128 for d
in log_entry
['changed_paths']:
130 if not is_child_path(path
, source_base
):
133 d
['kind'] = svnclient
.get_kind(source_repos_url
, path
, source_rev
, d
['action'], log_entry
['changed_paths'])
134 assert (d
['kind'] == 'file') or (d
['kind'] == 'dir')
135 path_is_dir
= True if d
['kind'] == 'dir' else False
136 path_is_file
= True if d
['kind'] == 'file' else False
137 path_offset
= path
[len(source_base
):].strip("/")
138 if d
['action'] == 'D':
139 remove_paths
.append(path_offset
)
140 elif not path_offset
in check_paths
:
141 ui
.status("verify_commit: path [mode=changed]: kind=%s: %s", d
['kind'], path
, level
=ui
.DEBUG
, color
='YELLOW')
143 ui
.status(" "+"verify_commit [mode=changed]: check_paths.append('%s')", path_offset
, level
=ui
.DEBUG
, color
='GREEN')
144 check_paths
.append(path_offset
)
146 if not d
['action'] in 'AR':
148 child_paths
= svnclient
.list(source_url
.rstrip("/")+"/"+path_offset
, source_rev
, recursive
=True)
149 for p
in child_paths
:
150 child_path_is_dir
= True if p
['kind'] == 'dir' else False
151 child_path_offset
= p
['path']
152 if not child_path_is_dir
:
154 working_path
= (path_offset
+"/" if path_offset
else "") + child_path_offset
155 if not working_path
in check_paths
:
156 ui
.status(" "+"verify_commit [mode=changed]: check_paths.append('%s'+'/'+'%s')", path_offset
, child_path_offset
, level
=ui
.DEBUG
, color
='GREEN')
157 check_paths
.append(working_path
)
158 if options
.verify
== 2: # All paths
159 ui
.status("Verifying source revision %s (all)...", source_rev
, level
=ui
.VERBOSE
)
160 child_paths
= svnclient
.list(source_url
, source_rev
, recursive
=True)
161 for p
in child_paths
:
162 child_path_is_dir
= True if p
['kind'] == 'dir' else False
163 child_path_offset
= p
['path']
164 if not child_path_is_dir
:
166 ui
.status("verify_commit [mode=all]: check_paths.append('%s')", child_path_offset
, level
=ui
.DEBUG
, color
='GREEN')
167 check_paths
.append(child_path_offset
)
169 # If there were any paths deleted in the last revision (options.verify=1 mode),
170 # check that they were correctly deleted.
172 count_total
= len(remove_paths
)
174 for path_offset
in remove_paths
:
176 if in_svn(path_offset
):
177 ui
.status(" (%s/%s) Verify path: FAIL: %s", str(count
).rjust(len(str(count_total
))), count_total
, path_offset
, level
=ui
.EXTRA
, color
='RED')
178 ui
.status("VerificationError: Path removed in source rev r%s, but still exists in target WC: %s", source_rev
, path_offset
, color
='RED')
181 ui
.status(" (%s/%s) Verify remove: OK: %s", str(count
).rjust(len(str(count_total
))), count_total
, path_offset
, level
=ui
.EXTRA
)
183 # Compare each of the check_path entries between source vs. target
185 source_rev_first
= int(min(rev_map
, key
=rev_map
.get
)) or 1 # The first source_rev we replayed into target
186 ui
.status("verify_commit: source_rev_first:%s", source_rev_first
, level
=ui
.DEBUG
, color
='YELLOW')
187 count_total
= len(check_paths
)
189 for path_offset
in check_paths
:
192 ui
.status("...processed %s (%s of %s)..." % (count
, count
, count_total
), level
=ui
.VERBOSE
)
193 ui
.status("verify_commit: path_offset:%s", path_offset
, level
=ui
.DEBUG
, color
='YELLOW')
194 source_log_entries
= svnclient
.run_svn_log(source_url
.rstrip("/")+"/"+path_offset
, source_rev
, 1, source_rev
-source_rev_first
+1)
195 target_log_entries
= svnclient
.run_svn_log(target_url
.rstrip("/")+"/"+path_offset
, target_rev
, 1, target_rev
)
196 # Build a list of commits in source_log_entries which matches our
197 # target path_offset.
198 working_path
= source_base
+"/"+path_offset
200 for log_entry
in source_log_entries
:
201 source_rev_tmp
= log_entry
['revision']
202 if source_rev_tmp
< source_rev_first
:
203 # Only process source revisions which have been replayed into target
205 #ui.status(" [verify_commit] source_rev_tmp:%s, working_path:%s\n%s", source_rev_tmp, working_path, pp.pformat(log_entry), level=ui.DEBUG, color='MAGENTA')
206 changed_paths_temp
= []
207 for d
in log_entry
['changed_paths']:
209 # Match working_path or any parents
210 if is_child_path(working_path
, path
):
211 ui
.status(" verify_commit: changed_path: %s %s@%s (parent:%s)", d
['action'], path
, source_rev_tmp
, working_path
, level
=ui
.DEBUG
, color
='YELLOW')
212 changed_paths_temp
.append({'path': path, 'data': d}
)
213 assert changed_paths_temp
214 # Reverse-sort any matches, so that we start with the most-granular (deepest in the tree) path.
215 changed_paths
= sorted(changed_paths_temp
, key
=operator
.itemgetter('path'), reverse
=True)
216 # Find the action for our working_path in this revision. Use a loop to check in reverse order,
217 # so that if the target file/folder is "M" but has a parent folder with an "A" copy-from.
218 working_path_next
= working_path
220 for v
in changed_paths
:
225 if d
['action'] not in svnclient
.valid_svn_actions
:
226 raise UnsupportedSVNAction("In SVN rev. %d: action '%s' not supported. Please report a bug!"
227 % (log_entry
['revision'], d
['action']))
228 if d
['action'] in 'AR' and d
['copyfrom_revision']:
229 # If we found a copy-from action for a parent path, adjust our
230 # working_path to follow the rename/copy-from, just like find_svn_ancestors().
231 working_path_next
= working_path
.replace(d
['path'], d
['copyfrom_path'])
234 if is_child_path(working_path
, source_base
):
235 # Only add source_rev's where the path changed in this revision was a child
236 # of source_base, so that we silently ignore any history that happened on
237 # non-source_base paths (e.g. ignore branch history if we're only replaying trunk).
240 if d
['action'] == 'M':
241 # For action="M", we need to throw out cases where the only change was to
242 # a property which we ignore, e.g. "svn:mergeinfo".
244 d
['kind'] = svnclient
.get_kind(source_repos_url
, working_path
, log_entry
['revision'], d
['action'], log_entry
['changed_paths'])
245 assert (d
['kind'] == 'file') or (d
['kind'] == 'dir')
246 if d
['kind'] == 'file':
247 # Check for file-content changes
248 # TODO: This should be made ancestor-aware, since the file won't always be at the same path in rev-1
249 sum1
= run_shell_command("svn cat -r %s '%s' | md5sum" % (source_rev_tmp
, source_repos_url
+working_path
+"@"+str(source_rev_tmp
)))
250 sum2
= run_shell_command("svn cat -r %s '%s' | md5sum" % (source_rev_tmp
-1, source_repos_url
+working_path_next
+"@"+str(source_rev_tmp
-1)))
251 is_diff
= True if sum1
<> sum2
else False
253 # Check for property changes
254 props1
= svnclient
.propget_all(source_repos_url
+working_path
, source_rev_tmp
)
255 props2
= svnclient
.propget_all(source_repos_url
+working_path_next
, source_rev_tmp
-1)
256 # Ignore changes to "svn:mergeinfo", since we don't copy that
257 if 'svn:mergeinfo' in props1
: del props1
['svn:mergeinfo']
258 if 'svn:mergeinfo' in props2
: del props2
['svn:mergeinfo']
260 if prop
not in props2
or \
261 props1
[prop
] != props2
[prop
]:
265 if prop
not in props1
or \
266 props1
[prop
] != props2
[prop
]:
270 ui
.status(" verify_commit: skip %s@%s", working_path
, source_rev_tmp
, level
=ui
.DEBUG
, color
='GREEN_B', bold
=True)
274 ui
.status(" verify_commit: source_revs.append(%s), working_path:%s", source_rev_tmp
, working_path
, level
=ui
.DEBUG
, color
='GREEN_B')
275 source_revs
.append({'path': working_path, 'revision': source_rev_tmp}
)
276 working_path
= working_path_next
277 # Build a list of all the target commits "svn log" returned
279 target_revs_rmndr
= []
280 for log_entry
in target_log_entries
:
281 target_rev_tmp
= log_entry
['revision']
282 ui
.status(" verify_commit: target_revs.append(%s)", target_rev_tmp
, level
=ui
.DEBUG
, color
='GREEN_B')
283 target_revs
.append(target_rev_tmp
)
284 target_revs_rmndr
.append(target_rev_tmp
)
285 # Compare the two lists
286 for d
in source_revs
:
287 working_path
= d
['path']
288 source_rev_tmp
= d
['revision']
289 target_rev_tmp
= get_rev_map(source_rev_tmp
, " ")
290 working_offset
= working_path
[len(source_base
):].strip("/")
291 sum1
= run_shell_command("svn cat -r %s '%s' | md5sum" % (source_rev_tmp
, source_repos_url
+working_path
+"@"+str(source_rev_tmp
)))
292 sum2
= run_shell_command("svn cat -r %s '%s' | md5sum" % (target_rev_tmp
, target_url
+"/"+working_offset
+"@"+str(target_rev_tmp
))) if target_rev_tmp
is not None else ""
293 #print "source@%s: %s" % (str(source_rev_tmp).ljust(6), sum1)
294 #print "target@%s: %s" % (str(target_rev_tmp).ljust(6), sum2)
295 ui
.status(" verify_commit: %s: source=%s target=%s", working_offset
, source_rev_tmp
, target_rev_tmp
, level
=ui
.DEBUG
, color
='GREEN')
296 if not target_rev_tmp
:
297 ui
.status(" (%s/%s) Verify path: FAIL: %s", str(count
).rjust(len(str(count_total
))), count_total
, path_offset
, level
=ui
.EXTRA
, color
='RED')
298 ui
.status("VerificationError: Unable to find corresponding target_rev for source_rev r%s in rev_map (path_offset='%s')", source_rev_tmp
, path_offset
, color
='RED')
301 if target_rev_tmp
not in target_revs
:
302 # If found a source_rev with no equivalent target_rev in target_revs,
303 # check if the only difference in source_rev vs. source_rev-1 is the
304 # removal/addition of a trailing newline char, since this seems to get
305 # stripped-out sometimes during the replay (via "svn export"?).
306 # Strip any trailing \r\n from file-content (http://stackoverflow.com/a/1656218/346778)
307 sum1
= run_shell_command("svn cat -r %s '%s' | perl -i -p0777we's/\\r\\n\z//' | md5sum" % (source_rev_tmp
, source_repos_url
+working_path
+"@"+str(source_rev_tmp
)))
308 sum2
= run_shell_command("svn cat -r %s '%s' | perl -i -p0777we's/\\r\\n\z//' | md5sum" % (source_rev_tmp
-1, source_repos_url
+working_path
+"@"+str(source_rev_tmp
-1)))
310 ui
.status(" (%s/%s) Verify path: FAIL: %s", str(count
).rjust(len(str(count_total
))), count_total
, path_offset
, level
=ui
.EXTRA
, color
='RED')
311 ui
.status("VerificationError: Found source_rev (r%s) with no corresponding target_rev: path_offset='%s'", source_rev_tmp
, path_offset
, color
='RED')
314 target_revs_rmndr
.remove(target_rev_tmp
)
315 if target_revs_rmndr
:
316 rmndr_list
= ", ".join(map(str, target_revs_rmndr
))
317 ui
.status(" (%s/%s) Verify path: FAIL: %s", str(count
).rjust(len(str(count_total
))), count_total
, path_offset
, level
=ui
.EXTRA
, color
='RED')
318 ui
.status("VerificationError: Found one or more *extra* target_revs: path_offset='%s', target_revs='%s'", path_offset
, rmndr_list
, color
='RED')
321 ui
.status(" (%s/%s) Verify path: OK: %s", str(count
).rjust(len(str(count_total
))), count_total
, path_offset
, level
=ui
.EXTRA
)
323 # Ensure there are no "extra" files in the target side
324 if options
.verify
== 2:
326 child_paths
= svnclient
.list(target_url
, target_rev
, recursive
=True)
327 for p
in child_paths
:
328 child_path_is_dir
= True if p
['kind'] == 'dir' else False
329 child_path_offset
= p
['path']
330 if not child_path_is_dir
:
331 target_paths
.append(child_path_offset
)
333 for path_offset
in target_paths
:
334 if not path_offset
in check_paths
:
335 ui
.status("VerificationError: Path exists in target (@%s) but not source (@%s): %s", target_rev
, source_rev
, path_offset
, color
='RED')
337 for path_offset
in check_paths
:
338 if not path_offset
in target_paths
:
339 ui
.status("VerificationError: Path exists in source (@%s) but not target (@%s): %s", source_rev
, target_rev
, path_offset
, color
='RED')
343 raise VerificationError("Found %s verification errors" % (error_cnt
))
344 ui
.status("Verified revision %s (%s).", target_rev
, "all" if options
.verify
== 2 else "only-changed")
346 def full_svn_revert():
348 Do an "svn revert" and proactively remove any extra files in the working copy.
350 run_svn(["revert", "--recursive", "."])
351 output
= run_svn(["status"])
353 output_lines
= output
.strip(os
.linesep
).split(os
.linesep
)
354 for line
in output_lines
:
356 path
= line
[4:].strip(" ")
357 if os
.path
.isfile(path
):
359 if os
.path
.isdir(path
):
362 def gen_tracking_revprops(source_rev
):
364 Build an array of svn2svn-specific source-tracking revprops.
366 revprops
= [{'name':'svn2svn:source_uuid', 'value':source_repos_uuid}
,
367 {'name':'svn2svn:source_url', 'value':urllib.quote(source_url, ":/")}
,
368 {'name':'svn2svn:source_rev', 'value':source_rev}
]
371 def sync_svn_props(source_url
, source_rev
, path_offset
):
373 Carry-forward any unversioned properties from the source repo to the
376 source_props
= svnclient
.propget_all(join_path(source_url
, path_offset
), source_rev
)
377 target_props
= svnclient
.propget_all(path_offset
)
378 if 'svn:mergeinfo' in source_props
:
379 # Never carry-forward "svn:mergeinfo"
380 del source_props
['svn:mergeinfo']
381 for prop
in target_props
:
382 if prop
not in source_props
:
383 # Remove any properties which exist in target but not source
384 run_svn(["propdel", prop
, svnclient
.safe_path(path_offset
)])
385 for prop
in source_props
:
386 if prop
not in target_props
or \
387 source_props
[prop
] != target_props
[prop
]:
388 # Set/update any properties which exist in source but not target or
389 # whose value differs between source vs. target.
390 run_svn(["propset", prop
, source_props
[prop
], svnclient
.safe_path(path_offset
)])
392 def get_rev_map(source_rev
, prefix
):
394 Find the equivalent rev # in the target repo for the given rev # from the source repo.
396 ui
.status(prefix
+ ">> get_rev_map(%s)", source_rev
, level
=ui
.DEBUG
, color
='GREEN')
397 # Find the highest entry less-than-or-equal-to source_rev
398 for rev
in range(int(source_rev
), 0, -1):
399 in_rev_map
= True if rev
in rev_map
else False
400 ui
.status(prefix
+ ">> get_rev_map: rev=%s in_rev_map=%s", rev
, str(in_rev_map
), level
=ui
.DEBUG
, color
='BLACK_B')
402 return int(rev_map
[rev
])
403 # Else, we fell off the bottom of the rev_map. Ruh-roh...
406 def set_rev_map(source_rev
, target_rev
):
407 #ui.status(">> set_rev_map: source_rev=%s target_rev=%s", source_rev, target_rev, level=ui.DEBUG, color='GREEN')
409 rev_map
[int(source_rev
)]=int(target_rev
)
411 def build_rev_map(target_url
, target_end_rev
, source_info
):
413 Check for any already-replayed history from source_url (source_info) and
414 build the mapping-table of source_rev -> target_rev.
418 ui
.status("Rebuilding target_rev -> source_rev rev_map...", level
=ui
.VERBOSE
)
420 it_log_entries
= svnclient
.iter_svn_log_entries(target_url
, 1, target_end_rev
, get_changed_paths
=False, get_revprops
=True)
421 for log_entry
in it_log_entries
:
422 if log_entry
['revprops']:
424 for v
in log_entry
['revprops']:
425 if v
['name'].startswith('svn2svn:'):
426 revprops
[v
['name']] = v
['value']
428 revprops
['svn2svn:source_uuid'] == source_info
['repos_uuid'] and \
429 revprops
['svn2svn:source_url'] == urllib
.quote(source_info
['url'], ":/"):
430 source_rev
= revprops
['svn2svn:source_rev']
431 target_rev
= log_entry
['revision']
432 set_rev_map(source_rev
, target_rev
)
434 if proc_count
% 500 == 0:
435 ui
.status("...processed %s (%s of %s)..." % (proc_count
, target_rev
, target_end_rev
), level
=ui
.VERBOSE
)
437 def path_in_list(paths
, path
):
439 if is_child_path(path
, p
):
443 def add_path(paths
, path
):
444 if not path_in_list(paths
, path
):
447 def in_ancestors(ancestors
, ancestor
):
449 for idx
in range(len(ancestors
)-1, 0, -1):
450 if int(ancestors
[idx
]['revision']) > ancestor
['revision']:
451 match
= is_child_path(ancestor
['path'], ancestors
[idx
]['path'])
455 def do_svn_add(source_url
, path_offset
, source_rev
, source_ancestors
, \
456 parent_copyfrom_path
="", parent_copyfrom_rev
="", \
457 export_paths
={}, is_dir
= False, skip_paths
=[], prefix
= ""):
459 Given the add'd source path, replay the "svn add/copy" commands to correctly
460 track renames across copy-from's.
462 For example, consider a sequence of events like this:
463 1. svn copy /trunk /branches/fix1
464 2. (Make some changes on /branches/fix1)
465 3. svn mv /branches/fix1/Proj1 /branches/fix1/Proj2 " Rename folder
466 4. svn mv /branches/fix1/Proj2/file1.txt /branches/fix1/Proj2/file2.txt " Rename file inside renamed folder
467 5. svn co /trunk && svn merge /branches/fix1
468 After the merge and commit, "svn log -v" with show a delete of /trunk/Proj1
469 and and add of /trunk/Proj2 copy-from /branches/fix1/Proj2. If we just did
470 a straight "svn export+add" based on the /branches/fix1/Proj2 folder, we'd
471 lose the logical history that Proj2/file2.txt is really a descendant of
474 'path_offset' is the offset from source_base to the file to check ancestry for,
475 e.g. 'projectA/file1.txt'. path = source_repos_url + source_base + path_offset.
476 'source_rev' is the revision ("svn log") that we're processing from the source repo.
477 'parent_copyfrom_path' and 'parent_copyfrom_rev' is the copy-from path of the parent
478 directory, when being called recursively by do_svn_add_dir().
479 'export_paths' is the list of path_offset's that we've deferred running "svn export" on.
480 'is_dir' is whether path_offset is a directory (rather than a file).
482 source_base
= source_url
[len(source_repos_url
):] # e.g. '/trunk'
483 ui
.status(prefix
+ ">> do_svn_add: %s %s", join_path(source_base
, path_offset
)+"@"+str(source_rev
),
484 " (parent-copyfrom: "+parent_copyfrom_path
+"@"+str(parent_copyfrom_rev
)+")" if parent_copyfrom_path
else "",
485 level
=ui
.DEBUG
, color
='GREEN')
486 # Check if the given path has ancestors which chain back to the current source_base
487 found_ancestor
= False
488 ancestors
= find_svn_ancestors(source_repos_url
, join_path(source_base
, path_offset
), source_rev
, stop_base_path
=source_base
, prefix
=prefix
+" ")
489 ancestor
= ancestors
[len(ancestors
)-1] if ancestors
else None # Choose the eldest ancestor, i.e. where we reached stop_base_path=source_base
490 if ancestor
and not in_ancestors(source_ancestors
, ancestor
):
492 copyfrom_path
= ancestor
['copyfrom_path'] if ancestor
else ""
493 copyfrom_rev
= ancestor
['copyfrom_rev'] if ancestor
else ""
495 # The copy-from path has ancestry back to source_url.
496 ui
.status(prefix
+ ">> do_svn_add: Check copy-from: Found parent: %s", copyfrom_path
+"@"+str(copyfrom_rev
),
497 level
=ui
.DEBUG
, color
='GREEN', bold
=True)
498 found_ancestor
= True
499 # Map the copyfrom_rev (source repo) to the equivalent target repo rev #. This can
500 # return None in the case where copyfrom_rev is *before* our source_start_rev.
501 tgt_rev
= get_rev_map(copyfrom_rev
, prefix
+" ")
502 ui
.status(prefix
+ ">> do_svn_add: get_rev_map: %s (source) -> %s (target)", copyfrom_rev
, tgt_rev
, level
=ui
.DEBUG
, color
='GREEN')
504 ui
.status(prefix
+ ">> do_svn_add: Check copy-from: No ancestor chain found.", level
=ui
.DEBUG
, color
='GREEN')
505 found_ancestor
= False
506 if found_ancestor
and tgt_rev
:
507 # Check if this path_offset in the target WC already has this ancestry, in which
508 # case there's no need to run the "svn copy" (again).
509 path_in_svn
= in_svn(path_offset
, prefix
=prefix
+" ")
510 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 []
511 if (not log_entry
or (log_entry
['revision'] != tgt_rev
)):
512 copyfrom_offset
= copyfrom_path
[len(source_base
):].strip('/')
513 ui
.status(prefix
+ ">> do_svn_add: svn_copy: Copy-from: %s", copyfrom_path
+"@"+str(copyfrom_rev
), level
=ui
.DEBUG
, color
='GREEN')
514 ui
.status(prefix
+ " copyfrom: %s", copyfrom_path
+"@"+str(copyfrom_rev
), level
=ui
.DEBUG
, color
='GREEN')
515 ui
.status(prefix
+ " p_copyfrom: %s", parent_copyfrom_path
+"@"+str(parent_copyfrom_rev
) if parent_copyfrom_path
else "", level
=ui
.DEBUG
, color
='GREEN')
517 ((parent_copyfrom_path
and is_child_path(copyfrom_path
, parent_copyfrom_path
)) and \
518 (parent_copyfrom_rev
and copyfrom_rev
== parent_copyfrom_rev
)):
519 # When being called recursively, if this child entry has the same ancestor as the
520 # the parent, then no need to try to run another "svn copy".
521 ui
.status(prefix
+ ">> do_svn_add: svn_copy: Same ancestry as parent: %s",
522 parent_copyfrom_path
+"@"+str(parent_copyfrom_rev
),level
=ui
.DEBUG
, color
='GREEN')
525 # Copy this path from the equivalent path+rev in the target repo, to create the
526 # equivalent history.
527 if parent_copyfrom_path
:
528 # If we have a parent copy-from path, we mis-match that so display a status
529 # message describing the action we're mimic'ing. If path_in_svn, then this
530 # is logically a "replace" rather than an "add".
531 ui
.status(" %s %s (from %s)", ('R' if path_in_svn
else 'A'), join_path(source_base
, path_offset
), ancestors
[0]['copyfrom_path']+"@"+str(copyfrom_rev
), level
=ui
.VERBOSE
)
533 # If local file is already under version-control, then this is a replace.
534 ui
.status(prefix
+ ">> do_svn_add: pre-copy: local path already exists: %s", path_offset
, level
=ui
.DEBUG
, color
='GREEN')
535 svnclient
.update(path_offset
)
536 svnclient
.remove(path_offset
, force
=True)
537 run_svn(["copy", "-r", tgt_rev
, svnclient
.safe_path(join_path(target_url
, copyfrom_offset
), tgt_rev
), svnclient
.safe_path(path_offset
)])
539 # Export the final verison of all files in this folder.
540 add_path(export_paths
, path_offset
)
542 # Export the final verison of this file.
543 svnclient
.export(source_repos_url
+join_path(source_base
, path_offset
), source_rev
, path_offset
, force
=True)
544 if options
.keep_prop
:
545 sync_svn_props(source_url
, source_rev
, path_offset
)
547 ui
.status(prefix
+ ">> do_svn_add: Skipped 'svn copy': %s", path_offset
, level
=ui
.DEBUG
, color
='GREEN')
549 # Else, either this copy-from path has no ancestry back to source_url OR copyfrom_rev comes
550 # before our initial source_start_rev (i.e. tgt_rev == None), so can't do a "svn copy".
551 # Create (parent) directory if needed.
552 # TODO: This is (nearly) a duplicate of code in process_svn_log_entry(). Should this be
553 # split-out to a shared tag?
554 p_path
= path_offset
if is_dir
else os
.path
.dirname(path_offset
).strip() or None
555 if p_path
and not os
.path
.exists(p_path
):
556 run_svn(["mkdir", svnclient
.safe_path(p_path
)])
557 if not in_svn(path_offset
, prefix
=prefix
+" "):
559 # Export the final verison of all files in this folder.
560 add_path(export_paths
, path_offset
)
562 # Export the final verison of this file. We *need* to do this before running
563 # the "svn add", even if we end-up re-exporting this file again via export_paths.
564 svnclient
.export(source_repos_url
+join_path(source_base
, path_offset
), source_rev
, path_offset
, force
=True)
565 # If not already under version-control, then "svn add" this file/folder.
566 run_svn(["add", "--parents", svnclient
.safe_path(path_offset
)])
567 if options
.keep_prop
:
568 sync_svn_props(source_url
, source_rev
, path_offset
)
570 # For any folders that we process, process any child contents, so that we correctly
571 # replay copies/replaces/etc.
572 do_svn_add_dir(source_url
, path_offset
, source_rev
, source_ancestors
,
573 copyfrom_path
, copyfrom_rev
, export_paths
, skip_paths
, prefix
+" ")
575 def do_svn_add_dir(source_url
, path_offset
, source_rev
, source_ancestors
, \
576 parent_copyfrom_path
, parent_copyfrom_rev
, \
577 export_paths
, skip_paths
, prefix
=""):
578 source_base
= source_url
[len(source_repos_url
):] # e.g. '/trunk'
579 # Get the directory contents, to compare between the local WC (target_url) vs. the remote repo (source_url)
580 # TODO: paths_local won't include add'd paths because "svn ls" lists the contents of the
581 # associated remote repo folder. (Is this a problem?)
582 paths_local
= svnclient
.list(path_offset
)
583 paths_remote
= svnclient
.list(join_path(source_url
, path_offset
), source_rev
)
584 ui
.status(prefix
+ ">> do_svn_add_dir: paths_local: %s", str(paths_local
), level
=ui
.DEBUG
, color
='GREEN')
585 ui
.status(prefix
+ ">> do_svn_add_dir: paths_remote: %s", str(paths_remote
), level
=ui
.DEBUG
, color
='GREEN')
586 # Update files/folders which exist in remote but not local
587 for p
in paths_remote
:
588 path_is_dir
= True if p
['kind'] == 'dir' else False
589 working_path
= join_path(path_offset
, p
['path']).lstrip('/')
590 #print "working_path:%s = path_offset:%s + path:%s" % (working_path, path_offset, path)
591 if not working_path
in skip_paths
:
592 do_svn_add(source_url
, working_path
, source_rev
, source_ancestors
,
593 parent_copyfrom_path
, parent_copyfrom_rev
,
594 export_paths
, path_is_dir
, skip_paths
, prefix
+" ")
595 # Remove files/folders which exist in local but not remote
596 for p
in paths_local
:
597 if not p
in paths_remote
:
598 working_path
= join_path(path_offset
, p
['path']).lstrip('/')
599 ui
.status(" %s %s", 'D', join_path(source_base
, working_path
), level
=ui
.VERBOSE
)
600 svnclient
.update(working_path
)
601 svnclient
.remove(working_path
, force
=True)
602 # TODO: Does this handle deleted folders too? Wouldn't want to have a case
603 # where we only delete all files from folder but leave orphaned folder around.
605 def process_svn_log_entry(log_entry
, ancestors
, commit_paths
, prefix
= ""):
607 Process SVN changes from the given log entry. Build an array (commit_paths)
608 of the paths in the working-copy that were changed, i.e. the paths which
609 we'll pass to "svn commit".
612 source_rev
= log_entry
['revision']
613 source_url
= log_entry
['url']
614 source_base
= source_url
[len(source_repos_url
):] # e.g. '/trunk'
615 ui
.status(prefix
+ ">> process_svn_log_entry: %s", source_url
+"@"+str(source_rev
), level
=ui
.DEBUG
, color
='GREEN')
616 for d
in log_entry
['changed_paths']:
617 # Get the full path for this changed_path
618 # e.g. '/branches/bug123/projectA/file1.txt'
620 if not is_child_path(path
, source_base
):
621 # Ignore changed files that are not part of this subdir
622 ui
.status(prefix
+ ">> process_svn_log_entry: Unrelated path: %s (base: %s)", path
, source_base
, level
=ui
.DEBUG
, color
='GREEN')
624 if d
['kind'] == "" or d
['kind'] == 'none':
625 # The "kind" value was introduced in SVN 1.6, and "svn log --xml" won't return a "kind"
626 # value for commits made on a pre-1.6 repo, even if the server is now running 1.6.
627 # We need to use other methods to fetch the node-kind for these cases.
628 d
['kind'] = svnclient
.get_kind(source_repos_url
, path
, source_rev
, d
['action'], log_entry
['changed_paths'])
629 assert (d
['kind'] == 'file') or (d
['kind'] == 'dir')
630 path_is_dir
= True if d
['kind'] == 'dir' else False
631 path_is_file
= True if d
['kind'] == 'file' else False
632 # Calculate the offset (based on source_base) for this changed_path
633 # e.g. 'projectA/file1.txt'
634 # (path = source_base + "/" + path_offset)
635 path_offset
= path
[len(source_base
):].strip("/")
636 # Get the action for this path
638 if action
not in svnclient
.valid_svn_actions
:
639 raise UnsupportedSVNAction("In SVN rev. %d: action '%s' not supported. Please report a bug!"
640 % (source_rev
, action
))
641 ui
.status(" %s %s%s", action
, d
['path'],
642 (" (from %s)" % (d
['copyfrom_path']+"@"+str(d
['copyfrom_revision']))) if d
['copyfrom_path'] else "",
645 # Try to be efficient and keep track of an explicit list of paths in the
646 # working copy that changed. If we commit from the root of the working copy,
647 # then SVN needs to crawl the entire working copy looking for pending changes.
648 commit_paths
.append(path_offset
)
650 # Special-handling for replace's
652 # If file was "replaced" (deleted then re-added, all in same revision),
653 # then we need to run the "svn rm" first, then change action='A'. This
654 # lets the normal code below handle re-"svn add"'ing the files. This
655 # should replicate the "replace".
656 if path_offset
and in_svn(path_offset
):
657 # Target path might not be under version-control yet, e.g. parent "add"
658 # was a copy-from a branch which had no ancestry back to trunk, and each
659 # child folder under that parent folder is a "replace" action on the final
660 # merge to trunk. Since the child folders will be in skip_paths, do_svn_add
661 # wouldn't have created them while processing the parent "add" path.
663 # Need to "svn update" before "svn remove" in case child contents are at
664 # a higher rev than the (parent) path_offset.
665 svnclient
.update(path_offset
)
666 svnclient
.remove(path_offset
, force
=True)
669 # Handle all the various action-types
670 # (Handle "add" first, for "svn copy/move" support)
672 # Determine where to export from.
674 # Handle cases where this "add" was a copy from another URL in the source repo
675 if d
['copyfrom_revision']:
676 copyfrom_path
= d
['copyfrom_path']
677 copyfrom_rev
= d
['copyfrom_revision']
679 for tmp_d
in log_entry
['changed_paths']:
680 tmp_path
= tmp_d
['path']
681 if is_child_path(tmp_path
, path
) and tmp_d
['action'] in 'ARD':
682 # Build list of child entries which are also in the changed_paths list,
683 # so that do_svn_add() can skip processing these entries when recursing
684 # since we'll end-up processing them later. Don't include action="M" paths
685 # in this list because it's non-conclusive: it could just mean that the
686 # file was modified *after* the copy-from, so we still want do_svn_add()
687 # to re-create the correct ancestry.
688 tmp_path_offset
= tmp_path
[len(source_base
):].strip("/")
689 skip_paths
.append(tmp_path_offset
)
690 do_svn_add(source_url
, path_offset
, source_rev
, ancestors
, "", "", export_paths
, path_is_dir
, skip_paths
, prefix
+" ")
691 # Else just "svn export" the files from the source repo and "svn add" them.
693 # Create (parent) directory if needed
694 p_path
= path_offset
if path_is_dir
else os
.path
.dirname(path_offset
).strip() or None
695 if p_path
and not os
.path
.exists(p_path
):
696 run_svn(["mkdir", svnclient
.safe_path(p_path
)])
697 # Export the entire added tree.
699 # For directories, defer the (recurisve) "svn export". Might have a
700 # situation in a branch merge where the entry in the svn-log is a
701 # non-copy-from'd "add" but there are child contents (that we haven't
702 # gotten to yet in log_entry) that are copy-from's. When we try do
703 # the "svn copy" later on in do_svn_add() for those copy-from'd paths,
704 # having pre-existing (svn-add'd) contents creates some trouble.
705 # Instead, just create the stub folders ("svn mkdir" above) and defer
706 # exporting the final file-state until the end.
707 add_path(export_paths
, path_offset
)
709 # Export the final verison of this file. We *need* to do this before running
710 # the "svn add", even if we end-up re-exporting this file again via export_paths.
711 svnclient
.export(join_path(source_url
, path_offset
), source_rev
, path_offset
, force
=True)
712 if not in_svn(path_offset
, prefix
=prefix
+" "):
713 # Need to use in_svn here to handle cases where client committed the parent
714 # folder and each indiv sub-folder.
715 run_svn(["add", "--parents", svnclient
.safe_path(path_offset
)])
716 if options
.keep_prop
:
717 sync_svn_props(source_url
, source_rev
, path_offset
)
721 # For dirs, need to "svn update" before "svn remove" because the final
722 # "svn commit" will fail if the parent (path_offset) is at a lower rev
723 # than any of the child contents. This needs to be a recursive update.
724 svnclient
.update(path_offset
)
725 svnclient
.remove(path_offset
, force
=True)
729 svnclient
.export(join_path(source_url
, path_offset
), source_rev
, path_offset
, force
=True, non_recursive
=True)
731 # For dirs, need to "svn update" before export/prop-sync because the
732 # final "svn commit" will fail if the parent is at a lower rev than
733 # child contents. Just need to update the rev-state of the dir (d['path']),
734 # don't need to recursively update all child contents.
735 # (??? is this the right reason?)
736 svnclient
.update(path_offset
, non_recursive
=True)
737 if options
.keep_prop
:
738 sync_svn_props(source_url
, source_rev
, path_offset
)
741 raise InternalError("Internal Error: process_svn_log_entry: Unhandled 'action' value: '%s'"
744 # Export the final version of all add'd paths from source_url
746 for path_offset
in export_paths
:
747 svnclient
.export(join_path(source_url
, path_offset
), source_rev
, path_offset
, force
=True)
749 def keep_revnum(source_rev
, target_rev_last
, wc_target_tmp
):
751 Add "padding" target revisions as needed to keep source and target
752 revision #'s identical.
755 if int(source_rev
) <= int(target_rev_last
):
756 raise InternalError("keep-revnum mode is enabled, "
757 "but source revision (r%s) is less-than-or-equal last target revision (r%s)" % \
758 (source_rev
, target_rev_last
))
759 if int(target_rev_last
) < int(source_rev
)-1:
760 # Add "padding" target revisions to keep source and target rev #'s identical
761 if os
.path
.exists(wc_target_tmp
):
762 shell
.rmtree(wc_target_tmp
)
763 run_svn(["checkout", "-r", "HEAD", "--depth=empty", svnclient
.safe_path(target_repos_url
, "HEAD"), svnclient
.safe_path(wc_target_tmp
)])
764 for rev_num
in range(int(target_rev_last
)+1, int(source_rev
)):
765 run_svn(["propset", "svn2svn:keep-revnum", rev_num
, svnclient
.safe_path(wc_target_tmp
)])
766 # Prevent Ctrl-C's during this inner part, so we'll always display
767 # the "Commit revision ..." message if we ran a "svn commit".
769 output
= run_svn(["commit", "-m", "", svnclient
.safe_path(wc_target_tmp
)])
770 rev_num_tmp
= parse_svn_commit_rev(output
) if output
else None
771 assert rev_num
== rev_num_tmp
772 ui
.status("Committed revision %s (keep-revnum).", rev_num
)
774 # Check if the user tried to press Ctrl-C
776 raise KeyboardInterrupt
777 target_rev_last
= rev_num
778 shell
.rmtree(wc_target_tmp
)
779 return target_rev_last
781 def disp_svn_log_summary(log_entry
):
782 ui
.status("------------------------------------------------------------------------", level
=ui
.VERBOSE
)
783 ui
.status("r%s | %s | %s",
784 log_entry
['revision'],
786 str(datetime
.fromtimestamp(int(log_entry
['date'])).isoformat(' ')), level
=ui
.VERBOSE
)
787 ui
.status(log_entry
['message'], level
=ui
.VERBOSE
)
790 global source_url
, target_url
, rev_map
791 # Use urllib.unquote() to URL-decode source_url/target_url values.
792 # All URLs passed to run_svn() should go through svnclient.safe_path()
793 # and we don't want to end-up *double* urllib.quote'ing if the user-
794 # supplied source/target URL's are already URL-encoded.
795 source_url
= urllib
.unquote(args
.pop(0).rstrip("/")) # e.g. 'http://server/svn/source/trunk'
796 target_url
= urllib
.unquote(args
.pop(0).rstrip("/")) # e.g. 'file:///svn/target/trunk'
797 ui
.status("options: %s", str(options
), level
=ui
.DEBUG
, color
='GREEN')
799 # Make sure that both the source and target URL's are valid
800 source_info
= svnclient
.info(source_url
)
801 assert is_child_path(source_url
, source_info
['repos_url'])
802 target_info
= svnclient
.info(target_url
)
803 assert is_child_path(target_url
, target_info
['repos_url'])
806 global source_repos_url
,source_base
,source_repos_uuid
807 source_repos_url
= source_info
['repos_url'] # e.g. 'http://server/svn/source'
808 source_base
= source_url
[len(source_repos_url
):] # e.g. '/trunk'
809 source_repos_uuid
= source_info
['repos_uuid']
810 global target_repos_url
,target_base
811 target_repos_url
= target_info
['repos_url'] # e.g. 'http://server/svn/target'
812 target_base
= target_url
[len(target_repos_url
):] # e.g. '/trunk'
814 # Init start and end revision
816 source_start_rev
= svnclient
.get_rev(source_repos_url
, options
.rev_start
if options
.rev_start
else 1)
817 except ExternalCommandFailed
:
818 print "Error: Invalid start source revision value: %s" % (options
.rev_start
)
821 source_end_rev
= svnclient
.get_rev(source_repos_url
, options
.rev_end
if options
.rev_end
else "HEAD")
822 except ExternalCommandFailed
:
823 print "Error: Invalid end source revision value: %s" % (options
.rev_end
)
825 ui
.status("Using source revision range %s:%s", source_start_rev
, source_end_rev
, level
=ui
.VERBOSE
)
827 # TODO: If options.keep_date, should we try doing a "svn propset" on an *existing* revision
828 # as a sanity check, so we check if the pre-revprop-change hook script is correctly setup
829 # before doing first replay-commit?
831 target_rev_last
= target_info
['revision'] # Last revision # in the target repo
832 wc_target
= options
.wc_path
if options
.wc_path
else os
.path
.abspath('_wc_target')
833 wc_target_tmp
= wc_target
+ '_tmp'
839 # Check out a working copy of target_url if needed
840 wc_exists
= os
.path
.exists(wc_target
)
841 if wc_exists
and not options
.cont_from_break
:
842 shell
.rmtree(wc_target
)
845 ui
.status("Checking-out wc_target: %s ...", wc_target
, level
=ui
.VERBOSE
)
846 svnclient
.svn_checkout(target_url
, wc_target
)
849 # If using an existing WC, make sure it's clean ("svn revert")
850 ui
.status("Cleaning-up wc_target: %s ...", wc_target
, level
=ui
.VERBOSE
)
854 if not options
.cont_from_break
:
855 # Warn user if trying to start (non-continue) into a non-empty target path
856 if not options
.force_nocont
:
857 top_paths
= svnclient
.list(target_url
, "HEAD")
859 print "Error: Trying to replay (non-continue-mode) into a non-empty target_url location. " \
860 "Use --force if you're sure this is what you want."
862 # Get the first log entry at/after source_start_rev, which is where
863 # we'll do the initial import from.
864 source_ancestors
= find_svn_ancestors(source_repos_url
, source_base
, source_end_rev
, prefix
=" ")
865 it_log_start
= svnclient
.iter_svn_log_entries(source_url
, source_start_rev
, source_end_rev
, get_changed_paths
=False, ancestors
=source_ancestors
)
866 source_start_log
= None
867 for log_entry
in it_log_start
:
868 # Pick the first entry. Need to use a "for ..." loop since we're using an iterator.
869 source_start_log
= log_entry
871 if not source_start_log
:
872 raise InternalError("Unable to find any matching revisions between %s:%s in source_url: %s" % \
873 (source_start_rev
, source_end_rev
, source_url
))
875 # This is the revision we will start from for source_url
876 source_start_rev
= int(source_start_log
['revision'])
877 ui
.status("Starting at source revision %s.", source_start_rev
, level
=ui
.VERBOSE
)
878 ui
.status("", level
=ui
.VERBOSE
)
879 if options
.keep_revnum
and source_rev
> target_rev_last
:
880 target_rev_last
= keep_revnum(source_rev
, target_rev_last
, wc_target_tmp
)
882 # For the initial commit to the target URL, export all the contents from
883 # the source URL at the start-revision.
884 disp_svn_log_summary(svnclient
.get_one_svn_log_entry(source_repos_url
, source_start_rev
, source_start_rev
))
885 # Export and add file-contents from source_url@source_start_rev
886 source_start_url
= source_url
if not source_ancestors
else source_repos_url
+source_ancestors
[len(source_ancestors
)-1]['copyfrom_path']
887 top_paths
= svnclient
.list(source_start_url
, source_start_rev
)
889 # For each top-level file/folder...
890 path_is_dir
= True if p
['kind'] == "dir" else False
891 path_offset
= p
['path']
892 if in_svn(path_offset
, prefix
=" "):
893 raise InternalError("Cannot replay history on top of pre-existing structure: %s" % join_path(source_start_url
, path_offset
))
894 if path_is_dir
and not os
.path
.exists(path_offset
):
895 os
.makedirs(path_offset
)
896 svnclient
.export(join_path(source_start_url
, path_offset
), source_start_rev
, path_offset
, force
=True)
897 run_svn(["add", svnclient
.safe_path(path_offset
)])
898 # Update any properties on the newly added content
899 paths
= svnclient
.list(source_start_url
, source_start_rev
, recursive
=True)
900 if options
.keep_prop
:
901 sync_svn_props(source_start_url
, source_start_rev
, "")
903 path_offset
= p
['path']
904 ui
.status(" A %s", join_path(source_base
, path_offset
), level
=ui
.VERBOSE
)
905 if options
.keep_prop
:
906 sync_svn_props(source_start_url
, source_start_rev
, path_offset
)
907 # Commit the initial import
908 num_entries_proc
+= 1
909 target_revprops
= gen_tracking_revprops(source_start_rev
) # Build source-tracking revprop's
910 target_rev
= commit_from_svn_log_entry(source_start_log
, target_revprops
=target_revprops
)
912 # Update rev_map, mapping table of source-repo rev # -> target-repo rev #
913 set_rev_map(source_start_rev
, target_rev
)
915 target_rev_last
= target_rev
917 verify_commit(source_rev
, target_rev_last
)
919 # Re-build the rev_map based on any already-replayed history in target_url
920 build_rev_map(target_url
, target_rev_last
, source_info
)
922 print "Error: Called with continue-mode, but no already-replayed source history found in target_url."
924 source_start_rev
= int(max(rev_map
, key
=rev_map
.get
))
925 assert source_start_rev
926 ui
.status("Continuing from source revision %s.", source_start_rev
, level
=ui
.VERBOSE
)
927 ui
.status("", level
=ui
.VERBOSE
)
929 svn_vers_t
= svnclient
.version()
930 svn_vers
= float(".".join(map(str, svn_vers_t
[0:2])))
932 # Load SVN log starting from source_start_rev + 1
933 source_ancestors
= find_svn_ancestors(source_repos_url
, source_base
, source_end_rev
, prefix
=" ")
934 it_log_entries
= svnclient
.iter_svn_log_entries(source_url
, source_start_rev
+1, source_end_rev
, get_revprops
=True, ancestors
=source_ancestors
) if source_start_rev
< source_end_rev
else []
935 source_rev_last
= source_start_rev
939 for log_entry
in it_log_entries
:
940 if options
.entries_proc_limit
:
941 if num_entries_proc
>= options
.entries_proc_limit
:
943 # Replay this revision from source_url into target_url
944 source_rev
= log_entry
['revision']
945 log_url
= log_entry
['url']
946 #print "source_url:%s log_url:%s" % (source_url, log_url)
947 if options
.keep_revnum
:
948 if source_rev
< target_rev_last
:
949 print "Error: Last target revision (r%s) is equal-or-higher than starting source revision (r%s). " \
950 "Cannot use --keep-revnum mode." % (target_rev_last
, source_start_rev
)
952 target_rev_last
= keep_revnum(source_rev
, target_rev_last
, wc_target_tmp
)
953 disp_svn_log_summary(log_entry
)
954 # Process all the changed-paths in this log entry
956 process_svn_log_entry(log_entry
, source_ancestors
, commit_paths
)
957 num_entries_proc
+= 1
958 # Commit any changes made to _wc_target
959 target_revprops
= gen_tracking_revprops(source_rev
) # Build source-tracking revprop's
960 target_rev
= commit_from_svn_log_entry(log_entry
, commit_paths
, target_revprops
=target_revprops
)
961 source_rev_last
= source_rev
963 # Update rev_map, mapping table of source-repo rev # -> target-repo rev #
964 source_rev
= log_entry
['revision']
965 set_rev_map(source_rev
, target_rev
)
966 target_rev_last
= target_rev
969 verify_commit(source_rev
, target_rev_last
, log_entry
)
970 # Run "svn cleanup" every 100 commits if SVN 1.7+, to clean-up orphaned ".svn/pristines/*"
971 if svn_vers
>= 1.7 and (commit_count
% 100 == 0):
973 if source_rev_last
== source_start_rev
:
974 # If there were no new source_url revisions to process, still trigger
975 # "full-mode" verify check (if enabled).
977 verify_commit(source_rev_last
, target_rev_last
)
979 except KeyboardInterrupt:
981 print "\nStopped by user."
982 print "\nCleaning-up..."
987 print "\nCommand failed with following error:\n"
988 traceback
.print_exc()
989 print "\nCleaning-up..."
991 print run_svn(["status"])
994 print "\nFinished at source revision %s%s." % (source_rev_last
, " (dry-run)" if options
.dry_run
else "")
999 # Defined as entry point. Must be callable without arguments.
1000 usage
= "svn2svn, version %s\n" % str(full_version
) + \
1001 "<http://nynim.org/code/svn2svn> <https://github.com/tonyduckles/svn2svn>\n\n" + \
1002 "Usage: %prog [OPTIONS] source_url target_url\n"
1004 Replicate (replay) history from one SVN repository to another. Maintain
1005 logical ancestry wherever possible, so that 'svn log' on the replayed repo
1006 will correctly follow file/folder renames.
1009 Create a copy of only /trunk from source repo, starting at r5000
1010 $ svnadmin create /svn/target
1011 $ svn mkdir -m 'Add trunk' file:///svn/target/trunk
1012 $ svnreplay -av -r 5000 http://server/source/trunk file:///svn/target/trunk
1013 1. The target_url will be checked-out to ./_wc_target
1014 2. The first commit to http://server/source/trunk at/after r5000 will be
1015 exported & added into _wc_target
1016 3. All revisions affecting http://server/source/trunk (starting at r5000)
1017 will be replayed to _wc_target. Any add/copy/move/replaces that are
1018 copy-from'd some path outside of /trunk (e.g. files renamed on a
1019 /branch and branch was merged into /trunk) will correctly maintain
1020 logical ancestry where possible.
1022 Use continue-mode (-c) to pick-up where the last run left-off
1023 $ svnreplay -avc http://server/source/trunk file:///svn/target/trunk
1024 1. The target_url will be checked-out to ./_wc_target, if not already
1026 2. All new revisions affecting http://server/source/trunk starting from
1027 the last replayed revision to file:///svn/target/trunk (based on the
1028 svn2svn:* revprops) will be replayed to _wc_target, maintaining all
1029 logical ancestry where possible."""
1030 parser
= optparse
.OptionParser(usage
, description
=description
,
1031 formatter
=HelpFormatter(), version
="%prog "+str(full_version
))
1032 parser
.add_option("-v", "--verbose", dest
="verbosity", action
="count", default
=1,
1033 help="Enable additional output (use -vv or -vvv for more).")
1034 parser
.add_option("-a", "--archive", action
="store_true", dest
="archive", default
=False,
1035 help="Archive/mirror mode; same as -UDP (see REQUIRES below).\n"
1036 "Maintain same commit author, same commit time, and file/dir properties.")
1037 parser
.add_option("-U", "--keep-author", action
="store_true", dest
="keep_author", default
=False,
1038 help="Maintain same commit authors (svn:author) as source.\n"
1039 "(REQUIRES 'pre-revprop-change' hook script to allow 'svn:author' changes.)")
1040 parser
.add_option("-D", "--keep-date", action
="store_true", dest
="keep_date", default
=False,
1041 help="Maintain same commit time (svn:date) as source.\n"
1042 "(REQUIRES 'pre-revprop-change' hook script to allow 'svn:date' changes.)")
1043 parser
.add_option("-P", "--keep-prop", action
="store_true", dest
="keep_prop", default
=False,
1044 help="Maintain same file/dir SVN properties as source.")
1045 parser
.add_option("-R", "--keep-revnum", action
="store_true", dest
="keep_revnum", default
=False,
1046 help="Maintain same rev #'s as source. Creates placeholder target "
1047 "revisions (by modifying a 'svn2svn:keep-revnum' property at the root of the target repo).")
1048 parser
.add_option("-c", "--continue", action
="store_true", dest
="cont_from_break",
1049 help="Continue from last source commit to target (based on svn2svn:* revprops).")
1050 parser
.add_option("-f", "--force", action
="store_true", dest
="force_nocont",
1051 help="Allow replaying into a non-empty target-repo folder.")
1052 parser
.add_option("-r", "--revision", type="string", dest
="revision", metavar
="ARG",
1053 help="Revision range to replay from source_url.\n"
1054 "A revision argument can be one of:\n"
1055 " START Start rev # (end will be 'HEAD')\n"
1056 " START:END Start and ending rev #'s\n"
1057 "Any revision # formats which SVN understands are "
1058 "supported, e.g. 'HEAD', '{2010-01-31}', etc.")
1059 parser
.add_option("-u", "--log-author", action
="store_true", dest
="log_author", default
=False,
1060 help="Append source commit author to replayed commit mesages.")
1061 parser
.add_option("-d", "--log-date", action
="store_true", dest
="log_date", default
=False,
1062 help="Append source commit time to replayed commit messages.")
1063 parser
.add_option("-l", "--limit", type="int", dest
="entries_proc_limit", metavar
="NUM",
1064 help="Maximum number of source revisions to process.")
1065 parser
.add_option("-n", "--dry-run", action
="store_true", dest
="dry_run", default
=False,
1066 help="Process next source revision but don't commit changes to "
1067 "target working-copy (forces --limit=1).")
1068 parser
.add_option("-x", "--verify", action
="store_const", const
=1, dest
="verify",
1069 help="Verify ancestry and content for changed paths in commit after every target commit or last target commit.")
1070 parser
.add_option("-X", "--verify-all", action
="store_const", const
=2, dest
="verify",
1071 help="Verify ancestry and content for entire target_url tree after every target commit or last target commit.")
1072 parser
.add_option("--pre-commit", type="string", dest
="beforecommit", metavar
="CMD",
1073 help="Run the given shell script before each replayed commit, e.g. "
1074 "to modify file-content during replay.\n"
1075 "Called as: CMD [wc_path] [source_rev]")
1076 parser
.add_option("--wc", type="string", dest
="wc_path", metavar
="WCPATH",
1077 help='Path to target WC to create and use. Defaults to "./_wc_target".')
1078 parser
.add_option("--debug", dest
="verbosity", const
=ui
.DEBUG
, action
="store_const",
1079 help="Enable debugging output (same as -vvv).")
1081 options
, args
= parser
.parse_args()
1083 parser
.error("incorrect number of arguments")
1084 if options
.verbosity
< 10:
1085 # Expand multiple "-v" arguments to a real ui._level value
1086 options
.verbosity
*= 10
1088 # When in dry-run mode, only try to process the next log_entry
1089 options
.entries_proc_limit
= 1
1090 options
.rev_start
= None
1091 options
.rev_end
= None
1092 if options
.revision
:
1093 # Reg-ex for matching a revision arg (http://svnbook.red-bean.com/en/1.5/svn.tour.revs.specifiers.html#svn.tour.revs.dates)
1094 rev_patt
= '[0-9A-Z]+|\{[0-9A-Za-z/\\ :-]+\}'
1096 match
= re
.match('^('+rev_patt
+'):('+rev_patt
+')$', options
.revision
) # First try start:end match
1097 if match
is None: match
= re
.match('^('+rev_patt
+')$', options
.revision
) # Next, try start match
1099 parser
.error("unexpected --revision argument format; see 'svn help log' for valid revision formats")
1100 rev
= match
.groups()
1101 options
.rev_start
= rev
[0] if len(rev
)>0 else None
1102 options
.rev_end
= rev
[1] if len(rev
)>1 else None
1104 options
.keep_author
= True
1105 options
.keep_date
= True
1106 options
.keep_prop
= True
1107 ui
.update_config(options
)
1108 return real_main(args
)
1111 if __name__
== "__main__":
1112 sys
.exit(main() or 0)