Update installation requirements.
[jelmer/subvertpy.git] / commit.py
index d77cc83946b9874323041a49c5b78dcadb5d2384..9adeda93c10af85d4d038afe285ba120484dab13 100644 (file)
--- a/commit.py
+++ b/commit.py
 import svn.delta
 from svn.core import Pool, SubversionException
 
-from bzrlib import osutils, urlutils
+from bzrlib import debug, osutils, urlutils
 from bzrlib.branch import Branch
-from bzrlib.errors import InvalidRevisionId, DivergedBranches
+from bzrlib.errors import InvalidRevisionId, DivergedBranches, UnrelatedBranches
 from bzrlib.inventory import Inventory
 from bzrlib.repository import RootCommitBuilder, InterRepository
 from bzrlib.revision import NULL_REVISION
 from bzrlib.trace import mutter
 
-from repository import (SVN_PROP_BZR_MERGE, SVN_PROP_BZR_FILEIDS,
+from copy import deepcopy
+from repository import (SVN_PROP_BZR_ANCESTRY, SVN_PROP_BZR_FILEIDS,
                         SVN_PROP_SVK_MERGE, SVN_PROP_BZR_REVISION_INFO, 
                         SVN_PROP_BZR_REVISION_ID, revision_id_to_svk_feature,
                         generate_revision_metadata, SvnRepositoryFormat, 
                         SvnRepository)
-from revids import escape_svn_path
+import urllib
 
-from copy import copy
-import os
 
 def _check_dirs_exist(transport, bp_parts, base_rev):
+    """Make sure that the specified directories exist.
+
+    :param transport: SvnRaTransport to use.
+    :param bp_parts: List of directory names in the format returned by 
+        os.path.split()
+    :param base_rev: Base revision to check.
+    :return: List of the directories that exists in base_rev.
+    """
     for i in range(len(bp_parts), 0, -1):
         current = bp_parts[:i]
-        if transport.check_path("/".join(current).strip("/"), base_rev) == svn.core.svn_node_dir:
+        path = "/".join(current).strip("/")
+        if transport.check_path(path, base_rev) == svn.core.svn_node_dir:
             return current
     return []
 
@@ -60,109 +68,156 @@ class SvnCommitBuilder(RootCommitBuilder):
         :param committer: Optional committer to set for commit.
         :param revprops: Revision properties to set.
         :param revision_id: Revision id for the new revision.
+        :param old_inv: Optional revision on top of which 
+            the commit is happening
         """
         super(SvnCommitBuilder, self).__init__(repository, parents, 
             config, timestamp, timezone, committer, revprops, revision_id)
         self.branch = branch
         self.pool = Pool()
 
+        # Keep track of what Subversion properties to set later on
         self._svnprops = {}
-        self._svnprops[SVN_PROP_BZR_REVISION_INFO] = generate_revision_metadata(timestamp, timezone, committer, revprops)
-
-        self.merges = filter(lambda x: x != self.branch.last_revision(),
-                             parents)
-
-        if len(self.merges) > 0:
-            # Bazaar Parents
-            if branch.last_revision():
-                (bp, revnum, scheme) = repository.lookup_revision_id(branch.last_revision())
-                old = repository.branchprop_list.get_property(bp, revnum, SVN_PROP_BZR_MERGE, "")
-            else:
-                old = ""
-            self._svnprops[SVN_PROP_BZR_MERGE] = old + "\t".join(self.merges) + "\n"
-
-            if branch.last_revision() is not None:
-                old = repository.branchprop_list.get_property(bp, revnum, SVN_PROP_SVK_MERGE)
-            else:
-                old = ""
+        self._svnprops[SVN_PROP_BZR_REVISION_INFO] = generate_revision_metadata(
+            timestamp, timezone, committer, revprops)
+        self._svnprops[SVN_PROP_BZR_FILEIDS] = ""
+
+        # Gather information about revision on top of which the commit is 
+        # happening
+        if parents == []:
+            self.base_revid = None
+        else:
+            self.base_revid = parents[0]
+        self.base_revno = self.branch.revision_id_to_revno(self.base_revid)
+        if self.base_revid is None:
+            self.base_revnum = -1
+            self.base_path = None
+            self.base_scheme = repository.get_scheme()
+        else:
+            (self.base_path, self.base_revnum, self.base_scheme) = \
+                repository.lookup_revision_id(self.base_revid)
 
-            new = ""
-            # SVK compatibility
-            for p in self.merges:
-                try:
-                    new += "%s\n" % revision_id_to_svk_feature(p)
-                except InvalidRevisionId:
-                    pass
+        # Determine revisions merged in this one
+        merges = filter(lambda x: x != self.base_revid, parents)
 
-            if new != "":
-                self._svnprops[SVN_PROP_SVK_MERGE] = old + new
+        if len(merges) > 0:
+            self._record_merges(merges)
 
+        # Set appropriate property if revision id was specified by 
+        # caller
         if revision_id is not None:
-            (previous_revno, previous_revid) = branch.last_revision_info()
-            if previous_revid is not None:
-                (bp, revnum, scheme) = repository.lookup_revision_id(branch.last_revision())
-                old = repository.branchprop_list.get_property(bp, revnum, 
-                            SVN_PROP_BZR_REVISION_ID+str(scheme), "")
-            else:
-                old = ""
-
-            self._svnprops[SVN_PROP_BZR_REVISION_ID+str(scheme)] = old + \
-                    "%d %s\n" % (previous_revno+1, revision_id)
-
-        # At least one of the parents has to be the last revision on the 
-        # mainline in # Subversion.
-        assert (self.branch.last_revision() is None or 
-                self.branch.last_revision() in parents)
+            self._record_revision_id(revision_id)
 
         if old_inv is None:
-            if self.branch.last_revision() is None:
+            if self.base_revid is None:
                 self.old_inv = Inventory(root_id=None)
             else:
-                self.old_inv = self.repository.get_inventory(
-                                   self.branch.last_revision())
+                self.old_inv = self.repository.get_inventory(self.base_revid)
         else:
             self.old_inv = old_inv
-            assert self.old_inv.revision_id == self.branch.last_revision()
+            # Not all repositories appear to set Inventory.revision_id, 
+            # so allow None as well.
+            assert self.old_inv.revision_id in (None, self.base_revid)
 
         self.modified_files = {}
-        self.modified_dirs = []
+        self.modified_dirs = set()
+
+    def mutter(self, text):
+        if 'commit' in debug.debug_flags:
+            mutter(text)
+
+    def _record_revision_id(self, revid):
+        """Store the revision id in a file property.
+
+        :param revid: The revision id.
+        """
+        if self.base_revid is not None:
+            old = self.repository.branchprop_list.get_property(
+                    self.base_path, self.base_revnum, 
+                        SVN_PROP_BZR_REVISION_ID+str(self.base_scheme), "")
+        else:
+            old = ""
+
+        self._svnprops[SVN_PROP_BZR_REVISION_ID+str(self.base_scheme)] = \
+                old + "%d %s\n" % (self.base_revno+1, revid)
+
+    def _record_merges(self, merges):
+        """Store the extra merges (non-LHS parents) in a file property.
+
+        :param merges: List of parents.
+        """
+        # Bazaar Parents
+        if self.base_revid is not None:
+            old = self.repository.branchprop_list.get_property(
+                  self.base_path, self.base_revnum, 
+                  SVN_PROP_BZR_ANCESTRY+str(self.base_scheme), "")
+        else:
+            old = ""
+        self._svnprops[SVN_PROP_BZR_ANCESTRY+str(self.base_scheme)] = old + "\t".join(merges) + "\n"
+
+        if self.base_revid is not None:
+            old = self.repository.branchprop_list.get_property(
+                self.base_path, self.base_revnum, SVN_PROP_SVK_MERGE, "")
+        else:
+            old = ""
+
+        new = ""
+        # SVK compatibility
+        for merge in merges:
+            try:
+                new += "%s\n" % revision_id_to_svk_feature(merge)
+            except InvalidRevisionId:
+                pass
+
+        if new != "":
+            self._svnprops[SVN_PROP_SVK_MERGE] = old + new
         
     def _generate_revision_if_needed(self):
-        pass
+        """See CommitBuilder._generate_revision_if_needed()."""
 
     def finish_inventory(self):
-        pass
+        """See CommitBuilder.finish_inventory()."""
 
     def modified_file_text(self, file_id, file_parents,
                            get_content_byte_lines, text_sha1=None,
                            text_size=None):
-        mutter('modifying file %s' % file_id)
+        """See CommitBuilder.modified_file_text()."""
         new_lines = get_content_byte_lines()
         self.modified_files[file_id] = "".join(new_lines)
         return osutils.sha_strings(new_lines), sum(map(len, new_lines))
 
     def modified_link(self, file_id, file_parents, link_target):
-        mutter('modifying link %s' % file_id)
+        """See CommitBuilder.modified_link()."""
         self.modified_files[file_id] = "link %s" % link_target
 
     def modified_directory(self, file_id, file_parents):
-        mutter('modifying directory %s' % file_id)
-        self.modified_dirs.append(file_id)
+        """See CommitBuilder.modified_directory()."""
+        self.modified_dirs.add(file_id)
 
     def _file_process(self, file_id, contents, baton):
+        """Pass the changes to a file to the Subversion commit editor.
+
+        :param file_id: Id of the file to modify.
+        :param contents: Contents of the file.
+        :param baton: Baton under which the file is known to the editor.
+        """
+        assert baton is not None
+        if contents == "" and not file_id in self.old_inv:
+            # Don't send diff if a new file with empty contents is 
+            # added, because it created weird exceptions over svn+ssh:// 
+            # or https://
+            return
         (txdelta, txbaton) = self.editor.apply_textdelta(baton, None, self.pool)
         svn.delta.svn_txdelta_send_string(contents, txdelta, txbaton, self.pool)
 
     def _dir_process(self, path, file_id, baton):
-        mutter('processing %r' % path)
-        if path == "":
-            # Set all the revprops
-            for prop, value in self._svnprops.items():
-                mutter('setting %r: %r on branch' % (prop, value))
-                if value is not None:
-                    value = value.encode('utf-8')
-                self.editor.change_dir_prop(baton, prop, value, self.pool)
+        """Pass the changes to a directory to the commit editor.
 
+        :param path: Path (from repository root) to the directory.
+        :param file_id: File id of the directory
+        :param baton: Baton of the directory for the editor.
+        """
+        assert baton is not None
         # Loop over entries of file_id in self.old_inv
         # remove if they no longer exist with the same name
         # or parents
@@ -170,18 +225,20 @@ class SvnCommitBuilder(RootCommitBuilder):
             for child_name in self.old_inv[file_id].children:
                 child_ie = self.old_inv.get_child(file_id, child_name)
                 # remove if...
-                #  ... path no longer exists
-                if (not child_ie.file_id in self.new_inventory or 
+                if (
+                    # ... path no longer exists
+                    not child_ie.file_id in self.new_inventory or 
                     # ... parent changed
                     child_ie.parent_id != self.new_inventory[child_ie.file_id].parent_id or
                     # ... name changed
                     self.new_inventory[child_ie.file_id].name != child_name):
-                    mutter('removing %r' % child_ie.file_id)
+                    self.mutter('removing %r(%r)' % (child_name, child_ie.file_id))
                     self.editor.delete_entry(
-                            os.path.join(self.branch.branch_path, self.old_inv.id2path(child_ie.file_id)), 
+                            urlutils.join(
+                                self.branch.get_branch_path(), path, child_name), 
                             self.base_revnum, baton, self.pool)
 
-        # Loop over file members of file_id in self.new_inventory
+        # Loop over file children of file_id in self.new_inventory
         for child_name in self.new_inventory[file_id].children:
             child_ie = self.new_inventory.get_child(file_id, child_name)
             assert child_ie is not None
@@ -189,36 +246,40 @@ class SvnCommitBuilder(RootCommitBuilder):
             if not (child_ie.kind in ('file', 'symlink')):
                 continue
 
+            new_child_path = self.new_inventory.id2path(child_ie.file_id)
             # add them if they didn't exist in old_inv 
             if not child_ie.file_id in self.old_inv:
-                mutter('adding %s %r' % (child_ie.kind, self.new_inventory.id2path(child_ie.file_id)))
-
+                self.mutter('adding %s %r' % (child_ie.kind, new_child_path))
+                self._record_file_id(child_ie, new_child_path)
                 child_baton = self.editor.add_file(
-                           os.path.join(self.branch.branch_path, self.new_inventory.id2path(child_ie.file_id)),
-                           baton, None, -1, self.pool)
+                    urlutils.join(self.branch.get_branch_path(), 
+                                  new_child_path), baton, None, -1, self.pool)
 
 
             # copy if they existed at different location
-            elif self.old_inv.id2path(child_ie.file_id) != self.new_inventory.id2path(child_ie.file_id):
-                mutter('copy %s %r -> %r' % (child_ie.kind, 
+            elif (self.old_inv.id2path(child_ie.file_id) != new_child_path or
+                    self.old_inv[child_ie.file_id].parent_id != child_ie.parent_id):
+                self.mutter('copy %s %r -> %r' % (child_ie.kind, 
                                   self.old_inv.id2path(child_ie.file_id), 
-                                  self.new_inventory.id2path(child_ie.file_id)))
-
+                                  new_child_path))
+                self._record_file_id(child_ie, new_child_path)
                 child_baton = self.editor.add_file(
-                           os.path.join(self.branch.branch_path, self.new_inventory.id2path(child_ie.file_id)), baton, 
-                           urlutils.join(self.repository.transport.svn_url, self.base_path, self.old_inv.id2path(child_ie.file_id)),
-                           self.base_revnum, self.pool)
+                    urlutils.join(self.branch.get_branch_path(), new_child_path), baton, 
+                    urlutils.join(self.repository.transport.svn_url, self.base_path, self.old_inv.id2path(child_ie.file_id)),
+                    self.base_revnum, self.pool)
 
             # open if they existed at the same location
             elif child_ie.revision is None:
-                mutter('open %s %r' % (child_ie.kind, 
-                                 self.new_inventory.id2path(child_ie.file_id)))
+                self.mutter('open %s %r' % (child_ie.kind, new_child_path))
 
                 child_baton = self.editor.open_file(
-                        os.path.join(self.branch.branch_path, self.new_inventory.id2path(child_ie.file_id)), 
-                        baton, self.base_revnum, self.pool)
+                    urlutils.join(self.branch.get_branch_path(), 
+                        new_child_path), 
+                    baton, self.base_revnum, self.pool)
 
             else:
+                # Old copy of the file was retained. No need to send changes
+                assert child_ie.file_id not in self.modified_files
                 child_baton = None
 
             if child_ie.file_id in self.old_inv:
@@ -248,8 +309,8 @@ class SvnCommitBuilder(RootCommitBuilder):
 
             # handle the file
             if child_ie.file_id in self.modified_files:
-                self._file_process(child_ie.file_id, self.modified_files[child_ie.file_id], 
-                                   child_baton)
+                self._file_process(child_ie.file_id, 
+                    self.modified_files[child_ie.file_id], child_baton)
 
             if child_baton is not None:
                 self.editor.close_file(child_baton, None, self.pool)
@@ -260,43 +321,45 @@ class SvnCommitBuilder(RootCommitBuilder):
             if child_ie.kind != 'directory':
                 continue
 
+            new_child_path = self.new_inventory.id2path(child_ie.file_id)
             # add them if they didn't exist in old_inv 
             if not child_ie.file_id in self.old_inv:
-                mutter('adding dir %r' % child_ie.name)
+                self.mutter('adding dir %r' % child_ie.name)
+                self._record_file_id(child_ie, new_child_path)
                 child_baton = self.editor.add_directory(
-                           os.path.join(self.branch.branch_path, self.new_inventory.id2path(child_ie.file_id)),
-                           baton, None, -1, self.pool)
+                    urlutils.join(self.branch.get_branch_path(), 
+                                  new_child_path), baton, None, -1, self.pool)
 
             # copy if they existed at different location
-            elif self.old_inv.id2path(child_ie.file_id) != self.new_inventory.id2path(child_ie.file_id):
-                mutter('copy dir %r -> %r' % (self.old_inv.id2path(child_ie.file_id), 
-                                         self.new_inventory.id2path(child_ie.file_id)))
+            elif self.old_inv.id2path(child_ie.file_id) != new_child_path:
+                old_child_path = self.old_inv.id2path(child_ie.file_id)
+                self.mutter('copy dir %r -> %r' % (old_child_path, new_child_path))
+                self._record_file_id(child_ie, new_child_path)
                 child_baton = self.editor.add_directory(
-                           os.path.join(self.branch.branch_path, self.new_inventory.id2path(child_ie.file_id)),
-                           baton, 
-                           urlutils.join(self.repository.transport.svn_url, self.base_path, self.old_inv.id2path(child_ie.file_id)),
-                           self.base_revnum, self.pool)
+                    urlutils.join(self.branch.get_branch_path(), new_child_path),
+                    baton, 
+                    urlutils.join(self.repository.transport.svn_url, self.base_path, old_child_path), self.base_revnum, self.pool)
 
             # open if they existed at the same location and 
             # the directory was touched
             elif self.new_inventory[child_ie.file_id].revision is None:
-                mutter('open dir %r' % self.new_inventory.id2path(child_ie.file_id))
+                self.mutter('open dir %r' % new_child_path)
 
                 child_baton = self.editor.open_directory(
-                        os.path.join(self.branch.branch_path, self.new_inventory.id2path(child_ie.file_id)), 
+                        urlutils.join(self.branch.get_branch_path(), new_child_path), 
                         baton, self.base_revnum, self.pool)
             else:
+                assert child_ie.file_id not in self.modified_dirs
                 continue
 
             # Handle this directory
             if child_ie.file_id in self.modified_dirs:
-                self._dir_process(self.new_inventory.id2path(child_ie.file_id), 
-                        child_ie.file_id, child_baton)
+                self._dir_process(new_child_path, child_ie.file_id, child_baton)
 
             self.editor.close_directory(child_baton, self.pool)
 
     def open_branch_batons(self, root, elements, existing_elements, 
-                           base_path, base_rev):
+                           base_path, base_rev, replace_existing):
         """Open a specified directory given a baton for the repository root.
 
         :param root: Baton for the repository root
@@ -304,14 +367,15 @@ class SvnCommitBuilder(RootCommitBuilder):
         :param existing_elements: List of directory names that exist
         :param base_path: Path to base top-level branch on
         :param base_rev: Revision of path to base top-level branch on
+        :param replace_existing: Whether the current branch should be replaced
         """
         ret = [root]
 
-        mutter('opening branch %r (base %r:%r)' % (elements, base_path, 
+        self.mutter('opening branch %r (base %r:%r)' % (elements, base_path, 
                                                    base_rev))
 
         # Open paths leading up to branch
-        for i in range(1, len(elements)-1):
+        for i in range(0, len(elements)-1):
             # Does directory already exist?
             ret.append(self.editor.open_directory(
                 "/".join(existing_elements[0:i+1]), ret[-1], -1, self.pool))
@@ -325,55 +389,84 @@ class SvnCommitBuilder(RootCommitBuilder):
         # This needs to also check that base_rev was the latest version of 
         # branch_path.
         if (len(existing_elements) == len(elements) and 
-            base_path == "/".join(elements)):
+            not replace_existing):
             ret.append(self.editor.open_directory(
                 "/".join(elements), ret[-1], base_rev, self.pool))
         else: # Branch has to be created
             # Already exists, old copy needs to be removed
-            if len(existing_elements) == len(elements):
-                self.editor.delete_entry("/".join(elements), -1, ret[-1])
+            name = "/".join(elements)
+            if replace_existing:
+                self.mutter("removing branch dir %r" % name)
+                self.editor.delete_entry(name, -1, ret[-1])
+            if base_path is not None:
+                base_url = urlutils.join(self.repository.transport.svn_url, base_path)
+            else:
+                base_url = None
+            self.mutter("adding branch dir %r" % name)
             ret.append(self.editor.add_directory(
-                "/".join(elements), ret[-1], 
-                urlutils.join(self.repository.transport.svn_url, base_path), 
-                base_rev, self.pool))
+                name, ret[-1], base_url, base_rev, self.pool))
 
         return ret
 
     def commit(self, message):
+        """Finish the commit.
+
+        """
         def done(revision, date, author):
+            """Callback that is called by the Subversion commit editor 
+            once the commit finishes.
+
+            :param revision: Revision number
+            :param date: Date recorded for this commit
+            """
             assert revision > 0
             self.revnum = revision
             self.date = date
             self.author = author
         
-        bp_parts = self.branch.branch_path.split("/")
+        bp_parts = self.branch.get_branch_path().split("/")
+        repository_latest_revnum = self.repository.transport.get_latest_revnum()
         lock = self.repository.transport.lock_write(".")
-        if self.branch.last_revision() is None:
-            self.base_revnum = 0
-            self.base_path = self.branch.branch_path
-        else:
-            (self.base_path, 
-                self.base_revnum, _) = self.repository.lookup_revision_id(
-                    self.branch.last_revision())
-        existing_bp_parts =_check_dirs_exist(self.repository.transport, 
-                                              bp_parts, -1)
+
         try:
-            mutter('obtaining commit editor')
+            existing_bp_parts = _check_dirs_exist(self.repository.transport, 
+                                              bp_parts, -1)
             self.revnum = None
             self.editor = self.repository.transport.get_commit_editor(
-                message.encode("utf-8"), done, None, False)
+                  {svn.core.SVN_PROP_REVISION_LOG: message.encode("utf-8")}, 
+                  done, None, False)
 
             root = self.editor.open_root(self.base_revnum)
-            
+
+            replace_existing = False
+            if len(bp_parts) == len(existing_bp_parts):
+                if self.base_path.strip("/") != "/".join(bp_parts).strip("/"):
+                    replace_existing = True
+                elif self.base_revnum < self.repository._log.find_latest_change(self.branch.get_branch_path(), repository_latest_revnum, include_children=True):
+                    replace_existing = True
+
             # TODO: Accept create_prefix argument
             branch_batons = self.open_branch_batons(root, bp_parts,
-                existing_bp_parts, self.base_path, self.base_revnum)
+                existing_bp_parts, self.base_path, self.base_revnum,
+                replace_existing)
+
+            # Make sure the root id is stored properly
+            if (self.old_inv.root is None or 
+                self.new_inventory.root.file_id != self.old_inv.root.file_id):
+                self._record_file_id(self.new_inventory.root, "")
 
             self._dir_process("", self.new_inventory.root.file_id, 
                 branch_batons[-1])
 
-            branch_batons.reverse()
-            for baton in branch_batons:
+            # Set all the revprops
+            for prop, value in self._svnprops.items():
+                if value is not None:
+                    value = value.encode('utf-8')
+                self.editor.change_dir_prop(branch_batons[-1], prop, value, 
+                                            self.pool)
+                self.mutter("setting revision property %r to %r" % (prop, value))
+
+            for baton in reversed(branch_batons):
                 self.editor.close_directory(baton, self.pool)
 
             self.editor.close()
@@ -381,24 +474,30 @@ class SvnCommitBuilder(RootCommitBuilder):
             lock.unlock()
 
         assert self.revnum is not None
-        revid = self.branch.generate_revision_id(self.revnum)
-
-        self.repository._latest_revnum = self.revnum
-
-        #FIXME: Use public API:
-        if self.branch._revision_history is not None:
-            self.branch._revision_history.append(revid)
-
-        mutter('commit %d finished. author: %r, date: %r' % 
-               (self.revnum, self.author, self.date))
 
         # Make sure the logwalker doesn't try to use ra 
         # during checkouts...
         self.repository._log.fetch_revisions(self.revnum)
 
+        revid = self.branch.generate_revision_id(self.revnum)
+
+        assert self._new_revision_id is None or self._new_revision_id == revid
+
+        self.mutter('commit %d finished. author: %r, date: %r, revid: %r' % 
+               (self.revnum, self.author, self.date, revid))
+
         return revid
 
-    def record_entry_contents(self, ie, parent_invs, path, tree):
+    def _record_file_id(self, ie, path):
+        """Store the file id of an inventory entry in a file property.
+
+        :param ie: Inventory entry.
+        :param path: Path of the inventory entry.
+        """
+        self._svnprops[SVN_PROP_BZR_FILEIDS] += "%s\t%s\n" % (urllib.quote(path), ie.file_id)
+
+    def record_entry_contents(self, ie, parent_invs, path, tree,
+                              content_summary):
         """Record the content of ie from tree into the commit if needed.
 
         Side effect: sets ie.revision when unchanged
@@ -413,38 +512,17 @@ class SvnCommitBuilder(RootCommitBuilder):
         assert self.new_inventory.root is not None or ie.parent_id is None
         self.new_inventory.add(ie)
 
-        # ie.revision is always None if the InventoryEntry is considered
-        # for committing. ie.snapshot will record the correct revision 
-        # which may be the sole parent if it is untouched.
-        mutter('recording %s' % ie.file_id)
-        if ie.revision is not None:
-            return
 
-        # Make sure that ie.file_id exists in the map
-        if not ie.file_id in self.old_inv:
-            if not self._svnprops.has_key(SVN_PROP_BZR_FILEIDS):
-                self._svnprops[SVN_PROP_BZR_FILEIDS] = ""
-            mutter('adding fileid mapping %s -> %s' % (path, ie.file_id))
-            self._svnprops[SVN_PROP_BZR_FILEIDS] += "%s\t%s\n" % (escape_svn_path(path), ie.file_id)
-
-        previous_entries = ie.find_previous_heads(parent_invs, 
-            self.repository.weave_store, self.repository.get_transaction())
-
-        # we are creating a new revision for ie in the history store
-        # and inventory.
-        ie.snapshot(self._new_revision_id, path, previous_entries, tree, self)
-
-
-def replay_delta(builder, delta, old_tree):
+def replay_delta(builder, old_tree, new_tree):
     """Replays a delta to a commit builder.
 
     :param builder: The commit builder.
-    :param delta: Treedelta to apply
     :param old_tree: Original tree on top of which the delta should be applied
+    :param new_tree: New tree that should be committed
     """
-    for (_, ie) in builder.new_inventory.entries():
-        if not delta.touches_file_id(ie.file_id):
-            continue
+    delta = new_tree.changes_from(old_tree)
+    def touch_id(id):
+        ie = builder.new_inventory[id]
 
         id = ie.file_id
         while builder.new_inventory[id].parent_id is not None:
@@ -459,98 +537,102 @@ def replay_delta(builder, delta, old_tree):
             builder.modified_link(ie.file_id, [], ie.symlink_target)
         elif ie.kind == 'file':
             def get_text():
-                return old_tree.get_file_text(ie.file_id)
+                return new_tree.get_file_text(ie.file_id)
             builder.modified_file_text(ie.file_id, [], get_text)
 
+    for (_, id, _) in delta.added:
+        touch_id(id)
 
-def push_as_merged(target, source, revision_id):
-    """Push a revision as merged revision.
+    for (_, id, _, _, _) in delta.modified:
+        touch_id(id)
 
-    This will create a new revision in the target repository that 
-    merges the specified revision but does not contain any other differences. 
-    This is done so that the revision that is being pushed does not need 
-    to completely match the target revision and so it can not have the 
-    same revision id.
+    for (oldpath, _, id, _, _, _) in delta.renamed:
+        touch_id(id)
+        old_parent_id = old_tree.inventory.path2id(urlutils.dirname(oldpath))
+        if old_parent_id in builder.new_inventory:
+            touch_id(old_parent_id)
 
-    :param target: Repository to push to
-    :param source: Repository to pull the revision from
-    :param revision_id: Revision id of the revision to push
-    :return: The revision id of the created revision
-    """
-    assert isinstance(source, Branch)
-    rev = source.repository.get_revision(revision_id)
-    inv = source.repository.get_inventory(revision_id)
+    for (path, _, _) in delta.removed:
+        old_parent_id = old_tree.inventory.path2id(urlutils.dirname(path))
+        if old_parent_id in builder.new_inventory:
+            touch_id(old_parent_id)
 
-    # revision on top of which to commit
-    prev_revid = target.last_revision()
+    builder.finish_inventory()
 
-    mutter('committing %r on top of %r' % (revision_id, prev_revid))
 
-    old_tree = source.repository.revision_tree(revision_id)
-    if source.repository.has_revision(prev_revid):
-        new_tree = source.repository.revision_tree(prev_revid)
-    else:
-        new_tree = target.repository.revision_tree(prev_revid)
-
-    builder = SvnCommitBuilder(target.repository, target, 
-                               [revision_id, prev_revid],
-                               target.get_config(),
-                               None,
-                               None,
-                               None,
-                               rev.properties, 
-                               None,
-                               new_tree.inventory)
-                         
-    delta = new_tree.changes_from(old_tree)
-    builder.new_inventory = inv
-    replay_delta(builder, delta, old_tree)
-
-    try:
-        return builder.commit(rev.message)
-    except SubversionException, (_, num):
-        if num == svn.core.SVN_ERR_FS_TXN_OUT_OF_DATE:
-            raise DivergedBranches(source, target)
-        raise
-
-
-def push_new(target_repository, target_branch_path, source, stop_revision=None):
+def push_new(target_repository, target_branch_path, source, 
+             stop_revision=None, validate=False):
     """Push a revision into Subversion, creating a new branch.
 
     This will do a new commit in the target branch.
 
-    :param target: Branch to push to
+    :param target_branch_path: Path to create new branch at
     :param source: Branch to pull the revision from
     :param revision_id: Revision id of the revision to push
+    :param validate: Whether to check the committed revision matches 
+        the source revision.
     """
+    assert isinstance(source, Branch)
     if stop_revision is None:
         stop_revision = source.last_revision()
     history = source.revision_history()
-    revhistory = copy(history)
-    revhistory.reverse()
-    start_revid = None
-    for revid in revhistory:
-        if target_repository.has_revision(revid):
+    revhistory = deepcopy(history)
+    start_revid = NULL_REVISION
+    while len(revhistory) > 0:
+        revid = revhistory.pop()
+        # We've found the revision to push if there is a revision 
+        # which LHS parent is present or if this is the first revision.
+        if (len(revhistory) == 0 or 
+            target_repository.has_revision(revhistory[-1])):
             start_revid = revid
             break
 
-    if start_revid is not None:
-        (copy_path, copy_revnum, 
-            scheme) = target_repository.lookup_revision_id(start_revid)
-    else:
-        # None of the revisions are already present in the repository
-        copy_path = None
-        copy_revnum = None
-    
-    # TODO: Get commit builder but specify that target_branch_path should
+    # Get commit builder but specify that target_branch_path should
     # be created and copied from (copy_path, copy_revnum)
-
-    branch = self.open_branch()
-    branch.pull(source)
-    return branch
-
-
-def push(target, source, revision_id):
+    class ImaginaryBranch:
+        """Simple branch that pretends to be empty but already exist."""
+        def __init__(self, repository):
+            self.repository = repository
+            self._revision_history = None
+
+        def get_config(self):
+            """See Branch.get_config()."""
+            return None
+
+        def revision_id_to_revno(self, revid):
+            if revid is None:
+                return 0
+            return history.index(revid)
+
+        def last_revision_info(self):
+            """See Branch.last_revision_info()."""
+            last_revid = self.last_revision()
+            if last_revid is None:
+                return (0, None)
+            return (history.index(last_revid), last_revid)
+
+        def last_revision(self):
+            """See Branch.last_revision()."""
+            parents = source.repository.revision_parents(start_revid)
+            if parents == []:
+                return None
+            return parents[0]
+
+        def get_branch_path(self, revnum=None):
+            """See SvnBranch.get_branch_path()."""
+            return target_branch_path
+
+        def generate_revision_id(self, revnum):
+            """See SvnBranch.generate_revision_id()."""
+            return self.repository.generate_revision_id(
+                revnum, self.get_branch_path(revnum), 
+                str(self.repository.get_scheme()))
+
+    push(ImaginaryBranch(target_repository), source, start_revid, 
+         validate=validate)
+
+
+def push(target, source, revision_id, validate=False):
     """Push a revision into Subversion.
 
     This will do a new commit in the target branch.
@@ -558,40 +640,46 @@ def push(target, source, revision_id):
     :param target: Branch to push to
     :param source: Branch to pull the revision from
     :param revision_id: Revision id of the revision to push
+    :param validate: Whether to check the committed revision matches 
+        the source revision.
     """
     assert isinstance(source, Branch)
     rev = source.repository.get_revision(revision_id)
-    inv = source.repository.get_inventory(revision_id)
+    mutter('pushing %r (%r)' % (revision_id, rev.parent_ids))
 
     # revision on top of which to commit
-    assert target.last_revision() in rev.parent_ids
-
-    mutter('pushing %r' % (revision_id))
+    if rev.parent_ids == []:
+        base_revid = None
+    else:
+        base_revid = rev.parent_ids[0]
 
     old_tree = source.repository.revision_tree(revision_id)
-    new_tree = source.repository.revision_tree(target.last_revision())
-
-    builder = SvnCommitBuilder(target.repository, target, 
-                               rev.parent_ids,
-                               target.get_config(),
-                               rev.timestamp,
-                               rev.timezone,
-                               rev.committer,
-                               rev.properties, 
-                               revision_id,
-                               new_tree.inventory)
+    base_tree = source.repository.revision_tree(base_revid)
+
+    builder = SvnCommitBuilder(target.repository, target, rev.parent_ids,
+                               target.get_config(), rev.timestamp,
+                               rev.timezone, rev.committer, rev.properties, 
+                               revision_id, base_tree.inventory)
                          
-    delta = new_tree.changes_from(old_tree)
-    builder.new_inventory = inv
-    replay_delta(builder, delta, old_tree)
+    builder.new_inventory = source.repository.get_inventory(revision_id)
+    replay_delta(builder, base_tree, old_tree)
     try:
-        return builder.commit(rev.message)
-    except SubversionException, (msg, num):
-        import pdb
-        pdb.set_trace()
+        builder.commit(rev.message)
+    except SubversionException, (_, num):
         if num == svn.core.SVN_ERR_FS_TXN_OUT_OF_DATE:
             raise DivergedBranches(source, target)
         raise
+    if validate:
+        crev = target.repository.get_revision(revision_id)
+        ctree = target.repository.revision_tree(revision_id)
+        treedelta = ctree.changes_from(old_tree)
+        assert not treedelta.has_changed(), "treedelta: %r" % treedelta
+        assert crev.committer == rev.committer
+        assert crev.timezone == rev.timezone
+        assert crev.timestamp == rev.timestamp
+        assert crev.message == rev.message
+        assert crev.properties == rev.properties
+
 
 class InterToSvnRepository(InterRepository):
     """Any to Subversion repository actions."""
@@ -600,9 +688,10 @@ class InterToSvnRepository(InterRepository):
 
     @staticmethod
     def _get_repo_format_to_test():
+        """See InterRepository._get_repo_format_to_test()."""
         return None
 
-    def copy_content(self, revision_id=None, basis=None, pb=None):
+    def copy_content(self, revision_id=None, pb=None):
         """See InterRepository.copy_content."""
         assert revision_id is not None, "fetching all revisions not supported"
         # Go back over the LHS parent until we reach a revid we know
@@ -611,37 +700,36 @@ class InterToSvnRepository(InterRepository):
             todo.append(revision_id)
             revision_id = self.source.revision_parents(revision_id)[0]
             if revision_id == NULL_REVISION:
-                raise "Unrelated repositories."
-        todo.reverse()
+                raise UnrelatedBranches()
+        if todo == []:
+            # Nothing to do
+            return
         mutter("pushing %r into svn" % todo)
-        while len(todo) > 0:
-            revision_id = todo.pop()
-
+        target_branch = None
+        for revision_id in todo:
+            if pb is not None:
+                pb.update("pushing revisions", todo.index(revision_id), len(todo))
             rev = self.source.get_revision(revision_id)
-            inv = self.source.get_inventory(revision_id)
 
             mutter('pushing %r' % (revision_id))
 
             old_tree = self.source.revision_tree(revision_id)
-            parent_revid = self.source.revision_parents(revision_id)[0]
-            new_tree = self.source.revision_tree(parent_revid)
+            parent_revid = rev.parent_ids[0]
+            base_tree = self.source.revision_tree(parent_revid)
 
-            (bp, _, scheme) = self.target.lookup_revision_id(parent_revid)
-            target_branch = Branch.open("%s/%s" % (self.target.base, bp))
+            (bp, _, _) = self.target.lookup_revision_id(parent_revid)
+            if target_branch is None:
+                target_branch = Branch.open(urlutils.join(self.target.base, bp))
+            if target_branch.get_branch_path() != bp:
+                target_branch.set_branch_path(bp)
 
             builder = SvnCommitBuilder(self.target, target_branch, 
-                               rev.parent_ids,
-                               target_branch.get_config(),
-                               rev.timestamp,
-                               rev.timezone,
-                               rev.committer,
-                               rev.properties, 
-                               revision_id,
-                               new_tree.inventory)
+                               rev.parent_ids, target_branch.get_config(),
+                               rev.timestamp, rev.timezone, rev.committer,
+                               rev.properties, revision_id, base_tree.inventory)
                          
-            delta = new_tree.changes_from(old_tree)
-            builder.new_inventory = inv
-            replay_delta(builder, delta, old_tree)
+            builder.new_inventory = self.source.get_inventory(revision_id)
+            replay_delta(builder, base_tree, old_tree)
             builder.commit(rev.message)