1 # Copyright (C) 2006 Jelmer Vernooij <jelmer@samba.org>
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.
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.
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."""
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
24 from bzrlib.plugins.svn import core, properties, ra
25 from bzrlib.plugins.svn import properties
26 from bzrlib.plugins.svn.auth import create_auth_baton
27 from bzrlib.plugins.svn.core import SubversionException, get_config
28 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
29 from bzrlib.plugins.svn.ra import DIRENT_KIND, RemoteAccess
33 svn_config = get_config()
35 def get_client_string():
36 """Return a string that can be send as part of the User Agent string."""
37 return "bzr%s+bzr-svn%s" % (bzrlib.__version__, bzrlib.plugins.svn.__version__)
40 # Don't run any tests on SvnTransport as it is not intended to be
41 # a full implementation of Transport
42 def get_test_permutations():
46 def get_svn_ra_transport(bzr_transport):
47 """Obtain corresponding SvnRaTransport for a stock Bazaar transport."""
48 if isinstance(bzr_transport, SvnRaTransport):
51 return SvnRaTransport(bzr_transport.base)
54 def _url_unescape_uri(url):
55 (scheme, netloc, path, query, fragment) = urlparse.urlsplit(url)
56 path = urllib.unquote(path)
57 return urlparse.urlunsplit((scheme, netloc, path, query, fragment))
60 def bzr_to_svn_url(url):
61 """Convert a Bazaar URL to a URL understood by Subversion.
63 This will possibly remove the svn+ prefix.
65 if (url.startswith("svn+http://") or
66 url.startswith("svn+file://") or
67 url.startswith("svn+https://")):
68 url = url[len("svn+"):] # Skip svn+
70 if url.startswith("http"):
71 # Without this, URLs with + in them break
72 url = _url_unescape_uri(url)
74 # The SVN libraries don't like trailing slashes...
82 mutter('opening SVN RA connection to %r' % url)
83 ret = ra.RemoteAccess(url.encode('utf8'),
84 auth=create_auth_baton(url))
86 except SubversionException, (_, num):
87 if num in (ERR_RA_SVN_REPOS_NOT_FOUND,):
88 raise NoSvnRepositoryPresent(url=url)
89 if num == ERR_BAD_URL:
93 from bzrlib.plugins.svn import lazy_check_versions
99 class ConnectionPool(object):
100 """Collection of connections to a Subversion repository."""
102 self.connections = set()
105 # Check if there is an existing connection we can use
106 for c in self.connections:
107 assert not c.busy, "busy connection in pool"
109 self.connections.remove(c)
111 # Nothing available? Just pick an existing one and reparent:
112 if len(self.connections) == 0:
113 return RemoteAccess(url)
114 c = self.connections.pop()
118 except NotImplementedError:
119 self.connections.add(c)
120 return RemoteAccess(url)
122 self.connections.add(c)
125 def add(self, connection):
126 assert not connection.busy, "adding busy connection in pool"
127 self.connections.add(connection)
130 class SvnRaTransport(Transport):
131 """Fake transport for Subversion-related namespaces.
133 This implements just as much of Transport as is necessary
136 def __init__(self, url="", _backing_url=None, pool=None):
138 self.svn_url = bzr_to_svn_url(url)
139 # _backing_url is an evil hack so the root directory of a repository
140 # can be accessed on some HTTP repositories.
141 if _backing_url is None:
142 _backing_url = self.svn_url
143 self._backing_url = _backing_url.rstrip("/")
144 Transport.__init__(self, bzr_url)
147 self.connections = ConnectionPool()
149 # Make sure that the URL is valid by connecting to it.
150 self.connections.add(self.connections.get(self._backing_url))
152 self.connections = pool
154 from bzrlib.plugins.svn import lazy_check_versions
155 lazy_check_versions()
157 def get_connection(self):
158 return self.connections.get(self._backing_url)
160 def add_connection(self, conn):
161 self.connections.add(conn)
163 def has(self, relpath):
164 """See Transport.has()."""
165 # TODO: Raise TransportNotPossible here instead and
166 # catch it in bzrdir.py
169 def get(self, relpath):
170 """See Transport.get()."""
171 # TODO: Raise TransportNotPossible here instead and
172 # catch it in bzrdir.py
173 raise NoSuchFile(path=relpath)
175 def stat(self, relpath):
176 """See Transport.stat()."""
177 raise TransportNotPossible('stat not supported on Subversion')
180 conn = self.get_connection()
181 self.mutter('svn get-uuid')
183 return conn.get_uuid()
185 self.add_connection(conn)
187 def get_repos_root(self):
188 root = self.get_svn_repos_root()
189 if (self.base.startswith("svn+http:") or
190 self.base.startswith("svn+https:")):
191 return "svn+%s" % root
194 def get_svn_repos_root(self):
195 conn = self.get_connection()
196 self.mutter('svn get-repos-root')
198 return conn.get_repos_root()
200 self.add_connection(conn)
202 def get_latest_revnum(self):
203 conn = self.get_connection()
204 self.mutter('svn get-latest-revnum')
206 return conn.get_latest_revnum()
208 self.add_connection(conn)
210 def do_switch(self, switch_rev, recurse, switch_url, editor):
211 conn = self._open_real_transport()
212 self.mutter('svn do-switch -r%d %s' % (switch_rev, switch_url))
213 return conn.do_switch(switch_rev, "", recurse, switch_url, editor)
215 def iter_log(self, paths, from_revnum, to_revnum, limit, discover_changed_paths,
216 strict_node_history, revprops):
217 assert paths is None or isinstance(paths, list)
218 assert paths is None or all([isinstance(x, str) for x in paths])
219 assert isinstance(from_revnum, int) and isinstance(to_revnum, int)
220 assert isinstance(limit, int)
221 from threading import Thread, Semaphore
223 class logfetcher(Thread):
224 def __init__(self, transport, *args, **kwargs):
225 Thread.__init__(self)
227 self.transport = transport
232 self.semaphore = Semaphore(0)
235 self.semaphore.acquire()
236 ret = self.pending.pop(0)
238 self.transport.add_connection(self.conn)
239 elif isinstance(ret, Exception):
240 self.transport.add_connection(self.conn)
245 assert self.conn is None, "already running"
247 self.pending.append(args)
248 self.semaphore.release()
249 self.conn = self.transport.get_connection()
251 self.conn.get_log(callback=rcvr, *self.args, **self.kwargs)
252 self.pending.append(None)
254 self.pending.append(e)
255 self.semaphore.release()
260 newpaths = [self._request_path(path) for path in paths]
262 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, revprops=revprops)
264 return iter(fetcher.next, None)
266 def get_log(self, rcvr, paths, from_revnum, to_revnum, limit, discover_changed_paths,
267 strict_node_history, revprops):
268 assert paths is None or isinstance(paths, list), "Invalid paths"
269 assert paths is None or all([isinstance(x, str) for x in paths])
271 self.mutter('svn log -r%d:%d %r' % (from_revnum, to_revnum, paths))
276 newpaths = [self._request_path(path) for path in paths]
278 conn = self.get_connection()
280 return conn.get_log(rcvr, newpaths,
281 from_revnum, to_revnum,
282 limit, discover_changed_paths, strict_node_history,
285 self.add_connection(conn)
287 def _open_real_transport(self):
288 if self._backing_url != self.svn_url:
289 return self.connections.get(self.svn_url)
290 return self.get_connection()
292 def change_rev_prop(self, revnum, name, value):
293 conn = self.get_connection()
294 self.mutter('svn change-revprop -r%d %s=%s' % (revnum, name, value))
296 return conn.change_rev_prop(revnum, name, value)
298 self.add_connection(conn)
300 def get_dir(self, path, revnum, kind=False):
301 path = self._request_path(path)
302 conn = self.get_connection()
303 self.mutter('svn get-dir -r%d %s' % (revnum, path))
305 return conn.get_dir(path, revnum, kind)
307 self.add_connection(conn)
309 def mutter(self, text, *args):
310 if 'transport' in debug.debug_flags:
313 def _request_path(self, relpath):
314 if self._backing_url == self.svn_url:
315 return relpath.strip("/")
316 newsvnurl = urlutils.join(self.svn_url, relpath)
317 if newsvnurl == self._backing_url:
319 newrelpath = urlutils.relative_url(self._backing_url+"/", newsvnurl+"/").strip("/")
320 self.mutter('request path %r -> %r', relpath, newrelpath)
323 def list_dir(self, relpath):
324 assert len(relpath) == 0 or relpath[0] != "/"
328 (dirents, _, _) = self.get_dir(relpath, self.get_latest_revnum())
329 except SubversionException, (msg, num):
330 if num == ERR_FS_NOT_DIRECTORY:
331 raise NoSuchFile(relpath)
333 return dirents.keys()
335 def check_path(self, path, revnum):
336 path = self._request_path(path)
337 conn = self.get_connection()
338 self.mutter('svn check-path -r%d %s' % (revnum, path))
340 return conn.check_path(path, revnum)
342 self.add_connection(conn)
344 def mkdir(self, relpath, message="Creating directory"):
345 conn = self.get_connection()
346 self.mutter('svn mkdir %s' % (relpath,))
348 ce = conn.get_commit_editor({"svn:log": message})
349 node = ce.open_root(-1)
350 batons = relpath.split("/")
352 for i in range(len(batons)):
353 node = node.open_directory("/".join(batons[:i]), -1)
355 toclose.append(node.add_directory(relpath, None, -1))
356 for c in reversed(toclose):
360 self.add_connection(conn)
362 def replay(self, revision, low_water_mark, send_deltas, editor):
363 conn = self._open_real_transport()
364 self.mutter('svn replay -r%d:%d' % (low_water_mark,revision))
366 return conn.replay(revision, low_water_mark,
369 self.add_connection(conn)
371 def do_update(self, revnum, recurse, editor):
372 conn = self._open_real_transport()
373 self.mutter('svn do-update -r%d' % (revnum,))
374 return conn.do_update(revnum, "", recurse, editor)
376 def has_capability(self, cap):
377 conn = self.get_connection()
378 self.mutter('svn has-capability %s' % (cap,))
380 return conn.has_capability(cap)
382 self.add_connection(conn)
384 def revprop_list(self, revnum):
385 conn = self.get_connection()
386 self.mutter('svn revprop-list -r%d' % (revnum,))
388 return conn.rev_proplist(revnum)
390 self.add_connection(conn)
392 def get_commit_editor(self, revprops, done_cb=None,
393 lock_token=None, keep_locks=False):
394 conn = self._open_real_transport()
395 self.mutter('svn get-commit-editor %r' % (revprops,))
396 return conn.get_commit_editor(revprops, done_cb, lock_token, keep_locks)
399 """See Transport.listable().
403 # There is no real way to do locking directly on the transport
404 # nor is there a need to as the remote server will take care of
406 class PhonyLock(object):
410 def lock_read(self, relpath):
411 """See Transport.lock_read()."""
412 return self.PhonyLock()
414 def lock_write(self, path_revs, comment=None, steal_lock=False):
415 return self.PhonyLock() # FIXME
417 def _is_http_transport(self):
418 return (self.svn_url.startswith("http://") or
419 self.svn_url.startswith("https://"))
421 def clone_root(self):
422 if self._is_http_transport():
423 return SvnRaTransport(self.get_repos_root(),
424 bzr_to_svn_url(self.base),
425 pool=self.connections)
426 return SvnRaTransport(self.get_repos_root(),
427 pool=self.connections)
429 def clone(self, offset=None):
430 """See Transport.clone()."""
432 return SvnRaTransport(self.base, pool=self.connections)
434 return SvnRaTransport(urlutils.join(self.base, offset), pool=self.connections)
436 def local_abspath(self, relpath):
437 """See Transport.local_abspath()."""
438 absurl = self.abspath(relpath)
439 if self.base.startswith("file:///"):
440 return urlutils.local_path_from_url(absurl)
441 raise NotLocalUrl(absurl)
443 def abspath(self, relpath):
444 """See Transport.abspath()."""
445 return urlutils.join(self.base, relpath)