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
17 from bzrlib.errors import (NoSuchFile, NotBranchError, TransportNotPossible,
19 from bzrlib.trace import mutter
20 from bzrlib.transport import Transport
21 import bzrlib.urlutils as urlutils
23 from cStringIO import StringIO
25 from tempfile import mktemp
27 from svn.core import SubversionException, Pool
32 from errors import convert_svn_error
34 # Some older versions of the Python bindings need to be
35 # explicitly initialized
38 svn_config = svn.core.svn_config_get_config(None)
41 def need_lock(unbound):
42 def locked(self, *args, **kwargs):
45 return unbound(self, *args, **kwargs)
48 locked.__doc__ = unbound.__doc__
49 locked.__name__ = unbound.__name__
53 def _create_auth_baton(pool):
54 """Create a Subversion authentication baton. """
55 # Give the client context baton a suite of authentication
58 svn.client.get_simple_provider(pool),
59 svn.client.get_username_provider(pool),
60 svn.client.get_ssl_client_cert_file_provider(pool),
61 svn.client.get_ssl_client_cert_pw_file_provider(pool),
62 svn.client.get_ssl_server_trust_file_provider(pool),
64 return svn.core.svn_auth_open(providers, pool)
67 # Don't run any tests on SvnTransport as it is not intended to be
68 # a full implementation of Transport
69 def get_test_permutations():
73 def get_svn_ra_transport(bzr_transport):
74 """Obtain corresponding SvnRaTransport for a stock Bazaar transport."""
75 if isinstance(bzr_transport, SvnRaTransport):
78 return SvnRaTransport(bzr_transport.base)
81 def bzr_to_svn_url(url):
82 """Convert a Bazaar URL to a URL understood by Subversion.
84 This will possibly remove the svn+ prefix.
86 if (url.startswith("svn+http://") or
87 url.startswith("svn+file://") or
88 url.startswith("svn+https://")):
89 url = url[len("svn+"):] # Skip svn+
91 # The SVN libraries don't like trailing slashes...
92 return url.rstrip('/')
95 class SvnRaTransport(Transport):
96 """Fake transport for Subversion-related namespaces.
98 This implements just as much of Transport as is necessary
101 def __init__(self, url=""):
103 self.is_locked = False
105 self.svn_url = bzr_to_svn_url(url)
106 Transport.__init__(self, bzr_url)
108 self._client = svn.client.create_context(self.pool)
109 self._client.auth_baton = _create_auth_baton(self.pool)
110 self._client.config = svn_config
113 mutter('opening SVN RA connection to %r' % self.svn_url)
114 self._ra = svn.client.open_ra_session(self.svn_url.encode('utf8'),
115 self._client, self.pool)
116 except SubversionException, (msg, num):
117 if num in (svn.core.SVN_ERR_RA_ILLEGAL_URL, \
118 svn.core.SVN_ERR_RA_LOCAL_REPOS_OPEN_FAILED, \
119 svn.core.SVN_ERR_BAD_URL):
120 raise NotBranchError(path=url)
124 assert (not self.is_locked)
125 self.is_locked = True
128 assert self.is_locked
129 self.is_locked = False
131 def has(self, relpath):
132 """See Transport.has()."""
133 # TODO: Raise TransportNotPossible here instead and
134 # catch it in bzrdir.py
137 def get(self, relpath):
138 """See Transport.get()."""
139 # TODO: Raise TransportNotPossible here instead and
140 # catch it in bzrdir.py
141 raise NoSuchFile(path=relpath)
143 def stat(self, relpath):
144 """See Transport.stat()."""
145 raise TransportNotPossible('stat not supported on Subversion')
150 mutter('svn get-uuid')
151 return svn.ra.get_uuid(self._ra)
155 def get_repos_root(self):
156 mutter("svn get-repos-root")
157 return svn.ra.get_repos_root(self._ra)
161 def get_latest_revnum(self):
162 mutter("svn get-latest-revnum")
163 return svn.ra.get_latest_revnum(self._ra)
167 def do_switch(self, switch_rev, switch_target, recurse, switch_url, *args, **kwargs):
168 mutter('svn switch -r %d %r -> %r' % (switch_rev, switch_target, switch_url))
169 return svn.ra.do_switch(self._ra, switch_rev, switch_target, recurse, switch_url, *args, **kwargs)
173 def get_log(self, path, from_revnum, to_revnum, *args, **kwargs):
174 mutter('svn log %r:%r %r' % (from_revnum, to_revnum, path))
175 return svn.ra.get_log(self._ra, [path], from_revnum, to_revnum, *args, **kwargs)
179 def reparent(self, url):
180 url = url.rstrip("/")
181 if url == self.svn_url:
185 if hasattr(svn.ra, 'reparent'):
186 mutter('svn reparent %r' % url)
187 svn.ra.reparent(self._ra, url, self.pool)
189 self._ra = svn.client.open_ra_session(self.svn_url.encode('utf8'),
190 self._client, self.pool)
193 def get_dir(self, path, revnum, pool=None, kind=False):
194 mutter("svn ls -r %d '%r'" % (revnum, path))
195 path = path.rstrip("/")
196 # ra_dav backends fail with strange errors if the path starts with a
197 # slash while other backends don't.
198 assert len(path) == 0 or path[0] != "/"
199 if hasattr(svn.ra, 'get_dir2'):
202 fields += svn.core.SVN_DIRENT_KIND
203 return svn.ra.get_dir2(self._ra, path, revnum, fields)
205 return svn.ra.get_dir(self._ra, path, revnum)
208 def list_dir(self, relpath):
209 assert len(relpath) == 0 or relpath[0] != "/"
213 (dirents, _, _) = self.get_dir(relpath.rstrip("/"),
214 self.get_latest_revnum())
215 except SubversionException, (msg, num):
216 if num == svn.core.SVN_ERR_FS_NOT_DIRECTORY:
217 raise NoSuchFile(relpath)
219 return dirents.keys()
223 def check_path(self, path, revnum, *args, **kwargs):
224 assert len(path) == 0 or path[0] != "/"
225 mutter("svn check_path -r%d %s" % (revnum, path))
226 return svn.ra.check_path(self._ra, path, revnum, *args, **kwargs)
230 def mkdir(self, relpath, mode=None):
231 path = "%s/%s" % (self.svn_url, relpath)
233 svn.client.mkdir([path.encode("utf-8")], self._client)
234 except SubversionException, (msg, num):
235 if num == svn.core.SVN_ERR_FS_NOT_FOUND:
236 raise NoSuchFile(path)
237 if num == svn.core.SVN_ERR_FS_ALREADY_EXISTS:
238 raise FileExists(path)
243 def do_update(self, revnum, path, *args, **kwargs):
244 mutter('svn update -r %r %r' % (revnum, path))
245 return svn.ra.do_update(self._ra, revnum, path, *args, **kwargs)
249 def get_commit_editor(self, *args, **kwargs):
250 return svn.ra.get_commit_editor(self._ra, *args, **kwargs)
253 """See Transport.listable().
257 # There is no real way to do locking directly on the transport
258 # nor is there a need to as the remote server will take care of
264 def lock_write(self, relpath):
265 """See Transport.lock_write()."""
266 return self.PhonyLock()
268 def lock_read(self, relpath):
269 """See Transport.lock_read()."""
270 return self.PhonyLock()
272 def clone(self, offset=None):
273 """See Transport.clone()."""
275 return SvnRaTransport(self.base)
277 return SvnRaTransport(urlutils.join(self.base, offset))