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, "",
305 recurse, switch_url, *args, **kwargs))
309 def get_log(self, path, from_revnum, to_revnum, *args, **kwargs):
310 self.mutter('svn log %r:%r %r' % (from_revnum, to_revnum, path))
311 return svn.ra.get_log(self._ra, [self._request_path(path)],
312 from_revnum, to_revnum, *args, **kwargs)
314 def _open_real_transport(self):
315 if self._backing_url != self.svn_url:
316 self.reparent(self.svn_url)
317 assert self._backing_url == self.svn_url
319 def reparent_root(self):
320 if self._is_http_transport():
321 self.svn_url = self.base = self.get_repos_root()
323 self.reparent(self.get_repos_root())
327 def reparent(self, url):
328 url = url.rstrip("/")
331 if url == self._backing_url:
333 if hasattr(svn.ra, 'reparent'):
334 self.mutter('svn reparent %r' % url)
335 svn.ra.reparent(self._ra, url, self.pool)
337 self.mutter('svn reparent (reconnect) %r' % url)
338 self._ra = svn.client.open_ra_session(self.svn_url.encode('utf8'),
339 self._client, self.pool)
340 self._backing_url = url
344 def get_dir(self, path, revnum, pool=None, kind=False):
345 self.mutter("svn ls -r %d '%r'" % (revnum, path))
346 assert len(path) == 0 or path[0] != "/"
347 path = self._request_path(path)
348 # ra_dav backends fail with strange errors if the path starts with a
349 # slash while other backends don't.
350 if hasattr(svn.ra, 'get_dir2'):
353 fields += svn.core.SVN_DIRENT_KIND
354 return svn.ra.get_dir2(self._ra, path, revnum, fields)
356 return svn.ra.get_dir(self._ra, path, revnum)
358 def _request_path(self, relpath):
359 if self._backing_url == self.svn_url:
361 newrelpath = urlutils.join(
362 urlutils.relative_url(self._backing_url+"/", self.svn_url+"/"),
364 self.mutter('request path %r -> %r' % (relpath, newrelpath))
368 def list_dir(self, relpath):
369 assert len(relpath) == 0 or relpath[0] != "/"
373 (dirents, _, _) = self.get_dir(self._request_path(relpath),
374 self.get_latest_revnum())
375 except SubversionException, (msg, num):
376 if num == svn.core.SVN_ERR_FS_NOT_DIRECTORY:
377 raise NoSuchFile(relpath)
379 return dirents.keys()
383 def get_lock(self, path):
384 return svn.ra.get_lock(self._ra, path)
387 def __init__(self, transport, tokens):
388 self._tokens = tokens
389 self._transport = transport
392 self.transport.unlock(self.locks)
396 def unlock(self, locks, break_lock=False):
397 def lock_cb(baton, path, do_lock, lock, ra_err, pool):
399 return svn.ra.unlock(self._ra, locks, break_lock, lock_cb)
403 def lock_write(self, path_revs, comment=None, steal_lock=False):
404 return self.PhonyLock() # FIXME
406 def lock_cb(baton, path, do_lock, lock, ra_err, pool):
408 svn.ra.lock(self._ra, path_revs, comment, steal_lock, lock_cb)
409 return SvnLock(self, tokens)
413 def check_path(self, path, revnum, *args, **kwargs):
414 assert len(path) == 0 or path[0] != "/"
415 path = self._request_path(path)
416 self.mutter("svn check_path -r%d %s" % (revnum, path))
417 return svn.ra.check_path(self._ra, path.encode('utf-8'), revnum, *args, **kwargs)
421 def mkdir(self, relpath, mode=None):
422 assert len(relpath) == 0 or relpath[0] != "/"
423 path = urlutils.join(self.svn_url, relpath)
425 svn.client.mkdir([path.encode("utf-8")], self._client)
426 except SubversionException, (msg, num):
427 if num == svn.core.SVN_ERR_FS_NOT_FOUND:
428 raise NoSuchFile(path)
429 if num == svn.core.SVN_ERR_FS_ALREADY_EXISTS:
430 raise FileExists(path)
434 def do_update(self, revnum, *args, **kwargs):
435 self._open_real_transport()
436 self.mutter('svn update -r %r' % revnum)
438 return self.Reporter(self, svn.ra.do_update(self._ra, revnum, "",
442 def get_commit_editor(self, *args, **kwargs):
443 self._open_real_transport()
445 return Editor(self, svn.ra.get_commit_editor(self._ra, *args, **kwargs))
448 """See Transport.listable().
452 # There is no real way to do locking directly on the transport
453 # nor is there a need to as the remote server will take care of
459 def lock_read(self, relpath):
460 """See Transport.lock_read()."""
461 return self.PhonyLock()
463 def _is_http_transport(self):
464 return (self.svn_url.startswith("http://") or
465 self.svn_url.startswith("https://"))
467 def clone_root(self):
468 if self._is_http_transport():
469 return SvnRaTransport(self.get_repos_root(),
470 bzr_to_svn_url(self.base))
471 return SvnRaTransport(self.get_repos_root())
473 def clone(self, offset=None):
474 """See Transport.clone()."""
476 return SvnRaTransport(self.base)
478 return SvnRaTransport(urlutils.join(self.base, offset))
480 def local_abspath(self, relpath):
481 """See Transport.local_abspath()."""
482 absurl = self.abspath(relpath)
483 if self.base.startswith("file:///"):
484 return urlutils.local_path_from_url(absurl)
485 raise NotLocalUrl(absurl)
487 def abspath(self, relpath):
488 """See Transport.abspath()."""
489 return urlutils.join(self.base, relpath)