diff hgsubversion/svnwrap/subvertpy_wrapper.py @ 676:2a9c009790ce

svnwrap: add subvertpy wrapper Subvertpy is, in many ways, a better interface to Subversion than the SWIG bindings. It's faster, leaks less and offers a cleaner API. The added wrapper is able to coexist with the SWIG wrapper, and not enabled by default. In order to allow this, the wrapper adapts the output from Subvertpy so that it is similar to the output from the SWIG bindings. An example of this can be seen in the modules that work with editors: the nested editors offered by Subvertpy had to be flattened to work with our editor code. This change does not activate the Subvertpy wrapper, yet, and thus does not affect any functionality.
author Dan Villiom Podlaski Christiansen <danchr@gmail.com>
date Wed, 11 Aug 2010 19:57:35 +0200
parents hgsubversion/svnwrap/svn_swig_wrapper.py@31cd9f41ec09
children e32ed1802478
line wrap: on
line diff
copy from hgsubversion/svnwrap/svn_swig_wrapper.py
copy to hgsubversion/svnwrap/subvertpy_wrapper.py
--- a/hgsubversion/svnwrap/svn_swig_wrapper.py
+++ b/hgsubversion/svnwrap/subvertpy_wrapper.py
@@ -16,148 +16,147 @@ warnings.filterwarnings('ignore',
                         module='svn.core',
                         category=DeprecationWarning)
 
-required_bindings = (1, 5, 0)
+subvertpy_required = (0, 7, 3)
+subversion_required = (1, 5, 0)
 
 try:
-    from svn import client
-    from svn import core
-    from svn import delta
-    from svn import ra
-
-    current_bindings = (core.SVN_VER_MAJOR, core.SVN_VER_MINOR,
-                        core.SVN_VER_MICRO)
+    from subvertpy import client
+    from subvertpy import delta
+    from subvertpy import properties
+    from subvertpy import ra
+    import subvertpy
 except ImportError:
-    raise ImportError('Subversion %d.%d.%d or later required, '
-                      'but no bindings were found' % required_bindings)
+    raise ImportError('subvertpy %d.%d.%d or later required, but not found'
+                      % subvertpy_required)
+
+if subvertpy.__version__ < subvertpy_required: #pragma: no cover
+    raise ImportError('subvertpy %d.%d.%d or later required, '
+                      'but %d.%d.%d found' %
+                      (subvertpy_required + subvertpy.__version__))
 
-if current_bindings < required_bindings: #pragma: no cover
+if subvertpy.wc.version()[:3] < subversion_required:
     raise ImportError('Subversion %d.%d.%d or later required, '
-                      'but bindings for %d.%d.%d found' %
-                      (required_bindings + current_bindings))
+                      'but %d.%d.%d found' %
+                      (subversion_required + subvertpy.wc.version()[:3]))
+
 
 def version():
-    return '%d.%d.%d' % current_bindings, 'SWIG'
+    return ('%d.%d.%d' % subvertpy.wc.version()[:3],
+            'Subvertpy %d.%d.%d' % subvertpy.__version__)
 
 # exported values
-ERR_FS_CONFLICT = core.SVN_ERR_FS_CONFLICT
-ERR_FS_NOT_FOUND = core.SVN_ERR_FS_NOT_FOUND
-ERR_FS_TXN_OUT_OF_DATE = core.SVN_ERR_FS_TXN_OUT_OF_DATE
-ERR_INCOMPLETE_DATA = core.SVN_ERR_INCOMPLETE_DATA
-ERR_RA_DAV_REQUEST_FAILED = core.SVN_ERR_RA_DAV_REQUEST_FAILED
-SubversionException = core.SubversionException
-Editor = delta.Editor
-
-def apply_txdelta(base, target):
-    handler, baton = delta.svn_txdelta_apply(cStringIO.StringIO(base),
-                                             target, None)
-    return (lambda window: handler(window, baton))
-
-def optrev(revnum):
-    optrev = core.svn_opt_revision_t()
-    optrev.kind = core.svn_opt_revision_number
-    optrev.value.number = revnum
-    return optrev
-
-svn_config = core.svn_config_get_config(None)
-class RaCallbacks(ra.Callbacks):
-    @staticmethod
-    def open_tmp_file(pool): #pragma: no cover
-        (fd, fn) = tempfile.mkstemp()
-        os.close(fd)
-        return fn
-
-    @staticmethod
-    def get_client_string(pool):
-        return 'hgsubversion'
+ERR_FS_CONFLICT = subvertpy.ERR_FS_CONFLICT
+ERR_FS_NOT_FOUND = subvertpy.ERR_FS_NOT_FOUND
+ERR_FS_TXN_OUT_OF_DATE = subvertpy.ERR_FS_TXN_OUT_OF_DATE
+ERR_INCOMPLETE_DATA = subvertpy.ERR_INCOMPLETE_DATA
+ERR_RA_DAV_PATH_NOT_FOUND = subvertpy.ERR_RA_DAV_PATH_NOT_FOUND
+ERR_RA_DAV_REQUEST_FAILED = subvertpy.ERR_RA_DAV_REQUEST_FAILED
+SubversionException = subvertpy.SubversionException
+apply_txdelta = delta.apply_txdelta_handler
+# superclass for editor.HgEditor
+Editor = object
 
 def ieditor(fn):
-    """Helps identify methods used by the SVN editor interface.
-
-    Stash any exception raised in the method on self.
+    """No-op decorator to identify methods used by the SVN editor interface.
 
-    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.
+    This decorator is not needed for Subvertpy, but is retained for
+    compatibility with the SWIG bindings.
     """
-    def fun(self, *args, **kwargs):
-        try:
-            return fn(self, *args, **kwargs)
-        except: #pragma: no cover
-            if self.current.exception is not None:
-                self.current.exception = sys.exc_info()
-            raise
-    return fun
-
-def user_pass_prompt(realm, default_username, ms, pool): #pragma: no cover
-    # FIXME: should use getpass() and username() from mercurial.ui
-    creds = core.svn_auth_cred_simple_t()
-    creds.may_save = ms
-    if default_username:
-        sys.stderr.write('Auth realm: %s\n' % (realm,))
-        creds.username = default_username
-    else:
-        sys.stderr.write('Auth realm: %s\n' % (realm,))
-        sys.stderr.write('Username: ')
-        sys.stderr.flush()
-        creds.username = sys.stdin.readline().strip()
-    creds.password = getpass.getpass('Password for %s: ' % creds.username)
-    return creds
-
-def _create_auth_baton(pool):
-    """Create a Subversion authentication baton. """
-    # Give the client context baton a suite of authentication
-    # providers.h
-    platform_specific = ['svn_auth_get_gnome_keyring_simple_provider',
-                         'svn_auth_get_gnome_keyring_ssl_client_cert_pw_provider',
-                         'svn_auth_get_keychain_simple_provider',
-                         'svn_auth_get_keychain_ssl_client_cert_pw_provider',
-                         'svn_auth_get_kwallet_simple_provider',
-                         'svn_auth_get_kwallet_ssl_client_cert_pw_provider',
-                         'svn_auth_get_ssl_client_cert_file_provider',
-                         'svn_auth_get_windows_simple_provider',
-                         'svn_auth_get_windows_ssl_server_trust_provider',
-                         ]
-
-    providers = []
-    # Platform-dependant authentication methods
-    getprovider = getattr(core, 'svn_auth_get_platform_specific_provider',
-                          None)
-    if getprovider:
-        # Available in svn >= 1.6
-        for name in ('gnome_keyring', 'keychain', 'kwallet', 'windows'):
-            for type in ('simple', 'ssl_client_cert_pw', 'ssl_server_trust'):
-                p = getprovider(name, type, pool)
-                if p:
-                    providers.append(p)
-    else:
-        for p in platform_specific:
-            if hasattr(core, p):
-                try:
-                    providers.append(getattr(core, p)())
-                except RuntimeError:
-                    pass
-
-    providers += [
-        client.get_simple_provider(),
-        client.get_username_provider(),
-        client.get_ssl_client_cert_file_provider(),
-        client.get_ssl_client_cert_pw_file_provider(),
-        client.get_ssl_server_trust_file_provider(),
-        client.get_simple_prompt_provider(user_pass_prompt, 2),
-        ]
 
-    return core.svn_auth_open(providers, pool)
+    return fn
 
 _svntypes = {
-    core.svn_node_dir: 'd',
-    core.svn_node_file: 'f',
-    }
+    subvertpy.NODE_DIR: 'd',
+    subvertpy.NODE_FILE: 'f',
+}
+
+class PathAdapter(object):
+    __slots__ = ('action', 'copyfrom_path', 'copyfrom_rev')
+
+    def __init__(self, path):
+        self.action, self.copyfrom_path, self.copyfrom_rev = path
+        if self.copyfrom_path:
+            self.copyfrom_path = intern(self.copyfrom_path)
+
+class AbstractEditor(object):
+    __slots__ = ('editor', )
+
+    def __init__(self, editor):
+        self.editor = editor
+
+    def set_target_revision(self, rev):
+        pass
+
+    def open_root(self, base_revnum):
+        return self.open_directory('', base_revnum)
+
+    def open_directory(self, path, base_revnum):
+        self.editor.open_directory(path, None, base_revnum)
+        return DirectoryEditor(self.editor, path)
+
+    def open_file(self, path, base_revnum):
+        self.editor.open_file(path, None, base_revnum)
+        return FileEditor(self.editor, path)
+
+    def add_directory(self, path, copyfrom_path=None, copyfrom_rev=-1):
+        self.editor.add_directory(path, None, copyfrom_path, copyfrom_rev)
+        return DirectoryEditor(self.editor, path)
+
+    def add_file(self, path, copyfrom_path=None, copyfrom_rev=-1):
+        self.editor.add_file(path, None, copyfrom_path, copyfrom_rev)
+        return FileEditor(self.editor, path)
+
+    def apply_textdelta(self, base_checksum):
+        return self.editor.apply_textdelta(self, None, base_checksum)
+
+    def change_prop(self, name, value):
+        raise NotImplementedError()
+
+    def abort(self):
+        # TODO: should we do something special here?
+        self.close()
+
+    def close(self):
+        del self.editor
+
+    def delete_entry(self, path, revnum):
+        self.editor.delete_entry(path, revnum, None)
+
+class FileEditor(AbstractEditor):
+    __slots__ = ('path', )
+
+    def __init__(self, editor, path):
+        super(FileEditor, self).__init__(editor)
+        self.path = path
+
+    def change_prop(self, name, value):
+        self.editor.change_file_prop(self.path, name, value, pool=None)
+
+    def close(self, checksum=None):
+        super(FileEditor, self).close()
+        del self.path
+
+class DirectoryEditor(AbstractEditor):
+    __slots__ = ('path', )
+
+    def __init__(self, editor, path):
+        super(DirectoryEditor, self).__init__(editor)
+        self.path = path
+
+    def change_prop(self, name, value):
+        self.editor.change_dir_prop(self.path, name, value, pool=None)
+
+    def close(self):
+        self.editor.close_directory(self.path)
+        super(DirectoryEditor, self).close()
+        del self.path
 
 class SubversionRepo(object):
     """Wrapper for a Subversion repository.
 
-    It uses the SWIG Python bindings, see above for requirements.
+    This wrapper uses Subvertpy, an alternate set of bindings for Subversion
+    that's more pythonic and sucks less. See earlier in this file for version
+    requirements.
     """
     def __init__(self, url='', username='', password='', head=None):
         parsed = common.parse_url(url, username, password)
@@ -165,14 +164,12 @@ class SubversionRepo(object):
         self.username = parsed[0]
         self.password = parsed[1]
         self.svn_url = parsed[2]
-        self.auth_baton_pool = core.Pool()
-        self.auth_baton = _create_auth_baton(self.auth_baton_pool)
-        # self.init_ra_and_client() assumes that a pool already exists
-        self.pool = core.Pool()
 
         self.init_ra_and_client()
-        self.uuid = ra.get_uuid(self.ra, self.pool)
-        self.root = urllib.unquote(ra.get_repos_root(self.ra, self.pool))
+
+        self.uuid = self.remote.get_uuid()
+        self.root = self.remote.get_repos_root()
+
         # *will* have a leading '/', would not if we used get_repos_root2
         self.subdir = url[len(self.root):]
         if not self.subdir or self.subdir[-1] != '/':
@@ -180,74 +177,65 @@ class SubversionRepo(object):
         self.hasdiff3 = True
 
     def init_ra_and_client(self):
-        """Initializes the RA and client layers, because sometimes getting
-        unified diffs runs the remote server out of open files.
         """
-        # while we're in here we'll recreate our pool
-        self.pool = core.Pool()
-        if self.username:
-            core.svn_auth_set_parameter(self.auth_baton,
-                                        core.SVN_AUTH_PARAM_DEFAULT_USERNAME,
-                                        self.username)
-        if self.password:
-            core.svn_auth_set_parameter(self.auth_baton,
-                                        core.SVN_AUTH_PARAM_DEFAULT_PASSWORD,
-                                        self.password)
-        self.client_context = client.create_context()
-
-        self.client_context.auth_baton = self.auth_baton
-        self.client_context.config = svn_config
-        callbacks = RaCallbacks()
-        callbacks.auth_baton = self.auth_baton
-        self.callbacks = callbacks
-        try:
-            url = self.svn_url.encode('utf-8')
-            scheme, netloc, path, params, query, fragment = urlparse.urlparse(url)
-            path=urllib.quote(path)
-            url = urlparse.urlunparse((scheme, netloc, path, params, query, fragment))
-            self.ra = ra.open2(url, callbacks,
-                               svn_config, self.pool)
-        except SubversionException, e:
-            if e.apr_err == core.SVN_ERR_RA_SERF_SSL_CERT_UNTRUSTED:
-                msg = ('Subversion does not trust the SSL certificate for this '
-                       'site; please try running \'svn ls %s\' first.'
-                       % self.svn_url)
-            elif e.apr_err == core.SVN_ERR_RA_DAV_REQUEST_FAILED:
-                msg = ('Failed to open Subversion repository; please try '
-                       'running \'svn ls %s\' for details.' % self.svn_url)
-            else:
-                msg = e.args[0]
-                for k, v in vars(core).iteritems():
-                    if k.startswith('SVN_ERR_') and v == e.apr_err:
-                        msg = '%s (%s)' % (msg, k)
-                        break
-            raise common.SubversionConnectionException(msg)
+        Initializes the RA and client layers.
+
+        With the SWIG bindings, getting unified diffs runs the remote server
+        sometimes runs out of open files. It is not known whether the Subvertpy
+        is affected by this.
+        """
+        def getclientstring():
+            return 'hgsubversion'
+        # TODO: handle certificate authentication, Mercurial style
+        def getpass(realm, username, may_save):
+            return self.username or username, self.password or '', False
+        def getuser(realm, may_save):
+            return self.username or '', False
+
+        providers = ra.get_platform_specific_client_providers()
+        providers += [
+            ra.get_simple_provider(),
+            ra.get_username_provider(),
+            ra.get_ssl_client_cert_file_provider(),
+            ra.get_ssl_client_cert_pw_file_provider(),
+            ra.get_ssl_server_trust_file_provider(),
+            ra.get_username_prompt_provider(getuser, 0),
+            ra.get_simple_prompt_provider(getpass, 0),
+        ]
+
+        auth = ra.Auth(providers)
+
+        self.remote = ra.RemoteAccess(url=self.svn_url.encode('utf-8'),
+                                      client_string_func=getclientstring,
+                                      auth=auth)
+
+        self.client = client.Client()
+        self.client.auth = auth
 
     @property
     def HEAD(self):
-        return ra.get_latest_revnum(self.ra, self.pool)
+        return self.remote.get_latest_revnum()
 
     @property
     def last_changed_rev(self):
         try:
             holder = []
-            ra.get_log(self.ra, [''],
-                       self.HEAD, 1,
-                       1, #limit of how many log messages to load
-                       True, # don't need to know changed paths
-                       True, # stop on copies
-                       lambda paths, revnum, author, date, message, pool:
-                           holder.append(revnum),
-                       self.pool)
+            def callback(paths, revnum, props, haschildren):
+                holder.append(revnum)
+
+            self.remote.get_log(paths=[''],
+                                start=self.HEAD, end=1, limit=1,
+                                discover_changed_paths=False,
+                                callback=callback)
 
             return holder[-1]
         except SubversionException, e:
-            if e.apr_err not in [core.SVN_ERR_FS_NOT_FOUND]:
+            if e.args[0] == ERR_FS_NOT_FOUND:
                 raise
             else:
                 return self.HEAD
 
-    def list_dir(self, dir, revision=None):
+    def list_dir(self, path, revision=None):
         """List the contents of a server-side directory.
 
         Returns a dict-like object with one dict key per directory entry.
@@ -256,14 +244,13 @@ class SubversionRepo(object):
           dir: the directory to list, no leading slash
           rev: the revision at which to list the directory, defaults to HEAD
         """
-        # TODO this should just not accept leading slashes like the docstring says
-        if dir and dir[-1] == '/':
-            dir = dir[:-1]
-        if revision is None:
-            revision = self.HEAD
-        r = ra.get_dir2(self.ra, dir, revision, core.SVN_DIRENT_KIND, self.pool)
-        folders, props, junk = r
-        return folders
+        # TODO: reject leading slashes like the docstring says
+        if path:
+            path = path.rstrip('/') + '/'
+
+        r = self.remote.get_dir(path, revision or self.HEAD, ra.DIRENT_ALL)
+        dirents, fetched_rev, properties = r
+        return dirents
 
     def revisions(self, paths=None, start=0, stop=0,
                   chunk_size=common.chunk_size):
@@ -280,33 +267,38 @@ class SubversionRepo(object):
         if not stop:
             stop = self.HEAD
         while stop > start:
-            def callback(paths, revnum, author, date, message, pool):
-                r = common.Revision(revnum, author, message, date, paths,
-                                    strip_path=self.subdir)
+            def callback(paths, revnum, props, haschildren):
+                if paths is None:
+                    return
+                r = common.Revision(revnum,
+                             props.get(properties.PROP_REVISION_AUTHOR),
+                             props.get(properties.PROP_REVISION_LOG),
+                             props.get(properties.PROP_REVISION_DATE),
+                             dict([(k, PathAdapter(v))
+                                   for k, v in paths.iteritems()]),
+                             strip_path=self.subdir)
                 revisions.append(r)
             # we only access revisions in a FIFO manner
             revisions = collections.deque()
 
+            revprops = [properties.PROP_REVISION_AUTHOR,
+                        properties.PROP_REVISION_DATE,
+                        properties.PROP_REVISION_LOG]
             try:
                 # TODO: using min(start + chunk_size, stop) may be preferable;
                 #       ra.get_log(), even with chunk_size set, takes a while
                 #       when converting the 65k+ rev. in LLVM.
-                ra.get_log(self.ra,
-                           paths,
-                           start+1,
-                           stop,
-                           chunk_size, #limit of how many log messages to load
-                           True, # don't need to know changed paths
-                           True, # stop on copies
-                           callback,
-                           self.pool)
-            except core.SubversionException, e:
-                if e.apr_err == core.SVN_ERR_FS_NOT_FOUND:
+                self.remote.get_log(paths=paths, revprops=revprops,
+                                    start=start+1, end=stop, limit=chunk_size,
+                                    discover_changed_paths=True,
+                                    callback=callback)
+            except SubversionException, e:
+                if e.args[1] == ERR_FS_NOT_FOUND:
                     msg = ('%s not found at revision %d!'
                            % (self.subdir.rstrip('/'), stop))
                     raise common.SubversionConnectionException(msg)
-                elif e.apr_err == core.SVN_ERR_FS_NO_SUCH_REVISION:
-                    raise common.SubversionConnectionException(e.message)
+                elif e.args[1] == subvertpy.ERR_FS_NO_SUCH_REVISION:
+                    raise common.SubversionConnectionException(e.args[0])
                 else:
                     raise
 
@@ -320,93 +312,100 @@ class SubversionRepo(object):
                 r = revisions.popleft()
                 start = r.revnum
                 yield r
-            self.init_ra_and_client()
 
     def commit(self, paths, message, file_data, base_revision, addeddirs,
-               deleteddirs, properties, copies):
+               deleteddirs, props, copies):
         """Commits the appropriate targets from revision in editor's store.
         """
-        self.init_ra_and_client()
+        def commitcb(*args):
+            commit_info.append(args)
         commit_info = []
-        def commit_cb(_commit_info, pool):
-            commit_info.append(_commit_info)
-        editor, edit_baton = ra.get_commit_editor2(self.ra,
-                                                   message,
-                                                   commit_cb,
-                                                   None,
-                                                   False,
-                                                   self.pool)
-        checksum = []
-        # internal dir batons can fall out of scope and get GCed before svn is
-        # done with them. This prevents that (credit to gvn for the idea).
-        batons = [edit_baton, ]
-        def driver_cb(parent, path, pool):
-            if not parent:
-                bat = editor.open_root(edit_baton, base_revision, self.pool)
-                batons.append(bat)
-                return bat
-            if path in deleteddirs:
-                bat = editor.delete_entry(path, base_revision, parent, pool)
-                batons.append(bat)
-                return bat
-            if path not in file_data:
-                if path in addeddirs:
-                    bat = editor.add_directory(path, parent, None, -1, pool)
+        revprops = { properties.PROP_REVISION_LOG: message }
+        #revprops.update(props)
+        commiteditor = self.remote.get_commit_editor(revprops, commitcb)
+
+        paths = set(paths)
+        paths.update(addeddirs)
+        paths.update(deleteddirs)
+
+        # ensure that all parents are visited too; this may be slow
+        for path in paths.copy():
+            for i in xrange(path.count('/'), -1, -1):
+                p = path.rsplit('/', i)[0]
+                if p in paths:
+                    continue
+                paths.add(p)
+        paths = sorted(paths)
+
+        def visitdir(editor, directory, paths, pathidx):
+            while pathidx < len(paths):
+                path = paths[pathidx]
+                if directory and not path.startswith(directory + '/'):
+                    return pathidx
+
+                dirent = path[len(directory):].lstrip('/')
+                pathidx += 1
+
+                if path in file_data:
+                    # visiting a file
+                    base_text, new_text, action = file_data[path]
+                    if action == 'modify':
+                        fileeditor = editor.open_file(dirent, base_revision)
+                    elif action == 'add':
+                        frompath, fromrev = copies.get(path, (None, -1))
+                        if frompath:
+                            frompath = self.path2url(frompath)
+                        fileeditor = editor.add_file(dirent, frompath, fromrev)
+                    elif action == 'delete':
+                        editor.delete_entry(dirent, base_revision)
+                        continue
+                    else:
+                        assert False, 'invalid action \'%s\'' % action
+
+                    if path in props:
+                        if props[path].get('svn:special', None):
+                            new_text = 'link %s' % new_text
+                        for p, v in props[path].iteritems():
+                            fileeditor.change_prop(p, v)
+
+
+                    handler = fileeditor.apply_textdelta()
+                    delta.send_stream(cStringIO.StringIO(new_text), handler)
+                    fileeditor.close()
+
                 else:
-                    bat = editor.open_directory(path, parent, base_revision, pool)
-                batons.append(bat)
-                props = properties.get(path, {})
-                if 'svn:externals' in props:
-                    value = props['svn:externals']
-                    editor.change_dir_prop(bat, 'svn:externals', value, pool)
-                return bat
-            base_text, new_text, action = file_data[path]
-            compute_delta = True
-            if action == 'modify':
-                baton = editor.open_file(path, parent, base_revision, pool)
-            elif action == 'add':
-                frompath, fromrev = copies.get(path, (None, -1))
-                if frompath:
-                    frompath = self.path2url(frompath)
-                baton = editor.add_file(path, parent, frompath, fromrev, pool)
-            elif action == 'delete':
-                baton = editor.delete_entry(path, base_revision, parent, pool)
-                compute_delta = False
-
-            if path in properties:
-                if properties[path].get('svn:special', None):
-                    new_text = 'link %s' % new_text
-                for p, v in properties[path].iteritems():
-                    editor.change_file_prop(baton, p, v)
-
-            if compute_delta:
-                handler, wh_baton = editor.apply_textdelta(baton, None,
-                                                           self.pool)
-
-                txdelta_stream = delta.svn_txdelta(
-                    cStringIO.StringIO(base_text), cStringIO.StringIO(new_text),
-                    self.pool)
-                delta.svn_txdelta_send_txstream(txdelta_stream, handler,
-                                                wh_baton, pool)
-
-                # TODO pass md5(new_text) instead of None
-                editor.close_file(baton, None, pool)
-
-        delta.path_driver(editor, edit_baton, base_revision, paths, driver_cb,
-                          self.pool)
-        editor.close_edit(edit_baton, self.pool)
-
-    def get_replay(self, revision, editor, oldest_rev_i_have=0):
-        # this method has a tendency to chew through RAM if you don't re-init
-        self.init_ra_and_client()
-        e_ptr, e_baton = delta.make_editor(editor)
+                    # visiting a directory
+                    if path in addeddirs:
+                        direditor = editor.add_directory(dirent)
+                    elif path in deleteddirs:
+                        direditor = editor.delete_entry(dirent, base_revision)
+                        continue
+                    else:
+                        direditor = editor.open_directory(path)
+
+                    if path in props:
+                        for p, v in props[path].iteritems():
+                            direditor.change_prop(p, v)
+
+                    pathidx = visitdir(direditor, '/'.join((directory, dirent)),
+                                       paths, pathidx)
+                    direditor.close()
+
+            return pathidx
+
+        rooteditor = commiteditor.open_root()
+        visitdir(rooteditor, '', paths, 0)
+        rooteditor.close()
+        commiteditor.close()
+
+    def get_replay(self, revision, editor, oldestrev=0):
+
         try:
-            ra.replay(self.ra, revision, oldest_rev_i_have, True, e_ptr,
-                      e_baton, self.pool)
+            self.remote.replay(revision, oldestrev, AbstractEditor(editor))
         except SubversionException, e: #pragma: no cover
             # can I depend on this number being constant?
-            if (e.apr_err == core.SVN_ERR_RA_NOT_IMPLEMENTED or
-                e.apr_err == core.SVN_ERR_UNSUPPORTED_FEATURE):
+            if (e.args[1] == subvertpy.ERR_RA_NOT_IMPLEMENTED or
+                e.args[1] == subvertpy.ERR_UNSUPPORTED_FEATURE):
                 msg = ('This Subversion server is older than 1.4.0, and '
                        'cannot satisfy replay requests.')
                 raise common.SubversionRepoCanNotReplay(msg)
@@ -415,62 +414,29 @@ class SubversionRepo(object):
 
     def get_revision(self, revision, editor):
         ''' feed the contents of the given revision to the given editor '''
-
-        e_ptr, e_baton = delta.make_editor(editor)
-
-        reporter, reporter_baton = ra.do_update(self.ra, revision, "", True,
-                                                e_ptr, e_baton)
-
-        reporter.set_path(reporter_baton, "", revision, True, None)
-        reporter.finish_report(reporter_baton)
+        reporter = self.remote.do_update(revision, '', True,
+                                         AbstractEditor(editor))
+        reporter.set_path('', revision, True)
+        reporter.finish()
 
     def get_unified_diff(self, path, revision, other_path=None, other_rev=None,
                          deleted=True, ignore_type=False):
         """Gets a unidiff of path at revision against revision-1.
         """
-        if not self.hasdiff3:
-            raise common.SubversionRepoCanNotDiff()
-        # works around an svn server keeping too many open files (observed
-        # in an svnserve from the 1.2 era)
-        self.init_ra_and_client()
 
         url = self.path2url(path)
-        url2 = url
         url2 = (other_path and self.path2url(other_path) or url)
+
         if other_rev is None:
             other_rev = revision - 1
-        old_cwd = os.getcwd()
-        tmpdir = tempfile.mkdtemp('svnwrap_temp')
-        out, err = None, None
-        try:
-            # hot tip: the swig bridge doesn't like StringIO for these bad boys
-            out_path = os.path.join(tmpdir, 'diffout')
-            error_path = os.path.join(tmpdir, 'differr')
-            out = open(out_path, 'w')
-            err = open(error_path, 'w')
-            try:
-                client.diff3([], url2, optrev(other_rev), url, optrev(revision),
-                             True, True, deleted, ignore_type, 'UTF-8', out, err,
-                             self.client_context, self.pool)
-            except SubversionException, e:
-                # "Can't write to stream: The handle is invalid."
-                # This error happens systematically under Windows, possibly
-                # related to file handles being non-write shareable by default.
-                if e.apr_err != 720006:
-                    raise
-                self.hasdiff3 = False
-                raise common.SubversionRepoCanNotDiff()
-            out.close()
-            err.close()
-            out, err = None, None
-            assert len(open(error_path).read()) == 0
-            diff = open(out_path).read()
-            return diff
-        finally:
-            if out: out.close()
-            if err: err.close()
-            shutil.rmtree(tmpdir)
-            os.chdir(old_cwd)
+
+        outfile, errfile = self.client.diff(other_rev, revision, url2, url,
+                                            no_diff_deleted=deleted,
+                                            ignore_content_type=ignore_type)
+        error = errfile.read()
+        assert not error, error
+
+        return outfile.read()
 
     def get_file(self, path, revision):
         """Return content and mode of file at given path and revision.
@@ -480,21 +446,19 @@ class SubversionRepo(object):
         otherwise. If the file does not exist at this revision, raise
         IOError.
         """
-        assert not path.startswith('/')
         mode = ''
         try:
             out = cStringIO.StringIO()
-            info = ra.get_file(self.ra, path, revision, out)
+            rev, info = self.remote.get_file(path, out, revision)
             data = out.getvalue()
             out.close()
             if isinstance(info, list):
                 info = info[-1]
-            mode = ("svn:executable" in info) and 'x' or ''
-            mode = ("svn:special" in info) and 'l' or mode
+            mode = (properties.PROP_EXECUTABLE in info) and 'x' or ''
+            mode = (properties.PROP_SPECIAL in info) and 'l' or mode
         except SubversionException, e:
-            notfound = (core.SVN_ERR_FS_NOT_FOUND,
-                        core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
-            if e.args[1] in notfound: # File not found
+            if e.args[1] in (ERR_FS_NOT_FOUND, ERR_RA_DAV_PATH_NOT_FOUND):
+                # File not found
                 raise IOError(errno.ENOENT, e.args[0])
             raise
         if mode  == 'l':
@@ -507,20 +471,15 @@ class SubversionRepo(object):
         """Return a mapping of property names to values, raise IOError if
         specified path does not exist.
         """
-        self.init_ra_and_client()
-        rev = optrev(revision)
-        rpath = self.path2url(path)
         try:
-            pl = client.proplist2(rpath, rev, rev, False,
-                                  self.client_context, self.pool)
+            pl = self.client.proplist(self.path2url(path), revision,
+                                      client.depth_empty)
         except SubversionException, e:
             # Specified path does not exist at this revision
-            if e.apr_err == core.SVN_ERR_NODE_UNKNOWN_KIND:
+            if e.args[1] == subvertpy.ERR_NODE_UNKNOWN_KIND:
                 raise IOError(errno.ENOENT, e.args[0])
             raise
-        if not pl:
-            return {}
-        return pl[0][1]
+        return pl and pl[0][1] or {}
 
     def list_files(self, dirpath, revision):
         """List the content of a directory at a given revision, recursively.
@@ -530,25 +489,24 @@ class SubversionRepo(object):
         directory. Raise IOError if the directory cannot be found at given
         revision.
         """
-        rpath = self.path2url(dirpath)
-        pool = core.Pool()
-        rev = optrev(revision)
         try:
-            entries = client.ls(rpath, rev, True, self.client_context, pool)
+            entries = self.client.list(self.path2url(dirpath), revision,
+                                       client.depth_infinity, ra.DIRENT_KIND)
         except SubversionException, e:
-            if e.apr_err == core.SVN_ERR_FS_NOT_FOUND:
+            if e.args[1] == subvertpy.ERR_FS_NOT_FOUND:
                 raise IOError(errno.ENOENT,
                               '%s cannot be found at r%d' % (dirpath, revision))
             raise
         for path, e in entries.iteritems():
-            kind = _svntypes.get(e.kind)
+            if not path: continue
+            kind = _svntypes.get(e['kind'])
             yield path, kind
 
     def checkpath(self, path, revision):
         """Return the entry type at the given revision, 'f', 'd' or None
         if the entry does not exist.
         """
-        kind = ra.check_path(self.ra, path.strip('/'), revision)
+        kind = self.remote.check_path(path, revision)
         return _svntypes.get(kind)
 
     def path2url(self, path):
@@ -557,6 +515,4 @@ class SubversionRepo(object):
         if not path or path == '.':
             return self.svn_url
         assert path[0] != '/', path
-        return '/'.join((self.svn_url,
-                         urllib.quote(path).rstrip('/'),
-                         ))
+        return '/'.join((self.svn_url, urllib.quote(path).rstrip('/'), ))