1 # Copyright (C) 2006-2008 Jelmer Vernooij <jelmer@samba.org>
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation; either version 3 of the License, or
6 # (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software
15 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
16 """Committing and pushing to Subversion repositories."""
18 from core import SubversionException, time_to_cstring
21 from bzrlib import debug, osutils, urlutils
22 from bzrlib.branch import Branch
23 from bzrlib.errors import (BzrError, InvalidRevisionId, DivergedBranches,
24 UnrelatedBranches, AppendRevisionsOnlyViolation,
26 from bzrlib.inventory import Inventory
27 from bzrlib.repository import RootCommitBuilder, InterRepository
28 from bzrlib.revision import NULL_REVISION
29 from bzrlib.trace import mutter, warning
31 from bzrlib.plugins.svn import util
33 from cStringIO import StringIO
35 from bzrlib.plugins.svn import constants
36 from bzrlib.plugins.svn.errors import ChangesRootLHSHistory, MissingPrefix, RevpropChangeFailed
37 from bzrlib.plugins.svn.svk import (generate_svk_feature, serialize_svk_features,
38 parse_svk_features, SVN_PROP_SVK_MERGE)
39 from bzrlib.plugins.svn.logwalker import lazy_dict
40 from bzrlib.plugins.svn.mapping import parse_revision_id
41 from bzrlib.plugins.svn.ra import txdelta_send_stream
42 from bzrlib.plugins.svn.repository import SvnRepositoryFormat, SvnRepository
47 def _revision_id_to_svk_feature(revid):
48 """Create a SVK feature identifier from a revision id.
50 :param revid: Revision id to convert.
51 :return: Matching SVK feature identifier.
53 assert isinstance(revid, str)
54 (uuid, branch, revnum, _) = parse_revision_id(revid)
55 # TODO: What about renamed revisions? Should use
56 # repository.lookup_revision_id here.
57 return generate_svk_feature(uuid, branch, revnum)
60 def _check_dirs_exist(transport, bp_parts, base_rev):
61 """Make sure that the specified directories exist.
63 :param transport: SvnRaTransport to use.
64 :param bp_parts: List of directory names in the format returned by
66 :param base_rev: Base revision to check.
67 :return: List of the directories that exists in base_rev.
69 for i in range(len(bp_parts), 0, -1):
70 current = bp_parts[:i]
71 path = "/".join(current).strip("/")
72 if transport.check_path(path, base_rev) == core.NODE_DIR:
77 def set_svn_revprops(transport, revnum, revprops):
78 """Attempt to change the revision properties on the
81 :param transport: SvnRaTransport connected to target repository
82 :param revnum: Revision number of revision to change metadata of.
83 :param revprops: Dictionary with revision properties to set.
85 for (name, value) in revprops.items():
87 transport.change_rev_prop(revnum, name, value)
88 except SubversionException, (_, constants.ERR_REPOS_DISABLED_FEATURE):
89 raise RevpropChangeFailed(name)
92 class SvnCommitBuilder(RootCommitBuilder):
93 """Commit Builder implementation wrapped around svn_delta_editor. """
95 def __init__(self, repository, branch, parents, config, timestamp,
96 timezone, committer, revprops, revision_id, old_inv=None):
97 """Instantiate a new SvnCommitBuilder.
99 :param repository: SvnRepository to commit to.
100 :param branch: SvnBranch to commit to.
101 :param parents: List of parent revision ids.
102 :param config: Branch configuration to use.
103 :param timestamp: Optional timestamp recorded for commit.
104 :param timezone: Optional timezone for timestamp.
105 :param committer: Optional committer to set for commit.
106 :param revprops: Revision properties to set.
107 :param revision_id: Revision id for the new revision.
108 :param old_inv: Optional revision on top of which
109 the commit is happening
111 super(SvnCommitBuilder, self).__init__(repository, parents,
112 config, timestamp, timezone, committer, revprops, revision_id)
115 # Gather information about revision on top of which the commit is
118 self.base_revid = None
120 self.base_revid = parents[0]
122 self.base_revno = self.branch.revision_id_to_revno(self.base_revid)
123 if self.base_revid is None:
124 self.base_revnum = -1
125 self.base_path = None
126 self.base_mapping = repository.get_mapping()
128 (self.base_path, self.base_revnum, self.base_mapping) = \
129 repository.lookup_revision_id(self.base_revid)
132 if self.base_revid is None:
133 self.old_inv = Inventory(root_id=None)
135 self.old_inv = self.repository.get_inventory(self.base_revid)
137 self.old_inv = old_inv
138 # Not all repositories appear to set Inventory.revision_id,
139 # so allow None as well.
140 assert self.old_inv.revision_id in (None, self.base_revid)
142 # Determine revisions merged in this one
143 merges = filter(lambda x: x != self.base_revid, parents)
145 self.modified_files = {}
146 self.modified_dirs = set()
147 if self.base_revid is None:
148 base_branch_props = {}
150 base_branch_props = lazy_dict({}, self.repository.branchprop_list.get_properties, self.base_path, self.base_revnum)
151 (self._svn_revprops, self._svnprops) = self.base_mapping.export_revision(self.branch.get_branch_path(), timestamp, timezone, committer, revprops, revision_id, self.base_revno+1, merges, base_branch_props)
154 old_svk_features = parse_svk_features(base_branch_props.get(SVN_PROP_SVK_MERGE, ""))
155 svk_features = set(old_svk_features)
160 svk_features.add(_revision_id_to_svk_feature(merge))
161 except InvalidRevisionId:
164 if old_svk_features != svk_features:
165 self._svnprops[SVN_PROP_SVK_MERGE] = serialize_svk_features(svk_features)
167 def mutter(self, text):
168 if 'commit' in debug.debug_flags:
171 def _generate_revision_if_needed(self):
172 """See CommitBuilder._generate_revision_if_needed()."""
174 def finish_inventory(self):
175 """See CommitBuilder.finish_inventory()."""
177 def modified_file_text(self, file_id, file_parents,
178 get_content_byte_lines, text_sha1=None,
180 """See CommitBuilder.modified_file_text()."""
181 new_lines = get_content_byte_lines()
182 self.modified_files[file_id] = "".join(new_lines)
183 return osutils.sha_strings(new_lines), sum(map(len, new_lines))
185 def modified_link(self, file_id, file_parents, link_target):
186 """See CommitBuilder.modified_link()."""
187 self.modified_files[file_id] = "link %s" % link_target
189 def modified_directory(self, file_id, file_parents):
190 """See CommitBuilder.modified_directory()."""
191 self.modified_dirs.add(file_id)
193 def _file_process(self, file_id, contents, file_editor):
194 """Pass the changes to a file to the Subversion commit editor.
196 :param file_id: Id of the file to modify.
197 :param contents: Contents of the file.
198 :param file_editor: Editor for this file
200 assert file_editor is not None
201 txdelta = file_editor.apply_textdelta(None)
202 digest = txdelta_send_stream(StringIO(contents), txdelta)
203 if 'validate' in debug.debug_flags:
204 from fetch import md5_strings
205 assert digest == md5_strings(contents)
207 def _dir_process(self, path, file_id, dir_editor):
208 """Pass the changes to a directory to the commit editor.
210 :param path: Path (from repository root) to the directory.
211 :param file_id: File id of the directory
212 :param dir_editor: Editor for the directory.
214 assert dir_editor is not None
215 # Loop over entries of file_id in self.old_inv
216 # remove if they no longer exist with the same name
218 if file_id in self.old_inv:
219 for child_name in self.old_inv[file_id].children:
220 child_ie = self.old_inv.get_child(file_id, child_name)
223 # ... path no longer exists
224 not child_ie.file_id in self.new_inventory or
226 child_ie.parent_id != self.new_inventory[child_ie.file_id].parent_id or
228 self.new_inventory[child_ie.file_id].name != child_name):
229 self.mutter('removing %r(%r)' % (child_name, child_ie.file_id))
230 dir_editor.delete_entry(
231 urlutils.join(self.branch.get_branch_path(), path, child_name),
234 # Loop over file children of file_id in self.new_inventory
235 for child_name in self.new_inventory[file_id].children:
236 child_ie = self.new_inventory.get_child(file_id, child_name)
237 assert child_ie is not None
239 if not (child_ie.kind in ('file', 'symlink')):
242 new_child_path = self.new_inventory.id2path(child_ie.file_id).encode("utf-8")
243 full_new_child_path = urlutils.join(self.branch.get_branch_path(),
245 # add them if they didn't exist in old_inv
246 if not child_ie.file_id in self.old_inv:
247 self.mutter('adding %s %r' % (child_ie.kind, new_child_path))
248 child_editor = dir_editor.add_file(full_new_child_path)
251 # copy if they existed at different location
252 elif (self.old_inv.id2path(child_ie.file_id) != new_child_path or
253 self.old_inv[child_ie.file_id].parent_id != child_ie.parent_id):
254 self.mutter('copy %s %r -> %r' % (child_ie.kind,
255 self.old_inv.id2path(child_ie.file_id),
257 child_editor = dir_editor.add_file(
259 urlutils.join(self.repository.transport.svn_url, self.base_path, self.old_inv.id2path(child_ie.file_id)),
262 # open if they existed at the same location
263 elif child_ie.revision is None:
264 self.mutter('open %s %r' % (child_ie.kind, new_child_path))
266 child_editor = dir_editor.open_file(full_new_child_path, self.base_revnum)
269 # Old copy of the file was retained. No need to send changes
270 assert child_ie.file_id not in self.modified_files
273 if child_ie.file_id in self.old_inv:
274 old_executable = self.old_inv[child_ie.file_id].executable
275 old_special = (self.old_inv[child_ie.file_id].kind == 'symlink')
278 old_executable = False
280 if child_editor is not None:
281 if old_executable != child_ie.executable:
282 if child_ie.executable:
283 value = core.SVN_PROP_EXECUTABLE_VALUE
286 child_editor.change_prop(core.SVN_PROP_EXECUTABLE, value)
288 if old_special != (child_ie.kind == 'symlink'):
289 if child_ie.kind == 'symlink':
290 value = core.SVN_PROP_SPECIAL_VALUE
294 child_editor.change_prop(core.SVN_PROP_SPECIAL, value)
297 if child_ie.file_id in self.modified_files:
298 self._file_process(child_ie.file_id,
299 self.modified_files[child_ie.file_id], child_editor)
301 if child_editor is not None:
304 # Loop over subdirectories of file_id in self.new_inventory
305 for child_name in self.new_inventory[file_id].children:
306 child_ie = self.new_inventory.get_child(file_id, child_name)
307 if child_ie.kind != 'directory':
310 new_child_path = self.new_inventory.id2path(child_ie.file_id)
311 # add them if they didn't exist in old_inv
312 if not child_ie.file_id in self.old_inv:
313 self.mutter('adding dir %r' % child_ie.name)
314 child_editor = dir_editor.add_directory(
315 urlutils.join(self.branch.get_branch_path(),
318 # copy if they existed at different location
319 elif self.old_inv.id2path(child_ie.file_id) != new_child_path:
320 old_child_path = self.old_inv.id2path(child_ie.file_id)
321 self.mutter('copy dir %r -> %r' % (old_child_path, new_child_path))
322 child_editor = dir_editor.add_directory(
323 urlutils.join(self.branch.get_branch_path(), new_child_path),
324 urlutils.join(self.repository.transport.svn_url, self.base_path, old_child_path), self.base_revnum)
326 # open if they existed at the same location and
327 # the directory was touched
328 elif self.new_inventory[child_ie.file_id].revision is None:
329 self.mutter('open dir %r' % new_child_path)
331 child_editor = dir_editor.open_directory(
332 urlutils.join(self.branch.get_branch_path(), new_child_path),
335 assert child_ie.file_id not in self.modified_dirs
338 # Handle this directory
339 if child_ie.file_id in self.modified_dirs:
340 self._dir_process(new_child_path, child_ie.file_id, child_editor)
344 def open_branch_editors(self, root, elements, existing_elements,
345 base_path, base_rev, replace_existing):
346 """Open a specified directory given an editor for the repository root.
348 :param root: Editor for the repository root
349 :param elements: List of directory names to open
350 :param existing_elements: List of directory names that exist
351 :param base_path: Path to base top-level branch on
352 :param base_rev: Revision of path to base top-level branch on
353 :param replace_existing: Whether the current branch should be replaced
357 self.mutter('opening branch %r (base %r:%r)' % (elements, base_path,
360 # Open paths leading up to branch
361 for i in range(0, len(elements)-1):
362 # Does directory already exist?
363 ret.append(ret[-1].open_directory("/".join(existing_elements[0:i+1]), -1))
365 if (len(existing_elements) != len(elements) and
366 len(existing_elements)+1 != len(elements)):
367 raise MissingPrefix("/".join(elements))
369 # Branch already exists and stayed at the same location, open:
370 # TODO: What if the branch didn't change but the new revision
371 # was based on an older revision of the branch?
372 # This needs to also check that base_rev was the latest version of
374 if (len(existing_elements) == len(elements) and
375 not replace_existing):
376 ret.append(ret[-1].open_directory(
377 "/".join(elements), base_rev))
378 else: # Branch has to be created
379 # Already exists, old copy needs to be removed
380 name = "/".join(elements)
383 raise ChangesRootLHSHistory()
384 self.mutter("removing branch dir %r" % name)
385 ret[-1].delete_entry(name, -1)
386 if base_path is not None:
387 base_url = urlutils.join(self.repository.transport.svn_url, base_path)
390 self.mutter("adding branch dir %r" % name)
391 ret.append(ret[-1].add_directory(
392 name, base_url, base_rev))
396 def commit(self, message):
397 """Finish the commit.
400 def done(revision, author, date):
401 """Callback that is called by the Subversion commit editor
402 once the commit finishes.
405 self.revision_metadata = object()
406 self.revision_metadata.revision = revision
407 self.revision_metadata.author = author
408 self.revision_metadata.date = date
410 bp_parts = self.branch.get_branch_path().split("/")
411 repository_latest_revnum = self.repository.get_latest_revnum()
412 lock = self.repository.transport.lock_write(".")
413 set_revprops = self._config.get_set_revprops()
414 remaining_revprops = self._svn_revprops # Keep track of the revprops that haven't been set yet
417 def _dir_process_file_id(old_inv, new_inv, path, file_id):
419 for child_name in new_inv[file_id].children:
420 child_ie = new_inv.get_child(file_id, child_name)
421 new_child_path = new_inv.id2path(child_ie.file_id)
422 assert child_ie is not None
424 if (not child_ie.file_id in old_inv or
425 old_inv.id2path(child_ie.file_id) != new_child_path or
426 old_inv[child_ie.file_id].parent_id != child_ie.parent_id):
427 ret.append((child_ie.file_id, new_child_path))
429 if (child_ie.kind == 'directory' and
430 child_ie.file_id in self.modified_dirs):
431 ret += _dir_process_file_id(old_inv, new_inv, new_child_path, child_ie.file_id)
436 if (self.old_inv.root is None or
437 self.new_inventory.root.file_id != self.old_inv.root.file_id):
438 fileids[""] = self.new_inventory.root.file_id
440 for id, path in _dir_process_file_id(self.old_inv, self.new_inventory, "", self.new_inventory.root.file_id):
443 self.base_mapping.export_fileid_map(fileids, self._svn_revprops, self._svnprops)
444 if self._config.get_log_strip_trailing_newline():
445 self.base_mapping.export_message(message, self._svn_revprops, self._svnprops)
446 message = message.rstrip("\n")
447 self._svn_revprops[constants.PROP_REVISION_LOG] = message.encode("utf-8")
450 existing_bp_parts = _check_dirs_exist(self.repository.transport,
452 for prop in self._svn_revprops:
453 if not util.is_valid_property_name(prop):
454 warning("Setting property %r with invalid characters in name" % prop)
455 conn = self.repository.transport.get_connection()
458 self.editor = conn.get_commit_editor(
459 self._svn_revprops, done, None, False)
460 self._svn_revprops = {}
461 except NotImplementedError:
464 # Try without bzr: revprops
465 self.editor = conn.get_commit_editor({
466 constants.PROP_REVISION_LOG: self._svn_revprops[constants.PROP_REVISION_LOG]},
468 del self._svn_revprops[constants.PROP_REVISION_LOG]
470 root = self.editor.open_root(self.base_revnum)
472 replace_existing = False
473 # See whether the base of the commit matches the lhs parent
474 # if not, we need to replace the existing directory
475 if len(bp_parts) == len(existing_bp_parts):
476 if self.base_path.strip("/") != "/".join(bp_parts).strip("/"):
477 replace_existing = True
478 elif self.base_revnum < self.repository._log.find_latest_change(self.branch.get_branch_path(), repository_latest_revnum):
479 replace_existing = True
481 if replace_existing and self.branch._get_append_revisions_only():
482 raise AppendRevisionsOnlyViolation(self.branch.base)
484 # TODO: Accept create_prefix argument (#118787)
485 branch_editors = self.open_branch_editors(root, bp_parts,
486 existing_bp_parts, self.base_path, self.base_revnum,
489 self._dir_process("", self.new_inventory.root.file_id,
492 # Set all the revprops
493 for prop, value in self._svnprops.items():
494 if not util.is_valid_property_name(prop):
495 warning("Setting property %r with invalid characters in name" % prop)
496 if value is not None:
497 value = value.encode('utf-8')
498 branch_editors[-1].change_prop(prop, value)
499 self.mutter("Setting root file property %r -> %r" % (prop, value))
501 for dir_editor in reversed(branch_editors):
506 self.repository.transport.add_connection(conn)
510 assert self.revision_metadata is not None
512 self.repository._clear_cached_state()
514 revid = self.branch.generate_revision_id(self.revision_metadata.revision)
516 assert self._new_revision_id is None or self._new_revision_id == revid
518 self.mutter('commit %d finished. author: %r, date: %r, revid: %r' %
519 (self.revision_metadata.revision, self.revision_metadata.author,
520 self.revision_metadata.date, revid))
522 override_svn_revprops = self._config.get_override_svn_revprops()
523 if override_svn_revprops is not None:
525 if constants.PROP_REVISION_AUTHOR in override_svn_revprops:
526 new_revprops[constants.PROP_REVISION_AUTHOR] = self._committer.encode("utf-8")
527 if constants.PROP_REVISION_DATE in override_svn_revprops:
528 new_revprops[constants.PROP_REVISION_DATE] = svn_time_to_cstring(1000000*self._timestamp)
529 set_svn_revprops(self.repository.transport, self.revision_metadata.revision, new_revprops)
532 set_svn_revprops(self.repository.transport, self.revision_metadata.revision,
534 except RevpropChangeFailed:
535 pass # Ignore for now
539 def record_entry_contents(self, ie, parent_invs, path, tree,
541 """Record the content of ie from tree into the commit if needed.
543 Side effect: sets ie.revision when unchanged
545 :param ie: An inventory entry present in the commit.
546 :param parent_invs: The inventories of the parent revisions of the
548 :param path: The path the entry is at in the tree.
549 :param tree: The tree which contains this entry and should be used to
551 :param content_summary: Summary data from the tree about the paths
552 content - stat, length, exec, sha/link target. This is only
553 accessed when the entry has a revision of None - that is when
554 it is a candidate to commit.
556 self.new_inventory.add(ie)
557 return self._get_delta(ie, parent_invs[0], path), True
560 def replay_delta(builder, old_tree, new_tree):
561 """Replays a delta to a commit builder.
563 :param builder: The commit builder.
564 :param old_tree: Original tree on top of which the delta should be applied
565 :param new_tree: New tree that should be committed
567 for path, ie in new_tree.inventory.iter_entries():
568 builder.record_entry_contents(ie.copy(), [old_tree.inventory],
569 path, new_tree, None)
570 builder.finish_inventory()
571 delta = new_tree.changes_from(old_tree)
573 ie = builder.new_inventory[id]
576 while builder.new_inventory[id].parent_id is not None:
577 if builder.new_inventory[id].revision is None:
579 builder.new_inventory[id].revision = None
580 if builder.new_inventory[id].kind == 'directory':
581 builder.modified_directory(id, [])
582 id = builder.new_inventory[id].parent_id
584 assert ie.kind in ('symlink', 'file', 'directory')
585 if ie.kind == 'symlink':
586 builder.modified_link(ie.file_id, [], ie.symlink_target)
587 elif ie.kind == 'file':
589 return new_tree.get_file_text(ie.file_id)
590 builder.modified_file_text(ie.file_id, [], get_text)
592 for (_, id, _) in delta.added:
595 for (_, id, _, _, _) in delta.modified:
598 for (oldpath, _, id, _, _, _) in delta.renamed:
600 old_parent_id = old_tree.inventory.path2id(urlutils.dirname(oldpath))
601 if old_parent_id in builder.new_inventory:
602 touch_id(old_parent_id)
604 for (path, _, _) in delta.removed:
605 old_parent_id = old_tree.inventory.path2id(urlutils.dirname(path))
606 if old_parent_id in builder.new_inventory:
607 touch_id(old_parent_id)
610 def push_new(target_repository, target_branch_path, source,
612 """Push a revision into Subversion, creating a new branch.
614 This will do a new commit in the target branch.
616 :param target_branch_path: Path to create new branch at
617 :param source: Branch to pull the revision from
618 :param revision_id: Revision id of the revision to push
620 assert isinstance(source, Branch)
621 if stop_revision is None:
622 stop_revision = source.last_revision()
623 history = source.revision_history()
624 revhistory = list(history)
625 start_revid = NULL_REVISION
626 while len(revhistory) > 0:
627 revid = revhistory.pop()
628 # We've found the revision to push if there is a revision
629 # which LHS parent is present or if this is the first revision.
630 if (len(revhistory) == 0 or
631 target_repository.has_revision(revhistory[-1])):
635 # Get commit builder but specify that target_branch_path should
636 # be created and copied from (copy_path, copy_revnum)
637 class ImaginaryBranch(object):
638 """Simple branch that pretends to be empty but already exist."""
639 def __init__(self, repository):
640 self.repository = repository
641 self._revision_history = None
643 def get_config(self):
644 """See Branch.get_config()."""
645 return self.repository.get_config()
647 def revision_id_to_revno(self, revid):
650 return history.index(revid)
652 def last_revision_info(self):
653 """See Branch.last_revision_info()."""
654 last_revid = self.last_revision()
655 if last_revid is None:
656 return (0, NULL_REVISION)
657 return (history.index(last_revid), last_revid)
659 def last_revision(self):
660 """See Branch.last_revision()."""
661 parents = source.repository.get_parent_map(start_revid)[start_revid]
666 def get_branch_path(self, revnum=None):
667 """See SvnBranch.get_branch_path()."""
668 return target_branch_path
670 def generate_revision_id(self, revnum):
671 """See SvnBranch.generate_revision_id()."""
672 return self.repository.generate_revision_id(
673 revnum, self.get_branch_path(revnum),
674 self.repository.get_mapping())
676 push(ImaginaryBranch(target_repository), source, start_revid)
679 def push_revision_tree(target, config, source_repo, base_revid, revision_id,
681 old_tree = source_repo.revision_tree(revision_id)
682 base_tree = source_repo.revision_tree(base_revid)
684 builder = SvnCommitBuilder(target.repository, target, rev.parent_ids,
685 config, rev.timestamp,
686 rev.timezone, rev.committer, rev.properties,
687 revision_id, base_tree.inventory)
689 replay_delta(builder, base_tree, old_tree)
691 builder.commit(rev.message)
692 except SubversionException, (_, num):
693 if num == svn.core.SVN_ERR_FS_TXN_OUT_OF_DATE:
694 raise DivergedBranches(source, target)
696 except ChangesRootLHSHistory:
697 raise BzrError("Unable to push revision %r because it would change the ordering of existing revisions on the Subversion repository root. Use rebase and try again or push to a non-root path" % revision_id)
699 if source_repo.has_signature_for_revision_id(revision_id):
700 pass # FIXME: Copy revision signature for rev
703 def push(target, source, revision_id):
704 """Push a revision into Subversion.
706 This will do a new commit in the target branch.
708 :param target: Branch to push to
709 :param source: Branch to pull the revision from
710 :param revision_id: Revision id of the revision to push
712 assert isinstance(source, Branch)
713 rev = source.repository.get_revision(revision_id)
714 mutter('pushing %r (%r)' % (revision_id, rev.parent_ids))
716 # revision on top of which to commit
717 if rev.parent_ids == []:
720 base_revid = rev.parent_ids[0]
724 push_revision_tree(target, target.get_config(), source.repository, base_revid, revision_id, rev)
728 if 'validate' in debug.debug_flags:
729 crev = target.repository.get_revision(revision_id)
730 ctree = target.repository.revision_tree(revision_id)
731 treedelta = ctree.changes_from(old_tree)
732 assert not treedelta.has_changed(), "treedelta: %r" % treedelta
733 assert crev.committer == rev.committer
734 assert crev.timezone == rev.timezone
735 assert crev.timestamp == rev.timestamp
736 assert crev.message == rev.message
737 assert crev.properties == rev.properties
740 class InterToSvnRepository(InterRepository):
741 """Any to Subversion repository actions."""
743 _matching_repo_format = SvnRepositoryFormat()
746 def _get_repo_format_to_test():
747 """See InterRepository._get_repo_format_to_test()."""
750 def copy_content(self, revision_id=None, pb=None):
751 """See InterRepository.copy_content."""
752 self.source.lock_read()
754 assert revision_id is not None, "fetching all revisions not supported"
755 # Go back over the LHS parent until we reach a revid we know
757 while not self.target.has_revision(revision_id):
758 todo.append(revision_id)
760 revision_id = self.source.get_parent_map(revision_id)[revision_id][0]
764 if revision_id == NULL_REVISION:
765 raise UnrelatedBranches()
769 mutter("pushing %r into svn" % todo)
771 for revision_id in todo:
773 pb.update("pushing revisions", todo.index(revision_id), len(todo))
774 rev = self.source.get_revision(revision_id)
776 mutter('pushing %r' % (revision_id))
778 parent_revid = rev.parent_ids[0]
780 (bp, _, _) = self.target.lookup_revision_id(parent_revid)
781 if target_branch is None:
782 target_branch = Branch.open(urlutils.join(self.target.base, bp))
783 if target_branch.get_branch_path() != bp:
784 target_branch.set_branch_path(bp)
786 push_revision_tree(target_branch, target_branch.get_config(), self.source, parent_revid, revision_id, rev)
791 def fetch(self, revision_id=None, pb=None, find_ghosts=False):
792 """Fetch revisions. """
793 self.copy_content(revision_id=revision_id, pb=pb)
796 def is_compatible(source, target):
797 """Be compatible with SvnRepository."""
798 return isinstance(target, SvnRepository)