~bzr/ubuntu/maverick/bzr-svn/bzr-ppa

« back to all changes in this revision

Viewing changes to transport.py

  • Committer: Jelmer Vernooij
  • Date: 2008-05-11 19:29:26 UTC
  • mfrom: (220.36.144 0.4)
  • Revision ID: jelmer@samba.org-20080511192926-7mh02j45r25qmzkz
Merge 0.4 branch.

Show diffs side-by-side

added added

removed removed

Lines of Context:
2
2
 
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
5
 
# the Free Software Foundation; either version 2 of the License, or
 
5
# the Free Software Foundation; either version 3 of the License, or
6
6
# (at your option) any later version.
7
7
 
8
8
# This program is distributed in the hope that it will be useful,
27
27
import svn.client
28
28
 
29
29
from errors import convert_svn_error, NoSvnRepositoryPresent
 
30
import urlparse
 
31
import urllib
30
32
 
31
33
svn_config = svn.core.svn_config_get_config(None)
32
34
 
34
36
    """Return a string that can be send as part of the User Agent string."""
35
37
    return "bzr%s+bzr-svn%s" % (bzrlib.__version__, bzrlib.plugins.svn.__version__)
36
38
 
37
 
 
38
 
def _create_auth_baton(pool):
39
 
    """Create a Subversion authentication baton. """
40
 
    # Give the client context baton a suite of authentication
41
 
    # providers.h
42
 
    providers = []
43
 
 
44
 
    if svn.core.SVN_VER_MAJOR == 1 and svn.core.SVN_VER_MINOR >= 5:
45
 
        import auth
46
 
        providers += auth.SubversionAuthenticationConfig().get_svn_auth_providers()
47
 
        providers += [auth.get_ssl_client_cert_pw_provider(1)]
48
 
 
49
 
    providers += [
50
 
        svn.client.get_simple_provider(pool),
51
 
        svn.client.get_username_provider(pool),
52
 
        svn.client.get_ssl_client_cert_file_provider(pool),
53
 
        svn.client.get_ssl_client_cert_pw_file_provider(pool),
54
 
        svn.client.get_ssl_server_trust_file_provider(pool),
55
 
        ]
56
 
 
57
 
    if hasattr(svn.client, 'get_windows_simple_provider'):
58
 
        providers.append(svn.client.get_windows_simple_provider(pool))
59
 
 
60
 
    if hasattr(svn.client, 'get_keychain_simple_provider'):
61
 
        providers.append(svn.client.get_keychain_simple_provider(pool))
62
 
 
63
 
    if hasattr(svn.client, 'get_windows_ssl_server_trust_provider'):
64
 
        providers.append(svn.client.get_windows_ssl_server_trust_provider(pool))
65
 
 
66
 
    return svn.core.svn_auth_open(providers, pool)
67
 
 
68
 
 
69
 
def create_svn_client(pool):
70
 
    client = svn.client.create_context(pool)
71
 
    client.auth_baton = _create_auth_baton(pool)
 
39
 
 
40
def create_svn_client(url):
 
41
    from auth import create_auth_baton
 
42
    client = svn.client.create_context()
 
43
    client.auth_baton = create_auth_baton(url)
72
44
    client.config = svn_config
73
45
    return client
74
46
 
87
59
    return SvnRaTransport(bzr_transport.base)
88
60
 
89
61
 
 
62
def _url_unescape_uri(url):
 
63
    (scheme, netloc, path, query, fragment) = urlparse.urlsplit(url)
 
64
    path = urllib.unquote(path)
 
65
    return urlparse.urlunsplit((scheme, netloc, path, query, fragment))
 
66
 
 
67
 
90
68
def bzr_to_svn_url(url):
91
69
    """Convert a Bazaar URL to a URL understood by Subversion.
92
70
 
97
75
        url.startswith("svn+https://")):
98
76
        url = url[len("svn+"):] # Skip svn+
99
77
 
 
78
    if url.startswith("http"):
 
79
        # Without this, URLs with + in them break
 
80
        url = _url_unescape_uri(url)
 
81
 
100
82
    # The SVN libraries don't like trailing slashes...
101
 
    return url.rstrip('/')
 
83
    url = url.rstrip('/')
 
84
 
 
85
    return url
102
86
 
103
87
 
104
88
def needs_busy(unbound):
116
100
    return convert
117
101
 
118
102
 
119
 
class Editor:
 
103
class Editor(object):
120
104
    """Simple object wrapper around the Subversion delta editor interface."""
121
105
    def __init__(self, transport, (editor, editor_baton)):
122
106
        self.editor = editor
204
188
        return baton
205
189
 
206
190
 
207
 
class SvnRaTransport(Transport):
208
 
    """Fake transport for Subversion-related namespaces.
209
 
    
210
 
    This implements just as much of Transport as is necessary 
211
 
    to fool Bazaar. """
212
 
    @convert_svn_error
213
 
    def __init__(self, url="", _backing_url=None):
214
 
        self.pool = Pool()
215
 
        bzr_url = url
216
 
        self.svn_url = bzr_to_svn_url(url)
 
191
class Connection(object):
 
192
    """An single connection to a Subversion repository. This usually can 
 
193
    only do one operation at a time."""
 
194
    def __init__(self, url):
 
195
        self._busy = False
217
196
        self._root = None
218
 
        # _backing_url is an evil hack so the root directory of a repository 
219
 
        # can be accessed on some HTTP repositories. 
220
 
        if _backing_url is None:
221
 
            _backing_url = self.svn_url
222
 
        self._backing_url = _backing_url.rstrip("/")
223
 
        Transport.__init__(self, bzr_url)
224
 
 
225
 
        self._client = create_svn_client(self.pool)
 
197
        self._client = create_svn_client(url)
226
198
        try:
227
 
            self.mutter('opening SVN RA connection to %r' % self._backing_url)
228
 
            self._ra = svn.client.open_ra_session(self._backing_url.encode('utf8'), 
229
 
                    self._client, self.pool)
 
199
            self.mutter('opening SVN RA connection to %r' % url)
 
200
            self._ra = svn.client.open_ra_session(url.encode('utf8'), 
 
201
                    self._client)
230
202
        except SubversionException, (_, num):
231
203
            if num in (svn.core.SVN_ERR_RA_SVN_REPOS_NOT_FOUND,):
232
204
                raise NoSvnRepositoryPresent(url=url)
233
205
            if num == svn.core.SVN_ERR_BAD_URL:
234
206
                raise InvalidURL(url)
235
207
            raise
236
 
 
237
 
        from bzrlib.plugins.svn import lazy_check_versions
238
 
        lazy_check_versions()
239
 
 
240
 
        self._busy = False
241
 
 
242
 
    def _mark_busy(self):
243
 
        assert not self._busy
244
 
        self._busy = True
245
 
 
246
 
    def _unmark_busy(self):
247
 
        assert self._busy
248
 
        self._busy = False
249
 
 
250
 
    def mutter(self, text):
251
 
        if 'transport' in debug.debug_flags:
252
 
            mutter(text)
253
 
 
254
 
    class Reporter:
 
208
        self.url = url
 
209
 
 
210
    class Reporter(object):
255
211
        def __init__(self, transport, (reporter, report_baton)):
256
212
            self._reporter = reporter
257
213
            self._baton = report_baton
286
242
                    self._baton, pool)
287
243
            self._transport._unmark_busy()
288
244
 
289
 
    def has(self, relpath):
290
 
        """See Transport.has()."""
291
 
        # TODO: Raise TransportNotPossible here instead and 
292
 
        # catch it in bzrdir.py
293
 
        return False
294
 
 
295
 
    def get(self, relpath):
296
 
        """See Transport.get()."""
297
 
        # TODO: Raise TransportNotPossible here instead and 
298
 
        # catch it in bzrdir.py
299
 
        raise NoSuchFile(path=relpath)
300
 
 
301
 
    def stat(self, relpath):
302
 
        """See Transport.stat()."""
303
 
        raise TransportNotPossible('stat not supported on Subversion')
 
245
    def is_busy(self):
 
246
        return self._busy
 
247
 
 
248
    def _mark_busy(self):
 
249
        assert not self._busy, "already busy"
 
250
        self._busy = True
 
251
 
 
252
    def _unmark_busy(self):
 
253
        assert self._busy, "not busy"
 
254
        self._busy = False
 
255
 
 
256
    def mutter(self, text):
 
257
        if 'transport' in debug.debug_flags:
 
258
            mutter(text)
304
259
 
305
260
    @convert_svn_error
306
261
    @needs_busy
308
263
        self.mutter('svn get-uuid')
309
264
        return svn.ra.get_uuid(self._ra)
310
265
 
311
 
    def get_repos_root(self):
312
 
        root = self.get_svn_repos_root()
313
 
        if (self.base.startswith("svn+http:") or 
314
 
            self.base.startswith("svn+https:")):
315
 
            return "svn+%s" % root
316
 
        return root
317
 
 
318
266
    @convert_svn_error
319
267
    @needs_busy
320
 
    def get_svn_repos_root(self):
 
268
    def get_repos_root(self):
321
269
        if self._root is None:
322
270
            self.mutter("svn get-repos-root")
323
271
            self._root = svn.ra.get_repos_root(self._ra)
337
285
 
338
286
    @convert_svn_error
339
287
    def do_switch(self, switch_rev, recurse, switch_url, editor, pool=None):
340
 
        self._open_real_transport()
341
288
        self.mutter('svn switch -r %d -> %r' % (switch_rev, switch_url))
342
289
        self._mark_busy()
343
290
        edit, edit_baton = self._make_editor(editor, pool)
345
292
                             recurse, switch_url, edit, edit_baton, pool))
346
293
 
347
294
    @convert_svn_error
348
 
    @needs_busy
349
 
    def get_log(self, path, from_revnum, to_revnum, limit, discover_changed_paths, 
350
 
                strict_node_history, revprops, rcvr, pool=None):
351
 
        self.mutter('svn log %r:%r %r' % (from_revnum, to_revnum, path))
 
295
    def change_rev_prop(self, revnum, name, value, pool=None):
 
296
        self.mutter('svn revprop -r%d --set %s=%s' % (revnum, name, value))
 
297
        svn.ra.change_rev_prop(self._ra, revnum, name, value)
 
298
 
 
299
    @convert_svn_error
 
300
    @needs_busy
 
301
    def get_lock(self, path):
 
302
        return svn.ra.get_lock(self._ra, path)
 
303
 
 
304
    @convert_svn_error
 
305
    @needs_busy
 
306
    def unlock(self, locks, break_lock=False):
 
307
        def lock_cb(baton, path, do_lock, lock, ra_err, pool):
 
308
            pass
 
309
        return svn.ra.unlock(self._ra, locks, break_lock, lock_cb)
 
310
 
 
311
    @convert_svn_error
 
312
    @needs_busy
 
313
    def get_dir(self, path, revnum, pool=None, kind=False):
 
314
        self.mutter("svn ls -r %d '%r'" % (revnum, path))
 
315
        assert len(path) == 0 or path[0] != "/"
 
316
        # ra_dav backends fail with strange errors if the path starts with a 
 
317
        # slash while other backends don't.
 
318
        if hasattr(svn.ra, 'get_dir2'):
 
319
            fields = 0
 
320
            if kind:
 
321
                fields += svn.core.SVN_DIRENT_KIND
 
322
            return svn.ra.get_dir2(self._ra, path, revnum, fields)
 
323
        else:
 
324
            return svn.ra.get_dir(self._ra, path, revnum)
 
325
 
 
326
    @convert_svn_error
 
327
    @needs_busy
 
328
    def check_path(self, path, revnum):
 
329
        assert len(path) == 0 or path[0] != "/"
 
330
        self.mutter("svn check_path -r%d %s" % (revnum, path))
 
331
        return svn.ra.check_path(self._ra, path.encode('utf-8'), revnum)
 
332
 
 
333
    @convert_svn_error
 
334
    @needs_busy
 
335
    def mkdir(self, relpath, mode=None):
 
336
        assert len(relpath) == 0 or relpath[0] != "/"
 
337
        path = urlutils.join(self.url, relpath)
 
338
        try:
 
339
            svn.client.mkdir([path.encode("utf-8")], self._client)
 
340
        except SubversionException, (msg, num):
 
341
            if num == svn.core.SVN_ERR_FS_NOT_FOUND:
 
342
                raise NoSuchFile(path)
 
343
            if num == svn.core.SVN_ERR_FS_ALREADY_EXISTS:
 
344
                raise FileExists(path)
 
345
            raise
 
346
 
 
347
    @convert_svn_error
 
348
    def replay(self, revision, low_water_mark, send_deltas, editor, pool=None):
 
349
        self.mutter('svn replay -r%r:%r' % (low_water_mark, revision))
 
350
        self._mark_busy()
 
351
        edit, edit_baton = self._make_editor(editor, pool)
 
352
        svn.ra.replay(self._ra, revision, low_water_mark, send_deltas,
 
353
                      edit, edit_baton, pool)
 
354
 
 
355
    @convert_svn_error
 
356
    def do_update(self, revnum, recurse, editor, pool=None):
 
357
        self.mutter('svn update -r %r' % revnum)
 
358
        self._mark_busy()
 
359
        edit, edit_baton = self._make_editor(editor, pool)
 
360
        return self.Reporter(self, svn.ra.do_update(self._ra, revnum, "", 
 
361
                             recurse, edit, edit_baton, pool))
 
362
 
 
363
    @convert_svn_error
 
364
    def has_capability(self, cap):
 
365
        return svn.ra.has_capability(self._ra, cap)
 
366
 
 
367
    @convert_svn_error
 
368
    def revprop_list(self, revnum, pool=None):
 
369
        self.mutter('svn revprop-list -r %r' % revnum)
 
370
        return svn.ra.rev_proplist(self._ra, revnum, pool)
 
371
 
 
372
    @convert_svn_error
 
373
    def get_commit_editor(self, revprops, done_cb, lock_token, keep_locks):
 
374
        self._mark_busy()
 
375
        try:
 
376
            if hasattr(svn.ra, 'get_commit_editor3'):
 
377
                editor = svn.ra.get_commit_editor3(self._ra, revprops, done_cb, 
 
378
                                                  lock_token, keep_locks)
 
379
            elif revprops.keys() != [svn.core.SVN_PROP_REVISION_LOG]:
 
380
                raise NotImplementedError()
 
381
            else:
 
382
                editor = svn.ra.get_commit_editor2(self._ra, 
 
383
                            revprops[svn.core.SVN_PROP_REVISION_LOG],
 
384
                            done_cb, lock_token, keep_locks)
 
385
 
 
386
            return Editor(self, editor)
 
387
        except:
 
388
            self._unmark_busy()
 
389
            raise
 
390
 
 
391
    class SvnLock(object):
 
392
        def __init__(self, transport, tokens):
 
393
            self._tokens = tokens
 
394
            self._transport = transport
 
395
 
 
396
        def unlock(self):
 
397
            self.transport.unlock(self.locks)
 
398
 
 
399
    @convert_svn_error
 
400
    @needs_busy
 
401
    def lock_write(self, path_revs, comment=None, steal_lock=False):
 
402
        tokens = {}
 
403
        def lock_cb(baton, path, do_lock, lock, ra_err, pool):
 
404
            tokens[path] = lock
 
405
        svn.ra.lock(self._ra, path_revs, comment, steal_lock, lock_cb)
 
406
        return SvnLock(self, tokens)
 
407
 
 
408
    @convert_svn_error
 
409
    @needs_busy
 
410
    def get_log(self, paths, from_revnum, to_revnum, limit, 
 
411
                discover_changed_paths, strict_node_history, revprops, rcvr, 
 
412
                pool=None):
 
413
        if paths is None:
 
414
            paths = [""]
 
415
        self.mutter('svn log %r:%r %r' % (from_revnum, to_revnum, paths))
352
416
        if hasattr(svn.ra, 'get_log2'):
353
 
            return svn.ra.get_log2(self._ra, [self._request_path(path)], 
354
 
                           from_revnum, to_revnum, limit, discover_changed_paths,
355
 
                           strict_node_history, False, 
 
417
            return svn.ra.get_log2(self._ra, paths, 
 
418
                           from_revnum, to_revnum, limit, 
 
419
                           discover_changed_paths, strict_node_history, False, 
356
420
                           revprops, rcvr, pool)
357
421
 
358
 
        class LogEntry:
 
422
        class LogEntry(object):
359
423
            def __init__(self, changed_paths, rev, author, date, message):
360
424
                self.changed_paths = changed_paths
361
425
                self.revprops = {}
373
437
        def rcvr_convert(orig_paths, rev, author, date, message, pool):
374
438
            rcvr(LogEntry(orig_paths, rev, author, date, message), pool)
375
439
 
376
 
        return svn.ra.get_log(self._ra, [self._request_path(path)], 
 
440
        return svn.ra.get_log(self._ra, paths, 
377
441
                              from_revnum, to_revnum, limit, discover_changed_paths, 
378
442
                              strict_node_history, rcvr_convert, pool)
379
443
 
380
 
    def _open_real_transport(self):
381
 
        if self._backing_url != self.svn_url:
382
 
            self.reparent(self.base)
383
 
        assert self._backing_url == self.svn_url
384
 
 
385
 
    def reparent_root(self):
386
 
        if self._is_http_transport():
387
 
            self.svn_url = self.get_svn_repos_root()
388
 
            self.base = self.get_repos_root()
389
 
        else:
390
 
            self.reparent(self.get_repos_root())
391
 
 
392
 
    @convert_svn_error
393
 
    def change_rev_prop(self, revnum, name, value, pool=None):
394
 
        self.mutter('svn revprop -r%d --set %s=%s' % (revnum, name, value))
395
 
        svn.ra.change_rev_prop(self._ra, revnum, name, value)
396
 
 
397
444
    @convert_svn_error
398
445
    @needs_busy
399
446
    def reparent(self, url):
400
 
        url = url.rstrip("/")
401
 
        self.base = url
402
 
        self.svn_url = bzr_to_svn_url(url)
403
 
        if self.svn_url == self._backing_url:
 
447
        if self.url == url:
404
448
            return
405
449
        if hasattr(svn.ra, 'reparent'):
406
450
            self.mutter('svn reparent %r' % url)
407
 
            svn.ra.reparent(self._ra, self.svn_url, self.pool)
 
451
            svn.ra.reparent(self._ra, url)
 
452
            self.url = url
408
453
        else:
409
 
            self.mutter('svn reparent (reconnect) %r' % url)
410
 
            self._ra = svn.client.open_ra_session(self.svn_url.encode('utf8'), 
411
 
                    self._client, self.pool)
412
 
        self._backing_url = self.svn_url
413
 
 
 
454
            raise NotImplementedError(self.reparent)
 
455
 
 
456
 
 
457
class ConnectionPool(object):
 
458
    """Collection of connections to a Subversion repository."""
 
459
    def __init__(self):
 
460
        self.connections = set()
 
461
 
 
462
    def get(self, url):
 
463
        # Check if there is an existing connection we can use
 
464
        for c in self.connections:
 
465
            if c.url == url:
 
466
                self.connections.remove(c)
 
467
                return c
 
468
        # Nothing available? Just pick an existing one and reparent:
 
469
        if len(self.connections) == 0:
 
470
            return Connection(url)
 
471
        c = self.connections.pop()
 
472
        try:
 
473
            c.reparent(url)
 
474
            return c
 
475
        except NotImplementedError:
 
476
            self.connections.add(c)
 
477
            return Connection(url)
 
478
        except:
 
479
            self.connections.add(c)
 
480
            raise
 
481
 
 
482
    def add(self, connection):
 
483
        self.connections.add(connection)
 
484
    
 
485
 
 
486
class SvnRaTransport(Transport):
 
487
    """Fake transport for Subversion-related namespaces.
 
488
    
 
489
    This implements just as much of Transport as is necessary 
 
490
    to fool Bazaar. """
414
491
    @convert_svn_error
415
 
    @needs_busy
 
492
    def __init__(self, url="", _backing_url=None, pool=None):
 
493
        self.pool = Pool()
 
494
        bzr_url = url
 
495
        self.svn_url = bzr_to_svn_url(url)
 
496
        # _backing_url is an evil hack so the root directory of a repository 
 
497
        # can be accessed on some HTTP repositories. 
 
498
        if _backing_url is None:
 
499
            _backing_url = self.svn_url
 
500
        self._backing_url = _backing_url.rstrip("/")
 
501
        Transport.__init__(self, bzr_url)
 
502
 
 
503
        if pool is None:
 
504
            self.connections = ConnectionPool()
 
505
        else:
 
506
            self.connections = pool
 
507
 
 
508
        # Make sure that the URL is valid by connecting to it.
 
509
        self.connections.add(self.connections.get(self._backing_url))
 
510
 
 
511
        from bzrlib.plugins.svn import lazy_check_versions
 
512
        lazy_check_versions()
 
513
 
 
514
    def get_connection(self):
 
515
        return self.connections.get(self._backing_url)
 
516
 
 
517
    def add_connection(self, conn):
 
518
        self.connections.add(conn)
 
519
 
 
520
    def has(self, relpath):
 
521
        """See Transport.has()."""
 
522
        # TODO: Raise TransportNotPossible here instead and 
 
523
        # catch it in bzrdir.py
 
524
        return False
 
525
 
 
526
    def get(self, relpath):
 
527
        """See Transport.get()."""
 
528
        # TODO: Raise TransportNotPossible here instead and 
 
529
        # catch it in bzrdir.py
 
530
        raise NoSuchFile(path=relpath)
 
531
 
 
532
    def stat(self, relpath):
 
533
        """See Transport.stat()."""
 
534
        raise TransportNotPossible('stat not supported on Subversion')
 
535
 
 
536
    def get_uuid(self):
 
537
        conn = self.get_connection()
 
538
        try:
 
539
            return conn.get_uuid()
 
540
        finally:
 
541
            self.add_connection(conn)
 
542
 
 
543
    def get_repos_root(self):
 
544
        root = self.get_svn_repos_root()
 
545
        if (self.base.startswith("svn+http:") or 
 
546
            self.base.startswith("svn+https:")):
 
547
            return "svn+%s" % root
 
548
        return root
 
549
 
 
550
    def get_svn_repos_root(self):
 
551
        conn = self.get_connection()
 
552
        try:
 
553
            return conn.get_repos_root()
 
554
        finally:
 
555
            self.add_connection(conn)
 
556
 
 
557
    def get_latest_revnum(self):
 
558
        conn = self.get_connection()
 
559
        try:
 
560
            return conn.get_latest_revnum()
 
561
        finally:
 
562
            self.add_connection(conn)
 
563
 
 
564
    def do_switch(self, switch_rev, recurse, switch_url, editor, pool=None):
 
565
        conn = self._open_real_transport()
 
566
        try:
 
567
            return conn.do_switch(switch_rev, recurse, switch_url, editor, pool)
 
568
        finally:
 
569
            self.add_connection(conn)
 
570
 
 
571
    def iter_log(self, paths, from_revnum, to_revnum, limit, discover_changed_paths, 
 
572
                 strict_node_history, revprops):
 
573
 
 
574
        assert paths is None or isinstance(paths, list)
 
575
        assert paths is None or all([isinstance(x, str) for x in paths])
 
576
        assert isinstance(from_revnum, int) and isinstance(to_revnum, int)
 
577
        assert isinstance(limit, int)
 
578
        from threading import Thread, Semaphore
 
579
 
 
580
        class logfetcher(Thread):
 
581
            def __init__(self, transport, *args, **kwargs):
 
582
                Thread.__init__(self)
 
583
                self.setDaemon(True)
 
584
                self.transport = transport
 
585
                self.args = args
 
586
                self.kwargs = kwargs
 
587
                self.pending = []
 
588
                self.conn = None
 
589
                self.semaphore = Semaphore(0)
 
590
 
 
591
            def next(self):
 
592
                self.semaphore.acquire()
 
593
                ret = self.pending.pop(0)
 
594
                if ret is None:
 
595
                    self.transport.add_connection(self.conn)
 
596
                if isinstance(ret, Exception):
 
597
                    self.transport.add_connection(self.conn)
 
598
                    raise ret
 
599
                return ret
 
600
 
 
601
            def run(self):
 
602
                def rcvr(log_entry, pool):
 
603
                    self.pending.append((log_entry.changed_paths, log_entry.revision, log_entry.revprops))
 
604
                    self.semaphore.release()
 
605
                self.conn = self.transport.get_connection()
 
606
                try:
 
607
                    self.conn.get_log(rcvr=rcvr, *self.args, **self.kwargs)
 
608
                    self.pending.append(None)
 
609
                except Exception, e:
 
610
                    self.pending.append(e)
 
611
                self.semaphore.release()
 
612
 
 
613
        if paths is None:
 
614
            newpaths = None
 
615
        else:
 
616
            newpaths = [self._request_path(path) for path in paths]
 
617
        
 
618
        fetcher = logfetcher(self, paths, from_revnum, to_revnum, limit, discover_changed_paths, strict_node_history, revprops)
 
619
        fetcher.start()
 
620
        return iter(fetcher.next, None)
 
621
 
 
622
    def get_log(self, paths, from_revnum, to_revnum, limit, discover_changed_paths, 
 
623
                strict_node_history, revprops, rcvr, pool=None):
 
624
        assert paths is None or isinstance(paths, list), "Invalid paths"
 
625
        assert paths is None or all([isinstance(x, str) for x in paths])
 
626
 
 
627
        if paths is None:
 
628
            newpaths = None
 
629
        else:
 
630
            newpaths = [self._request_path(path) for path in paths]
 
631
 
 
632
        conn = self.get_connection()
 
633
        try:
 
634
            return conn.get_log(newpaths, 
 
635
                    from_revnum, to_revnum,
 
636
                    limit, discover_changed_paths, strict_node_history, 
 
637
                    revprops, rcvr, pool)
 
638
        finally:
 
639
            self.add_connection(conn)
 
640
 
 
641
    def _open_real_transport(self):
 
642
        if self._backing_url != self.svn_url:
 
643
            return self.connections.get(self.svn_url)
 
644
        return self.get_connection()
 
645
 
 
646
    def change_rev_prop(self, revnum, name, value, pool=None):
 
647
        conn = self.get_connection()
 
648
        try:
 
649
            return conn.change_rev_prop(revnum, name, value, pool)
 
650
        finally:
 
651
            self.add_connection(conn)
 
652
 
416
653
    def get_dir(self, path, revnum, pool=None, kind=False):
417
 
        self.mutter("svn ls -r %d '%r'" % (revnum, path))
418
 
        assert len(path) == 0 or path[0] != "/"
419
654
        path = self._request_path(path)
420
 
        # ra_dav backends fail with strange errors if the path starts with a 
421
 
        # slash while other backends don't.
422
 
        if hasattr(svn.ra, 'get_dir2'):
423
 
            fields = 0
424
 
            if kind:
425
 
                fields += svn.core.SVN_DIRENT_KIND
426
 
            return svn.ra.get_dir2(self._ra, path, revnum, fields)
427
 
        else:
428
 
            return svn.ra.get_dir(self._ra, path, revnum)
 
655
        conn = self.get_connection()
 
656
        try:
 
657
            return conn.get_dir(path, revnum, pool, kind)
 
658
        finally:
 
659
            self.add_connection(conn)
 
660
 
 
661
    def mutter(self, text):
 
662
        if 'transport' in debug.debug_flags:
 
663
            mutter(text)
429
664
 
430
665
    def _request_path(self, relpath):
431
666
        if self._backing_url == self.svn_url:
436
671
        self.mutter('request path %r -> %r' % (relpath, newrelpath))
437
672
        return newrelpath
438
673
 
439
 
    @convert_svn_error
440
674
    def list_dir(self, relpath):
441
675
        assert len(relpath) == 0 or relpath[0] != "/"
442
676
        if relpath == ".":
443
677
            relpath = ""
444
678
        try:
445
 
            (dirents, _, _) = self.get_dir(self._request_path(relpath),
446
 
                                           self.get_latest_revnum())
 
679
            (dirents, _, _) = self.get_dir(relpath, self.get_latest_revnum())
447
680
        except SubversionException, (msg, num):
448
681
            if num == svn.core.SVN_ERR_FS_NOT_DIRECTORY:
449
682
                raise NoSuchFile(relpath)
450
683
            raise
451
684
        return dirents.keys()
452
685
 
453
 
    @convert_svn_error
454
 
    @needs_busy
455
 
    def get_lock(self, path):
456
 
        return svn.ra.get_lock(self._ra, path)
457
 
 
458
 
    class SvnLock:
459
 
        def __init__(self, transport, tokens):
460
 
            self._tokens = tokens
461
 
            self._transport = transport
462
 
 
463
 
        def unlock(self):
464
 
            self.transport.unlock(self.locks)
465
 
 
466
 
    @convert_svn_error
467
 
    @needs_busy
468
 
    def unlock(self, locks, break_lock=False):
469
 
        def lock_cb(baton, path, do_lock, lock, ra_err, pool):
470
 
            pass
471
 
        return svn.ra.unlock(self._ra, locks, break_lock, lock_cb)
472
 
 
473
 
    @convert_svn_error
474
 
    @needs_busy
475
 
    def lock_write(self, path_revs, comment=None, steal_lock=False):
476
 
        return self.PhonyLock() # FIXME
477
 
        tokens = {}
478
 
        def lock_cb(baton, path, do_lock, lock, ra_err, pool):
479
 
            tokens[path] = lock
480
 
        svn.ra.lock(self._ra, path_revs, comment, steal_lock, lock_cb)
481
 
        return SvnLock(self, tokens)
482
 
 
483
 
    @convert_svn_error
484
 
    @needs_busy
485
 
    def check_path(self, path, revnum, *args, **kwargs):
486
 
        assert len(path) == 0 or path[0] != "/"
 
686
    def check_path(self, path, revnum):
487
687
        path = self._request_path(path)
488
 
        self.mutter("svn check_path -r%d %s" % (revnum, path))
489
 
        return svn.ra.check_path(self._ra, path.encode('utf-8'), revnum, *args, **kwargs)
 
688
        conn = self.get_connection()
 
689
        try:
 
690
            return conn.check_path(path, revnum)
 
691
        finally:
 
692
            self.add_connection(conn)
490
693
 
491
 
    @convert_svn_error
492
 
    @needs_busy
493
694
    def mkdir(self, relpath, mode=None):
494
 
        assert len(relpath) == 0 or relpath[0] != "/"
495
 
        path = urlutils.join(self.svn_url, relpath)
 
695
        conn = self.get_connection()
496
696
        try:
497
 
            svn.client.mkdir([path.encode("utf-8")], self._client)
498
 
        except SubversionException, (msg, num):
499
 
            if num == svn.core.SVN_ERR_FS_NOT_FOUND:
500
 
                raise NoSuchFile(path)
501
 
            if num == svn.core.SVN_ERR_FS_ALREADY_EXISTS:
502
 
                raise FileExists(path)
503
 
            raise
 
697
            return conn.mkdir(relpath, mode)
 
698
        finally:
 
699
            self.add_connection(conn)
504
700
 
505
 
    @convert_svn_error
506
701
    def replay(self, revision, low_water_mark, send_deltas, editor, pool=None):
507
 
        self._open_real_transport()
508
 
        self.mutter('svn replay -r%r:%r' % (low_water_mark, revision))
509
 
        self._mark_busy()
510
 
        edit, edit_baton = self._make_editor(editor, pool)
511
 
        svn.ra.replay(self._ra, revision, low_water_mark, send_deltas,
512
 
                      edit, edit_baton, pool)
 
702
        conn = self._open_real_transport()
 
703
        try:
 
704
            return conn.replay(revision, low_water_mark, 
 
705
                                             send_deltas, editor, pool)
 
706
        finally:
 
707
            self.add_connection(conn)
513
708
 
514
 
    @convert_svn_error
515
709
    def do_update(self, revnum, recurse, editor, pool=None):
516
 
        self._open_real_transport()
517
 
        self.mutter('svn update -r %r' % revnum)
518
 
        self._mark_busy()
519
 
        edit, edit_baton = self._make_editor(editor, pool)
520
 
        return self.Reporter(self, svn.ra.do_update(self._ra, revnum, "", 
521
 
                             recurse, edit, edit_baton, pool))
 
710
        conn = self._open_real_transport()
 
711
        try:
 
712
            return conn.do_update(revnum, recurse, editor, pool)
 
713
        finally:
 
714
            self.add_connection(conn)
522
715
 
523
 
    @convert_svn_error
524
716
    def has_capability(self, cap):
525
 
        return svn.ra.has_capability(self._ra, cap)
 
717
        conn = self.get_connection()
 
718
        try:
 
719
            return conn.has_capability(cap)
 
720
        finally:
 
721
            self.add_connection(conn)
526
722
 
527
 
    @convert_svn_error
528
723
    def revprop_list(self, revnum, pool=None):
529
 
        self.mutter('svn revprop-list -r %r' % revnum)
530
 
        return svn.ra.rev_proplist(self._ra, revnum, pool)
 
724
        conn = self.get_connection()
 
725
        try:
 
726
            return conn.revprop_list(revnum, pool)
 
727
        finally:
 
728
            self.add_connection(conn)
531
729
 
532
 
    @convert_svn_error
533
730
    def get_commit_editor(self, revprops, done_cb, lock_token, keep_locks):
534
 
        self._open_real_transport()
535
 
        self._mark_busy()
 
731
        conn = self._open_real_transport()
536
732
        try:
537
 
            if hasattr(svn.ra, 'get_commit_editor3'):
538
 
                editor = svn.ra.get_commit_editor3(self._ra, revprops, done_cb, 
539
 
                                                  lock_token, keep_locks)
540
 
            elif revprops.keys() != [svn.core.SVN_PROP_REVISION_LOG]:
541
 
                raise NotImplementedError()
542
 
            else:
543
 
                editor = svn.ra.get_commit_editor2(self._ra, 
544
 
                            revprops[svn.core.SVN_PROP_REVISION_LOG],
545
 
                            done_cb, lock_token, keep_locks)
546
 
 
547
 
            return Editor(self, editor)
548
 
        except:
549
 
            self._unmark_busy()
550
 
            raise
 
733
            return conn.get_commit_editor(revprops, done_cb,
 
734
                                         lock_token, keep_locks)
 
735
        finally:
 
736
            self.add_connection(conn)
551
737
 
552
738
    def listable(self):
553
739
        """See Transport.listable().
557
743
    # There is no real way to do locking directly on the transport 
558
744
    # nor is there a need to as the remote server will take care of 
559
745
    # locking
560
 
    class PhonyLock:
 
746
    class PhonyLock(object):
561
747
        def unlock(self):
562
748
            pass
563
749
 
565
751
        """See Transport.lock_read()."""
566
752
        return self.PhonyLock()
567
753
 
 
754
    def lock_write(self, path_revs, comment=None, steal_lock=False):
 
755
        return self.PhonyLock() # FIXME
 
756
 
568
757
    def _is_http_transport(self):
569
758
        return (self.svn_url.startswith("http://") or 
570
759
                self.svn_url.startswith("https://"))
572
761
    def clone_root(self):
573
762
        if self._is_http_transport():
574
763
            return SvnRaTransport(self.get_repos_root(), 
575
 
                                  bzr_to_svn_url(self.base))
576
 
        return SvnRaTransport(self.get_repos_root())
 
764
                                  bzr_to_svn_url(self.base),
 
765
                                  pool=self.connections)
 
766
        return SvnRaTransport(self.get_repos_root(),
 
767
                              pool=self.connections)
577
768
 
578
769
    def clone(self, offset=None):
579
770
        """See Transport.clone()."""
580
771
        if offset is None:
581
 
            return SvnRaTransport(self.base)
 
772
            return SvnRaTransport(self.base, pool=self.connections)
582
773
 
583
 
        return SvnRaTransport(urlutils.join(self.base, offset))
 
774
        return SvnRaTransport(urlutils.join(self.base, offset), pool=self.connections)
584
775
 
585
776
    def local_abspath(self, relpath):
586
777
        """See Transport.local_abspath()."""