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 2 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 svn.core import SubversionException, Pool
29 from errors import convert_svn_error, NoSvnRepositoryPresent
31 svn_config = svn.core.svn_config_get_config(None)
34 def _create_auth_baton(pool):
35 """Create a Subversion authentication baton. """
36 # Give the client context baton a suite of authentication
39 svn.client.get_simple_provider(pool),
40 svn.client.get_username_provider(pool),
41 svn.client.get_ssl_client_cert_file_provider(pool),
42 svn.client.get_ssl_client_cert_pw_file_provider(pool),
43 svn.client.get_ssl_server_trust_file_provider(pool),
45 return svn.core.svn_auth_open(providers, pool)
48 # Don't run any tests on SvnTransport as it is not intended to be
49 # a full implementation of Transport
50 def get_test_permutations():
54 def get_svn_ra_transport(bzr_transport):
55 """Obtain corresponding SvnRaTransport for a stock Bazaar transport."""
56 if isinstance(bzr_transport, SvnRaTransport):
59 return SvnRaTransport(bzr_transport.base)
62 def bzr_to_svn_url(url):
63 """Convert a Bazaar URL to a URL understood by Subversion.
65 This will possibly remove the svn+ prefix.
67 if (url.startswith("svn+http://") or
68 url.startswith("svn+file://") or
69 url.startswith("svn+https://")):
70 url = url[len("svn+"):] # Skip svn+
72 # The SVN libraries don't like trailing slashes...
73 return url.rstrip('/')
76 def needs_busy(unbound):
77 """Decorator that marks a transport as busy before running a methd on it.
79 def convert(self, *args, **kwargs):
81 ret = unbound(self, *args, **kwargs)
85 convert.__doc__ = unbound.__doc__
86 convert.__name__ = unbound.__name__
91 """Simple object wrapper around the Subversion delta editor interface."""
92 def __init__(self, transport, (editor, editor_baton)):
94 self.editor_baton = editor_baton
95 self.recent_baton = []
96 self._transport = transport
99 def open_root(self, base_revnum):
100 assert self.recent_baton == [], "root already opened"
101 baton = svn.delta.editor_invoke_open_root(self.editor,
102 self.editor_baton, base_revnum)
103 self.recent_baton.append(baton)
107 def close_directory(self, baton, *args, **kwargs):
108 assert self.recent_baton.pop() == baton, \
109 "only most recently opened baton can be closed"
110 svn.delta.editor_invoke_close_directory(self.editor, baton, *args, **kwargs)
114 assert self.recent_baton == []
115 svn.delta.editor_invoke_close_edit(self.editor, self.editor_baton)
116 self._transport._unmark_busy()
119 def apply_textdelta(self, baton, *args, **kwargs):
120 assert self.recent_baton[-1] == baton
121 return svn.delta.editor_invoke_apply_textdelta(self.editor, baton,
125 def change_dir_prop(self, baton, *args, **kwargs):
126 assert self.recent_baton[-1] == baton
127 return svn.delta.editor_invoke_change_dir_prop(self.editor, baton,
131 def delete_entry(self, *args, **kwargs):
132 return svn.delta.editor_invoke_delete_entry(self.editor, *args, **kwargs)
135 def add_file(self, path, parent_baton, *args, **kwargs):
136 assert self.recent_baton[-1] == parent_baton
137 baton = svn.delta.editor_invoke_add_file(self.editor, path,
138 parent_baton, *args, **kwargs)
139 self.recent_baton.append(baton)
143 def open_file(self, path, parent_baton, *args, **kwargs):
144 assert self.recent_baton[-1] == parent_baton
145 baton = svn.delta.editor_invoke_open_file(self.editor, path,
146 parent_baton, *args, **kwargs)
147 self.recent_baton.append(baton)
151 def change_file_prop(self, baton, *args, **kwargs):
152 assert self.recent_baton[-1] == baton
153 svn.delta.editor_invoke_change_file_prop(self.editor, baton, *args,
157 def close_file(self, baton, *args, **kwargs):
158 assert self.recent_baton.pop() == baton
159 svn.delta.editor_invoke_close_file(self.editor, baton, *args, **kwargs)
162 def add_directory(self, path, parent_baton, *args, **kwargs):
163 assert self.recent_baton[-1] == parent_baton
164 baton = svn.delta.editor_invoke_add_directory(self.editor, path,
165 parent_baton, *args, **kwargs)
166 self.recent_baton.append(baton)
170 def open_directory(self, path, parent_baton, *args, **kwargs):
171 assert self.recent_baton[-1] == parent_baton
172 baton = svn.delta.editor_invoke_open_directory(self.editor, path,
173 parent_baton, *args, **kwargs)
174 self.recent_baton.append(baton)
178 class SvnRaTransport(Transport):
179 """Fake transport for Subversion-related namespaces.
181 This implements just as much of Transport as is necessary
184 def __init__(self, url="", _backing_url=None):
187 self.svn_url = bzr_to_svn_url(url)
189 # _backing_url is an evil hack so the root directory of a repository
190 # can be accessed on some HTTP repositories.
191 if _backing_url is None:
192 _backing_url = self.svn_url
193 self._backing_url = _backing_url.rstrip("/")
194 Transport.__init__(self, bzr_url)
196 self._client = svn.client.create_context(self.pool)
197 self._client.auth_baton = _create_auth_baton(self.pool)
198 self._client.config = svn_config
201 self.mutter('opening SVN RA connection to %r' % self._backing_url)
202 self._ra = svn.client.open_ra_session(self._backing_url.encode('utf8'),
203 self._client, self.pool)
204 except SubversionException, (_, num):
205 if num in (svn.core.SVN_ERR_RA_SVN_REPOS_NOT_FOUND,):
206 raise NoSvnRepositoryPresent(url=url)
207 if num == svn.core.SVN_ERR_BAD_URL:
208 raise InvalidURL(url)
211 from bzrlib.plugins.svn import lazy_check_versions
212 lazy_check_versions()
216 def _mark_busy(self):
217 assert not self._busy
220 def _unmark_busy(self):
224 def mutter(self, text):
225 if 'transport' in debug.debug_flags:
229 def __init__(self, transport, (reporter, report_baton)):
230 self._reporter = reporter
231 self._baton = report_baton
232 self._transport = transport
235 def set_path(self, path, revnum, start_empty, lock_token, pool=None):
236 svn.ra.reporter2_invoke_set_path(self._reporter, self._baton,
237 path, revnum, start_empty, lock_token, pool)
240 def delete_path(self, path, pool=None):
241 svn.ra.reporter2_invoke_delete_path(self._reporter, self._baton,
245 def link_path(self, path, url, revision, start_empty, lock_token,
247 svn.ra.reporter2_invoke_link_path(self._reporter, self._baton,
248 path, url, revision, start_empty, lock_token,
252 def finish_report(self, pool=None):
253 svn.ra.reporter2_invoke_finish_report(self._reporter,
255 self._transport._unmark_busy()
258 def abort_report(self, pool=None):
259 svn.ra.reporter2_invoke_abort_report(self._reporter,
261 self._transport._unmark_busy()
263 def has(self, relpath):
264 """See Transport.has()."""
265 # TODO: Raise TransportNotPossible here instead and
266 # catch it in bzrdir.py
269 def get(self, relpath):
270 """See Transport.get()."""
271 # TODO: Raise TransportNotPossible here instead and
272 # catch it in bzrdir.py
273 raise NoSuchFile(path=relpath)
275 def stat(self, relpath):
276 """See Transport.stat()."""
277 raise TransportNotPossible('stat not supported on Subversion')
282 self.mutter('svn get-uuid')
283 return svn.ra.get_uuid(self._ra)
287 def get_repos_root(self):
288 if self._root is None:
289 self.mutter("svn get-repos-root")
290 self._root = svn.ra.get_repos_root(self._ra)
295 def get_latest_revnum(self):
296 self.mutter("svn get-latest-revnum")
297 return svn.ra.get_latest_revnum(self._ra)
300 def do_switch(self, switch_rev, recurse, switch_url, *args, **kwargs):
301 self._open_real_transport()
302 self.mutter('svn switch -r %d -> %r' % (switch_rev, switch_url))
304 return self.Reporter(self, svn.ra.do_switch(self._ra, switch_rev, "", recurse, switch_url, *args, **kwargs))
308 def get_log(self, path, from_revnum, to_revnum, *args, **kwargs):
309 self.mutter('svn log %r:%r %r' % (from_revnum, to_revnum, path))
310 return svn.ra.get_log(self._ra, [self._request_path(path)], from_revnum, to_revnum, *args, **kwargs)
312 def _open_real_transport(self):
313 if self._backing_url != self.svn_url:
314 self.reparent(self.svn_url)
315 assert self._backing_url == self.svn_url
317 def reparent_root(self):
318 if self._is_http_transport():
319 self.svn_url = self.base = self.get_repos_root()
321 self.reparent(self.get_repos_root())
325 def reparent(self, url):
326 url = url.rstrip("/")
329 if url == self._backing_url:
331 if hasattr(svn.ra, 'reparent'):
332 self.mutter('svn reparent %r' % url)
333 svn.ra.reparent(self._ra, url, self.pool)
335 self.mutter('svn reparent (reconnect) %r' % url)
336 self._ra = svn.client.open_ra_session(self.svn_url.encode('utf8'),
337 self._client, self.pool)
338 self._backing_url = url
342 def get_dir(self, path, revnum, pool=None, kind=False):
343 self.mutter("svn ls -r %d '%r'" % (revnum, path))
344 assert len(path) == 0 or path[0] != "/"
345 path = self._request_path(path)
346 # ra_dav backends fail with strange errors if the path starts with a
347 # slash while other backends don't.
348 if hasattr(svn.ra, 'get_dir2'):
351 fields += svn.core.SVN_DIRENT_KIND
352 return svn.ra.get_dir2(self._ra, path, revnum, fields)
354 return svn.ra.get_dir(self._ra, path, revnum)
356 def _request_path(self, relpath):
357 if self._backing_url == self.svn_url:
359 newrelpath = urlutils.join(
360 urlutils.relative_url(self._backing_url, self.svn_url),
362 self.mutter('request path %r -> %r' % (relpath, newrelpath))
366 def list_dir(self, relpath):
367 assert len(relpath) == 0 or relpath[0] != "/"
371 (dirents, _, _) = self.get_dir(self._request_path(relpath),
372 self.get_latest_revnum())
373 except SubversionException, (msg, num):
374 if num == svn.core.SVN_ERR_FS_NOT_DIRECTORY:
375 raise NoSuchFile(relpath)
377 return dirents.keys()
381 def get_lock(self, path):
382 return svn.ra.get_lock(self._ra, path)
385 def __init__(self, transport, tokens):
386 self._tokens = tokens
387 self._transport = transport
390 self.transport.unlock(self.locks)
394 def unlock(self, locks, break_lock=False):
395 def lock_cb(baton, path, do_lock, lock, ra_err, pool):
397 return svn.ra.unlock(self._ra, locks, break_lock, lock_cb)
401 def lock_write(self, path_revs, comment=None, steal_lock=False):
402 return self.PhonyLock() # FIXME
404 def lock_cb(baton, path, do_lock, lock, ra_err, pool):
406 svn.ra.lock(self._ra, path_revs, comment, steal_lock, lock_cb)
407 return SvnLock(self, tokens)
411 def check_path(self, path, revnum, *args, **kwargs):
412 assert len(path) == 0 or path[0] != "/"
413 path = self._request_path(path)
414 self.mutter("svn check_path -r%d %s" % (revnum, path))
415 return svn.ra.check_path(self._ra, path.encode('utf-8'), revnum, *args, **kwargs)
419 def mkdir(self, relpath, mode=None):
420 assert len(relpath) == 0 or relpath[0] != "/"
421 path = urlutils.join(self.svn_url, relpath)
423 svn.client.mkdir([path.encode("utf-8")], self._client)
424 except SubversionException, (msg, num):
425 if num == svn.core.SVN_ERR_FS_NOT_FOUND:
426 raise NoSuchFile(path)
427 if num == svn.core.SVN_ERR_FS_ALREADY_EXISTS:
428 raise FileExists(path)
432 def do_update(self, revnum, *args, **kwargs):
433 self._open_real_transport()
434 self.mutter('svn update -r %r' % revnum)
436 return self.Reporter(self, svn.ra.do_update(self._ra, revnum, "",
440 def get_commit_editor(self, *args, **kwargs):
441 self._open_real_transport()
443 return Editor(self, svn.ra.get_commit_editor(self._ra, *args, **kwargs))
446 """See Transport.listable().
450 # There is no real way to do locking directly on the transport
451 # nor is there a need to as the remote server will take care of
457 def lock_read(self, relpath):
458 """See Transport.lock_read()."""
459 return self.PhonyLock()
461 def _is_http_transport(self):
462 return (self.svn_url.startswith("http://") or
463 self.svn_url.startswith("https://"))
465 def clone_root(self):
466 if self._is_http_transport():
467 return SvnRaTransport(self.get_repos_root(),
468 bzr_to_svn_url(self.base))
469 return SvnRaTransport(self.get_repos_root())
471 def clone(self, offset=None):
472 """See Transport.clone()."""
474 return SvnRaTransport(self.base)
476 return SvnRaTransport(urlutils.join(self.base, offset))
478 def local_abspath(self, relpath):
479 """See Transport.local_abspath()."""
480 absurl = self.abspath(relpath)
481 if self.base.startswith("file:///"):
482 return urlutils.local_path_from_url(absurl)
483 raise NotLocalUrl(absurl)
485 def abspath(self, relpath):
486 """See Transport.abspath()."""
487 return urlutils.join(self.base, relpath)