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 def create_svn_client(pool):
49 client = svn.client.create_context(pool)
50 client.auth_baton = _create_auth_baton(pool)
51 client.config = svn_config
55 # Don't run any tests on SvnTransport as it is not intended to be
56 # a full implementation of Transport
57 def get_test_permutations():
61 def get_svn_ra_transport(bzr_transport):
62 """Obtain corresponding SvnRaTransport for a stock Bazaar transport."""
63 if isinstance(bzr_transport, SvnRaTransport):
66 return SvnRaTransport(bzr_transport.base)
69 def bzr_to_svn_url(url):
70 """Convert a Bazaar URL to a URL understood by Subversion.
72 This will possibly remove the svn+ prefix.
74 if (url.startswith("svn+http://") or
75 url.startswith("svn+file://") or
76 url.startswith("svn+https://")):
77 url = url[len("svn+"):] # Skip svn+
79 # The SVN libraries don't like trailing slashes...
80 return url.rstrip('/')
83 def needs_busy(unbound):
84 """Decorator that marks a transport as busy before running a methd on it.
86 def convert(self, *args, **kwargs):
88 ret = unbound(self, *args, **kwargs)
92 convert.__doc__ = unbound.__doc__
93 convert.__name__ = unbound.__name__
98 """Simple object wrapper around the Subversion delta editor interface."""
99 def __init__(self, transport, (editor, editor_baton)):
101 self.editor_baton = editor_baton
102 self.recent_baton = []
103 self._transport = transport
106 def open_root(self, base_revnum):
107 assert self.recent_baton == [], "root already opened"
108 baton = svn.delta.editor_invoke_open_root(self.editor,
109 self.editor_baton, base_revnum)
110 self.recent_baton.append(baton)
114 def close_directory(self, baton, *args, **kwargs):
115 assert self.recent_baton.pop() == baton, \
116 "only most recently opened baton can be closed"
117 svn.delta.editor_invoke_close_directory(self.editor, baton, *args, **kwargs)
121 assert self.recent_baton == []
122 svn.delta.editor_invoke_close_edit(self.editor, self.editor_baton)
123 self._transport._unmark_busy()
126 def apply_textdelta(self, baton, *args, **kwargs):
127 assert self.recent_baton[-1] == baton
128 return svn.delta.editor_invoke_apply_textdelta(self.editor, baton,
132 def change_dir_prop(self, baton, name, value, pool=None):
133 assert self.recent_baton[-1] == baton
134 return svn.delta.editor_invoke_change_dir_prop(self.editor, baton,
138 def delete_entry(self, *args, **kwargs):
139 return svn.delta.editor_invoke_delete_entry(self.editor, *args, **kwargs)
142 def add_file(self, path, parent_baton, *args, **kwargs):
143 assert self.recent_baton[-1] == parent_baton
144 baton = svn.delta.editor_invoke_add_file(self.editor, path,
145 parent_baton, *args, **kwargs)
146 self.recent_baton.append(baton)
150 def open_file(self, path, parent_baton, *args, **kwargs):
151 assert self.recent_baton[-1] == parent_baton
152 baton = svn.delta.editor_invoke_open_file(self.editor, path,
153 parent_baton, *args, **kwargs)
154 self.recent_baton.append(baton)
158 def change_file_prop(self, baton, name, value, pool=None):
159 assert self.recent_baton[-1] == baton
160 svn.delta.editor_invoke_change_file_prop(self.editor, baton, name,
164 def close_file(self, baton, *args, **kwargs):
165 assert self.recent_baton.pop() == baton
166 svn.delta.editor_invoke_close_file(self.editor, baton, *args, **kwargs)
169 def add_directory(self, path, parent_baton, *args, **kwargs):
170 assert self.recent_baton[-1] == parent_baton
171 baton = svn.delta.editor_invoke_add_directory(self.editor, path,
172 parent_baton, *args, **kwargs)
173 self.recent_baton.append(baton)
177 def open_directory(self, path, parent_baton, *args, **kwargs):
178 assert self.recent_baton[-1] == parent_baton
179 baton = svn.delta.editor_invoke_open_directory(self.editor, path,
180 parent_baton, *args, **kwargs)
181 self.recent_baton.append(baton)
185 class SvnRaTransport(Transport):
186 """Fake transport for Subversion-related namespaces.
188 This implements just as much of Transport as is necessary
191 def __init__(self, url="", _backing_url=None):
194 self.svn_url = bzr_to_svn_url(url)
196 # _backing_url is an evil hack so the root directory of a repository
197 # can be accessed on some HTTP repositories.
198 if _backing_url is None:
199 _backing_url = self.svn_url
200 self._backing_url = _backing_url.rstrip("/")
201 Transport.__init__(self, bzr_url)
203 self._client = create_svn_client(self.pool)
205 self.mutter('opening SVN RA connection to %r' % self._backing_url)
206 self._ra = svn.client.open_ra_session(self._backing_url.encode('utf8'),
207 self._client, self.pool)
208 except SubversionException, (_, num):
209 if num in (svn.core.SVN_ERR_RA_SVN_REPOS_NOT_FOUND,):
210 raise NoSvnRepositoryPresent(url=url)
211 if num == svn.core.SVN_ERR_BAD_URL:
212 raise InvalidURL(url)
215 from bzrlib.plugins.svn import lazy_check_versions
216 lazy_check_versions()
220 def _mark_busy(self):
221 assert not self._busy
224 def _unmark_busy(self):
228 def mutter(self, text):
229 if 'transport' in debug.debug_flags:
233 def __init__(self, transport, (reporter, report_baton)):
234 self._reporter = reporter
235 self._baton = report_baton
236 self._transport = transport
239 def set_path(self, path, revnum, start_empty, lock_token, pool=None):
240 svn.ra.reporter2_invoke_set_path(self._reporter, self._baton,
241 path, revnum, start_empty, lock_token, pool)
244 def delete_path(self, path, pool=None):
245 svn.ra.reporter2_invoke_delete_path(self._reporter, self._baton,
249 def link_path(self, path, url, revision, start_empty, lock_token,
251 svn.ra.reporter2_invoke_link_path(self._reporter, self._baton,
252 path, url, revision, start_empty, lock_token,
256 def finish_report(self, pool=None):
257 svn.ra.reporter2_invoke_finish_report(self._reporter,
259 self._transport._unmark_busy()
262 def abort_report(self, pool=None):
263 svn.ra.reporter2_invoke_abort_report(self._reporter,
265 self._transport._unmark_busy()
267 def has(self, relpath):
268 """See Transport.has()."""
269 # TODO: Raise TransportNotPossible here instead and
270 # catch it in bzrdir.py
273 def get(self, relpath):
274 """See Transport.get()."""
275 # TODO: Raise TransportNotPossible here instead and
276 # catch it in bzrdir.py
277 raise NoSuchFile(path=relpath)
279 def stat(self, relpath):
280 """See Transport.stat()."""
281 raise TransportNotPossible('stat not supported on Subversion')
286 self.mutter('svn get-uuid')
287 return svn.ra.get_uuid(self._ra)
289 def get_repos_root(self):
290 root = self.get_svn_repos_root()
291 if (self.base.startswith("svn+http:") or
292 self.base.startswith("svn+https:")):
293 return "svn+%s" % root
298 def get_svn_repos_root(self):
299 if self._root is None:
300 self.mutter("svn get-repos-root")
301 self._root = svn.ra.get_repos_root(self._ra)
306 def get_latest_revnum(self):
307 self.mutter("svn get-latest-revnum")
308 return svn.ra.get_latest_revnum(self._ra)
310 def _make_editor(self, editor, pool=None):
311 edit, edit_baton = svn.delta.make_editor(editor, pool)
313 self._edit_baton = edit_baton
314 return self._edit, self._edit_baton
317 def do_switch(self, switch_rev, recurse, switch_url, editor, pool=None):
318 self._open_real_transport()
319 self.mutter('svn switch -r %d -> %r' % (switch_rev, switch_url))
321 edit, edit_baton = self._make_editor(editor, pool)
322 return self.Reporter(self, svn.ra.do_switch(self._ra, switch_rev, "",
323 recurse, switch_url, edit, edit_baton, pool))
327 def get_log(self, path, from_revnum, to_revnum, *args, **kwargs):
328 self.mutter('svn log %r:%r %r' % (from_revnum, to_revnum, path))
329 return svn.ra.get_log(self._ra, [self._request_path(path)],
330 from_revnum, to_revnum, *args, **kwargs)
332 def _open_real_transport(self):
333 if self._backing_url != self.svn_url:
334 self.reparent(self.base)
335 assert self._backing_url == self.svn_url
337 def reparent_root(self):
338 if self._is_http_transport():
339 self.svn_url = self.get_svn_repos_root()
340 self.base = self.get_repos_root()
342 self.reparent(self.get_repos_root())
345 def change_rev_prop(self, revnum, name, value, pool=None):
346 svn.ra.change_rev_prop(self._ra, revnum, name, value)
350 def reparent(self, url):
351 url = url.rstrip("/")
353 self.svn_url = bzr_to_svn_url(url)
354 if self.svn_url == self._backing_url:
356 if hasattr(svn.ra, 'reparent'):
357 self.mutter('svn reparent %r' % url)
358 svn.ra.reparent(self._ra, self.svn_url, self.pool)
360 self.mutter('svn reparent (reconnect) %r' % url)
361 self._ra = svn.client.open_ra_session(self.svn_url.encode('utf8'),
362 self._client, self.pool)
363 self._backing_url = self.svn_url
367 def get_dir(self, path, revnum, pool=None, kind=False):
368 self.mutter("svn ls -r %d '%r'" % (revnum, path))
369 assert len(path) == 0 or path[0] != "/"
370 path = self._request_path(path)
371 # ra_dav backends fail with strange errors if the path starts with a
372 # slash while other backends don't.
373 if hasattr(svn.ra, 'get_dir2'):
376 fields += svn.core.SVN_DIRENT_KIND
377 return svn.ra.get_dir2(self._ra, path, revnum, fields)
379 return svn.ra.get_dir(self._ra, path, revnum)
381 def _request_path(self, relpath):
382 if self._backing_url == self.svn_url:
384 newrelpath = urlutils.join(
385 urlutils.relative_url(self._backing_url+"/", self.svn_url+"/"),
387 self.mutter('request path %r -> %r' % (relpath, newrelpath))
391 def list_dir(self, relpath):
392 assert len(relpath) == 0 or relpath[0] != "/"
396 (dirents, _, _) = self.get_dir(self._request_path(relpath),
397 self.get_latest_revnum())
398 except SubversionException, (msg, num):
399 if num == svn.core.SVN_ERR_FS_NOT_DIRECTORY:
400 raise NoSuchFile(relpath)
402 return dirents.keys()
406 def get_lock(self, path):
407 return svn.ra.get_lock(self._ra, path)
410 def __init__(self, transport, tokens):
411 self._tokens = tokens
412 self._transport = transport
415 self.transport.unlock(self.locks)
419 def unlock(self, locks, break_lock=False):
420 def lock_cb(baton, path, do_lock, lock, ra_err, pool):
422 return svn.ra.unlock(self._ra, locks, break_lock, lock_cb)
426 def lock_write(self, path_revs, comment=None, steal_lock=False):
427 return self.PhonyLock() # FIXME
429 def lock_cb(baton, path, do_lock, lock, ra_err, pool):
431 svn.ra.lock(self._ra, path_revs, comment, steal_lock, lock_cb)
432 return SvnLock(self, tokens)
436 def check_path(self, path, revnum, *args, **kwargs):
437 assert len(path) == 0 or path[0] != "/"
438 path = self._request_path(path)
439 self.mutter("svn check_path -r%d %s" % (revnum, path))
440 return svn.ra.check_path(self._ra, path.encode('utf-8'), revnum, *args, **kwargs)
444 def mkdir(self, relpath, mode=None):
445 assert len(relpath) == 0 or relpath[0] != "/"
446 path = urlutils.join(self.svn_url, relpath)
448 svn.client.mkdir([path.encode("utf-8")], self._client)
449 except SubversionException, (msg, num):
450 if num == svn.core.SVN_ERR_FS_NOT_FOUND:
451 raise NoSuchFile(path)
452 if num == svn.core.SVN_ERR_FS_ALREADY_EXISTS:
453 raise FileExists(path)
457 def replay(self, revision, low_water_mark, send_deltas, editor, pool=None):
458 self._open_real_transport()
459 self.mutter('svn replay -r%r:%r' % (low_water_mark, revision))
461 edit, edit_baton = self._make_editor(editor, pool)
462 svn.ra.replay(self._ra, revision, low_water_mark, send_deltas,
463 edit, edit_baton, pool)
466 def do_update(self, revnum, recurse, editor, pool=None):
467 self._open_real_transport()
468 self.mutter('svn update -r %r' % revnum)
470 edit, edit_baton = self._make_editor(editor, pool)
471 return self.Reporter(self, svn.ra.do_update(self._ra, revnum, "",
472 recurse, edit, edit_baton, pool))
474 def supports_custom_revprops(self):
475 return has_attr(svn.ra, 'get_commit_editor3')
478 def get_commit_editor(self, revprops, done_cb, lock_token, keep_locks):
479 self._open_real_transport()
481 if revprops.keys() == [svn.core.SVN_PROP_REVISION_LOG]:
482 editor = svn.ra.get_commit_editor(self._ra,
483 revprops[svn.core.SVN_PROP_REVISION_LOG],
484 done_cb, lock_token, keep_locks)
486 editor = svn.ra.get_commit_editor3(self._ra, revprops, done_cb,
487 lock_token, keep_locks)
488 return Editor(self, editor)
491 """See Transport.listable().
495 # There is no real way to do locking directly on the transport
496 # nor is there a need to as the remote server will take care of
502 def lock_read(self, relpath):
503 """See Transport.lock_read()."""
504 return self.PhonyLock()
506 def _is_http_transport(self):
507 return (self.svn_url.startswith("http://") or
508 self.svn_url.startswith("https://"))
510 def clone_root(self):
511 if self._is_http_transport():
512 return SvnRaTransport(self.get_repos_root(),
513 bzr_to_svn_url(self.base))
514 return SvnRaTransport(self.get_repos_root())
516 def clone(self, offset=None):
517 """See Transport.clone()."""
519 return SvnRaTransport(self.base)
521 return SvnRaTransport(urlutils.join(self.base, offset))
523 def local_abspath(self, relpath):
524 """See Transport.local_abspath()."""
525 absurl = self.abspath(relpath)
526 if self.base.startswith("file:///"):
527 return urlutils.local_path_from_url(absurl)
528 raise NotLocalUrl(absurl)
530 def abspath(self, relpath):
531 """See Transport.abspath()."""
532 return urlutils.join(self.base, relpath)