changeset 414:343da842dbe6

split parts of HgChangeReceiver out into an SVNMeta class Less obvious things: - my reordering in the previous was incomplete - _branch_for_path() was unused, so I removed it - _svnpath() was removed in favor of identical _remotename() - I've checked "no cover" bits manually
author Dirkjan Ochtman <dirkjan@ochtman.nl>
date Thu, 11 Jun 2009 18:56:35 +0200
parents ac0cc3c9ea63
children b17b2969861c
files hgsubversion/cmdutil.py hgsubversion/hg_delta_editor.py hgsubversion/stupid.py hgsubversion/svncommands.py hgsubversion/svnmeta.py hgsubversion/utility_commands.py hgsubversion/wrappers.py tests/test_rebuildmeta.py
diffstat 8 files changed, 119 insertions(+), 1085 deletions(-) [+]
line wrap: on
line diff
--- a/hgsubversion/cmdutil.py
+++ b/hgsubversion/cmdutil.py
@@ -57,7 +57,7 @@ def parentrev(ui, repo, hge, svn_commit_
 def replay_convert_rev(ui, hg_editor, svn, r, tbdelta):
     # ui is only passed in for similarity with stupid.convert_rev()
     hg_editor.current.rev = r
-    hg_editor.save_tbdelta(tbdelta) # needed by get_replay()
+    hg_editor.meta.save_tbdelta(tbdelta) # needed by get_replay()
     svn.get_replay(r.revnum, hg_editor)
     i = 1
     if hg_editor.current.missing:
--- a/hgsubversion/hg_delta_editor.py
+++ b/hgsubversion/hg_delta_editor.py
@@ -1,23 +1,17 @@
 import cStringIO
-import cPickle as pickle
-import os
 import sys
-import tempfile
 import traceback
 
 from mercurial import context
-from mercurial import hg
-from mercurial import ui
 from mercurial import util as hgutil
 from mercurial import revlog
 from mercurial import node
-from mercurial import error
 from svn import delta
 from svn import core
 
+import svnmeta
 import svnexternals
 import util
-import maps
 
 class MissingPlainTextError(Exception):
     """Exception raised when the repo lacks a source file required for replaying
@@ -29,22 +23,6 @@ class ReplayException(Exception):
     exception.
     """
 
-def pickle_atomic(data, file_path, dir=None):
-    """pickle some data to a path atomically.
-
-    This is present because I kept corrupting my revmap by managing to hit ^C
-    during the pickle of that file.
-    """
-    try:
-        f, path = tempfile.mkstemp(prefix='pickling', dir=dir)
-        f = os.fdopen(f, 'w')
-        pickle.dump(data, f)
-        f.close()
-    except: #pragma: no cover
-        raise
-    else:
-        hgutil.rename(path, file_path)
-
 def ieditor(fn):
     """Helps identify methods used by the SVN editor interface.
 
@@ -69,8 +47,7 @@ class RevisionData(object):
 
     __slots__ = [
         'file', 'files', 'deleted', 'rev', 'execfiles', 'symlinks', 'batons',
-        'copies', 'missing', 'emptybranches', 'base', 'closebranches',
-        'externals',
+        'copies', 'missing', 'emptybranches', 'base', 'externals',
     ]
 
     def __init__(self):
@@ -89,431 +66,16 @@ class RevisionData(object):
         self.missing = set()
         self.emptybranches = {}
         self.base = None
-        self.closebranches = set()
         self.externals = {}
 
 
 class HgChangeReceiver(delta.Editor):
 
     def __init__(self, repo, uuid=None, subdir=''):
-        """path is the path to the target hg repo.
-
-        subdir is the subdirectory of the edits *on the svn server*.
-        It is needed for stripping paths off in certain cases.
-        """
         self.ui = repo.ui
         self.repo = repo
-        self.path = os.path.normpath(repo.join('..'))
-
-        if not os.path.isdir(self.meta_data_dir):
-            os.makedirs(self.meta_data_dir)
-        self._set_uuid(uuid)
-        # TODO: validate subdir too
-        self.revmap = maps.RevMap(repo)
-
-        author_host = self.ui.config('hgsubversion', 'defaulthost', uuid)
-        authors = self.ui.config('hgsubversion', 'authormap')
-        tag_locations = self.ui.configlist('hgsubversion', 'tagpaths', ['tags'])
-        self.usebranchnames = self.ui.configbool('hgsubversion',
-                                                  'usebranchnames', True)
-
-        # FIXME: test that this hasn't changed! defer & compare?
-        self.subdir = subdir
-        if self.subdir and self.subdir[0] == '/':
-            self.subdir = self.subdir[1:]
-        self.branches = {}
-        if os.path.exists(self.branch_info_file):
-            f = open(self.branch_info_file)
-            self.branches = pickle.load(f)
-            f.close()
-        self.tags = {}
-        if os.path.exists(self.tag_locations_file):
-            f = open(self.tag_locations_file)
-            self.tag_locations = pickle.load(f)
-            f.close()
-        else:
-            self.tag_locations = tag_locations
-        pickle_atomic(self.tag_locations, self.tag_locations_file,
-                      self.meta_data_dir)
-        # ensure nested paths are handled properly
-        self.tag_locations.sort()
-        self.tag_locations.reverse()
-
+        self.meta = svnmeta.SVNMeta(repo, uuid, subdir)
         self.current = RevisionData()
-        self.authors = maps.AuthorMap(self.ui, self.authors_file,
-                                 defaulthost=author_host)
-        if authors: self.authors.load(authors)
-
-        self.lastdate = '1970-01-01 00:00:00 -0000'
-        self.filemap = maps.FileMap(repo)
-
-    def _get_uuid(self):
-        return open(os.path.join(self.meta_data_dir, 'uuid')).read()
-
-    def _set_uuid(self, uuid):
-        if not uuid:
-            return
-        elif os.path.isfile(os.path.join(self.meta_data_dir, 'uuid')):
-            stored_uuid = self._get_uuid()
-            assert stored_uuid
-            if uuid != stored_uuid:
-                raise hgutil.Abort('unable to operate on unrelated repository')
-        else:
-            if uuid:
-                f = open(os.path.join(self.meta_data_dir, 'uuid'), 'w')
-                f.write(uuid)
-                f.flush()
-                f.close()
-            else:
-                raise hgutil.Abort('unable to operate on unrelated repository')
-
-    uuid = property(_get_uuid, _set_uuid, None,
-                    'Error-checked UUID of source Subversion repository.')
-
-    @property
-    def meta_data_dir(self):
-        return os.path.join(self.path, '.hg', 'svn')
-
-    @property
-    def branch_info_file(self):
-        return os.path.join(self.meta_data_dir, 'branch_info')
-
-    @property
-    def tag_locations_file(self):
-        return os.path.join(self.meta_data_dir, 'tag_locations')
-
-    @property
-    def authors_file(self):
-        return os.path.join(self.meta_data_dir, 'authors')
-
-    def hashes(self):
-        return dict((v, k) for (k, v) in self.revmap.iteritems())
-
-    def branchedits(self, branch, rev):
-        check = lambda x: x[0][1] == branch and x[0][0] < rev.revnum
-        return sorted(filter(check, self.revmap.iteritems()), reverse=True)
-
-    def last_known_revision(self):
-        """Obtain the highest numbered -- i.e. latest -- revision known.
-
-        Currently, this function just iterates over the entire revision map
-        using the max() builtin. This may be slow for extremely large
-        repositories, but for now, it's fast enough.
-        """
-        try:
-            return max(k[0] for k in self.revmap.iterkeys())
-        except ValueError:
-            return 0
-
-    def fixdate(self, date):
-        if date is not None:
-            date = date.replace('T', ' ').replace('Z', '').split('.')[0]
-            date += ' -0000'
-            self.lastdate = date
-        else:
-            date = self.lastdate
-        return date
-
-    def _save_metadata(self):
-        '''Save the Subversion metadata. This should really be called after
-        every revision is created.
-        '''
-        pickle_atomic(self.branches, self.branch_info_file, self.meta_data_dir)
-
-    def _localname(self, path):
-        """Compute the local name for a branch located at path.
-        """
-        assert not path.startswith('tags/')
-        if path == 'trunk':
-            return None
-        elif path.startswith('branches/'):
-            return path[len('branches/'):]
-        return  '../%s' % path
-
-    def _remotename(self, branch):
-        if branch == 'default' or branch is None:
-            return 'trunk'
-        elif branch.startswith('../'):
-            return branch[3:]
-        return 'branches/%s' % branch
-
-    def _normalize_path(self, path):
-        '''Normalize a path to strip of leading slashes and our subdir if we
-        have one.
-        '''
-        if path and path[0] == '/':
-            path = path[1:]
-        if path and path.startswith(self.subdir):
-            path = path[len(self.subdir):]
-        if path and path[0] == '/':
-            path = path[1:]
-        return path
-
-    def _is_path_tag(self, path):
-        """If path could represent the path to a tag, returns the potential tag
-        name. Otherwise, returns False.
-
-        Note that it's only a tag if it was copied from the path '' in a branch
-        (or tag) we have, for our purposes.
-        """
-        path = self._normalize_path(path)
-        for tagspath in self.tag_locations:
-            onpath = path.startswith(tagspath)
-            longer = len(path) > len('%s/' % tagspath)
-            if path and onpath and longer:
-                tag, subpath = path[len(tagspath) + 1:], ''
-                return tag
-        return False
-
-    def _split_branch_path(self, path, existing=True):
-        """Figure out which branch inside our repo this path represents, and
-        also figure out which path inside that branch it is.
-
-        Returns a tuple of (path within branch, local branch name, server-side branch path).
-
-        If existing=True, will return None, None, None if the file isn't on some known
-        branch. If existing=False, then it will guess what the branch would be if it were
-        known.
-        """
-        path = self._normalize_path(path)
-        if path.startswith('tags/'):
-            return None, None, None
-        test = ''
-        path_comps = path.split('/')
-        while self._localname(test) not in self.branches and len(path_comps):
-            if not test:
-                test = path_comps.pop(0)
-            else:
-                test += '/%s' % path_comps.pop(0)
-        if self._localname(test) in self.branches:
-            return path[len(test)+1:], self._localname(test), test
-        if existing:
-            return None, None, None
-        if path == 'trunk' or path.startswith('trunk/'):
-            path = path.split('/')[1:]
-            test = 'trunk'
-        elif path.startswith('branches/'):
-            elts = path.split('/')
-            test = '/'.join(elts[:2])
-            path = '/'.join(elts[2:])
-        else:
-            path = test.split('/')[-1]
-            test = '/'.join(test.split('/')[:-1])
-        ln =  self._localname(test)
-        if ln and ln.startswith('../'):
-            return None, None, None
-        return path, ln, test
-
-    def _is_path_valid(self, path):
-        if path is None:
-            return False
-        subpath = self._split_branch_path(path)[0]
-        if subpath is None:
-            return False
-        return subpath in self.filemap
-
-    def get_parent_svn_branch_and_rev(self, number, branch):
-        number -= 1
-        if (number, branch) in self.revmap:
-            return number, branch
-        real_num = 0
-        for num, br in self.revmap.iterkeys():
-            if br != branch:
-                continue
-            if num <= number and num > real_num:
-                real_num = num
-        if branch in self.branches:
-            parent_branch = self.branches[branch][0]
-            parent_branch_rev = self.branches[branch][1]
-            # check to see if this branch already existed and is the same
-            if parent_branch_rev < real_num:
-                return real_num, branch
-            # if that wasn't true, then this is the a new branch with the
-            # same name as some old deleted branch
-            if parent_branch_rev <= 0 and real_num == 0:
-                return None, None
-            branch_created_rev = self.branches[branch][2]
-            if parent_branch == 'trunk':
-                parent_branch = None
-            if branch_created_rev <= number+1 and branch != parent_branch:
-                return self.get_parent_svn_branch_and_rev(
-                                                parent_branch_rev+1,
-                                                parent_branch)
-        if real_num != 0:
-            return real_num, branch
-        return None, None
-
-    def get_parent_revision(self, number, branch):
-        '''Get the parent revision hash for a commit on a specific branch.
-        '''
-        r, br = self.get_parent_svn_branch_and_rev(number, branch)
-        if r is not None:
-            return self.revmap[r, br]
-        return revlog.nullid
-
-    def update_branch_tag_map_for_rev(self, revision):
-        paths = revision.paths
-        added_branches = {}
-        added_tags = {}
-        self.current.closebranches = set()
-        tags_to_delete = set()
-        for p in sorted(paths):
-            t_name = self._is_path_tag(p)
-            if t_name != False:
-                src_p, src_rev = paths[p].copyfrom_path, paths[p].copyfrom_rev
-                # if you commit to a tag, I'm calling you stupid and ignoring
-                # you.
-                if src_p is not None and src_rev is not None:
-                    file, branch = self._path_and_branch_for_path(src_p)
-                    if file is None:
-                        # some crazy people make tags from other tags
-                        file = ''
-                        from_tag = self._is_path_tag(src_p)
-                        if not from_tag:
-                            continue
-                        branch, src_rev = self.tags[from_tag]
-                    if t_name not in added_tags and file is '':
-                        added_tags[t_name] = branch, src_rev
-                    elif file:
-                        t_name = t_name[:-(len(file)+1)]
-                        if src_rev > added_tags[t_name][1]:
-                            added_tags[t_name] = branch, src_rev
-                elif (paths[p].action == 'D' and p.endswith(t_name)
-                      and t_name in self.tags):
-                        tags_to_delete.add(t_name)
-                continue
-            # At this point we know the path is not a tag. In that
-            # case, we only care if it is the root of a new branch (in
-            # this function). This is determined by the following
-            # checks:
-            # 1. Is the file located inside any currently known
-            #    branch?  If yes, then we're done with it, this isn't
-            #    interesting.
-            # 2. Does the file have copyfrom information? If yes, then
-            #    we're done: this is a new branch, and we record the
-            #    copyfrom in added_branches if it comes from the root
-            #    of another branch, or create it from scratch.
-            # 3. Neither of the above. This could be a branch, but it
-            #    might never work out for us. It's only ever a branch
-            #    (as far as we're concerned) if it gets committed to,
-            #    which we have to detect at file-write time anyway. So
-            #    we do nothing here.
-            # 4. It's the root of an already-known branch, with an
-            #    action of 'D'. We mark the branch as deleted.
-            # 5. It's the parent directory of one or more
-            #    already-known branches, so we mark them as deleted.
-            # 6. It's a branch being replaced by another branch - the
-            #    action will be 'R'.
-            fi, br = self._path_and_branch_for_path(p)
-            if fi is not None:
-                if fi == '':
-                    if paths[p].action == 'D':
-                        self.current.closebranches.add(br) # case 4
-                    elif paths[p].action == 'R':
-                        parent = self._determine_parent_branch(
-                            p, paths[p].copyfrom_path, paths[p].copyfrom_rev,
-                            revision.revnum)
-                        added_branches.update(parent)
-                continue # case 1
-            if paths[p].action == 'D':
-                for known in self.branches:
-                    if self._svnpath(known).startswith(p):
-                        self.current.closebranches.add(known) # case 5
-            parent = self._determine_parent_branch(
-                p, paths[p].copyfrom_path, paths[p].copyfrom_rev, revision.revnum)
-            if not parent and paths[p].copyfrom_path:
-                bpath, branch = self._path_and_branch_for_path(p, False)
-                if (bpath is not None
-                    and branch not in self.branches
-                    and branch not in added_branches):
-                    parent = {branch: (None, 0, revision.revnum)}
-            added_branches.update(parent)
-        rmtags = dict((t, self.tags[t][0]) for t in tags_to_delete)
-        return {
-            'tags': (added_tags, rmtags),
-            'branches': (added_branches, self.current.closebranches),
-        }
-
-    def save_tbdelta(self, tbdelta):
-        for t in tbdelta['tags'][1]:
-            del self.tags[t]
-        for br in tbdelta['branches'][1]:
-            del self.branches[br]
-        for t, info in tbdelta['tags'][0].items():
-            self.ui.status('Tagged %s@%s as %s\n' %
-                           (info[0] or 'trunk', info[1], t))
-        self.tags.update(tbdelta['tags'][0])
-        self.branches.update(tbdelta['branches'][0])
-
-    def committags(self, delta, rev, endbranches):
-
-        date = self.fixdate(rev.date)
-        # determine additions/deletions per branch
-        branches = {}
-        for tag, source in delta[0].iteritems():
-            b, r = source
-            branches.setdefault(b, []).append(('add', tag, r))
-        for tag, branch in delta[1].iteritems():
-            branches.setdefault(branch, []).append(('rm', tag, None))
-
-        for b, tags in branches.iteritems():
-
-            # modify parent's .hgtags source
-            parent = self.repo[self.get_parent_revision(rev.revnum, b)]
-            if '.hgtags' not in parent:
-                src = ''
-            else:
-                src = parent['.hgtags'].data()
-            for op, tag, r in sorted(tags, reverse=True):
-                if op == 'add':
-                    tagged = node.hex(self.revmap[
-                        self.get_parent_svn_branch_and_rev(r+1, b)])
-                elif op == 'rm':
-                    tagged = node.hex(node.nullid)
-                src += '%s %s\n' % (tagged, tag)
-
-            # add new changeset containing updated .hgtags
-            def fctxfun(repo, memctx, path):
-                return context.memfilectx(path='.hgtags', data=src,
-                                          islink=False, isexec=False,
-                                          copied=None)
-            extra = util.build_extra(rev.revnum, b, self.uuid, self.subdir)
-            if not self.usebranchnames:
-                extra.pop('branch', None)
-            if b in endbranches:
-                extra['close'] = 1
-            ctx = context.memctx(self.repo,
-                                 (parent.node(), node.nullid),
-                                 rev.message or ' ',
-                                 ['.hgtags'],
-                                 fctxfun,
-                                 self.authors[rev.author],
-                                 date,
-                                 extra)
-            new = self.repo.commitctx(ctx)
-            if (rev.revnum, b) not in self.revmap:
-                self.revmap[rev.revnum, b] = new
-            if b in endbranches:
-                endbranches.pop(b)
-                bname = b or 'default'
-                self.ui.status('Marked branch %s as closed.\n' % bname)
-
-    def delbranch(self, branch, node, rev):
-        pctx = self.repo[node]
-        files = pctx.manifest().keys()
-        extra = {'close': 1}
-        if self.usebranchnames:
-            extra['branch'] = branch or 'default'
-        ctx = context.memctx(self.repo,
-                             (node, revlog.nullid),
-                             rev.message or util.default_commit_msg,
-                             [],
-                             lambda x, y, z: None,
-                             self.authors[rev.author],
-                             self.fixdate(rev.date),
-                             extra)
-        new = self.repo.commitctx(ctx)
-        self.ui.status('Marked branch %s as closed.\n' % (branch or 'default'))
 
     def set_file(self, path, data, isexec=False, islink=False):
         if islink:
@@ -534,34 +96,6 @@ class HgChangeReceiver(delta.Editor):
         self.current.symlinks[path] = False
         self.ui.note('D %s\n' % path)
 
-    def _svnpath(self, branch):
-        """Return the relative path in svn of branch.
-        """
-        if branch == None or branch == 'default':
-            return 'trunk'
-        elif branch.startswith('../'):
-            return branch[3:]
-        return 'branches/%s' % branch
-
-    def _path_and_branch_for_path(self, path, existing=True):
-        return self._split_branch_path(path, existing=existing)[:2]
-
-    def _branch_for_path(self, path, existing=True):
-        return self._path_and_branch_for_path(path, existing=existing)[1]
-
-    def _determine_parent_branch(self, p, src_path, src_rev, revnum):
-        if src_path is not None:
-            src_file, src_branch = self._path_and_branch_for_path(src_path)
-            src_tag = self._is_path_tag(src_path)
-            if src_tag != False:
-                # also case 2
-                src_branch, src_rev = self.tags[src_tag]
-                return {self._localname(p): (src_branch, src_rev, revnum )}
-            if src_file == '':
-                # case 2
-                return {self._localname(p): (src_branch, src_rev, revnum )}
-        return {}
-
     def _updateexternals(self):
         if not self.current.externals:
             return
@@ -569,13 +103,13 @@ class HgChangeReceiver(delta.Editor):
         revnum = self.current.rev.revnum
         branches = {}
         for path, entry in self.current.externals.iteritems():
-            if not self._is_path_valid(path):
+            if not self.meta._is_path_valid(path):
                 self.ui.warn('WARNING: Invalid path %s in externals\n' % path)
                 continue
-            p, b, bp = self._split_branch_path(path)
+            p, b, bp = self.meta._split_branch_path(path)
             if bp not in branches:
                 external = svnexternals.externalsfile()
-                parent = self.get_parent_revision(revnum, b)
+                parent = self.meta.get_parent_revision(revnum, b)
                 pctx = self.repo[parent]
                 if '.hgsvnexternals' in pctx:
                     external.read(pctx['.hgsvnexternals'].data())
@@ -609,20 +143,20 @@ class HgChangeReceiver(delta.Editor):
         files_to_commit.sort()
         branch_batches = {}
         rev = self.current.rev
-        date = self.fixdate(rev.date)
+        date = self.meta.fixdate(rev.date)
 
         # build up the branches that have files on them
         for f in files_to_commit:
-            if not  self._is_path_valid(f):
+            if not self.meta._is_path_valid(f):
                 continue
-            p, b = self._path_and_branch_for_path(f)
+            p, b = self.meta._path_and_branch_for_path(f)
             if b not in branch_batches:
                 branch_batches[b] = []
             branch_batches[b].append((p, f))
 
         closebranches = {}
         for branch in tbdelta['branches'][1]:
-            branchedits = self.branchedits(branch, rev)
+            branchedits = self.meta.branchedits(branch, rev)
             if len(branchedits) < 1:
                 # can't close a branch that never existed
                 continue
@@ -636,13 +170,13 @@ class HgChangeReceiver(delta.Editor):
                 del self.current.emptybranches[branch]
             files = dict(files)
 
-            parents = (self.get_parent_revision(rev.revnum, branch),
+            parents = (self.meta.get_parent_revision(rev.revnum, branch),
                        revlog.nullid)
-            if parents[0] in closedrevs and branch in self.current.closebranches:
+            if parents[0] in closedrevs and branch in self.meta.closebranches:
                 continue
-            extra = util.build_extra(rev.revnum, branch, self.uuid, self.subdir)
+            extra = util.build_extra(rev.revnum, branch, self.meta.uuid, self.meta.subdir)
             if branch is not None:
-                if (branch not in self.branches
+                if (branch not in self.meta.branches
                     and branch not in self.repo.branchtags()):
                     continue
             parent_ctx = self.repo.changectx(parents[0])
@@ -673,24 +207,24 @@ class HgChangeReceiver(delta.Editor):
                                           data=data,
                                           islink=is_link, isexec=is_exec,
                                           copied=copied)
-            if not self.usebranchnames:
+            if not self.meta.usebranchnames:
                 extra.pop('branch', None)
             current_ctx = context.memctx(self.repo,
                                          parents,
                                          rev.message or '...',
                                          files.keys(),
                                          filectxfn,
-                                         self.authors[rev.author],
+                                         self.meta.authors[rev.author],
                                          date,
                                          extra)
             new_hash = self.repo.commitctx(current_ctx)
             util.describe_commit(self.ui, new_hash, branch)
-            if (rev.revnum, branch) not in self.revmap:
-                self.revmap[rev.revnum, branch] = new_hash
+            if (rev.revnum, branch) not in self.meta.revmap:
+                self.meta.revmap[rev.revnum, branch] = new_hash
 
         # 2. handle branches that need to be committed without any files
         for branch in self.current.emptybranches:
-            ha = self.get_parent_revision(rev.revnum, branch)
+            ha = self.meta.get_parent_revision(rev.revnum, branch)
             if ha == node.nullid:
                 continue
             parent_ctx = self.repo.changectx(ha)
@@ -700,44 +234,44 @@ class HgChangeReceiver(delta.Editor):
             if self.current.emptybranches[branch]: #pragma: no cover
                raise hgutil.Abort('Empty commit to an open branch attempted. '
                                   'Please report this issue.')
-            extra = util.build_extra(rev.revnum, branch, self.uuid, self.subdir)
-            if not self.usebranchnames:
+            extra = util.build_extra(rev.revnum, branch, self.meta.uuid, self.meta.subdir)
+            if not self.meta.usebranchnames:
                 extra.pop('branch', None)
             current_ctx = context.memctx(self.repo,
                                          (ha, node.nullid),
                                          rev.message or ' ',
                                          [],
                                          del_all_files,
-                                         self.authors[rev.author],
+                                         self.meta.authors[rev.author],
                                          date,
                                          extra)
             new_hash = self.repo.commitctx(current_ctx)
             util.describe_commit(self.ui, new_hash, branch)
-            if (rev.revnum, branch) not in self.revmap:
-                self.revmap[rev.revnum, branch] = new_hash
+            if (rev.revnum, branch) not in self.meta.revmap:
+                self.meta.revmap[rev.revnum, branch] = new_hash
 
         # 3. handle tags
         if tbdelta['tags'][0] or tbdelta['tags'][1]:
-            self.committags(tbdelta['tags'], rev, closebranches)
+            self.meta.committags(tbdelta['tags'], rev, closebranches)
 
         # 4. close any branches that need it
         for branch, parent in closebranches.iteritems():
             if parent is None:
                 continue
-            self.delbranch(branch, parent, rev)
+            self.meta.delbranch(branch, parent, rev)
 
-        self._save_metadata()
+        self.meta._save_metadata()
         self.current.clear()
 
     # Here come all the actual editor methods
 
     @ieditor
     def delete_entry(self, path, revision_bogus, parent_baton, pool=None):
-        br_path, branch = self._path_and_branch_for_path(path)
+        br_path, branch = self.meta._path_and_branch_for_path(path)
         if br_path == '':
-            self.current.closebranches.add(branch)
+            self.meta.closebranches.add(branch)
         if br_path is not None:
-            ha = self.get_parent_revision(self.current.rev.revnum, branch)
+            ha = self.meta.get_parent_revision(self.current.rev.revnum, branch)
             if ha == revlog.nullid:
                 return
             ctx = self.repo.changectx(ha)
@@ -758,7 +292,7 @@ class HgChangeReceiver(delta.Editor):
     @ieditor
     def open_file(self, path, parent_baton, base_revision, p=None):
         self.current.file = None
-        fpath, branch = self._path_and_branch_for_path(path)
+        fpath, branch = self.meta._path_and_branch_for_path(path)
         if not fpath:
             self.ui.debug('WARNING: Opening non-existant file %s\n' % path)
             return
@@ -776,10 +310,10 @@ class HgChangeReceiver(delta.Editor):
         baserev = base_revision
         if baserev is None or baserev == -1:
             baserev = self.current.rev.revnum - 1
-        parent = self.get_parent_revision(baserev + 1, branch)
+        parent = self.meta.get_parent_revision(baserev + 1, branch)
 
         ctx = self.repo[parent]
-        if not self._is_path_valid(path):
+        if not self.meta._is_path_valid(path):
             return
 
         if fpath not in ctx:
@@ -798,12 +332,12 @@ class HgChangeReceiver(delta.Editor):
         self.current.base = None
         if path in self.current.deleted:
             del self.current.deleted[path]
-        fpath, branch = self._path_and_branch_for_path(path, existing=False)
+        fpath, branch = self.meta._path_and_branch_for_path(path, existing=False)
         if not fpath:
             return
-        if branch not in self.branches:
+        if branch not in self.meta.branches:
             # we know this branch will exist now, because it has at least one file. Rock.
-            self.branches[branch] = None, 0, self.current.rev.revnum
+            self.meta.branches[branch] = None, 0, self.current.rev.revnum
         self.current.file = path
         if not copyfrom_path:
             self.ui.note('A %s\n' % path)
@@ -811,20 +345,20 @@ class HgChangeReceiver(delta.Editor):
             return
         self.ui.note('A+ %s\n' % path)
         (from_file,
-         from_branch) = self._path_and_branch_for_path(copyfrom_path)
+         from_branch) = self.meta._path_and_branch_for_path(copyfrom_path)
         if not from_file:
             self.current.missing.add(path)
             return
-        ha = self.get_parent_revision(copyfrom_revision + 1,
-                                      from_branch)
+        ha = self.meta.get_parent_revision(copyfrom_revision + 1,
+                                           from_branch)
         ctx = self.repo.changectx(ha)
         if from_file in ctx:
             fctx = ctx.filectx(from_file)
             flags = fctx.flags()
             self.set_file(path, fctx.data(), 'x' in flags, 'l' in flags)
         if from_branch == branch:
-            parentid = self.get_parent_revision(self.current.rev.revnum,
-                                                branch)
+            parentid = self.meta.get_parent_revision(self.current.rev.revnum,
+                                                     branch)
             if parentid != revlog.nullid:
                 parentctx = self.repo.changectx(parentid)
                 if util.aresamefiles(parentctx, ctx, [from_file]):
@@ -834,7 +368,7 @@ class HgChangeReceiver(delta.Editor):
     def add_directory(self, path, parent_baton, copyfrom_path,
                       copyfrom_revision, dir_pool=None):
         self.current.batons[path] = path
-        br_path, branch = self._path_and_branch_for_path(path)
+        br_path, branch = self.meta._path_and_branch_for_path(path)
         if br_path is not None:
             if not copyfrom_path and not br_path:
                 self.current.emptybranches[branch] = True
@@ -843,23 +377,23 @@ class HgChangeReceiver(delta.Editor):
         if br_path is None or not copyfrom_path:
             return path
         if copyfrom_path:
-            tag = self._is_path_tag(copyfrom_path)
-            if tag not in self.tags:
+            tag = self.meta._is_path_tag(copyfrom_path)
+            if tag not in self.meta.tags:
                 tag = None
-            if not self._is_path_valid(copyfrom_path) and not tag:
+            if not self.meta._is_path_valid(copyfrom_path) and not tag:
                 self.current.missing.add('%s/' % path)
                 return path
         if tag:
-            source_branch, source_rev = self.tags[tag]
+            source_branch, source_rev = self.meta.tags[tag]
             cp_f = ''
         else:
             source_rev = copyfrom_revision
-            cp_f, source_branch = self._path_and_branch_for_path(copyfrom_path)
+            cp_f, source_branch = self.meta._path_and_branch_for_path(copyfrom_path)
             if cp_f == '' and br_path == '':
                 assert br_path is not None
-                self.branches[branch] = source_branch, source_rev, self.current.rev.revnum
-        new_hash = self.get_parent_revision(source_rev + 1,
-                                            source_branch)
+                tmp = source_branch, source_rev, self.current.rev.revnum
+                self.meta.branches[branch] = tmp
+        new_hash = self.meta.get_parent_revision(source_rev + 1, source_branch)
         if new_hash == node.nullid:
             self.current.missing.add('%s/' % path)
             return path
@@ -883,7 +417,7 @@ class HgChangeReceiver(delta.Editor):
         if copies:
             # Preserve the directory copy records if no file was changed between
             # the source and destination revisions, or discard it completely.
-            parentid = self.get_parent_revision(self.current.rev.revnum, branch)
+            parentid = self.meta.get_parent_revision(self.current.rev.revnum, branch)
             if parentid != revlog.nullid:
                 parentctx = self.repo.changectx(parentid)
                 if util.aresamefiles(parentctx, cp_f_ctx, copies.values()):
@@ -908,7 +442,7 @@ class HgChangeReceiver(delta.Editor):
     @ieditor
     def open_directory(self, path, parent_baton, base_revision, dir_pool=None):
         self.current.batons[path] = path
-        p_, branch = self._path_and_branch_for_path(path)
+        p_, branch = self.meta._path_and_branch_for_path(path)
         if p_ == '':
             self.current.emptybranches[branch] = False
         return path
@@ -925,7 +459,7 @@ class HgChangeReceiver(delta.Editor):
         # 2) Missing a base text (bail quick since we have to fetch a full plaintext)
         # 3) Has a base text in self.current.files, apply deltas
         base = ''
-        if not self._is_path_valid(self.current.file):
+        if not self.meta._is_path_valid(self.current.file):
             return lambda x: None
         assert self.current.file not in self.current.deleted, (
             'Cannot apply_textdelta to a deleted file: %s' % self.current.file)
@@ -944,7 +478,7 @@ class HgChangeReceiver(delta.Editor):
                                'cannot call handler!')
         def txdelt_window(window):
             try:
-                if not self._is_path_valid(self.current.file):
+                if not self.meta._is_path_valid(self.current.file):
                     return
                 handler(window, baton)
                 # window being None means commit this file
--- a/hgsubversion/stupid.py
+++ b/hgsubversion/stupid.py
@@ -82,7 +82,7 @@ def filteriterhunks(hg_editor):
         applycurrent = False
         for data in iterhunks(ui, fp, sourcefile):
             if data[0] == 'file':
-                if data[1][1] in hg_editor.filemap:
+                if data[1][1] in hg_editor.meta.filemap:
                     applycurrent = True
                 else:
                     applycurrent = False
@@ -106,7 +106,7 @@ def diff_branchrev(ui, svn, hg_editor, b
         elif branch.startswith('../'):
             return branch[3:]
         return 'branches/%s' % branch
-    parent_rev, br_p = hg_editor.get_parent_svn_branch_and_rev(r.revnum, branch)
+    parent_rev, br_p = hg_editor.meta.get_parent_svn_branch_and_rev(r.revnum, branch)
     diff_path = make_diff_path(branch)
     try:
         if br_p == branch:
@@ -307,7 +307,7 @@ def getcopies(svn, hg_editor, branch, br
     def getctx(svnrev):
         if svnrev in ctxs:
             return ctxs[svnrev]
-        changeid = hg_editor.get_parent_revision(svnrev + 1, branch)
+        changeid = hg_editor.meta.get_parent_revision(svnrev + 1, branch)
         ctx = None
         if changeid != revlog.nullid:
             ctx = hg_editor.repo.changectx(changeid)
@@ -396,7 +396,7 @@ def fetch_branchrev(svn, hg_editor, bran
         for path, e in r.paths.iteritems():
             if not path.startswith(branchprefix):
                 continue
-            if not hg_editor._is_path_valid(path):
+            if not hg_editor.meta._is_path_valid(path):
                 continue
             kind = svn.checkpath(path, r.revnum)
             path = path[len(branchprefix):]
@@ -431,7 +431,7 @@ def fetch_branchrev(svn, hg_editor, bran
     return files, filectxfn
 
 def checkbranch(hg_editor, r, branch):
-    branchedits = hg_editor.branchedits(branch, r)
+    branchedits = hg_editor.meta.branchedits(branch, r)
     if not branchedits:
         return None
     branchtip = branchedits[0][1]
@@ -448,14 +448,14 @@ def branches_in_paths(hge, tbdelta, path
     branches = {}
     paths_need_discovery = []
     for p in paths:
-        relpath, branch, branchpath = hge._split_branch_path(p)
+        relpath, branch, branchpath = hge.meta._split_branch_path(p)
         if relpath is not None:
             branches[branch] = branchpath
-        elif paths[p].action == 'D' and not hge._is_path_tag(p):
-            ln = hge._localname(p)
+        elif paths[p].action == 'D' and not hge.meta._is_path_tag(p):
+            ln = hge.meta._localname(p)
             # must check in branches_to_delete as well, because this runs after we
             # already updated the branch map
-            if ln in hge.branches or ln in tbdelta['branches'][1]:
+            if ln in hge.meta.branches or ln in tbdelta['branches'][1]:
                 branches[ln] = p
         else:
             paths_need_discovery.append(p)
@@ -497,12 +497,12 @@ def branches_in_paths(hge, tbdelta, path
             path = filepaths.pop(0)
             parentdir = '/'.join(path[:-1])
             filepaths = [p for p in filepaths if not '/'.join(p).startswith(parentdir)]
-            branchpath = hge._normalize_path(parentdir)
+            branchpath = hge.meta._normalize_path(parentdir)
             if branchpath.startswith('tags/'):
                 continue
-            branchname = hge._localname(branchpath)
+            branchname = hge.meta._localname(branchpath)
             if branchpath.startswith('trunk/'):
-                branches[hge._localname('trunk')] = 'trunk'
+                branches[hge.meta._localname('trunk')] = 'trunk'
                 continue
             if branchname and branchname.startswith('../'):
                 continue
@@ -513,7 +513,7 @@ def branches_in_paths(hge, tbdelta, path
 def convert_rev(ui, hg_editor, svn, r, tbdelta):
     # this server fails at replay
 
-    hg_editor.save_tbdelta(tbdelta)
+    hg_editor.meta.save_tbdelta(tbdelta)
     branches = branches_in_paths(hg_editor, tbdelta, r.paths, r.revnum,
                                  svn.checkpath, svn.list_files)
     brpaths = branches.values()
@@ -529,27 +529,27 @@ def convert_rev(ui, hg_editor, svn, r, t
 
         # We've go a branch that contains other branches. We have to be careful to
         # get results similar to real replay in this case.
-        for existingbr in hg_editor.branches:
-            bad = hg_editor._remotename(existingbr)
+        for existingbr in hg_editor.meta.branches:
+            bad = hg_editor.meta._remotename(existingbr)
             if bad.startswith(bp) and len(bad) > len(bp):
                 bad_branch_paths[br].append(bad[len(bp)+1:])
 
     deleted_branches = {}
     for p in r.paths:
-        if hg_editor._is_path_tag(p):
+        if hg_editor.meta._is_path_tag(p):
             continue
-        branch = hg_editor._localname(p)
-        if not (r.paths[p].action == 'R' and branch in hg_editor.branches):
+        branch = hg_editor.meta._localname(p)
+        if not (r.paths[p].action == 'R' and branch in hg_editor.meta.branches):
             continue
         closed = checkbranch(hg_editor, r, branch)
         if closed is not None:
             deleted_branches[branch] = closed
 
-    date = hg_editor.fixdate(r.date)
+    date = hg_editor.meta.fixdate(r.date)
     check_deleted_branches = set()
     for b in branches:
 
-        parentctx = hg_editor.repo[hg_editor.get_parent_revision(r.revnum, b)]
+        parentctx = hg_editor.repo[hg_editor.meta.get_parent_revision(r.revnum, b)]
         if parentctx.branch() != (b or 'default'):
             check_deleted_branches.add(b)
 
@@ -586,7 +586,7 @@ def convert_rev(ui, hg_editor, svn, r, t
 
         if '' in files_touched:
             files_touched.remove('')
-        excluded = [f for f in files_touched if f not in hg_editor.filemap]
+        excluded = [f for f in files_touched if f not in hg_editor.meta.filemap]
         for f in excluded:
             files_touched.remove(f)
 
@@ -600,7 +600,7 @@ def convert_rev(ui, hg_editor, svn, r, t
                 assert f[0] != '/'
 
         extra = util.build_extra(r.revnum, b, svn.uuid, svn.subdir)
-        if not hg_editor.usebranchnames:
+        if not hg_editor.meta.usebranchnames:
             extra.pop('branch', None)
 
         current_ctx = context.memctx(hg_editor.repo,
@@ -608,15 +608,15 @@ def convert_rev(ui, hg_editor, svn, r, t
                                      r.message or util.default_commit_msg,
                                      files_touched,
                                      filectxfn,
-                                     hg_editor.authors[r.author],
+                                     hg_editor.meta.authors[r.author],
                                      date,
                                      extra)
         ha = hg_editor.repo.commitctx(current_ctx)
 
         branch = extra.get('branch', None)
-        if not branch in hg_editor.branches:
-            hg_editor.branches[branch] = None, 0, r.revnum
-        hg_editor.revmap[r.revnum, b] = ha
+        if not branch in hg_editor.meta.branches:
+            hg_editor.meta.branches[branch] = None, 0, r.revnum
+        hg_editor.meta.revmap[r.revnum, b] = ha
         util.describe_commit(ui, ha, b)
 
     # These are branches with an 'R' status in svn log. This means they were
@@ -627,12 +627,12 @@ def convert_rev(ui, hg_editor, svn, r, t
             deleted_branches[branch] = closed
 
     if tbdelta['tags'][0] or tbdelta['tags'][1]:
-        hg_editor.committags(tbdelta['tags'], r, deleted_branches)
+        hg_editor.meta.committags(tbdelta['tags'], r, deleted_branches)
 
     for b, parent in deleted_branches.iteritems():
         if parent == node.nullid:
             continue
-        hg_editor.delbranch(b, parent, r)
+        hg_editor.meta.delbranch(b, parent, r)
 
     # save the changed metadata
-    hg_editor._save_metadata()
+    hg_editor.meta._save_metadata()
--- a/hgsubversion/svncommands.py
+++ b/hgsubversion/svncommands.py
@@ -5,8 +5,6 @@ from mercurial import hg
 from mercurial import node
 from mercurial import util as hgutil
 
-
-import hg_delta_editor
 import svnwrap
 import util
 import utility_commands
copy from hgsubversion/hg_delta_editor.py
copy to hgsubversion/svnmeta.py
--- a/hgsubversion/hg_delta_editor.py
+++ b/hgsubversion/svnmeta.py
@@ -1,33 +1,15 @@
-import cStringIO
 import cPickle as pickle
 import os
-import sys
 import tempfile
-import traceback
 
 from mercurial import context
-from mercurial import hg
-from mercurial import ui
 from mercurial import util as hgutil
 from mercurial import revlog
 from mercurial import node
-from mercurial import error
-from svn import delta
-from svn import core
 
-import svnexternals
 import util
 import maps
 
-class MissingPlainTextError(Exception):
-    """Exception raised when the repo lacks a source file required for replaying
-    a txdelta.
-    """
-
-class ReplayException(Exception):
-    """Exception raised when you try and commit but the replay encountered an
-    exception.
-    """
 
 def pickle_atomic(data, file_path, dir=None):
     """pickle some data to a path atomically.
@@ -45,55 +27,8 @@ def pickle_atomic(data, file_path, dir=N
     else:
         hgutil.rename(path, file_path)
 
-def ieditor(fn):
-    """Helps identify methods used by the SVN editor interface.
-
-    Stash any exception raised in the method on self.
-
-    This is required because the SWIG bindings just mutate any exception into
-    a generic Subversion exception with no way of telling what the original was.
-    This allows the editor object to notice when you try and commit and really
-    got an exception in the replay process.
-    """
-    def fun(self, *args, **kwargs):
-        try:
-            return fn(self, *args, **kwargs)
-        except: #pragma: no cover
-            if not hasattr(self, '_exception_info'):
-                self._exception_info = sys.exc_info()
-            raise
-    return fun
-
-
-class RevisionData(object):
-
-    __slots__ = [
-        'file', 'files', 'deleted', 'rev', 'execfiles', 'symlinks', 'batons',
-        'copies', 'missing', 'emptybranches', 'base', 'closebranches',
-        'externals',
-    ]
-
-    def __init__(self):
-        self.clear()
-
-    def clear(self):
-        self.file = None
-        self.files = {}
-        self.deleted = {}
-        self.rev = None
-        self.execfiles = {}
-        self.symlinks = {}
-        self.batons = {}
-        # Map fully qualified destination file paths to module source path
-        self.copies = {}
-        self.missing = set()
-        self.emptybranches = {}
-        self.base = None
-        self.closebranches = set()
-        self.externals = {}
 
-
-class HgChangeReceiver(delta.Editor):
+class SVNMeta(object):
 
     def __init__(self, repo, uuid=None, subdir=''):
         """path is the path to the target hg repo.
@@ -139,7 +74,6 @@ class HgChangeReceiver(delta.Editor):
         self.tag_locations.sort()
         self.tag_locations.reverse()
 
-        self.current = RevisionData()
         self.authors = maps.AuthorMap(self.ui, self.authors_file,
                                  defaulthost=author_host)
         if authors: self.authors.load(authors)
@@ -304,6 +238,20 @@ class HgChangeReceiver(delta.Editor):
             return None, None, None
         return path, ln, test
 
+    def _path_and_branch_for_path(self, path, existing=True):
+        return self._split_branch_path(path, existing=existing)[:2]
+
+    def _determine_parent_branch(self, p, src_path, src_rev, revnum):
+        if src_path is not None:
+            src_file, src_branch = self._path_and_branch_for_path(src_path)
+            src_tag = self._is_path_tag(src_path)
+            if src_tag != False or src_file == '': # case 2
+                ln = self._localname(p)
+                if src_tag != False:
+                    src_branch, src_rev = self.tags[src_tag]
+                return {ln: (src_branch, src_rev, revnum)}
+        return {}
+
     def _is_path_valid(self, path):
         if path is None:
             return False
@@ -355,7 +303,7 @@ class HgChangeReceiver(delta.Editor):
         paths = revision.paths
         added_branches = {}
         added_tags = {}
-        self.current.closebranches = set()
+        self.closebranches = set()
         tags_to_delete = set()
         for p in sorted(paths):
             t_name = self._is_path_tag(p)
@@ -408,7 +356,7 @@ class HgChangeReceiver(delta.Editor):
             if fi is not None:
                 if fi == '':
                     if paths[p].action == 'D':
-                        self.current.closebranches.add(br) # case 4
+                        self.closebranches.add(br) # case 4
                     elif paths[p].action == 'R':
                         parent = self._determine_parent_branch(
                             p, paths[p].copyfrom_path, paths[p].copyfrom_rev,
@@ -417,7 +365,7 @@ class HgChangeReceiver(delta.Editor):
                 continue # case 1
             if paths[p].action == 'D':
                 for known in self.branches:
-                    if self._svnpath(known).startswith(p):
+                    if self._remotename(known).startswith(p):
                         self.current.closebranches.add(known) # case 5
             parent = self._determine_parent_branch(
                 p, paths[p].copyfrom_path, paths[p].copyfrom_rev, revision.revnum)
@@ -431,7 +379,7 @@ class HgChangeReceiver(delta.Editor):
         rmtags = dict((t, self.tags[t][0]) for t in tags_to_delete)
         return {
             'tags': (added_tags, rmtags),
-            'branches': (added_branches, self.current.closebranches),
+            'branches': (added_branches, self.closebranches),
         }
 
     def save_tbdelta(self, tbdelta):
@@ -514,449 +462,3 @@ class HgChangeReceiver(delta.Editor):
                              extra)
         new = self.repo.commitctx(ctx)
         self.ui.status('Marked branch %s as closed.\n' % (branch or 'default'))
-
-    def set_file(self, path, data, isexec=False, islink=False):
-        if islink:
-            data = 'link ' + data
-        self.current.files[path] = data
-        self.current.execfiles[path] = isexec
-        self.current.symlinks[path] = islink
-        if path in self.current.deleted:
-            del self.current.deleted[path]
-        if path in self.current.missing:
-            self.current.missing.remove(path)
-
-    def delete_file(self, path):
-        self.current.deleted[path] = True
-        if path in self.current.files:
-            del self.current.files[path]
-        self.current.execfiles[path] = False
-        self.current.symlinks[path] = False
-        self.ui.note('D %s\n' % path)
-
-    def _svnpath(self, branch):
-        """Return the relative path in svn of branch.
-        """
-        if branch == None or branch == 'default':
-            return 'trunk'
-        elif branch.startswith('../'):
-            return branch[3:]
-        return 'branches/%s' % branch
-
-    def _path_and_branch_for_path(self, path, existing=True):
-        return self._split_branch_path(path, existing=existing)[:2]
-
-    def _branch_for_path(self, path, existing=True):
-        return self._path_and_branch_for_path(path, existing=existing)[1]
-
-    def _determine_parent_branch(self, p, src_path, src_rev, revnum):
-        if src_path is not None:
-            src_file, src_branch = self._path_and_branch_for_path(src_path)
-            src_tag = self._is_path_tag(src_path)
-            if src_tag != False:
-                # also case 2
-                src_branch, src_rev = self.tags[src_tag]
-                return {self._localname(p): (src_branch, src_rev, revnum )}
-            if src_file == '':
-                # case 2
-                return {self._localname(p): (src_branch, src_rev, revnum )}
-        return {}
-
-    def _updateexternals(self):
-        if not self.current.externals:
-            return
-        # Accumulate externals records for all branches
-        revnum = self.current.rev.revnum
-        branches = {}
-        for path, entry in self.current.externals.iteritems():
-            if not self._is_path_valid(path):
-                self.ui.warn('WARNING: Invalid path %s in externals\n' % path)
-                continue
-            p, b, bp = self._split_branch_path(path)
-            if bp not in branches:
-                external = svnexternals.externalsfile()
-                parent = self.get_parent_revision(revnum, b)
-                pctx = self.repo[parent]
-                if '.hgsvnexternals' in pctx:
-                    external.read(pctx['.hgsvnexternals'].data())
-                branches[bp] = external
-            else:
-                external = branches[bp]
-            external[p] = entry
-
-        # Register the file changes
-        for bp, external in branches.iteritems():
-            path = bp + '/.hgsvnexternals'
-            if external:
-                self.set_file(path, external.write(), False, False)
-            else:
-                self.delete_file(path)
-
-    def commit_current_delta(self, tbdelta):
-        if hasattr(self, '_exception_info'):  #pragma: no cover
-            traceback.print_exception(*self._exception_info)
-            raise ReplayException()
-        if self.current.missing:
-            raise MissingPlainTextError()
-        self._updateexternals()
-        # paranoidly generate the list of files to commit
-        files_to_commit = set(self.current.files.keys())
-        files_to_commit.update(self.current.symlinks.keys())
-        files_to_commit.update(self.current.execfiles.keys())
-        files_to_commit.update(self.current.deleted.keys())
-        # back to a list and sort so we get sane behavior
-        files_to_commit = list(files_to_commit)
-        files_to_commit.sort()
-        branch_batches = {}
-        rev = self.current.rev
-        date = self.fixdate(rev.date)
-
-        # build up the branches that have files on them
-        for f in files_to_commit:
-            if not  self._is_path_valid(f):
-                continue
-            p, b = self._path_and_branch_for_path(f)
-            if b not in branch_batches:
-                branch_batches[b] = []
-            branch_batches[b].append((p, f))
-
-        closebranches = {}
-        for branch in tbdelta['branches'][1]:
-            branchedits = self.branchedits(branch, rev)
-            if len(branchedits) < 1:
-                # can't close a branch that never existed
-                continue
-            ha = branchedits[0][1]
-            closebranches[branch] = ha
-
-        # 1. handle normal commits
-        closedrevs = closebranches.values()
-        for branch, files in branch_batches.iteritems():
-            if branch in self.current.emptybranches and files:
-                del self.current.emptybranches[branch]
-            files = dict(files)
-
-            parents = (self.get_parent_revision(rev.revnum, branch),
-                       revlog.nullid)
-            if parents[0] in closedrevs and branch in self.current.closebranches:
-                continue
-            extra = util.build_extra(rev.revnum, branch, self.uuid, self.subdir)
-            if branch is not None:
-                if (branch not in self.branches
-                    and branch not in self.repo.branchtags()):
-                    continue
-            parent_ctx = self.repo.changectx(parents[0])
-            if '.hgsvnexternals' not in parent_ctx and '.hgsvnexternals' in files:
-                # Do not register empty externals files
-                if (files['.hgsvnexternals'] in self.current.files
-                    and not self.current.files[files['.hgsvnexternals']]):
-                    del files['.hgsvnexternals']
-
-            def filectxfn(repo, memctx, path):
-                current_file = files[path]
-                if current_file in self.current.deleted:
-                    raise IOError()
-                copied = self.current.copies.get(current_file)
-                flags = parent_ctx.flags(path)
-                is_exec = self.current.execfiles.get(current_file, 'x' in flags)
-                is_link = self.current.symlinks.get(current_file, 'l' in flags)
-                if current_file in self.current.files:
-                    data = self.current.files[current_file]
-                    if is_link and data.startswith('link '):
-                        data = data[len('link '):]
-                    elif is_link:
-                        self.ui.warn('file marked as link, but contains data: '
-                                     '%s (%r)\n' % (current_file, flags))
-                else:
-                    data = parent_ctx.filectx(path).data()
-                return context.memfilectx(path=path,
-                                          data=data,
-                                          islink=is_link, isexec=is_exec,
-                                          copied=copied)
-            if not self.usebranchnames:
-                extra.pop('branch', None)
-            current_ctx = context.memctx(self.repo,
-                                         parents,
-                                         rev.message or '...',
-                                         files.keys(),
-                                         filectxfn,
-                                         self.authors[rev.author],
-                                         date,
-                                         extra)
-            new_hash = self.repo.commitctx(current_ctx)
-            util.describe_commit(self.ui, new_hash, branch)
-            if (rev.revnum, branch) not in self.revmap:
-                self.revmap[rev.revnum, branch] = new_hash
-
-        # 2. handle branches that need to be committed without any files
-        for branch in self.current.emptybranches:
-            ha = self.get_parent_revision(rev.revnum, branch)
-            if ha == node.nullid:
-                continue
-            parent_ctx = self.repo.changectx(ha)
-            def del_all_files(*args):
-                raise IOError
-            # True here meant nuke all files, shouldn't happen with branch closing
-            if self.current.emptybranches[branch]: #pragma: no cover
-               raise hgutil.Abort('Empty commit to an open branch attempted. '
-                                  'Please report this issue.')
-            extra = util.build_extra(rev.revnum, branch, self.uuid, self.subdir)
-            if not self.usebranchnames:
-                extra.pop('branch', None)
-            current_ctx = context.memctx(self.repo,
-                                         (ha, node.nullid),
-                                         rev.message or ' ',
-                                         [],
-                                         del_all_files,
-                                         self.authors[rev.author],
-                                         date,
-                                         extra)
-            new_hash = self.repo.commitctx(current_ctx)
-            util.describe_commit(self.ui, new_hash, branch)
-            if (rev.revnum, branch) not in self.revmap:
-                self.revmap[rev.revnum, branch] = new_hash
-
-        # 3. handle tags
-        if tbdelta['tags'][0] or tbdelta['tags'][1]:
-            self.committags(tbdelta['tags'], rev, closebranches)
-
-        # 4. close any branches that need it
-        for branch, parent in closebranches.iteritems():
-            if parent is None:
-                continue
-            self.delbranch(branch, parent, rev)
-
-        self._save_metadata()
-        self.current.clear()
-
-    # Here come all the actual editor methods
-
-    @ieditor
-    def delete_entry(self, path, revision_bogus, parent_baton, pool=None):
-        br_path, branch = self._path_and_branch_for_path(path)
-        if br_path == '':
-            self.current.closebranches.add(branch)
-        if br_path is not None:
-            ha = self.get_parent_revision(self.current.rev.revnum, branch)
-            if ha == revlog.nullid:
-                return
-            ctx = self.repo.changectx(ha)
-            if br_path not in ctx:
-                br_path2 = ''
-                if br_path != '':
-                    br_path2 = br_path + '/'
-                # assuming it is a directory
-                self.current.externals[path] = None
-                map(self.delete_file, [pat for pat in self.current.files.iterkeys()
-                                       if pat.startswith(path+'/')])
-                for f in ctx.walk(util.PrefixMatch(br_path2)):
-                    f_p = '%s/%s' % (path, f[len(br_path2):])
-                    if f_p not in self.current.files:
-                        self.delete_file(f_p)
-            self.delete_file(path)
-
-    @ieditor
-    def open_file(self, path, parent_baton, base_revision, p=None):
-        self.current.file = None
-        fpath, branch = self._path_and_branch_for_path(path)
-        if not fpath:
-            self.ui.debug('WARNING: Opening non-existant file %s\n' % path)
-            return
-
-        self.current.file = path
-        self.ui.note('M %s\n' % path)
-        if base_revision != -1:
-            self.current.base = base_revision
-        else:
-            self.current.base = None
-
-        if self.current.file in self.current.files:
-            return
-
-        baserev = base_revision
-        if baserev is None or baserev == -1:
-            baserev = self.current.rev.revnum - 1
-        parent = self.get_parent_revision(baserev + 1, branch)
-
-        ctx = self.repo[parent]
-        if not self._is_path_valid(path):
-            return
-
-        if fpath not in ctx:
-            self.current.missing.add(path)
-
-        fctx = ctx.filectx(fpath)
-        base = fctx.data()
-        if 'l' in fctx.flags():
-            base = 'link ' + base
-        self.set_file(path, base, 'x' in fctx.flags(), 'l' in fctx.flags())
-
-    @ieditor
-    def add_file(self, path, parent_baton=None, copyfrom_path=None,
-                 copyfrom_revision=None, file_pool=None):
-        self.current.file = None
-        self.current.base = None
-        if path in self.current.deleted:
-            del self.current.deleted[path]
-        fpath, branch = self._path_and_branch_for_path(path, existing=False)
-        if not fpath:
-            return
-        if branch not in self.branches:
-            # we know this branch will exist now, because it has at least one file. Rock.
-            self.branches[branch] = None, 0, self.current.rev.revnum
-        self.current.file = path
-        if not copyfrom_path:
-            self.ui.note('A %s\n' % path)
-            self.set_file(path, '', False, False)
-            return
-        self.ui.note('A+ %s\n' % path)
-        (from_file,
-         from_branch) = self._path_and_branch_for_path(copyfrom_path)
-        if not from_file:
-            self.current.missing.add(path)
-            return
-        ha = self.get_parent_revision(copyfrom_revision + 1,
-                                      from_branch)
-        ctx = self.repo.changectx(ha)
-        if from_file in ctx:
-            fctx = ctx.filectx(from_file)
-            flags = fctx.flags()
-            self.set_file(path, fctx.data(), 'x' in flags, 'l' in flags)
-        if from_branch == branch:
-            parentid = self.get_parent_revision(self.current.rev.revnum,
-                                                branch)
-            if parentid != revlog.nullid:
-                parentctx = self.repo.changectx(parentid)
-                if util.aresamefiles(parentctx, ctx, [from_file]):
-                    self.current.copies[path] = from_file
-
-    @ieditor
-    def add_directory(self, path, parent_baton, copyfrom_path,
-                      copyfrom_revision, dir_pool=None):
-        self.current.batons[path] = path
-        br_path, branch = self._path_and_branch_for_path(path)
-        if br_path is not None:
-            if not copyfrom_path and not br_path:
-                self.current.emptybranches[branch] = True
-            else:
-                self.current.emptybranches[branch] = False
-        if br_path is None or not copyfrom_path:
-            return path
-        if copyfrom_path:
-            tag = self._is_path_tag(copyfrom_path)
-            if tag not in self.tags:
-                tag = None
-            if not self._is_path_valid(copyfrom_path) and not tag:
-                self.current.missing.add('%s/' % path)
-                return path
-        if tag:
-            source_branch, source_rev = self.tags[tag]
-            cp_f = ''
-        else:
-            source_rev = copyfrom_revision
-            cp_f, source_branch = self._path_and_branch_for_path(copyfrom_path)
-            if cp_f == '' and br_path == '':
-                assert br_path is not None
-                self.branches[branch] = source_branch, source_rev, self.current.rev.revnum
-        new_hash = self.get_parent_revision(source_rev + 1,
-                                            source_branch)
-        if new_hash == node.nullid:
-            self.current.missing.add('%s/' % path)
-            return path
-        cp_f_ctx = self.repo.changectx(new_hash)
-        if cp_f != '/' and cp_f != '':
-            cp_f = '%s/' % cp_f
-        else:
-            cp_f = ''
-        copies = {}
-        for f in cp_f_ctx:
-            if not f.startswith(cp_f):
-                continue
-            f2 = f[len(cp_f):]
-            fctx = cp_f_ctx.filectx(f)
-            fp_c = path + '/' + f2
-            self.set_file(fp_c, fctx.data(), 'x' in fctx.flags(), 'l' in fctx.flags())
-            if fp_c in self.current.deleted:
-                del self.current.deleted[fp_c]
-            if branch == source_branch:
-                copies[fp_c] = f
-        if copies:
-            # Preserve the directory copy records if no file was changed between
-            # the source and destination revisions, or discard it completely.
-            parentid = self.get_parent_revision(self.current.rev.revnum, branch)
-            if parentid != revlog.nullid:
-                parentctx = self.repo.changectx(parentid)
-                if util.aresamefiles(parentctx, cp_f_ctx, copies.values()):
-                    self.current.copies.update(copies)
-        return path
-
-    @ieditor
-    def change_file_prop(self, file_baton, name, value, pool=None):
-        if name == 'svn:executable':
-            self.current.execfiles[self.current.file] = bool(value is not None)
-        elif name == 'svn:special':
-            self.current.symlinks[self.current.file] = bool(value is not None)
-
-    @ieditor
-    def change_dir_prop(self, dir_baton, name, value, pool=None):
-        if dir_baton is None:
-            return
-        path = self.current.batons[dir_baton]
-        if name == 'svn:externals':
-            self.current.externals[path] = value
-
-    @ieditor
-    def open_directory(self, path, parent_baton, base_revision, dir_pool=None):
-        self.current.batons[path] = path
-        p_, branch = self._path_and_branch_for_path(path)
-        if p_ == '':
-            self.current.emptybranches[branch] = False
-        return path
-
-    @ieditor
-    def close_directory(self, dir_baton, dir_pool=None):
-        if dir_baton is not None:
-            del self.current.batons[dir_baton]
-
-    @ieditor
-    def apply_textdelta(self, file_baton, base_checksum, pool=None):
-        # We know coming in here the file must be one of the following options:
-        # 1) Deleted (invalid, fail an assertion)
-        # 2) Missing a base text (bail quick since we have to fetch a full plaintext)
-        # 3) Has a base text in self.current.files, apply deltas
-        base = ''
-        if not self._is_path_valid(self.current.file):
-            return lambda x: None
-        assert self.current.file not in self.current.deleted, (
-            'Cannot apply_textdelta to a deleted file: %s' % self.current.file)
-        assert (self.current.file in self.current.files
-                or self.current.file in self.current.missing), '%s not found' % self.current.file
-        if self.current.file in self.current.missing:
-            return lambda x: None
-        base = self.current.files[self.current.file]
-        source = cStringIO.StringIO(base)
-        target = cStringIO.StringIO()
-        self.stream = target
-
-        handler, baton = delta.svn_txdelta_apply(source, target, None)
-        if not callable(handler): #pragma: no cover
-            raise hgutil.Abort('Error in Subversion bindings: '
-                               'cannot call handler!')
-        def txdelt_window(window):
-            try:
-                if not self._is_path_valid(self.current.file):
-                    return
-                handler(window, baton)
-                # window being None means commit this file
-                if not window:
-                    self.current.files[self.current.file] = target.getvalue()
-            except core.SubversionException, e: #pragma: no cover
-                if e.apr_err == core.SVN_ERR_INCOMPLETE_DATA:
-                    self.current.missing.add(self.current.file)
-                else: #pragma: no cover
-                    raise hgutil.Abort(*e.args)
-            except: #pragma: no cover
-                print len(base), self.current.file
-                self._exception_info = sys.exc_info()
-                raise
-        return txdelt_window
--- a/hgsubversion/utility_commands.py
+++ b/hgsubversion/utility_commands.py
@@ -19,7 +19,7 @@ def genignore(ui, repo, hg_repo_path, fo
     user, passwd = util.getuserpass(opts)
     svn = svnwrap.SubversionRepo(url, user, passwd)
     hge = hg_delta_editor.HgChangeReceiver(repo, svn.uuid)
-    hashes = hge.hashes()
+    hashes = hge.meta.hashes()
     parent = cmdutil.parentrev(ui, repo, hge, hashes)
     r, br = hashes[parent.node()]
     if br == None:
@@ -47,7 +47,7 @@ def info(ui, repo, hg_repo_path, **opts)
     user, passwd = util.getuserpass(opts)
     svn = svnwrap.SubversionRepo(url, user, passwd)
     hge = hg_delta_editor.HgChangeReceiver(repo, svn.uuid)
-    hashes = hge.hashes()
+    hashes = hge.meta.hashes()
     parent = cmdutil.parentrev(ui, repo, hge, hashes)
     pn = parent.node()
     if pn not in hashes:
@@ -66,7 +66,7 @@ def info(ui, repo, hg_repo_path, **opts)
     if url[-1] == '/':
         url = url[:-1]
     url = '%s%s' % (url, branchpath)
-    author = hge.authors.reverselookup(parent.user())
+    author = hge.meta.authors.reverselookup(parent.user())
     # cleverly figure out repo root w/o actually contacting the server
     reporoot = url[:len(url)-len(subdir)]
     ui.status('''URL: %(url)s
@@ -78,7 +78,7 @@ Last Changed Author: %(author)s
 Last Changed Rev: %(revision)s
 Last Changed Date: %(date)s\n''' %
               {'reporoot': reporoot,
-               'uuid': hge.uuid,
+               'uuid': hge.meta.uuid,
                'url': url,
                'author': author,
                'revision': r,
--- a/hgsubversion/wrappers.py
+++ b/hgsubversion/wrappers.py
@@ -37,7 +37,7 @@ def parents(orig, ui, repo, *args, **opt
     if not opts.get('svn', False):
         return orig(ui, repo, *args, **opts)
     hge = hg_delta_editor.HgChangeReceiver(repo)
-    hashes = hge.hashes()
+    hashes = hge.meta.hashes()
     ha = cmdutil.parentrev(ui, repo, hge, hashes)
     if ha.node() == node.nullid:
         raise hgutil.Abort('No parent svn revision!')
@@ -58,7 +58,7 @@ def incoming(orig, ui, repo, source='def
     user, passwd = util.getuserpass(opts)
     svn = svnwrap.SubversionRepo(other.svnurl, user, passwd)
     hg_editor = hg_delta_editor.HgChangeReceiver(repo)
-    start = hg_editor.last_known_revision()
+    start = hg_editor.meta.last_known_revision()
 
     ui.status('incoming changes from %s\n' % other.svnurl)
     for r in svn.revisions(start=start):
@@ -80,7 +80,7 @@ def outgoing(repo, dest=None, heads=None
     svnurl, revs, checkout = hg.parseurl(dest.svnurl, heads)
     hge = hg_delta_editor.HgChangeReceiver(repo)
     parent = repo.parents()[0].node()
-    return util.outgoing_revisions(repo.ui, repo, hge, hge.hashes(), parent)
+    return util.outgoing_revisions(repo.ui, repo, hge, hge.meta.hashes(), parent)
 
 
 def diff(orig, ui, repo, *args, **opts):
@@ -89,7 +89,7 @@ def diff(orig, ui, repo, *args, **opts):
     if not opts.get('svn', False) or opts.get('change', None):
         return orig(ui, repo, *args, **opts)
     hge = hg_delta_editor.HgChangeReceiver(repo)
-    hashes = hge.hashes()
+    hashes = hge.meta.hashes()
     if not opts.get('rev', None):
         parent = repo.parents()[0]
         o_r = util.outgoing_revisions(ui, repo, hge, hashes, parent.node())
@@ -132,7 +132,7 @@ def push(repo, dest, force, revs):
         return 1
     workingrev = repo.parents()[0]
     ui.status('searching for changes\n')
-    hashes = hge.hashes()
+    hashes = hge.meta.hashes()
     outgoing = util.outgoing_revisions(ui, repo, hge, hashes, workingrev.node())
     if not (outgoing and len(outgoing)):
         ui.status('no changes found\n')
@@ -183,7 +183,7 @@ def push(repo, dest, force, revs):
             # TODO: can we avoid calling our own rebase wrapper here?
             rebase(hgrebase.rebase, ui, repo, svn=True, svnextrafn=extrafn,
                    svnsourcerev=needs_transplant)
-            repo = hg.repository(ui, hge.path)
+            repo = hg.repository(ui, hge.meta.path)
             for child in repo[replacement.node()].children():
                 rebasesrc = node.bin(child.extra().get('rebase_source', node.hex(node.nullid)))
                 if rebasesrc in outgoing:
@@ -197,7 +197,7 @@ def push(repo, dest, force, revs):
                         rebasesrc = node.bin(child.extra().get('rebase_source', node.hex(node.nullid)))
         # TODO: stop constantly creating the HgChangeReceiver instances.
         hge = hg_delta_editor.HgChangeReceiver(hge.repo, svn.uuid)
-        hashes = hge.hashes()
+        hashes = hge.meta.hashes()
     util.swap_out_encoding(old_encoding)
     return 0
 
@@ -237,8 +237,8 @@ def pull(repo, source, heads=[], force=F
     svn = svnwrap.SubversionRepo(svn_url, user, passwd)
     hg_editor = hg_delta_editor.HgChangeReceiver(repo, svn.uuid, svn.subdir)
 
-    start = max(hg_editor.last_known_revision(), skipto_rev)
-    initializing_repo = (hg_editor.last_known_revision() <= 0)
+    start = max(hg_editor.meta.last_known_revision(), skipto_rev)
+    initializing_repo = (hg_editor.meta.last_known_revision() <= 0)
     ui = repo.ui
 
     if initializing_repo and start > 0:
@@ -253,7 +253,7 @@ def pull(repo, source, heads=[], force=F
                 if (r.author is None and
                     r.message == 'This is an empty revision for padding.'):
                     continue
-                tbdelta = hg_editor.update_branch_tag_map_for_rev(r)
+                tbdelta = hg_editor.meta.update_branch_tag_map_for_rev(r)
                 # got a 502? Try more than once!
                 tries = 0
                 converted = False
@@ -305,7 +305,7 @@ def rebase(orig, ui, repo, **opts):
     extrafn = opts.get('svnextrafn', extrafn2)
     sourcerev = opts.get('svnsourcerev', repo.parents()[0].node())
     hge = hg_delta_editor.HgChangeReceiver(repo)
-    hashes = hge.hashes()
+    hashes = hge.meta.hashes()
     o_r = util.outgoing_revisions(ui, repo, hge, hashes, sourcerev=sourcerev)
     if not o_r:
         ui.status('Nothing to rebase!\n')
--- a/tests/test_rebuildmeta.py
+++ b/tests/test_rebuildmeta.py
@@ -38,7 +38,7 @@ def _do_case(self, name, stupid):
     srcbi = pickle.load(open(os.path.join(src.path, 'svn', 'branch_info')))
     destbi = pickle.load(open(os.path.join(dest.path, 'svn', 'branch_info')))
     self.assertEqual(sorted(srcbi.keys()), sorted(destbi.keys()))
-    revkeys = hg_delta_editor.HgChangeReceiver(dest).revmap.keys()
+    revkeys = hg_delta_editor.HgChangeReceiver(dest).meta.revmap.keys()
     for branch in destbi:
         srcinfo = srcbi[branch]
         destinfo = destbi[branch]