Fix imports.
[jelmer/subvertpy.git] / transport.py
1 # Copyright (C) 2006 Jelmer Vernooij <jelmer@samba.org>
2
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation; either version 3 of the License, or
6 # (at your option) any later version.
7
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 # GNU General Public License for more details.
12
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software
15 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16 """Simple transport for accessing Subversion smart servers."""
17
18 from bzrlib import debug, urlutils
19 from bzrlib.errors import (NoSuchFile, NotBranchError, TransportNotPossible, 
20                            FileExists, NotLocalUrl, InvalidURL)
21 from bzrlib.trace import mutter
22 from bzrlib.transport import Transport
23
24 from svn.core import SubversionException, Pool
25 import svn.ra
26 import svn.core
27 import svn.client
28
29 from bzrlib.plugins.svn import properties
30 from bzrlib.plugins.svn.errors import convert_svn_error, NoSvnRepositoryPresent, ERR_BAD_URL, ERR_RA_SVN_REPOS_NOT_FOUND, ERR_FS_ALREADY_EXISTS, ERR_FS_NOT_FOUND, ERR_FS_NOT_DIRECTORY
31 import urlparse
32 import urllib
33
34 svn_config = svn.core.svn_config_get_config(None)
35
36 def get_client_string():
37     """Return a string that can be send as part of the User Agent string."""
38     return "bzr%s+bzr-svn%s" % (bzrlib.__version__, bzrlib.plugins.svn.__version__)
39
40  
41 def create_svn_client(url):
42     from auth import create_auth_baton
43     client = svn.client.create_context()
44     client.auth_baton = create_auth_baton(url)
45     client.config = svn_config
46     return client
47
48
49 # Don't run any tests on SvnTransport as it is not intended to be 
50 # a full implementation of Transport
51 def get_test_permutations():
52     return []
53
54
55 def get_svn_ra_transport(bzr_transport):
56     """Obtain corresponding SvnRaTransport for a stock Bazaar transport."""
57     if isinstance(bzr_transport, SvnRaTransport):
58         return bzr_transport
59
60     return SvnRaTransport(bzr_transport.base)
61
62
63 def _url_unescape_uri(url):
64     (scheme, netloc, path, query, fragment) = urlparse.urlsplit(url)
65     path = urllib.unquote(path)
66     return urlparse.urlunsplit((scheme, netloc, path, query, fragment))
67
68
69 def bzr_to_svn_url(url):
70     """Convert a Bazaar URL to a URL understood by Subversion.
71
72     This will possibly remove the svn+ prefix.
73     """
74     if (url.startswith("svn+http://") or 
75         url.startswith("svn+file://") or
76         url.startswith("svn+https://")):
77         url = url[len("svn+"):] # Skip svn+
78
79     if url.startswith("http"):
80         # Without this, URLs with + in them break
81         url = _url_unescape_uri(url)
82
83     # The SVN libraries don't like trailing slashes...
84     url = url.rstrip('/')
85
86     return url
87
88
89 def needs_busy(unbound):
90     """Decorator that marks a connection as busy before running a methd on it.
91     """
92     def convert(self, *args, **kwargs):
93         self._mark_busy()
94         try:
95             return unbound(self, *args, **kwargs)
96         finally:
97             self._unmark_busy()
98
99     convert.__doc__ = unbound.__doc__
100     convert.__name__ = unbound.__name__
101     return convert
102
103
104 class Editor(object):
105     """Simple object wrapper around the Subversion delta editor interface."""
106     def __init__(self, connection, (editor, editor_baton)):
107         self.editor = editor
108         self.editor_baton = editor_baton
109         self.recent_baton = []
110         self._connection = connection
111
112     @convert_svn_error
113     def open_root(self, base_revnum):
114         assert self.recent_baton == [], "root already opened"
115         baton = svn.delta.editor_invoke_open_root(self.editor, 
116                 self.editor_baton, base_revnum)
117         self.recent_baton.append(baton)
118         return baton
119
120     @convert_svn_error
121     def close_directory(self, baton, *args, **kwargs):
122         assert self.recent_baton.pop() == baton, \
123                 "only most recently opened baton can be closed"
124         svn.delta.editor_invoke_close_directory(self.editor, baton, *args, **kwargs)
125
126     @convert_svn_error
127     def close(self):
128         assert self.recent_baton == []
129         svn.delta.editor_invoke_close_edit(self.editor, self.editor_baton)
130         self._connection._unmark_busy()
131
132     @convert_svn_error
133     def apply_textdelta(self, baton, *args, **kwargs):
134         assert self.recent_baton[-1] == baton
135         return svn.delta.editor_invoke_apply_textdelta(self.editor, baton,
136                 *args, **kwargs)
137
138     @convert_svn_error
139     def change_dir_prop(self, baton, name, value, pool=None):
140         assert self.recent_baton[-1] == baton
141         return svn.delta.editor_invoke_change_dir_prop(self.editor, baton, 
142                                                        name, value, pool)
143
144     @convert_svn_error
145     def delete_entry(self, *args, **kwargs):
146         return svn.delta.editor_invoke_delete_entry(self.editor, *args, **kwargs)
147
148     @convert_svn_error
149     def add_file(self, path, parent_baton, *args, **kwargs):
150         assert self.recent_baton[-1] == parent_baton
151         baton = svn.delta.editor_invoke_add_file(self.editor, path, 
152             parent_baton, *args, **kwargs)
153         self.recent_baton.append(baton)
154         return baton
155
156     @convert_svn_error
157     def open_file(self, path, parent_baton, *args, **kwargs):
158         assert self.recent_baton[-1] == parent_baton
159         baton = svn.delta.editor_invoke_open_file(self.editor, path, 
160                                                  parent_baton, *args, **kwargs)
161         self.recent_baton.append(baton)
162         return baton
163
164     @convert_svn_error
165     def change_file_prop(self, baton, name, value, pool=None):
166         assert self.recent_baton[-1] == baton
167         svn.delta.editor_invoke_change_file_prop(self.editor, baton, name, 
168                                                  value, pool)
169
170     @convert_svn_error
171     def close_file(self, baton, *args, **kwargs):
172         assert self.recent_baton.pop() == baton
173         svn.delta.editor_invoke_close_file(self.editor, baton, *args, **kwargs)
174
175     @convert_svn_error
176     def add_directory(self, path, parent_baton, *args, **kwargs):
177         assert self.recent_baton[-1] == parent_baton
178         baton = svn.delta.editor_invoke_add_directory(self.editor, path, 
179             parent_baton, *args, **kwargs)
180         self.recent_baton.append(baton)
181         return baton
182
183     @convert_svn_error
184     def open_directory(self, path, parent_baton, *args, **kwargs):
185         assert self.recent_baton[-1] == parent_baton
186         baton = svn.delta.editor_invoke_open_directory(self.editor, path, 
187             parent_baton, *args, **kwargs)
188         self.recent_baton.append(baton)
189         return baton
190
191
192 class Connection(object):
193     """An single connection to a Subversion repository. This usually can 
194     only do one operation at a time."""
195     def __init__(self, url):
196         self._busy = False
197         self._root = None
198         self._client = create_svn_client(url)
199         self._unbusy_handler = None
200         try:
201             self.mutter('opening SVN RA connection to %r' % url)
202             self._ra = svn.client.open_ra_session(url.encode('utf8'), 
203                     self._client)
204         except SubversionException, (_, num):
205             if num == ERR_RA_SVN_REPOS_NOT_FOUND:
206                 raise NoSvnRepositoryPresent(url=url)
207             if num == ERR_BAD_URL:
208                 raise InvalidURL(url)
209             raise
210         self.url = url
211
212     class Reporter(object):
213         def __init__(self, connection, (reporter, report_baton)):
214             self._reporter = reporter
215             self._baton = report_baton
216             self._connection = connection
217
218         @convert_svn_error
219         def set_path(self, path, revnum, start_empty, lock_token, pool=None):
220             svn.ra.reporter2_invoke_set_path(self._reporter, self._baton, 
221                         path, revnum, start_empty, lock_token, pool)
222
223         @convert_svn_error
224         def delete_path(self, path, pool=None):
225             svn.ra.reporter2_invoke_delete_path(self._reporter, self._baton,
226                     path, pool)
227
228         @convert_svn_error
229         def link_path(self, path, url, revision, start_empty, lock_token, 
230                       pool=None):
231             svn.ra.reporter2_invoke_link_path(self._reporter, self._baton,
232                     path, url, revision, start_empty, lock_token,
233                     pool)
234
235         @convert_svn_error
236         def finish_report(self, pool=None):
237             try:
238                 svn.ra.reporter2_invoke_finish_report(self._reporter, 
239                         self._baton, pool)
240             finally:
241                 self._connection._unmark_busy()
242
243         @convert_svn_error
244         def abort_report(self, pool=None):
245             try:
246                 svn.ra.reporter2_invoke_abort_report(self._reporter, 
247                         self._baton, pool)
248             finally:
249                 self._connection._unmark_busy()
250
251     def is_busy(self):
252         return self._busy
253
254     def _mark_busy(self):
255         assert not self._busy, "already busy"
256         self._busy = True
257
258     def set_unbusy_handler(self, handler):
259         self._unbusy_handler = handler
260
261     def _unmark_busy(self):
262         assert self._busy, "not busy"
263         self._busy = False
264         if self._unbusy_handler is not None:
265             self._unbusy_handler()
266             self._unbusy_handler = None
267
268     def mutter(self, text):
269         if 'transport' in debug.debug_flags:
270             mutter(text)
271
272     @convert_svn_error
273     @needs_busy
274     def get_uuid(self):
275         self.mutter('svn get-uuid')
276         return svn.ra.get_uuid(self._ra)
277
278     @convert_svn_error
279     @needs_busy
280     def get_repos_root(self):
281         if self._root is None:
282             self.mutter("svn get-repos-root")
283             self._root = svn.ra.get_repos_root(self._ra)
284         return self._root
285
286     @convert_svn_error
287     @needs_busy
288     def get_latest_revnum(self):
289         self.mutter("svn get-latest-revnum")
290         return svn.ra.get_latest_revnum(self._ra)
291
292     def _make_editor(self, editor, pool=None):
293         edit, edit_baton = svn.delta.make_editor(editor, pool)
294         self._edit = edit
295         self._edit_baton = edit_baton
296         return self._edit, self._edit_baton
297
298     @convert_svn_error
299     def do_switch(self, switch_rev, recurse, switch_url, editor, pool=None):
300         self.mutter('svn switch -r %d -> %r' % (switch_rev, switch_url))
301         self._mark_busy()
302         edit, edit_baton = self._make_editor(editor, pool)
303         return self.Reporter(self, svn.ra.do_switch(self._ra, switch_rev, "", 
304                              recurse, switch_url, edit, edit_baton, pool))
305
306     @convert_svn_error
307     def change_rev_prop(self, revnum, name, value, pool=None):
308         self.mutter('svn revprop -r%d --set %s=%s' % (revnum, name, value))
309         svn.ra.change_rev_prop(self._ra, revnum, name, value)
310
311     @convert_svn_error
312     @needs_busy
313     def get_lock(self, path):
314         return svn.ra.get_lock(self._ra, path)
315
316     @convert_svn_error
317     @needs_busy
318     def unlock(self, locks, break_lock=False):
319         def lock_cb(baton, path, do_lock, lock, ra_err, pool):
320             pass
321         return svn.ra.unlock(self._ra, locks, break_lock, lock_cb)
322  
323     @convert_svn_error
324     @needs_busy
325     def get_dir(self, path, revnum, pool=None, kind=False):
326         self.mutter("svn ls -r %d '%r'" % (revnum, path))
327         assert len(path) == 0 or path[0] != "/"
328         # ra_dav backends fail with strange errors if the path starts with a 
329         # slash while other backends don't.
330         if hasattr(svn.ra, 'get_dir2'):
331             fields = 0
332             if kind:
333                 fields += svn.core.SVN_DIRENT_KIND
334             return svn.ra.get_dir2(self._ra, path, revnum, fields)
335         else:
336             return svn.ra.get_dir(self._ra, path, revnum)
337
338     @convert_svn_error
339     @needs_busy
340     def check_path(self, path, revnum):
341         assert len(path) == 0 or path[0] != "/"
342         self.mutter("svn check_path -r%d %s" % (revnum, path))
343         return svn.ra.check_path(self._ra, path.encode('utf-8'), revnum)
344
345     @convert_svn_error
346     @needs_busy
347     def mkdir(self, relpath, mode=None):
348         assert len(relpath) == 0 or relpath[0] != "/"
349         path = urlutils.join(self.url, relpath)
350         try:
351             svn.client.mkdir([path.encode("utf-8")], self._client)
352         except SubversionException, (msg, num):
353             if num == ERR_FS_NOT_FOUND:
354                 raise NoSuchFile(path)
355             if num == ERR_FS_ALREADY_EXISTS:
356                 raise FileExists(path)
357             raise
358
359     @convert_svn_error
360     def replay(self, revision, low_water_mark, send_deltas, editor, pool=None):
361         self.mutter('svn replay -r%r:%r' % (low_water_mark, revision))
362         self._mark_busy()
363         edit, edit_baton = self._make_editor(editor, pool)
364         svn.ra.replay(self._ra, revision, low_water_mark, send_deltas,
365                       edit, edit_baton, pool)
366
367     @convert_svn_error
368     def do_update(self, revnum, recurse, editor, pool=None):
369         self.mutter('svn update -r %r' % revnum)
370         self._mark_busy()
371         edit, edit_baton = self._make_editor(editor, pool)
372         return self.Reporter(self, svn.ra.do_update(self._ra, revnum, "", 
373                              recurse, edit, edit_baton, pool))
374
375     @convert_svn_error
376     def has_capability(self, cap):
377         return svn.ra.has_capability(self._ra, cap)
378
379     @convert_svn_error
380     def revprop_list(self, revnum, pool=None):
381         self.mutter('svn revprop-list -r %r' % revnum)
382         return svn.ra.rev_proplist(self._ra, revnum, pool)
383
384     @convert_svn_error
385     def get_commit_editor(self, revprops, done_cb, lock_token, keep_locks):
386         self._mark_busy()
387         try:
388             if hasattr(svn.ra, 'get_commit_editor3'):
389                 editor = svn.ra.get_commit_editor3(self._ra, revprops, done_cb, 
390                                                   lock_token, keep_locks)
391             elif revprops.keys() != [properties.PROP_REVISION_LOG]:
392                 raise NotImplementedError()
393             else:
394                 editor = svn.ra.get_commit_editor2(self._ra, 
395                             revprops[properties.PROP_REVISION_LOG],
396                             done_cb, lock_token, keep_locks)
397
398             return Editor(self, editor)
399         except:
400             self._unmark_busy()
401             raise
402
403     class SvnLock(object):
404         def __init__(self, connection, tokens):
405             self._tokens = tokens
406             self._connection = connection
407
408         def unlock(self):
409             self._connection.unlock(self.locks)
410
411     @convert_svn_error
412     @needs_busy
413     def lock_write(self, path_revs, comment=None, steal_lock=False):
414         tokens = {}
415         def lock_cb(baton, path, do_lock, lock, ra_err, pool):
416             tokens[path] = lock
417         svn.ra.lock(self._ra, path_revs, comment, steal_lock, lock_cb)
418         return SvnLock(self, tokens)
419
420     @convert_svn_error
421     @needs_busy
422     def get_log(self, paths, from_revnum, to_revnum, limit, 
423                 discover_changed_paths, strict_node_history, revprops, rcvr, 
424                 pool=None):
425         # No paths starting with slash, please
426         assert paths is None or all([not p.startswith("/") for p in paths])
427         if (paths is None and 
428             (svn.core.SVN_VER_MINOR < 6 or (
429              svn.core.SVN_VER_REVISION < 31470 and svn.core.SVN_VER_REVISION != 0))):
430             paths = ["/"]
431         self.mutter('svn log %r:%r %r (limit: %r)' % (from_revnum, to_revnum, paths, limit))
432         if hasattr(svn.ra, 'get_log2'):
433             return svn.ra.get_log2(self._ra, paths, 
434                            from_revnum, to_revnum, limit, 
435                            discover_changed_paths, strict_node_history, False, 
436                            revprops, rcvr, pool)
437
438         class LogEntry(object):
439             def __init__(self, changed_paths, rev, author, date, message):
440                 self.changed_paths = changed_paths
441                 self.revprops = {}
442                 if properties.PROP_REVISION_AUTHOR in revprops:
443                     self.revprops[properties.PROP_REVISION_AUTHOR] = author
444                 if properties.PROP_REVISION_LOG in revprops:
445                     self.revprops[properties.PROP_REVISION_LOG] = message
446                 if properties.PROP_REVISION_DATE in revprops:
447                     self.revprops[properties.PROP_REVISION_DATE] = date
448                 # FIXME: Check other revprops
449                 # FIXME: Handle revprops is None
450                 self.revision = rev
451                 self.has_children = None
452
453         def rcvr_convert(orig_paths, rev, author, date, message, pool):
454             rcvr(LogEntry(orig_paths, rev, author, date, message), pool)
455
456         return svn.ra.get_log(self._ra, paths, 
457                               from_revnum, to_revnum, limit, discover_changed_paths, 
458                               strict_node_history, rcvr_convert, pool)
459
460     @convert_svn_error
461     @needs_busy
462     def reparent(self, url):
463         if self.url == url:
464             return
465         if hasattr(svn.ra, 'reparent'):
466             self.mutter('svn reparent %r' % url)
467             svn.ra.reparent(self._ra, url)
468             self.url = url
469         else:
470             raise NotImplementedError(self.reparent)
471
472
473 class ConnectionPool(object):
474     """Collection of connections to a Subversion repository."""
475     def __init__(self):
476         self.connections = set()
477
478     def get(self, url):
479         # Check if there is an existing connection we can use
480         for c in self.connections:
481             assert not c.is_busy(), "busy connection in pool"
482             if c.url == url:
483                 self.connections.remove(c)
484                 return c
485         # Nothing available? Just pick an existing one and reparent:
486         if len(self.connections) == 0:
487             return Connection(url)
488         c = self.connections.pop()
489         try:
490             c.reparent(url)
491             return c
492         except NotImplementedError:
493             self.connections.add(c)
494             return Connection(url)
495         except:
496             self.connections.add(c)
497             raise
498
499     def add(self, connection):
500         assert not connection.is_busy(), "adding busy connection in pool"
501         self.connections.add(connection)
502     
503
504 class SvnRaTransport(Transport):
505     """Fake transport for Subversion-related namespaces.
506     
507     This implements just as much of Transport as is necessary 
508     to fool Bazaar. """
509     @convert_svn_error
510     def __init__(self, url="", _backing_url=None, pool=None):
511         self.pool = Pool()
512         bzr_url = url
513         self.svn_url = bzr_to_svn_url(url)
514         # _backing_url is an evil hack so the root directory of a repository 
515         # can be accessed on some HTTP repositories. 
516         if _backing_url is None:
517             _backing_url = self.svn_url
518         self._backing_url = _backing_url.rstrip("/")
519         Transport.__init__(self, bzr_url)
520
521         if pool is None:
522             self.connections = ConnectionPool()
523
524             # Make sure that the URL is valid by connecting to it.
525             self.connections.add(self.connections.get(self._backing_url))
526         else:
527             self.connections = pool
528
529         from bzrlib.plugins.svn import lazy_check_versions
530         lazy_check_versions()
531
532     def get_connection(self):
533         return self.connections.get(self._backing_url)
534
535     def add_connection(self, conn):
536         self.connections.add(conn)
537
538     def has(self, relpath):
539         """See Transport.has()."""
540         # TODO: Raise TransportNotPossible here instead and 
541         # catch it in bzrdir.py
542         return False
543
544     def get(self, relpath):
545         """See Transport.get()."""
546         # TODO: Raise TransportNotPossible here instead and 
547         # catch it in bzrdir.py
548         raise NoSuchFile(path=relpath)
549
550     def stat(self, relpath):
551         """See Transport.stat()."""
552         raise TransportNotPossible('stat not supported on Subversion')
553
554     def get_uuid(self):
555         conn = self.get_connection()
556         try:
557             return conn.get_uuid()
558         finally:
559             self.add_connection(conn)
560
561     def get_repos_root(self):
562         root = self.get_svn_repos_root()
563         if (self.base.startswith("svn+http:") or 
564             self.base.startswith("svn+https:")):
565             return "svn+%s" % root
566         return root
567
568     def get_svn_repos_root(self):
569         conn = self.get_connection()
570         try:
571             return conn.get_repos_root()
572         finally:
573             self.add_connection(conn)
574
575     def get_latest_revnum(self):
576         conn = self.get_connection()
577         try:
578             return conn.get_latest_revnum()
579         finally:
580             self.add_connection(conn)
581
582     def do_switch(self, switch_rev, recurse, switch_url, editor, pool=None):
583         conn = self._open_real_transport()
584         conn.set_unbusy_handler(lambda: self.add_connection(conn))
585         return conn.do_switch(switch_rev, recurse, switch_url, editor, pool)
586
587     def iter_log(self, paths, from_revnum, to_revnum, limit, discover_changed_paths, 
588                  strict_node_history, revprops):
589
590         assert paths is None or isinstance(paths, list)
591         assert paths is None or all([isinstance(x, str) for x in paths])
592         assert isinstance(from_revnum, int) and isinstance(to_revnum, int)
593         assert isinstance(limit, int)
594         from threading import Thread, Semaphore
595
596         class logfetcher(Thread):
597             def __init__(self, transport, **kwargs):
598                 Thread.__init__(self)
599                 self.setDaemon(True)
600                 self.transport = transport
601                 self.kwargs = kwargs
602                 self.pending = []
603                 self.conn = None
604                 self.semaphore = Semaphore(0)
605
606             def next(self):
607                 self.semaphore.acquire()
608                 ret = self.pending.pop(0)
609                 if ret is None:
610                     self.transport.add_connection(self.conn)
611                 elif isinstance(ret, Exception):
612                     self.transport.add_connection(self.conn)
613                     raise ret
614                 return ret
615
616             def run(self):
617                 assert self.conn is None, "already running"
618                 def rcvr(log_entry, pool):
619                     self.pending.append((log_entry.changed_paths, log_entry.revision, log_entry.revprops))
620                     self.semaphore.release()
621                 self.conn = self.transport.get_connection()
622                 try:
623                     self.conn.get_log(rcvr=rcvr, **self.kwargs)
624                     self.pending.append(None)
625                 except Exception, e:
626                     self.pending.append(e)
627                 self.semaphore.release()
628
629         if paths is None:
630             newpaths = None
631         else:
632             newpaths = [self._request_path(path) for path in paths]
633         
634         fetcher = logfetcher(self, paths=newpaths, from_revnum=from_revnum, to_revnum=to_revnum, limit=limit, discover_changed_paths=discover_changed_paths, strict_node_history=strict_node_history, revprops=revprops)
635         fetcher.start()
636         return iter(fetcher.next, None)
637
638     def get_log(self, paths, from_revnum, to_revnum, limit, discover_changed_paths, 
639                 strict_node_history, revprops, rcvr, pool=None):
640         assert paths is None or isinstance(paths, list), "Invalid paths"
641         assert paths is None or all([isinstance(x, str) for x in paths])
642
643         if paths is None:
644             newpaths = None
645         else:
646             newpaths = [self._request_path(path) for path in paths]
647
648         conn = self.get_connection()
649         try:
650             return conn.get_log(newpaths, 
651                     from_revnum, to_revnum,
652                     limit, discover_changed_paths, strict_node_history, 
653                     revprops, rcvr, pool)
654         finally:
655             self.add_connection(conn)
656
657     def _open_real_transport(self):
658         if self._backing_url != self.svn_url:
659             return self.connections.get(self.svn_url)
660         return self.get_connection()
661
662     def change_rev_prop(self, revnum, name, value, pool=None):
663         conn = self.get_connection()
664         try:
665             return conn.change_rev_prop(revnum, name, value, pool)
666         finally:
667             self.add_connection(conn)
668
669     def get_dir(self, path, revnum, pool=None, kind=False):
670         path = self._request_path(path)
671         conn = self.get_connection()
672         try:
673             return conn.get_dir(path, revnum, pool, kind)
674         finally:
675             self.add_connection(conn)
676
677     def mutter(self, text):
678         if 'transport' in debug.debug_flags:
679             mutter(text)
680
681     def _request_path(self, relpath):
682         if self._backing_url == self.svn_url:
683             return relpath.strip("/")
684         newsvnurl = urlutils.join(self.svn_url, relpath)
685         if newsvnurl == self._backing_url:
686             return ""
687         newrelpath = urlutils.relative_url(self._backing_url+"/", newsvnurl+"/").strip("/")
688         self.mutter('request path %r -> %r' % (relpath, newrelpath))
689         return newrelpath
690
691     def list_dir(self, relpath):
692         assert len(relpath) == 0 or relpath[0] != "/"
693         if relpath == ".":
694             relpath = ""
695         try:
696             (dirents, _, _) = self.get_dir(relpath, self.get_latest_revnum())
697         except SubversionException, (msg, num):
698             if num == ERR_FS_NOT_DIRECTORY:
699                 raise NoSuchFile(relpath)
700             raise
701         return dirents.keys()
702
703     def check_path(self, path, revnum):
704         path = self._request_path(path)
705         conn = self.get_connection()
706         try:
707             return conn.check_path(path, revnum)
708         finally:
709             self.add_connection(conn)
710
711     def mkdir(self, relpath, mode=None):
712         conn = self.get_connection()
713         try:
714             return conn.mkdir(relpath, mode)
715         finally:
716             self.add_connection(conn)
717
718     def replay(self, revision, low_water_mark, send_deltas, editor, pool=None):
719         conn = self._open_real_transport()
720         try:
721             return conn.replay(revision, low_water_mark, 
722                                              send_deltas, editor, pool)
723         finally:
724             self.add_connection(conn)
725
726     def do_update(self, revnum, recurse, editor, pool=None):
727         conn = self._open_real_transport()
728         conn.set_unbusy_handler(lambda: self.add_connection(conn))
729         return conn.do_update(revnum, recurse, editor, pool)
730
731     def has_capability(self, cap):
732         conn = self.get_connection()
733         try:
734             return conn.has_capability(cap)
735         finally:
736             self.add_connection(conn)
737
738     def revprop_list(self, revnum, pool=None):
739         conn = self.get_connection()
740         try:
741             return conn.revprop_list(revnum, pool)
742         finally:
743             self.add_connection(conn)
744
745     def get_commit_editor(self, revprops, done_cb, lock_token, keep_locks):
746         conn = self._open_real_transport()
747         conn.set_unbusy_handler(lambda: self.add_connection(conn))
748         return conn.get_commit_editor(revprops, done_cb,
749                                      lock_token, keep_locks)
750
751     def listable(self):
752         """See Transport.listable().
753         """
754         return True
755
756     # There is no real way to do locking directly on the transport 
757     # nor is there a need to as the remote server will take care of 
758     # locking
759     class PhonyLock(object):
760         def unlock(self):
761             pass
762
763     def lock_read(self, relpath):
764         """See Transport.lock_read()."""
765         return self.PhonyLock()
766
767     def lock_write(self, path_revs, comment=None, steal_lock=False):
768         return self.PhonyLock() # FIXME
769
770     def _is_http_transport(self):
771         return (self.svn_url.startswith("http://") or 
772                 self.svn_url.startswith("https://"))
773
774     def clone_root(self):
775         if self._is_http_transport():
776             return SvnRaTransport(self.get_repos_root(), 
777                                   bzr_to_svn_url(self.base),
778                                   pool=self.connections)
779         return SvnRaTransport(self.get_repos_root(),
780                               pool=self.connections)
781
782     def clone(self, offset=None):
783         """See Transport.clone()."""
784         if offset is None:
785             return SvnRaTransport(self.base, pool=self.connections)
786
787         return SvnRaTransport(urlutils.join(self.base, offset), pool=self.connections)
788
789     def local_abspath(self, relpath):
790         """See Transport.local_abspath()."""
791         absurl = self.abspath(relpath)
792         if self.base.startswith("file:///"):
793             return urlutils.local_path_from_url(absurl)
794         raise NotLocalUrl(absurl)
795
796     def abspath(self, relpath):
797         """See Transport.abspath()."""
798         return urlutils.join(self.base, relpath)