1 # Copyright (C) 2006 Jelmer Vernooij <jelmer@samba.org>
2 # -*- coding: utf-8 -*-
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17 """Simple transport for accessing Subversion smart servers."""
20 from bzrlib import debug, urlutils
21 from bzrlib.errors import (NoSuchFile, TransportNotPossible,
22 FileExists, NotLocalUrl, InvalidURL, RedirectRequested)
23 from bzrlib.trace import mutter, warning
24 from bzrlib.transport import Transport
26 import bzrlib.plugins.svn
27 from bzrlib.plugins.svn.auth import create_auth_baton
28 from bzrlib.plugins.svn.client import get_config
29 from bzrlib.plugins.svn.subvertpy import SubversionException, ra
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_DIRECTORY, ERR_RA_DAV_RELOCATED, ERR_RA_DAV_PATH_NOT_FOUND, ERR_UNKNOWN_CAPABILITY
34 svn_config = get_config()
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__)
41 # Don't run any tests on SvnTransport as it is not intended to be
42 # a full implementation of Transport
43 def get_test_permutations():
47 def get_svn_ra_transport(bzr_transport):
48 """Obtain corresponding SvnRaTransport for a stock Bazaar transport."""
49 if isinstance(bzr_transport, SvnRaTransport):
52 ra_transport = getattr(bzr_transport, "_svn_ra", None)
53 if ra_transport is not None:
56 # Save _svn_ra transport here so we don't have to connect again next time
57 # we try to use bzr svn on this transport
58 ra_transport = SvnRaTransport(bzr_transport.base)
59 bzr_transport._svn_ra = ra_transport
63 def _url_unescape_uri(url):
64 (scheme, netloc, path, query, fragment) = urlparse.urlsplit(url)
65 if scheme in ("http", "https"):
66 # Without this, URLs with + in them break
67 path = urllib.unquote(path)
68 return urlparse.urlunsplit((scheme, netloc, path, query, fragment))
71 def _url_escape_uri(url):
72 (scheme, netloc, path, query, fragment) = urlparse.urlsplit(url)
73 if scheme in ("http", "https"):
74 # Without this, URLs with + in them break
75 path = urllib.quote(path)
76 return urlparse.urlunsplit((scheme, netloc, path, query, fragment))
79 svnplus_warning_showed = False
81 def warn_svnplus(url):
82 global svnplus_warning_showed
83 if not svnplus_warning_showed:
84 warning("The svn+ syntax is deprecated, use %s instead.", url)
85 svnplus_warning_showed = True
88 def bzr_to_svn_url(url):
89 """Convert a Bazaar URL to a URL understood by Subversion.
91 This will possibly remove the svn+ prefix.
93 if (url.startswith("svn+http://") or
94 url.startswith("svn+file://") or
95 url.startswith("svn+https://")):
96 url = url[len("svn+"):] # Skip svn+
99 url = _url_unescape_uri(url)
101 # The SVN libraries don't like trailing slashes...
102 url = url.rstrip('/')
109 mutter('opening SVN RA connection to %r' % url)
110 ret = ra.RemoteAccess(url.encode('utf8'),
111 auth=create_auth_baton(url),
112 client_string_func=get_client_string)
113 if 'transport' in debug.debug_flags:
114 ret = MutteringRemoteAccess(ret)
115 except SubversionException, (msg, num):
116 if num in (ERR_RA_SVN_REPOS_NOT_FOUND,):
117 raise NoSvnRepositoryPresent(url=url)
118 if num == ERR_BAD_URL:
119 raise InvalidURL(url)
120 if num == ERR_RA_DAV_PATH_NOT_FOUND:
121 raise NoSuchFile(url)
122 if num == ERR_RA_DAV_RELOCATED:
123 # Try to guess the new url
125 new_url = msg.split("'")[1]
127 new_url = msg[msg.index("»")+2:msg.index("«")]
129 raise AssertionError("Unable to parse error message: %s" % msg)
130 raise RedirectRequested(source=url, target=new_url,
134 from bzrlib.plugins.svn import lazy_check_versions
135 lazy_check_versions()
140 class ConnectionPool(object):
141 """Collection of connections to a Subversion repository."""
143 self.connections = set()
146 # Check if there is an existing connection we can use
147 for c in self.connections:
148 assert not c.busy, "busy connection in pool"
150 self.connections.remove(c)
152 # Nothing available? Just pick an existing one and reparent:
153 if len(self.connections) == 0:
154 return Connection(url)
155 c = self.connections.pop()
157 c.reparent(_url_escape_uri(url))
159 except NotImplementedError:
160 self.connections.add(c)
161 return Connection(url)
163 self.connections.add(c)
166 def add(self, connection):
167 assert not connection.busy, "adding busy connection in pool"
168 self.connections.add(connection)
171 class SvnRaTransport(Transport):
172 """Fake transport for Subversion-related namespaces.
174 This implements just as much of Transport as is necessary
177 def __init__(self, url="", pool=None, _uuid=None, _repos_root=None):
179 self.svn_url = bzr_to_svn_url(url)
180 Transport.__init__(self, bzr_url)
183 self.connections = ConnectionPool()
185 # Make sure that the URL is valid by connecting to it.
186 self.connections.add(self.connections.get(self.svn_url))
188 self.connections = pool
190 self._repos_root = _repos_root
192 self.capabilities = {}
194 from bzrlib.plugins.svn import lazy_check_versions
195 lazy_check_versions()
197 def get_connection(self, repos_path=None):
198 if repos_path is not None:
199 return self.connections.get(urlutils.join(self.get_svn_repos_root(),
202 return self.connections.get(self.svn_url)
204 def add_connection(self, conn):
205 self.connections.add(conn)
207 def has(self, relpath):
208 """See Transport.has()."""
209 # TODO: Raise TransportNotPossible here instead and
210 # catch it in bzrdir.py
213 def get(self, relpath):
214 """See Transport.get()."""
215 # TODO: Raise TransportNotPossible here instead and
216 # catch it in bzrdir.py
217 raise NoSuchFile(path=relpath)
219 def stat(self, relpath):
220 """See Transport.stat()."""
221 raise TransportNotPossible('stat not supported on Subversion')
223 def put_file(self, name, file, mode=0):
224 raise TransportNotPossible("put_file not supported on Subversion")
227 if self._uuid is None:
228 conn = self.get_connection()
230 return conn.get_uuid()
232 self.add_connection(conn)
235 def get_repos_root(self):
236 root = self.get_svn_repos_root()
237 if (self.base.startswith("svn+http:") or
238 self.base.startswith("svn+https:")):
239 return "svn+%s" % root
242 def get_svn_repos_root(self):
243 if self._repos_root is None:
244 conn = self.get_connection()
246 self._repos_root = conn.get_repos_root()
248 self.add_connection(conn)
249 return self._repos_root
251 def get_latest_revnum(self):
252 conn = self.get_connection()
254 return conn.get_latest_revnum()
256 self.add_connection(conn)
258 def iter_log(self, paths, from_revnum, to_revnum, limit, discover_changed_paths,
259 strict_node_history, include_merged_revisions, revprops):
260 assert paths is None or isinstance(paths, list)
261 assert isinstance(from_revnum, int) and isinstance(to_revnum, int)
262 assert isinstance(limit, int)
263 from threading import Thread, Semaphore
265 class logfetcher(Thread):
266 def __init__(self, transport, *args, **kwargs):
267 Thread.__init__(self)
269 self.transport = transport
273 self.conn = self.transport.get_connection()
274 self.semaphore = Semaphore(0)
278 self.semaphore.acquire()
279 ret = self.pending.pop(0)
280 if isinstance(ret, Exception):
285 assert not self.busy, "already running"
287 def rcvr(orig_paths, revision, revprops, has_children=None):
288 self.pending.append((orig_paths, revision, revprops, has_children))
289 self.semaphore.release()
292 self.conn.get_log(rcvr, *self.args, **self.kwargs)
293 self.pending.append(None)
295 self.pending.append(e)
297 self.pending.append(Exception("Some exception was not handled"))
298 self.semaphore.release()
299 self.transport.add_connection(self.conn)
304 newpaths = [p.rstrip("/") for p in paths]
306 fetcher = logfetcher(self, newpaths, from_revnum, to_revnum, limit, discover_changed_paths=discover_changed_paths, strict_node_history=strict_node_history, include_merged_revisions=include_merged_revisions, revprops=revprops)
308 return iter(fetcher.next, None)
310 def get_log(self, rcvr, paths, from_revnum, to_revnum, limit, discover_changed_paths,
311 strict_node_history, include_merged_revisions, revprops):
312 assert paths is None or isinstance(paths, list), "Invalid paths"
315 for item in [isinstance(x, str) for x in paths]:
320 assert paths is None or all_true
325 newpaths = [p.rstrip("/") for p in paths]
327 conn = self.get_connection()
329 return conn.get_log(rcvr, newpaths,
330 from_revnum, to_revnum,
331 limit, discover_changed_paths, strict_node_history,
332 include_merged_revisions,
335 self.add_connection(conn)
337 def change_rev_prop(self, revnum, name, value):
338 conn = self.get_connection()
340 return conn.change_rev_prop(revnum, name, value)
342 self.add_connection(conn)
344 def get_dir(self, path, revnum, kind=False):
345 conn = self.get_connection()
347 return conn.get_dir(path, revnum, kind)
349 self.add_connection(conn)
351 def get_file(self, path, stream, revnum):
352 conn = self.get_connection()
354 return conn.get_file(path, stream, revnum)
356 self.add_connection(conn)
358 def list_dir(self, relpath):
359 assert len(relpath) == 0 or relpath[0] != "/"
363 (dirents, _, _) = self.get_dir(relpath, self.get_latest_revnum())
364 except SubversionException, (msg, num):
365 if num == ERR_FS_NOT_DIRECTORY:
366 raise NoSuchFile(relpath)
368 return dirents.keys()
370 def check_path(self, path, revnum):
371 conn = self.get_connection()
373 return conn.check_path(path, revnum)
375 self.add_connection(conn)
378 def mkdir(self, relpath, message="Creating directory"):
379 conn = self.get_connection()
381 ce = conn.get_commit_editor({"svn:log": message})
383 node = ce.open_root(-1)
384 batons = relpath.split("/")
386 for i in range(len(batons)):
387 node = node.open_directory("/".join(batons[:i]), -1)
389 toclose.append(node.add_directory(relpath, None, -1))
390 for c in reversed(toclose):
393 except SubversionException, (msg, num):
395 if num == ERR_FS_NOT_DIRECTORY:
396 raise NoSuchFile(msg)
397 if num == ERR_FS_ALREADY_EXISTS:
398 raise FileExists(msg)
401 self.add_connection(conn)
403 def has_capability(self, cap):
404 if cap in self.capabilities:
405 return self.capabilities[cap]
406 conn = self.get_connection()
409 self.capabilities[cap] = conn.has_capability(cap)
410 except SubversionException, (msg, num):
411 if num != ERR_UNKNOWN_CAPABILITY:
413 self.capabilities[cap] = None
414 except NotImplementedError:
415 self.capabilities[cap] = None # None for unknown
416 return self.capabilities[cap]
418 self.add_connection(conn)
420 def revprop_list(self, revnum):
421 conn = self.get_connection()
423 return conn.rev_proplist(revnum)
425 self.add_connection(conn)
427 def get_locations(self, path, peg_revnum, revnums):
428 conn = self.get_connection()
430 return conn.get_locations(path, peg_revnum, revnums)
432 self.add_connection(conn)
435 """See Transport.listable().
439 # There is no real way to do locking directly on the transport
440 # nor is there a need to as the remote server will take care of
442 class PhonyLock(object):
446 def lock_read(self, relpath):
447 """See Transport.lock_read()."""
448 return self.PhonyLock()
450 def lock_write(self, path_revs, comment=None, steal_lock=False):
451 return self.PhonyLock() # FIXME
453 def _is_http_transport(self):
455 return (self.svn_url.startswith("http://") or
456 self.svn_url.startswith("https://"))
458 def clone_root(self):
459 if self._is_http_transport():
460 return SvnRaTransport(self.get_repos_root(),
461 bzr_to_svn_url(self.base),
462 pool=self.connections)
463 return SvnRaTransport(self.get_repos_root(),
464 pool=self.connections)
466 def clone(self, offset=None):
467 """See Transport.clone()."""
471 newurl = urlutils.join(self.base, offset)
473 return SvnRaTransport(newurl, pool=self.connections)
475 def local_abspath(self, relpath):
476 """See Transport.local_abspath()."""
477 absurl = self.abspath(relpath)
478 if self.base.startswith("file:///"):
479 return urlutils.local_path_from_url(absurl)
480 raise NotLocalUrl(absurl)
482 def abspath(self, relpath):
483 """See Transport.abspath()."""
484 return urlutils.join(self.base, relpath)
487 class MutteringRemoteAccess(object):
489 busy = property(lambda self: self.actual.busy)
490 url = property(lambda self: self.actual.url)
492 def __init__(self, actual):
495 def check_path(self, path, revnum):
496 mutter('svn check-path -r%d %s' % (revnum, path))
497 return self.actual.check_path(path, revnum)
499 def has_capability(self, cap):
500 mutter('svn has-capability %s' % (cap,))
501 return self.actual.has_capability(cap)
504 mutter('svn get-uuid')
505 return self.actual.get_uuid()
507 def get_repos_root(self):
508 mutter('svn get-repos-root')
509 return self.actual.get_repos_root()
511 def get_latest_revnum(self):
512 mutter('svn get-latest-revnum')
513 return self.actual.get_latest_revnum()
515 def get_log(self, callback, paths, from_revnum, to_revnum, *args, **kwargs):
516 mutter('svn log -r%d:%d %r' % (from_revnum, to_revnum, paths))
517 return self.actual.get_log(callback, paths,
518 from_revnum, to_revnum, *args, **kwargs)
520 def change_rev_prop(self, revnum, name, value):
521 mutter('svn change-revprop -r%d %s=%s' % (revnum, name, value))
522 return self.actual.change_rev_prop(revnum, name, value)
524 def get_dir(self, path, revnum=-1, fields=0):
525 mutter('svn get-dir -r%d %s' % (revnum, path))
526 return self.actual.get_dir(path, revnum, fields)
528 def get_file(self, path, revnum):
529 mutter('svn get-file -r%d %s' % (revnum, path))
530 return self.actual.get_file(path, revnum)
532 def revprop_list(self, revnum):
533 mutter('svn revprop-list -r%d' % (revnum,))
534 return self.actual.revprop_list(revnum)
536 def get_locations(self, path, peg_revnum, revnums):
537 mutter('svn get_locations -r%d %s (%r)' % (peg_revnum, path, revnums))
538 return self.actual.get_locations(path, peg_revnum, revnums)
540 def do_update(self, revnum, path, start_empty, editor):
541 mutter("svn update -r%d %s" % (revnum, path))
542 return self.actual.do_update(revnum, path, start_empty, editor)
544 def do_switch(self, revnum, path, start_empty, to_url, editor):
545 mutter("svn switch -r%d %s -> %s" % (revnum, path, to_url))
546 return self.actual.do_switch(revnum, path, start_empty, to_url, editor)
548 def reparent(self, url):
549 mutter("svn reparent %s" % url)
550 return self.actual.reparent(url)
552 def get_commit_editor(self, *args, **kwargs):
554 return self.actual.get_commit_editor(*args, **kwargs)
556 def rev_proplist(self, revnum):
557 mutter("svn rev-proplist -r%d" % revnum)
558 return self.actual.rev_proplist(revnum)