diff hgsubversion/svnmeta.py @ 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 hgsubversion/hg_delta_editor.py@ac0cc3c9ea63
children b17b2969861c
line wrap: on
line diff
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