Change API to be more similar to cext branch.
[jelmer/subvertpy.git] / ra.py
diff --git a/ra.py b/ra.py
index 9549d27f62b43317fca5aff70e162e2b00b46275..d6208240d0e9cfcd0a8d8b8ced3c683458dda911 100644 (file)
--- a/ra.py
+++ b/ra.py
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-import svn.core
+import svn.core, svn.client, svn.delta, svn.ra
+
+from bzrlib import debug, urlutils
+from bzrlib.trace import mutter
+from bzrlib.errors import InvalidURL, NoSuchFile, FileExists
+
+from bzrlib.plugins.svn import properties
+from bzrlib.plugins.svn.core import get_config, SubversionException
+
+import bzrlib.plugins.svn.errors as svn_errors
+
+def get_username_prompt_provider(fn, retries):
+    def wrap_fn(realm, may_save, pool):
+        username_cred = svn.core.svn_auth_cred_username_t()
+        (username_cred.username, username_cred.may_save) = fn(realm, may_save)
+        return username_cred
+    return svn.core.svn_auth_get_username_prompt_provider(wrap_fn, retries)
+
+def get_simple_prompt_provider(fn, retries):
+    def wrap_fn(realm, username, may_save, pool):
+        simple_cred = svn.core.svn_auth_cred_simple_t()
+        (simple_cred.username, simple_cred.password, simple_cred.may_save) = \
+                fn(realm, username, may_save)
+        return simple_cred
+    return svn.core.svn_auth_get_simple_prompt_provider(wrap_fn, retries)
+
+def get_ssl_server_trust_prompt_provider(fn):
+    def wrap_fn(realm, failures, cert_info, may_save, pool):
+        ssl_server_trust = svn.core.svn_auth_cred_ssl_server_trust_t()
+        (ssl_server_trust.accepted_failures, ssl_server_trust.may_save) = fn(realm, failures, cert_info, may_save)
+        return ssl_server_trust
+    return svn.core.svn_auth_get_ssl_server_trust_prompt_provider(wrap_fn)
+
+def get_ssl_client_cert_pw_prompt_provider(fn, retries):
+    def wrap_fn(realm, may_save, pool):
+        ssl_cred_pw = svn.core.svn_auth_cred_ssl_client_cert_pw_t()
+        (ssl_cred_pw.password, ssl_cred_pw.may_save) = fn(realm, may_save)
+        return ssl_cred_pw
+    return svn.core.svn_auth_get_ssl_client_cert_pw_prompt_provider(wrap_fn, retries)
+
+get_simple_provider = svn.client.get_simple_provider
+get_username_provider = svn.client.get_username_provider
+get_ssl_client_cert_file_provider = svn.client.get_ssl_client_cert_file_provider
+get_ssl_client_cert_pw_file_provider = svn.client.get_ssl_client_cert_pw_file_provider
+get_ssl_server_trust_file_provider = svn.client.get_ssl_server_trust_file_provider
+if hasattr(svn.client, 'get_windows_simple_provider'):
+    get_windows_simple_provider = svn.client.get_windows_simple_provider
+if hasattr(svn.client, 'get_keychain_simple_provider'):
+    get_keychain_simple_provider = svn.client.get_keychain_simple_provider
+if hasattr(svn.client, 'get_windows_ssl_server_trust_provider'):
+    get_windows_ssl_server_trust_provider = svn.client.get_windows_ssl_server_trust_provider
+
+txdelta_send_stream = svn.delta.svn_txdelta_send_stream
+
+DIRENT_KIND = 0x0001
+
+class FileEditor(object):
+    def __init__(self, base_editor, baton):
+        self.base_editor = base_editor
+        self.baton = baton
+
+    def apply_textdelta(self, base_checksum=None):
+        assert self.base_editor.recent_baton[-1] == self.baton
+        return svn.delta.editor_invoke_apply_textdelta(self.base_editor.editor, self.baton,
+                base_checksum)
+
+    def change_prop(self, name, value):
+        assert self.base_editor.recent_baton[-1] == self.baton
+        svn.delta.editor_invoke_change_file_prop(self.base_editor.editor, self.baton, name, 
+                                                 value, None)
+
+    def close(self, checksum=None):
+        assert self.base_editor.recent_baton.pop() == self.baton
+        svn.delta.editor_invoke_close_file(self.base_editor.editor, self.baton, checksum)
+
+
+class DirEditor(object):
+    def __init__(self, base_editor, baton):
+        self.base_editor = base_editor
+        self.baton = baton
+
+    def close(self):
+        assert self.base_editor.recent_baton.pop() == self.baton, \
+                "only most recently opened baton can be closed"
+        svn.delta.editor_invoke_close_directory(self.base_editor.editor, self.baton)
+
+    def change_prop(self, name, value):
+        assert self.base_editor.recent_baton[-1] == self.baton
+        return svn.delta.editor_invoke_change_dir_prop(self.base_editor.editor, self.baton, 
+                                                       name, value, None)
+
+    def delete_entry(self, path, revnum):
+        assert self.base_editor.recent_baton[-1] == self.baton
+        return svn.delta.editor_invoke_delete_entry(self.base_editor.editor, path, revnum, self.baton, None)
+
+    def add_file(self, path, copy_path=None, copy_revision=-1):
+        assert self.base_editor.recent_baton[-1] == self.baton
+        baton = svn.delta.editor_invoke_add_file(self.base_editor.editor, path, 
+            self.baton, copy_path, copy_revision)
+        self.base_editor.recent_baton.append(baton)
+        return FileEditor(self.base_editor, baton)
+
+    def open_file(self, path, base_revision=-1):
+        assert self.base_editor.recent_baton[-1] == self.baton
+        baton = svn.delta.editor_invoke_open_file(self.base_editor.editor, path, self.baton,
+                                                 base_revision)
+        self.base_editor.recent_baton.append(baton)
+        return FileEditor(self.base_editor, baton)
+
+    def add_directory(self, path, copy_path=None, copy_revision=-1):
+        assert self.base_editor.recent_baton[-1] == self.baton
+        baton = svn.delta.editor_invoke_add_directory(self.base_editor.editor, path, 
+            self.baton, copy_path, copy_revision)
+        self.base_editor.recent_baton.append(baton)
+        return DirEditor(self.base_editor, baton)
+
+    def open_directory(self, path, base_revision=-1):
+        assert self.base_editor.recent_baton[-1] == self.baton
+        baton = svn.delta.editor_invoke_open_directory(self.base_editor.editor, path, 
+            self.baton, base_revision)
+        self.base_editor.recent_baton.append(baton)
+        return DirEditor(self.base_editor, baton)
+
+
+class Editor(object):
+    """Simple object wrapper around the Subversion delta editor interface."""
+    def __init__(self, connection, (editor, editor_baton)):
+        self.editor = editor
+        self.editor_baton = editor_baton
+        self.recent_baton = []
+        self._connection = connection
+
+    def set_target_revision(self, revnum):
+        svn.delta.editor_invoke_set_target_revision(self.editor, self.editor_baton, revnum)        
+
+    def open_root(self, base_revnum=-1):
+        assert self.recent_baton == [], "root already opened"
+        baton = svn.delta.editor_invoke_open_root(self.editor, 
+                self.editor_baton, base_revnum)
+        self.recent_baton.append(baton)
+        return DirEditor(self, baton)
+
+    def close(self):
+        assert self.recent_baton == []
+        svn.delta.editor_invoke_close_edit(self.editor, self.editor_baton)
+        self._connection._unmark_busy()
+
+
+class Auth:
+    def __init__(self, providers=[]):
+        self.providers = providers
+        self.auth_baton = svn.core.svn_auth_open(self.providers)
+        self.parameters = {}
+        self.auth_baton._base = self.auth_baton # evil hack
+
+    def set_parameter(self, name, value):
+        self.parameters[name] = value
+        svn.core.svn_auth_set_parameter(self.auth_baton, name, value)
+
+    def get_parameter(self, name):
+        return svn.core.svn_auth_get_parameter(self.auth_baton, name)
+
+
+def needs_busy(unbound):
+    """Decorator that marks a connection as busy before running a methd on it.
+    """
+    def convert(self, *args, **kwargs):
+        self._mark_busy()
+        try:
+            return unbound(self, *args, **kwargs)
+        finally:
+            self._unmark_busy()
+
+    convert.__doc__ = unbound.__doc__
+    convert.__name__ = unbound.__name__
+    return convert
+
+
+def create_svn_client(url):
+    from bzrlib.plugins.svn.auth import create_auth_baton
+    client = svn.client.create_context()
+    client.auth_baton = create_auth_baton(url)
+    client.config = get_config(None)
+    return client
+
+
+class WrappedEditor:
+    def __init__(self, actual):
+        self.actual = actual
+
+    def set_target_revision(self, revision):
+        if getattr(self.actual, "set_target_revision", None) is not None:
+            self.actual.set_target_revision(revision)
+
+    def open_root(self, base_revision):
+        if getattr(self.actual, "open_root", None) is not None:
+            return self.actual.open_root(base_revision)
+        return None
+
+    def add_directory(self, path, baton, copyfrom_path, copyfrom_rev):
+        if baton is not None and getattr(baton, "add_directory", None) is not None:
+            return baton.add_directory(path, copyfrom_path, copyfrom_rev)
+        return None
+
+    def open_directory(self, path, baton, base_rev):
+        if baton is not None and getattr(baton, "open_directory", None) is not None:
+            return baton.open_directory(path, base_rev)
+        return None
+
+    def close_directory(self, baton):
+        if baton is not None and getattr(baton, "close", None) is not None:
+            return baton.close()
+
+    def change_file_prop(self, baton, name, value):
+        if baton is not None and getattr(baton, "change_prop", None) is not None:
+            return baton.change_prop(name, value)
+
+    def change_dir_prop(self, baton, name, value):
+        if baton is not None and getattr(baton, "change_prop", None) is not None:
+            return baton.change_prop(name, value)
+
+    def apply_textdelta(self, baton, checksum):
+        if baton is not None and getattr(baton, "apply_textdelta", None) is not None:
+            return baton.apply_textdelta(checksum)
+
+    def close_file(self, baton, checksum):
+        if baton is not None and getattr(baton, "close", None) is not None:
+            return baton.close(checksum)
+
+    def open_file(self, path, baton, base_rev):
+        if baton is not None and getattr(self.actual, "open_file", None) is not None:
+            return baton.open_file(path, base_rev)
+        return None
+
+    def close_edit(self):
+        if getattr(self.actual, "close_edit", None) is not None:
+            self.actual.close_edit()
+
+
+class RemoteAccess(object):
+    """An single connection to a Subversion repository. This usually can 
+    only do one operation at a time."""
+    def __init__(self, url):
+        self._busy = False
+        self._root = None
+        self._client = create_svn_client(url)
+        self._unbusy_handler = None
+        try:
+            self.mutter('opening SVN RA connection to %r', url)
+            self._ra = svn.client.open_ra_session(url.encode('utf8'), 
+                    self._client)
+        except SubversionException, (_, num):
+            if num == svn_errors.ERR_RA_SVN_REPOS_NOT_FOUND:
+                raise svn_errors.NoSvnRepositoryPresent(url=url)
+            if num == svn_errors.ERR_BAD_URL:
+                raise InvalidURL(url)
+            raise
+        self.url = url
+
+    class Reporter(object):
+        def __init__(self, connection, (reporter, report_baton)):
+            self._reporter = reporter
+            self._baton = report_baton
+            self._connection = connection
+
+        def set_path(self, path, revnum, start_empty, lock_token):
+            svn.ra.reporter2_invoke_set_path(self._reporter, self._baton, 
+                        path, revnum, start_empty, lock_token, None)
+
+        def delete_path(self, path):
+            svn.ra.reporter2_invoke_delete_path(self._reporter, self._baton,
+                    path, None)
+
+        def link_path(self, path, url, revision, start_empty, lock_token):
+            svn.ra.reporter2_invoke_link_path(self._reporter, self._baton,
+                    path, url, revision, start_empty, lock_token,
+                    None)
+
+        def finish(self):
+            try:
+                svn.ra.reporter2_invoke_finish_report(self._reporter, 
+                        self._baton, None)
+            finally:
+                self._connection._unmark_busy()
+
+        def abort(self):
+            try:
+                svn.ra.reporter2_invoke_abort_report(self._reporter, 
+                        self._baton, None)
+            finally:
+                self._connection._unmark_busy()
+
+    def is_busy(self):
+        return self._busy
+
+    def _mark_busy(self):
+        assert not self._busy, "already busy"
+        self._busy = True
+
+    def set_unbusy_handler(self, handler):
+        self._unbusy_handler = handler
+
+    def _unmark_busy(self):
+        assert self._busy, "not busy"
+        self._busy = False
+        if self._unbusy_handler is not None:
+            self._unbusy_handler()
+            self._unbusy_handler = None
+
+    def mutter(self, text, *args):
+        if 'transport' in debug.debug_flags:
+            mutter(text, *args)
+
+    @needs_busy
+    def get_uuid(self):
+        self.mutter('svn get-uuid')
+        return svn.ra.get_uuid(self._ra)
+
+    @needs_busy
+    def get_repos_root(self):
+        if self._root is None:
+            self.mutter("svn get-repos-root")
+            self._root = svn.ra.get_repos_root(self._ra)
+        return self._root
+
+    @needs_busy
+    def get_latest_revnum(self):
+        self.mutter("svn get-latest-revnum")
+        return svn.ra.get_latest_revnum(self._ra)
+
+    def _make_editor(self, editor):
+        edit, edit_baton = svn.delta.make_editor(editor, None)
+        self._edit = edit
+        self._edit_baton = edit_baton
+        return self._edit, self._edit_baton
+
+    def do_switch(self, switch_rev, path, recurse, switch_url, editor):
+        self.mutter('svn switch -r %d -> %r', switch_rev, switch_url)
+        self._mark_busy()
+        edit, edit_baton = self._make_editor(editor)
+        return self.Reporter(self, svn.ra.do_switch(self._ra, switch_rev, path,
+                             recurse, switch_url, edit, edit_baton, None))
+
+    def change_rev_prop(self, revnum, name, value):
+        self.mutter('svn revprop -r%d --set %s=%s', revnum, name, value)
+        svn.ra.change_rev_prop(self._ra, revnum, name, value)
+
+    @needs_busy
+    def get_lock(self, path):
+        return svn.ra.get_lock(self._ra, path)
+
+    @needs_busy
+    def unlock(self, locks, break_lock=False):
+        def lock_cb(baton, path, do_lock, lock, ra_err, pool):
+            pass
+        return svn.ra.unlock(self._ra, locks, break_lock, lock_cb)
+    @needs_busy
+    def get_dir(self, path, revnum, kind=False):
+        self.mutter("svn ls -r %d '%r'", revnum, path)
+        assert len(path) == 0 or path[0] != "/"
+        # ra_dav backends fail with strange errors if the path starts with a 
+        # slash while other backends don't.
+        if hasattr(svn.ra, 'get_dir2'):
+            fields = 0
+            if kind:
+                fields += DIRENT_KIND
+            return svn.ra.get_dir2(self._ra, path, revnum, fields)
+        else:
+            return svn.ra.get_dir(self._ra, path, revnum)
+
+    @needs_busy
+    def check_path(self, path, revnum):
+        assert len(path) == 0 or path[0] != "/"
+        self.mutter("svn check_path -r%d %s", revnum, path)
+        return svn.ra.check_path(self._ra, path.encode('utf-8'), revnum)
+
+    @needs_busy
+    def mkdir(self, relpath, mode=None):
+        assert len(relpath) == 0 or relpath[0] != "/"
+        path = urlutils.join(self.url, relpath)
+        try:
+            svn.client.mkdir([path.encode("utf-8")], self._client)
+        except SubversionException, (msg, num):
+            if num == svn_errors.ERR_FS_NOT_FOUND:
+                raise NoSuchFile(path)
+            if num == svn_errors.ERR_FS_ALREADY_EXISTS:
+                raise FileExists(path)
+            raise
+
+    def replay(self, revision, low_water_mark, send_deltas, editor):
+        self.mutter('svn replay -r%r:%r', low_water_mark, revision)
+        self._mark_busy()
+        edit, edit_baton = self._make_editor(editor)
+        svn.ra.replay(self._ra, revision, low_water_mark, send_deltas,
+                      edit, edit_baton, None)
+
+    def do_update(self, revnum, path, recurse, editor):
+        self.mutter('svn update -r %r', revnum)
+        self._mark_busy()
+        edit, edit_baton = self._make_editor(editor)
+        return self.Reporter(self, svn.ra.do_update(self._ra, revnum, path,
+                             recurse, edit, edit_baton, None))
+
+    def has_capability(self, cap):
+        return svn.ra.has_capability(self._ra, cap)
+
+    def revprop_list(self, revnum):
+        self.mutter('svn revprop-list -r %r', revnum)
+        return svn.ra.rev_proplist(self._ra, revnum, None)
+
+    def get_commit_editor(self, revprops, done_cb=None, lock_token=None, keep_locks=False):
+        self._mark_busy()
+        try:
+            if hasattr(svn.ra, 'get_commit_editor3'):
+                editor = svn.ra.get_commit_editor3(self._ra, revprops, done_cb, 
+                                                  lock_token, keep_locks)
+            elif revprops.keys() != [properties.PROP_REVISION_LOG]:
+                raise NotImplementedError()
+            else:
+                editor = svn.ra.get_commit_editor2(self._ra, 
+                            revprops[properties.PROP_REVISION_LOG],
+                            done_cb, lock_token, keep_locks)
+
+            return Editor(self, editor)
+        except:
+            self._unmark_busy()
+            raise
+
+    class SvnLock(object):
+        def __init__(self, connection, tokens):
+            self._tokens = tokens
+            self._connection = connection
+
+        def unlock(self):
+            self._connection.unlock(self.locks)
+
+    @needs_busy
+    def lock_write(self, path_revs, comment=None, steal_lock=False):
+        tokens = {}
+        def lock_cb(baton, path, do_lock, lock, ra_err, pool):
+            tokens[path] = lock
+        svn.ra.lock(self._ra, path_revs, comment, steal_lock, lock_cb)
+        return SvnLock(self, tokens)
+
+    @needs_busy
+    def get_log(self, paths, from_revnum, to_revnum, limit, 
+                discover_changed_paths, strict_node_history, revprops, rcvr):
+        # No paths starting with slash, please
+        assert paths is None or all([not p.startswith("/") for p in paths])
+        if (paths is None and 
+            (svn.core.SVN_VER_MINOR < 6 or (
+             svn.core.SVN_VER_REVISION < 31470 and svn.core.SVN_VER_REVISION != 0))):
+            paths = ["/"]
+        self.mutter('svn log %r:%r %r (limit: %r)', from_revnum, to_revnum, paths, limit)
+        if hasattr(svn.ra, 'get_log2'):
+            return svn.ra.get_log2(self._ra, paths, 
+                           from_revnum, to_revnum, limit, 
+                           discover_changed_paths, strict_node_history, False, 
+                           revprops, rcvr, None)
+
+        class LogEntry(object):
+            def __init__(self, changed_paths, rev, author, date, message):
+                self.changed_paths = changed_paths
+                self.revprops = {}
+                if properties.PROP_REVISION_AUTHOR in revprops:
+                    self.revprops[properties.PROP_REVISION_AUTHOR] = author
+                if properties.PROP_REVISION_LOG in revprops:
+                    self.revprops[properties.PROP_REVISION_LOG] = message
+                if properties.PROP_REVISION_DATE in revprops:
+                    self.revprops[properties.PROP_REVISION_DATE] = date
+                # FIXME: Check other revprops
+                # FIXME: Handle revprops is None
+                self.revision = rev
+                self.has_children = None
+
+        def rcvr_convert(orig_paths, rev, author, date, message, pool):
+            rcvr(LogEntry(orig_paths, rev, author, date, message), pool)
+
+        return svn.ra.get_log(self._ra, paths, 
+                              from_revnum, to_revnum, limit, discover_changed_paths, 
+                              strict_node_history, rcvr_convert, None)
+
+    @needs_busy
+    def reparent(self, url):
+        if self.url == url:
+            return
+        if hasattr(svn.ra, 'reparent'):
+            self.mutter('svn reparent %r', url)
+            svn.ra.reparent(self._ra, url)
+            self.url = url
+        else:
+            raise NotImplementedError(self.reparent)
+
+
 
-get_username_prompt_provider = svn.core.svn_auth_get_username_prompt_provider
-get_simple_prompt_provider = svn.core.svn_auth_get_simple_prompt_provider
-get_ssl_client_cert_pw_prompt_provider = svn.core.svn_auth_get_ssl_client_cert_pw_prompt_provider
-get_ssl_server_trust_prompt_provider = svn.core.svn_auth_get_ssl_server_trust_prompt_provider