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 import ra
28 from bzrlib.plugins.svn.auth import create_auth_baton
29 from bzrlib.plugins.svn.client import get_config
30 from bzrlib.plugins.svn.core import SubversionException
31 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
35 svn_config = get_config()
37 def get_client_string():
38 """Return a string that can be send as part of the User Agent string."""
39 return "bzr%s+bzr-svn%s" % (bzrlib.__version__, bzrlib.plugins.svn.__version__)
42 # Don't run any tests on SvnTransport as it is not intended to be
43 # a full implementation of Transport
44 def get_test_permutations():
48 def get_svn_ra_transport(bzr_transport):
49 """Obtain corresponding SvnRaTransport for a stock Bazaar transport."""
50 if isinstance(bzr_transport, SvnRaTransport):
53 ra_transport = getattr(bzr_transport, "_svn_ra", None)
54 if ra_transport is not None:
57 # Save _svn_ra transport here so we don't have to connect again next time
58 # we try to use bzr svn on this transport
59 ra_transport = SvnRaTransport(bzr_transport.base)
60 bzr_transport._svn_ra = ra_transport
64 def _url_unescape_uri(url):
65 (scheme, netloc, path, query, fragment) = urlparse.urlsplit(url)
66 if scheme in ("http", "https"):
67 # Without this, URLs with + in them break
68 path = urllib.unquote(path)
69 return urlparse.urlunsplit((scheme, netloc, path, query, fragment))
72 def _url_escape_uri(url):
73 (scheme, netloc, path, query, fragment) = urlparse.urlsplit(url)
74 if scheme in ("http", "https"):
75 # Without this, URLs with + in them break
76 path = urllib.quote(path)
77 return urlparse.urlunsplit((scheme, netloc, path, query, fragment))
80 svnplus_warning_showed = False
82 def warn_svnplus(url):
83 global svnplus_warning_showed
84 if not svnplus_warning_showed:
85 warning("The svn+ syntax is deprecated, use %s instead.", url)
86 svnplus_warning_showed = True
89 def bzr_to_svn_url(url):
90 """Convert a Bazaar URL to a URL understood by Subversion.
92 This will possibly remove the svn+ prefix.
94 if (url.startswith("svn+http://") or
95 url.startswith("svn+file://") or
96 url.startswith("svn+https://")):
97 url = url[len("svn+"):] # Skip svn+
100 url = _url_unescape_uri(url)
102 # The SVN libraries don't like trailing slashes...
103 url = url.rstrip('/')
110 mutter('opening SVN RA connection to %r' % url)
111 ret = ra.RemoteAccess(url.encode('utf8'),
112 auth=create_auth_baton(url),
113 client_string_func=get_client_string)
114 except SubversionException, (msg, num):
115 if num in (ERR_RA_SVN_REPOS_NOT_FOUND,):
116 raise NoSvnRepositoryPresent(url=url)
117 if num == ERR_BAD_URL:
118 raise InvalidURL(url)
119 if num == ERR_RA_DAV_PATH_NOT_FOUND:
120 raise NoSuchFile(url)
121 if num == ERR_RA_DAV_RELOCATED:
122 # Try to guess the new url
124 new_url = msg.split("'")[1]
126 new_url = msg[msg.index("»")+2:msg.index("«")]
128 raise AssertionError("Unable to parse error message: %s" % msg)
129 raise RedirectRequested(source=url, target=new_url,
133 from bzrlib.plugins.svn import lazy_check_versions
134 lazy_check_versions()
139 class ConnectionPool(object):
140 """Collection of connections to a Subversion repository."""
142 self.connections = set()
145 # Check if there is an existing connection we can use
146 for c in self.connections:
147 assert not c.busy, "busy connection in pool"
149 self.connections.remove(c)
151 # Nothing available? Just pick an existing one and reparent:
152 if len(self.connections) == 0:
153 return Connection(url)
154 c = self.connections.pop()
156 c.reparent(_url_escape_uri(url))
158 except NotImplementedError:
159 self.connections.add(c)
160 return Connection(url)
162 self.connections.add(c)
165 def add(self, connection):
166 assert not connection.busy, "adding busy connection in pool"
167 self.connections.add(connection)
170 class SvnRaTransport(Transport):
171 """Fake transport for Subversion-related namespaces.
173 This implements just as much of Transport as is necessary
176 def __init__(self, url="", pool=None, _uuid=None, _repos_root=None):
178 self.svn_url = bzr_to_svn_url(url)
179 Transport.__init__(self, bzr_url)
182 self.connections = ConnectionPool()
184 # Make sure that the URL is valid by connecting to it.
185 self.connections.add(self.connections.get(self.svn_url))
187 self.connections = pool
189 self._repos_root = _repos_root
191 self.capabilities = {}
193 from bzrlib.plugins.svn import lazy_check_versions
194 lazy_check_versions()
196 def get_connection(self, repos_path=None):
197 if repos_path is not None:
198 return self.connections.get(urlutils.join(self.get_svn_repos_root(),
201 return self.connections.get(self.svn_url)
203 def add_connection(self, conn):
204 self.connections.add(conn)
206 def has(self, relpath):
207 """See Transport.has()."""
208 # TODO: Raise TransportNotPossible here instead and
209 # catch it in bzrdir.py
212 def get(self, relpath):
213 """See Transport.get()."""
214 # TODO: Raise TransportNotPossible here instead and
215 # catch it in bzrdir.py
216 raise NoSuchFile(path=relpath)
218 def stat(self, relpath):
219 """See Transport.stat()."""
220 raise TransportNotPossible('stat not supported on Subversion')
222 def put_file(self, name, file, mode=0):
223 raise TransportNotPossible("put_file not supported on Subversion")
226 if self._uuid is None:
227 conn = self.get_connection()
228 self.mutter('svn get-uuid')
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 self.mutter('svn get-repos-root')
245 conn = self.get_connection()
247 self._repos_root = conn.get_repos_root()
249 self.add_connection(conn)
250 return self._repos_root
252 def get_latest_revnum(self):
253 conn = self.get_connection()
254 self.mutter('svn get-latest-revnum')
256 return conn.get_latest_revnum()
258 self.add_connection(conn)
260 def iter_log(self, paths, from_revnum, to_revnum, limit, discover_changed_paths,
261 strict_node_history, include_merged_revisions, revprops):
262 assert paths is None or isinstance(paths, list)
263 assert isinstance(from_revnum, int) and isinstance(to_revnum, int)
264 assert isinstance(limit, int)
265 from threading import Thread, Semaphore
267 self.mutter('svn iter-log -r%d:%d %r ' % (from_revnum, to_revnum, paths))
269 class logfetcher(Thread):
270 def __init__(self, transport, *args, **kwargs):
271 Thread.__init__(self)
273 self.transport = transport
277 self.conn = self.transport.get_connection()
278 self.semaphore = Semaphore(0)
282 self.semaphore.acquire()
283 ret = self.pending.pop(0)
285 self.transport.add_connection(self.conn)
286 elif isinstance(ret, Exception):
287 self.transport.add_connection(self.conn)
292 assert not self.busy, "already running"
295 self.pending.append(args)
296 self.semaphore.release()
299 self.conn.get_log(callback=rcvr, *self.args, **self.kwargs)
300 self.pending.append(None)
302 self.pending.append(e)
304 self.pending.append(Exception("Some exception was not handled"))
305 self.semaphore.release()
310 newpaths = [p.rstrip("/") for p in paths]
312 fetcher = logfetcher(self, paths=newpaths, start=from_revnum, end=to_revnum, limit=limit, discover_changed_paths=discover_changed_paths, strict_node_history=strict_node_history, include_merged_revisions=include_merged_revisions, revprops=revprops)
314 return iter(fetcher.next, None)
316 def get_log(self, rcvr, paths, from_revnum, to_revnum, limit, discover_changed_paths,
317 strict_node_history, include_merged_revisions, revprops):
318 assert paths is None or isinstance(paths, list), "Invalid paths"
321 for item in [isinstance(x, str) for x in paths]:
326 assert paths is None or all_true
328 self.mutter('svn log -r%d:%d %r' % (from_revnum, to_revnum, paths))
333 newpaths = [p.rstrip("/") for p in paths]
335 conn = self.get_connection()
337 return conn.get_log(rcvr, newpaths,
338 from_revnum, to_revnum,
339 limit, discover_changed_paths, strict_node_history,
340 include_merged_revisions,
343 self.add_connection(conn)
345 def change_rev_prop(self, revnum, name, value):
346 conn = self.get_connection()
347 self.mutter('svn change-revprop -r%d %s=%s' % (revnum, name, value))
349 return conn.change_rev_prop(revnum, name, value)
351 self.add_connection(conn)
353 def get_dir(self, path, revnum, kind=False):
354 conn = self.get_connection()
355 self.mutter('svn get-dir -r%d %s' % (revnum, path))
357 return conn.get_dir(path, revnum, kind)
359 self.add_connection(conn)
361 def get_file(self, path, stream, revnum):
362 conn = self.get_connection()
363 self.mutter('svn get-file -r%d %s' % (revnum, path))
365 return conn.get_file(path, stream, revnum)
367 self.add_connection(conn)
369 def mutter(self, text, *args):
370 if 'transport' in debug.debug_flags:
373 def list_dir(self, relpath):
374 assert len(relpath) == 0 or relpath[0] != "/"
378 (dirents, _, _) = self.get_dir(relpath, self.get_latest_revnum())
379 except SubversionException, (msg, num):
380 if num == ERR_FS_NOT_DIRECTORY:
381 raise NoSuchFile(relpath)
383 return dirents.keys()
385 def check_path(self, path, revnum):
386 conn = self.get_connection()
387 self.mutter('svn check-path -r%d %s' % (revnum, path))
389 return conn.check_path(path, revnum)
391 self.add_connection(conn)
394 def mkdir(self, relpath, message="Creating directory"):
395 conn = self.get_connection()
396 self.mutter('svn mkdir %s' % (relpath,))
398 ce = conn.get_commit_editor({"svn:log": message})
400 node = ce.open_root(-1)
401 batons = relpath.split("/")
403 for i in range(len(batons)):
404 node = node.open_directory("/".join(batons[:i]), -1)
406 toclose.append(node.add_directory(relpath, None, -1))
407 for c in reversed(toclose):
410 except SubversionException, (msg, num):
412 if num == ERR_FS_NOT_DIRECTORY:
413 raise NoSuchFile(msg)
414 if num == ERR_FS_ALREADY_EXISTS:
415 raise FileExists(msg)
418 self.add_connection(conn)
420 def has_capability(self, cap):
421 if cap in self.capabilities:
422 return self.capabilities[cap]
423 conn = self.get_connection()
424 self.mutter('svn has-capability %s' % (cap,))
427 self.capabilities[cap] = conn.has_capability(cap)
428 except NotImplementedError:
429 self.capabilities[cap] = False # Assume the worst
430 return self.capabilities[cap]
432 self.add_connection(conn)
434 def revprop_list(self, revnum):
435 conn = self.get_connection()
436 self.mutter('svn revprop-list -r%d' % (revnum,))
438 return conn.rev_proplist(revnum)
440 self.add_connection(conn)
442 def get_locations(self, path, peg_revnum, revnums):
443 conn = self.get_connection()
444 self.mutter('svn get_locations -r%d %s (%r)' % (peg_revnum, path, revnums))
446 return conn.get_locations(path, peg_revnum, revnums)
448 self.add_connection(conn)
451 """See Transport.listable().
455 # There is no real way to do locking directly on the transport
456 # nor is there a need to as the remote server will take care of
458 class PhonyLock(object):
462 def lock_read(self, relpath):
463 """See Transport.lock_read()."""
464 return self.PhonyLock()
466 def lock_write(self, path_revs, comment=None, steal_lock=False):
467 return self.PhonyLock() # FIXME
469 def _is_http_transport(self):
471 return (self.svn_url.startswith("http://") or
472 self.svn_url.startswith("https://"))
474 def clone_root(self):
475 if self._is_http_transport():
476 return SvnRaTransport(self.get_repos_root(),
477 bzr_to_svn_url(self.base),
478 pool=self.connections)
479 return SvnRaTransport(self.get_repos_root(),
480 pool=self.connections)
482 def clone(self, offset=None):
483 """See Transport.clone()."""
487 newurl = urlutils.join(self.base, offset)
489 return SvnRaTransport(newurl, pool=self.connections)
491 def local_abspath(self, relpath):
492 """See Transport.local_abspath()."""
493 absurl = self.abspath(relpath)
494 if self.base.startswith("file:///"):
495 return urlutils.local_path_from_url(absurl)
496 raise NotLocalUrl(absurl)
498 def abspath(self, relpath):
499 """See Transport.abspath()."""
500 return urlutils.join(self.base, relpath)