Fix bug in revid caching.
[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 """Simple transport for accessing Subversion smart servers."""
17
18 from bzrlib.errors import (NoSuchFile, NotBranchError, TransportNotPossible, 
19                            FileExists)
20 from bzrlib.trace import mutter
21 from bzrlib.transport import Transport
22 import bzrlib.urlutils as urlutils
23
24 from svn.core import SubversionException, Pool
25 import svn.ra
26 import svn.core
27 import svn.client
28
29 from errors import convert_svn_error
30
31 svn_config = svn.core.svn_config_get_config(None)
32
33
34 def need_lock(unbound):
35     def locked(self, *args, **kwargs):
36         self.lock()
37         try:
38             return unbound(self, *args, **kwargs)
39         finally:
40             self.unlock()
41     locked.__doc__ = unbound.__doc__
42     locked.__name__ = unbound.__name__
43     return locked
44
45
46 def _create_auth_baton(pool):
47     """Create a Subversion authentication baton. """
48     # Give the client context baton a suite of authentication
49     # providers.h
50     providers = [
51         svn.client.get_simple_provider(pool),
52         svn.client.get_username_provider(pool),
53         svn.client.get_ssl_client_cert_file_provider(pool),
54         svn.client.get_ssl_client_cert_pw_file_provider(pool),
55         svn.client.get_ssl_server_trust_file_provider(pool),
56         ]
57     return svn.core.svn_auth_open(providers, pool)
58
59
60 # Don't run any tests on SvnTransport as it is not intended to be 
61 # a full implementation of Transport
62 def get_test_permutations():
63     return []
64
65
66 def get_svn_ra_transport(bzr_transport):
67     """Obtain corresponding SvnRaTransport for a stock Bazaar transport."""
68     if isinstance(bzr_transport, SvnRaTransport):
69         return bzr_transport
70
71     return SvnRaTransport(bzr_transport.base)
72
73
74 def bzr_to_svn_url(url):
75     """Convert a Bazaar URL to a URL understood by Subversion.
76
77     This will possibly remove the svn+ prefix.
78     """
79     if (url.startswith("svn+http://") or 
80         url.startswith("svn+file://") or
81         url.startswith("svn+https://")):
82         url = url[len("svn+"):] # Skip svn+
83
84     # The SVN libraries don't like trailing slashes...
85     return url.rstrip('/')
86
87
88 class SvnRaTransport(Transport):
89     """Fake transport for Subversion-related namespaces.
90     
91     This implements just as much of Transport as is necessary 
92     to fool Bazaar. """
93     @convert_svn_error
94     def __init__(self, url=""):
95         self.pool = Pool()
96         self.is_locked = False
97         bzr_url = url
98         self.svn_url = bzr_to_svn_url(url)
99         Transport.__init__(self, bzr_url)
100
101         self._client = svn.client.create_context(self.pool)
102         self._client.auth_baton = _create_auth_baton(self.pool)
103         self._client.config = svn_config
104
105         try:
106             mutter('opening SVN RA connection to %r' % self.svn_url)
107             self._ra = svn.client.open_ra_session(self.svn_url.encode('utf8'), 
108                     self._client, self.pool)
109         except SubversionException, (_, num):
110             if num in (svn.core.SVN_ERR_RA_ILLEGAL_URL, \
111                        svn.core.SVN_ERR_RA_LOCAL_REPOS_OPEN_FAILED, \
112                        svn.core.SVN_ERR_BAD_URL):
113                 raise NotBranchError(path=url)
114             raise
115
116     class Reporter:
117         def __init__(self, (reporter, report_baton)):
118             self._reporter = reporter
119             self._baton = report_baton
120
121         def set_path(self, path, revnum, start_empty, lock_token, pool=None):
122             svn.ra.reporter2_invoke_set_path(self._reporter, self._baton, 
123                         path, revnum, start_empty, lock_token, pool)
124
125         def delete_path(self, path, pool=None):
126             svn.ra.reporter2_invoke_delete_path(self._reporter, self._baton,
127                     path, pool)
128
129         def link_path(self, path, url, revision, start_empty, lock_token, 
130                       pool=None):
131             svn.ra.reporter2_invoke_link_path(self._reporter, self._baton,
132                     path, url, revision, start_empty, lock_token,
133                     pool)
134
135         def finish_report(self, pool=None):
136             svn.ra.reporter2_invoke_finish_report(self._reporter, 
137                     self._baton, pool)
138
139         def abort_report(self, pool=None):
140             svn.ra.reporter2_invoke_abort_report(self._reporter, 
141                     self._baton, pool)
142
143     def lock(self):
144         assert (not self.is_locked)
145         self.is_locked = True
146
147     def unlock(self):
148         assert self.is_locked
149         self.is_locked = False
150
151     def has(self, relpath):
152         """See Transport.has()."""
153         # TODO: Raise TransportNotPossible here instead and 
154         # catch it in bzrdir.py
155         return False
156
157     def get(self, relpath):
158         """See Transport.get()."""
159         # TODO: Raise TransportNotPossible here instead and 
160         # catch it in bzrdir.py
161         raise NoSuchFile(path=relpath)
162
163     def stat(self, relpath):
164         """See Transport.stat()."""
165         raise TransportNotPossible('stat not supported on Subversion')
166
167     @need_lock
168     @convert_svn_error
169     def get_uuid(self):
170         mutter('svn get-uuid')
171         return svn.ra.get_uuid(self._ra)
172
173     @need_lock
174     @convert_svn_error
175     def get_repos_root(self):
176         mutter("svn get-repos-root")
177         return svn.ra.get_repos_root(self._ra)
178
179     @need_lock
180     @convert_svn_error
181     def get_latest_revnum(self):
182         mutter("svn get-latest-revnum")
183         return svn.ra.get_latest_revnum(self._ra)
184
185     @need_lock
186     @convert_svn_error
187     def do_switch(self, switch_rev, switch_target, recurse, switch_url, *args, **kwargs):
188         mutter('svn switch -r %d %r -> %r' % (switch_rev, switch_target, switch_url))
189         return self.Reporter(svn.ra.do_switch(self._ra, switch_rev, switch_target, recurse, switch_url, *args, **kwargs))
190
191     @need_lock
192     @convert_svn_error
193     def get_log(self, path, from_revnum, to_revnum, *args, **kwargs):
194         mutter('svn log %r:%r %r' % (from_revnum, to_revnum, path))
195         return svn.ra.get_log(self._ra, [path], from_revnum, to_revnum, *args, **kwargs)
196
197     @need_lock
198     @convert_svn_error
199     def reparent(self, url):
200         url = url.rstrip("/")
201         if url == self.svn_url:
202             return
203         self.base = url
204         self.svn_url = url
205         if hasattr(svn.ra, 'reparent'):
206             mutter('svn reparent %r' % url)
207             svn.ra.reparent(self._ra, url, self.pool)
208         else:
209             self._ra = svn.client.open_ra_session(self.svn_url.encode('utf8'), 
210                     self._client, self.pool)
211     @need_lock
212     @convert_svn_error
213     def get_dir(self, path, revnum, pool=None, kind=False):
214         mutter("svn ls -r %d '%r'" % (revnum, path))
215         path = path.rstrip("/")
216         # ra_dav backends fail with strange errors if the path starts with a 
217         # slash while other backends don't.
218         assert len(path) == 0 or path[0] != "/"
219         if hasattr(svn.ra, 'get_dir2'):
220             fields = 0
221             if kind:
222                 fields += svn.core.SVN_DIRENT_KIND
223             return svn.ra.get_dir2(self._ra, path, revnum, fields)
224         else:
225             return svn.ra.get_dir(self._ra, path, revnum)
226
227     @convert_svn_error
228     def list_dir(self, relpath):
229         assert len(relpath) == 0 or relpath[0] != "/"
230         if relpath == ".":
231             relpath = ""
232         try:
233             (dirents, _, _) = self.get_dir(relpath.rstrip("/"), 
234                                            self.get_latest_revnum())
235         except SubversionException, (msg, num):
236             if num == svn.core.SVN_ERR_FS_NOT_DIRECTORY:
237                 raise NoSuchFile(relpath)
238             raise
239         return dirents.keys()
240
241     @need_lock
242     @convert_svn_error
243     def check_path(self, path, revnum, *args, **kwargs):
244         assert len(path) == 0 or path[0] != "/"
245         mutter("svn check_path -r%d %s" % (revnum, path))
246         return svn.ra.check_path(self._ra, path, revnum, *args, **kwargs)
247
248     @need_lock
249     @convert_svn_error
250     def mkdir(self, relpath, mode=None):
251         path = "%s/%s" % (self.svn_url, relpath)
252         try:
253             svn.client.mkdir([path.encode("utf-8")], self._client)
254         except SubversionException, (msg, num):
255             if num == svn.core.SVN_ERR_FS_NOT_FOUND:
256                 raise NoSuchFile(path)
257             if num == svn.core.SVN_ERR_FS_ALREADY_EXISTS:
258                 raise FileExists(path)
259             raise
260
261     @need_lock
262     @convert_svn_error
263     def do_update(self, revnum, path, *args, **kwargs):
264         mutter('svn update -r %r %r' % (revnum, path))
265         return self.Reporter(svn.ra.do_update(self._ra, revnum, path, *args, **kwargs))
266
267     @need_lock
268     @convert_svn_error
269     def get_commit_editor(self, *args, **kwargs):
270         return svn.ra.get_commit_editor(self._ra, *args, **kwargs)
271
272     def listable(self):
273         """See Transport.listable().
274         """
275         return True
276
277     # There is no real way to do locking directly on the transport 
278     # nor is there a need to as the remote server will take care of 
279     # locking
280     class PhonyLock:
281         def unlock(self):
282             pass
283
284     def lock_write(self, relpath):
285         """See Transport.lock_write()."""
286         return self.PhonyLock()
287
288     def lock_read(self, relpath):
289         """See Transport.lock_read()."""
290         return self.PhonyLock()
291
292     def clone(self, offset=None):
293         """See Transport.clone()."""
294         if offset is None:
295             return SvnRaTransport(self.base)
296
297         return SvnRaTransport(urlutils.join(self.base, offset))