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)
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('/')
77 """Simple object wrapper around the Subversion delta editor interface."""
78 def __init__(self, (editor, editor_baton)):
80 self.editor_baton = editor_baton
81 self.recent_baton = []
84 def open_root(self, base_revnum):
85 assert self.recent_baton == [], "root already opened"
86 baton = svn.delta.editor_invoke_open_root(self.editor,
87 self.editor_baton, base_revnum)
88 self.recent_baton.append(baton)
92 def close_directory(self, baton, *args, **kwargs):
93 assert self.recent_baton.pop() == baton, \
94 "only most recently opened baton can be closed"
95 svn.delta.editor_invoke_close_directory(self.editor, baton, *args, **kwargs)
99 assert self.recent_baton == []
100 svn.delta.editor_invoke_close_edit(self.editor, self.editor_baton)
103 def apply_textdelta(self, baton, *args, **kwargs):
104 assert self.recent_baton[-1] == baton
105 return svn.delta.editor_invoke_apply_textdelta(self.editor, baton,
109 def change_dir_prop(self, baton, *args, **kwargs):
110 assert self.recent_baton[-1] == baton
111 return svn.delta.editor_invoke_change_dir_prop(self.editor, baton,
115 def delete_entry(self, *args, **kwargs):
116 return svn.delta.editor_invoke_delete_entry(self.editor, *args, **kwargs)
119 def add_file(self, path, parent_baton, *args, **kwargs):
120 assert self.recent_baton[-1] == parent_baton
121 baton = svn.delta.editor_invoke_add_file(self.editor, path,
122 parent_baton, *args, **kwargs)
123 self.recent_baton.append(baton)
127 def open_file(self, path, parent_baton, *args, **kwargs):
128 assert self.recent_baton[-1] == parent_baton
129 baton = svn.delta.editor_invoke_open_file(self.editor, path,
130 parent_baton, *args, **kwargs)
131 self.recent_baton.append(baton)
135 def change_file_prop(self, baton, *args, **kwargs):
136 assert self.recent_baton[-1] == baton
137 svn.delta.editor_invoke_change_file_prop(self.editor, baton, *args,
141 def close_file(self, baton, *args, **kwargs):
142 assert self.recent_baton.pop() == baton
143 svn.delta.editor_invoke_close_file(self.editor, baton, *args, **kwargs)
146 def add_directory(self, path, parent_baton, *args, **kwargs):
147 assert self.recent_baton[-1] == parent_baton
148 baton = svn.delta.editor_invoke_add_directory(self.editor, path,
149 parent_baton, *args, **kwargs)
150 self.recent_baton.append(baton)
154 def open_directory(self, path, parent_baton, *args, **kwargs):
155 assert self.recent_baton[-1] == parent_baton
156 baton = svn.delta.editor_invoke_open_directory(self.editor, path,
157 parent_baton, *args, **kwargs)
158 self.recent_baton.append(baton)
162 class SvnRaTransport(Transport):
163 """Fake transport for Subversion-related namespaces.
165 This implements just as much of Transport as is necessary
168 def __init__(self, url="", _backing_url=None):
171 self.svn_url = bzr_to_svn_url(url)
172 # _backing_url is an evil hack so the root directory of a repository
173 # can be accessed on some HTTP repositories.
174 if _backing_url is None:
175 _backing_url = self.svn_url
176 self._backing_url = _backing_url.rstrip("/")
177 Transport.__init__(self, bzr_url)
179 self._client = svn.client.create_context(self.pool)
180 self._client.auth_baton = _create_auth_baton(self.pool)
181 self._client.config = svn_config
184 self.mutter('opening SVN RA connection to %r' % self._backing_url)
185 self._ra = svn.client.open_ra_session(self._backing_url.encode('utf8'),
186 self._client, self.pool)
187 except SubversionException, (_, num):
188 if num in (svn.core.SVN_ERR_RA_ILLEGAL_URL, \
189 svn.core.SVN_ERR_RA_LOCAL_REPOS_OPEN_FAILED, \
190 svn.core.SVN_ERR_BAD_URL):
191 raise NotBranchError(path=url)
192 if num in (svn.core.SVN_ERR_RA_SVN_REPOS_NOT_FOUND,):
193 raise NoSvnRepositoryPresent(url=url)
196 from bzrlib.plugins.svn import lazy_check_versions
197 lazy_check_versions()
199 def mutter(self, text):
200 if 'transport' in debug.debug_flags:
204 def __init__(self, (reporter, report_baton)):
205 self._reporter = reporter
206 self._baton = report_baton
209 def set_path(self, path, revnum, start_empty, lock_token, pool=None):
210 svn.ra.reporter2_invoke_set_path(self._reporter, self._baton,
211 path, revnum, start_empty, lock_token, pool)
214 def delete_path(self, path, pool=None):
215 svn.ra.reporter2_invoke_delete_path(self._reporter, self._baton,
219 def link_path(self, path, url, revision, start_empty, lock_token,
221 svn.ra.reporter2_invoke_link_path(self._reporter, self._baton,
222 path, url, revision, start_empty, lock_token,
226 def finish_report(self, pool=None):
227 svn.ra.reporter2_invoke_finish_report(self._reporter,
231 def abort_report(self, pool=None):
232 svn.ra.reporter2_invoke_abort_report(self._reporter,
235 def has(self, relpath):
236 """See Transport.has()."""
237 # TODO: Raise TransportNotPossible here instead and
238 # catch it in bzrdir.py
241 def get(self, relpath):
242 """See Transport.get()."""
243 # TODO: Raise TransportNotPossible here instead and
244 # catch it in bzrdir.py
245 raise NoSuchFile(path=relpath)
247 def stat(self, relpath):
248 """See Transport.stat()."""
249 raise TransportNotPossible('stat not supported on Subversion')
253 self.mutter('svn get-uuid')
254 return svn.ra.get_uuid(self._ra)
257 def get_repos_root(self):
258 self.mutter("svn get-repos-root")
259 return svn.ra.get_repos_root(self._ra)
262 def get_latest_revnum(self):
263 self.mutter("svn get-latest-revnum")
264 return svn.ra.get_latest_revnum(self._ra)
267 def do_switch(self, switch_rev, recurse, switch_url, *args, **kwargs):
268 assert self._backing_url == self.svn_url, "backing url invalid: %r != %r" % (self._backing_url, self.svn_url)
269 self.mutter('svn switch -r %d -> %r' % (switch_rev, switch_url))
270 return self.Reporter(svn.ra.do_switch(self._ra, switch_rev, "", recurse, switch_url, *args, **kwargs))
273 def get_log(self, path, from_revnum, to_revnum, *args, **kwargs):
274 self.mutter('svn log %r:%r %r' % (from_revnum, to_revnum, path))
275 return svn.ra.get_log(self._ra, [self._request_path(path)], from_revnum, to_revnum, *args, **kwargs)
277 def reparent_root(self):
278 if self._is_http_transport():
279 self.svn_url = self.base = self.get_repos_root()
281 self.reparent(self.get_repos_root())
284 def reparent(self, url):
285 url = url.rstrip("/")
286 if url == self.svn_url:
290 self._backing_url = url
291 if hasattr(svn.ra, 'reparent'):
292 self.mutter('svn reparent %r' % url)
293 svn.ra.reparent(self._ra, url, self.pool)
295 self._ra = svn.client.open_ra_session(self.svn_url.encode('utf8'),
296 self._client, self.pool)
299 def get_dir(self, path, revnum, pool=None, kind=False):
300 self.mutter("svn ls -r %d '%r'" % (revnum, path))
301 path = self._request_path(path)
302 # ra_dav backends fail with strange errors if the path starts with a
303 # slash while other backends don't.
304 assert len(path) == 0 or path[0] != "/"
305 if hasattr(svn.ra, 'get_dir2'):
308 fields += svn.core.SVN_DIRENT_KIND
309 return svn.ra.get_dir2(self._ra, path, revnum, fields)
311 return svn.ra.get_dir(self._ra, path, revnum)
313 def _request_path(self, relpath):
314 if self._backing_url != self.svn_url:
315 relpath = urlutils.join(
316 urlutils.relative_url(self._backing_url, self.svn_url),
318 relpath = relpath.rstrip("/")
322 def list_dir(self, relpath):
323 assert len(relpath) == 0 or relpath[0] != "/"
327 (dirents, _, _) = self.get_dir(self._request_path(relpath),
328 self.get_latest_revnum())
329 except SubversionException, (msg, num):
330 if num == svn.core.SVN_ERR_FS_NOT_DIRECTORY:
331 raise NoSuchFile(relpath)
333 return dirents.keys()
336 def get_lock(self, path):
337 return svn.ra.get_lock(self._ra, path)
340 def __init__(self, transport, tokens):
341 self._tokens = tokens
342 self._transport = transport
345 self.transport.unlock(self.locks)
349 def unlock(self, locks, break_lock=False):
350 def lock_cb(baton, path, do_lock, lock, ra_err, pool):
352 return svn.ra.unlock(self._ra, locks, break_lock, lock_cb)
355 def lock_write(self, path_revs, comment=None, steal_lock=False):
356 return self.PhonyLock() # FIXME
358 def lock_cb(baton, path, do_lock, lock, ra_err, pool):
360 svn.ra.lock(self._ra, path_revs, comment, steal_lock, lock_cb)
361 return SvnLock(self, tokens)
364 def check_path(self, path, revnum, *args, **kwargs):
365 path = self._request_path(path)
366 assert len(path) == 0 or path[0] != "/"
367 self.mutter("svn check_path -r%d %s" % (revnum, path))
368 return svn.ra.check_path(self._ra, path.encode('utf-8'), revnum, *args, **kwargs)
371 def mkdir(self, relpath, mode=None):
372 assert len(relpath) == 0 or relpath[0] != "/"
373 path = urlutils.join(self.svn_url, relpath)
375 svn.client.mkdir([path.encode("utf-8")], self._client)
376 except SubversionException, (msg, num):
377 if num == svn.core.SVN_ERR_FS_NOT_FOUND:
378 raise NoSuchFile(path)
379 if num == svn.core.SVN_ERR_FS_ALREADY_EXISTS:
380 raise FileExists(path)
384 def do_update(self, revnum, *args, **kwargs):
385 assert self._backing_url == self.svn_url, "backing url invalid: %r != %r" % (self._backing_url, self.svn_url)
386 self.mutter('svn update -r %r' % revnum)
387 return self.Reporter(svn.ra.do_update(self._ra, revnum, "", *args, **kwargs))
390 def get_commit_editor(self, *args, **kwargs):
391 assert self._backing_url == self.svn_url, "backing url invalid: %r != %r" % (self._backing_url, self.svn_url)
392 return Editor(svn.ra.get_commit_editor(self._ra, *args, **kwargs))
395 """See Transport.listable().
399 # There is no real way to do locking directly on the transport
400 # nor is there a need to as the remote server will take care of
406 def lock_read(self, relpath):
407 """See Transport.lock_read()."""
408 return self.PhonyLock()
410 def _is_http_transport(self):
411 return (self.svn_url.startswith("http://") or
412 self.svn_url.startswith("https://"))
414 def clone_root(self):
415 if self._is_http_transport():
416 return SvnRaTransport(self.get_repos_root(), self.base)
417 return SvnRaTransport(self.get_repos_root())
419 def clone(self, offset=None):
420 """See Transport.clone()."""
422 return SvnRaTransport(self.base)
424 return SvnRaTransport(urlutils.join(self.base, offset))
426 def local_abspath(self, relpath):
427 """See Transport.local_abspath()."""
428 absurl = self.abspath(relpath)
429 if self.base.startswith("file:///"):
430 return urlutils.local_path_from_url(absurl)
431 raise NotLocalUrl(absurl)
433 def abspath(self, relpath):
434 """See Transport.abspath()."""
435 return urlutils.join(self.base, relpath)