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:
358 relpath = urlutils.join(
359 urlutils.relative_url(self._backing_url, self.svn_url),
364 def list_dir(self, relpath):
365 assert len(relpath) == 0 or relpath[0] != "/"
369 (dirents, _, _) = self.get_dir(self._request_path(relpath),
370 self.get_latest_revnum())
371 except SubversionException, (msg, num):
372 if num == svn.core.SVN_ERR_FS_NOT_DIRECTORY:
373 raise NoSuchFile(relpath)
375 return dirents.keys()
379 def get_lock(self, path):
380 return svn.ra.get_lock(self._ra, path)
383 def __init__(self, transport, tokens):
384 self._tokens = tokens
385 self._transport = transport
388 self.transport.unlock(self.locks)
392 def unlock(self, locks, break_lock=False):
393 def lock_cb(baton, path, do_lock, lock, ra_err, pool):
395 return svn.ra.unlock(self._ra, locks, break_lock, lock_cb)
399 def lock_write(self, path_revs, comment=None, steal_lock=False):
400 return self.PhonyLock() # FIXME
402 def lock_cb(baton, path, do_lock, lock, ra_err, pool):
404 svn.ra.lock(self._ra, path_revs, comment, steal_lock, lock_cb)
405 return SvnLock(self, tokens)
409 def check_path(self, path, revnum, *args, **kwargs):
410 assert len(path) == 0 or path[0] != "/"
411 path = self._request_path(path)
412 self.mutter("svn check_path -r%d %s" % (revnum, path))
413 return svn.ra.check_path(self._ra, path.encode('utf-8'), revnum, *args, **kwargs)
417 def mkdir(self, relpath, mode=None):
418 assert len(relpath) == 0 or relpath[0] != "/"
419 path = urlutils.join(self.svn_url, relpath)
421 svn.client.mkdir([path.encode("utf-8")], self._client)
422 except SubversionException, (msg, num):
423 if num == svn.core.SVN_ERR_FS_NOT_FOUND:
424 raise NoSuchFile(path)
425 if num == svn.core.SVN_ERR_FS_ALREADY_EXISTS:
426 raise FileExists(path)
430 def do_update(self, revnum, *args, **kwargs):
431 self._open_real_transport()
432 self.mutter('svn update -r %r' % revnum)
434 return self.Reporter(self, svn.ra.do_update(self._ra, revnum, "",
438 def get_commit_editor(self, *args, **kwargs):
439 self._open_real_transport()
441 return Editor(self, svn.ra.get_commit_editor(self._ra, *args, **kwargs))
444 """See Transport.listable().
448 # There is no real way to do locking directly on the transport
449 # nor is there a need to as the remote server will take care of
455 def lock_read(self, relpath):
456 """See Transport.lock_read()."""
457 return self.PhonyLock()
459 def _is_http_transport(self):
460 return (self.svn_url.startswith("http://") or
461 self.svn_url.startswith("https://"))
463 def clone_root(self):
464 if self._is_http_transport():
465 return SvnRaTransport(self.get_repos_root(),
466 bzr_to_svn_url(self.base))
467 return SvnRaTransport(self.get_repos_root())
469 def clone(self, offset=None):
470 """See Transport.clone()."""
472 return SvnRaTransport(self.base)
474 return SvnRaTransport(urlutils.join(self.base, offset))
476 def local_abspath(self, relpath):
477 """See Transport.local_abspath()."""
478 absurl = self.abspath(relpath)
479 if self.base.startswith("file:///"):
480 return urlutils.local_path_from_url(absurl)
481 raise NotLocalUrl(absurl)
483 def abspath(self, relpath):
484 """See Transport.abspath()."""
485 return urlutils.join(self.base, relpath)