Use the error-convert decorator. TCP connections being dropped will now no longer...
[jelmer/subvertpy.git] / transport.py
1 # Copyright (C) 2006 Jelmer Vernooij <jelmer@samba.org>
2
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.
7
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.
12
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
17 from bzrlib.errors import (NoSuchFile, NotBranchError, TransportNotPossible, 
18                            FileExists)
19 from bzrlib.trace import mutter
20 from bzrlib.transport import Transport
21 import bzrlib.urlutils as urlutils
22
23 from cStringIO import StringIO
24 import os
25 from tempfile import mktemp
26
27 from svn.core import SubversionException, Pool
28 import svn.ra
29 import svn.core
30 import svn.client
31
32 from errors import convert_svn_error
33
34 # Some older versions of the Python bindings need to be 
35 # explicitly initialized
36 svn.ra.initialize()
37
38 svn_config = svn.core.svn_config_get_config(None)
39
40
41 def need_lock(unbound):
42     def locked(self, *args, **kwargs):
43         self.lock()
44         try:
45             return unbound(self, *args, **kwargs)
46         finally:
47             self.unlock()
48     locked.__doc__ = unbound.__doc__
49     locked.__name__ = unbound.__name__
50     return locked
51
52
53 def _create_auth_baton(pool):
54     """Create a Subversion authentication baton. """
55     # Give the client context baton a suite of authentication
56     # providers.h
57     providers = [
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),
63         ]
64     return svn.core.svn_auth_open(providers, pool)
65
66
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():
70     return []
71
72
73 def get_svn_ra_transport(bzr_transport):
74     """Obtain corresponding SvnRaTransport for a stock Bazaar transport."""
75     if isinstance(bzr_transport, SvnRaTransport):
76         return bzr_transport
77
78     return SvnRaTransport(bzr_transport.base)
79
80
81 def bzr_to_svn_url(url):
82     """Convert a Bazaar URL to a URL understood by Subversion.
83
84     This will possibly remove the svn+ prefix.
85     """
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+
90
91     # The SVN libraries don't like trailing slashes...
92     return url.rstrip('/')
93
94
95 class SvnRaTransport(Transport):
96     """Fake transport for Subversion-related namespaces.
97     
98     This implements just as much of Transport as is necessary 
99     to fool Bazaar. """
100     @convert_svn_error
101     def __init__(self, url=""):
102         self.pool = Pool()
103         self.is_locked = False
104         bzr_url = url
105         self.svn_url = bzr_to_svn_url(url)
106         Transport.__init__(self, bzr_url)
107
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
111
112         try:
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)
121             raise
122
123     def lock(self):
124         assert (not self.is_locked)
125         self.is_locked = True
126
127     def unlock(self):
128         assert self.is_locked
129         self.is_locked = False
130
131     def has(self, relpath):
132         """See Transport.has()."""
133         # TODO: Raise TransportNotPossible here instead and 
134         # catch it in bzrdir.py
135         return False
136
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)
142
143     def stat(self, relpath):
144         """See Transport.stat()."""
145         raise TransportNotPossible('stat not supported on Subversion')
146
147     @need_lock
148     @convert_svn_error
149     def get_uuid(self):
150         mutter('svn get-uuid')
151         return svn.ra.get_uuid(self._ra)
152
153     @need_lock
154     @convert_svn_error
155     def get_repos_root(self):
156         mutter("svn get-repos-root")
157         return svn.ra.get_repos_root(self._ra)
158
159     @need_lock
160     @convert_svn_error
161     def get_latest_revnum(self):
162         mutter("svn get-latest-revnum")
163         return svn.ra.get_latest_revnum(self._ra)
164
165     @need_lock
166     @convert_svn_error
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)
170
171     @need_lock
172     @convert_svn_error
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)
176
177     @need_lock
178     @convert_svn_error
179     def reparent(self, url):
180         url = url.rstrip("/")
181         if url == self.svn_url:
182             return
183         self.base = url
184         self.svn_url = url
185         if hasattr(svn.ra, 'reparent'):
186             mutter('svn reparent %r' % url)
187             svn.ra.reparent(self._ra, url, self.pool)
188         else:
189             self._ra = svn.client.open_ra_session(self.svn_url.encode('utf8'), 
190                     self._client, self.pool)
191     @need_lock
192     @convert_svn_error
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'):
200             fields = 0
201             if kind:
202                 fields += svn.core.SVN_DIRENT_KIND
203             return svn.ra.get_dir2(self._ra, path, revnum, fields)
204         else:
205             return svn.ra.get_dir(self._ra, path, revnum)
206
207     @convert_svn_error
208     def list_dir(self, relpath):
209         assert len(relpath) == 0 or relpath[0] != "/"
210         if relpath == ".":
211             relpath = ""
212         try:
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)
218             raise
219         return dirents.keys()
220
221     @need_lock
222     @convert_svn_error
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)
227
228     @need_lock
229     @convert_svn_error
230     def mkdir(self, relpath, mode=None):
231         path = "%s/%s" % (self.svn_url, relpath)
232         try:
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)
239             raise
240
241     @need_lock
242     @convert_svn_error
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)
246
247     @need_lock
248     @convert_svn_error
249     def get_commit_editor(self, *args, **kwargs):
250         return svn.ra.get_commit_editor(self._ra, *args, **kwargs)
251
252     def listable(self):
253         """See Transport.listable().
254         """
255         return True
256
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 
259     # locking
260     class PhonyLock:
261         def unlock(self):
262             pass
263
264     def lock_write(self, relpath):
265         """See Transport.lock_write()."""
266         return self.PhonyLock()
267
268     def lock_read(self, relpath):
269         """See Transport.lock_read()."""
270         return self.PhonyLock()
271
272     def clone(self, offset=None):
273         """See Transport.clone()."""
274         if offset is None:
275             return SvnRaTransport(self.base)
276
277         return SvnRaTransport(urlutils.join(self.base, offset))