work around another assert in the subversion bindings.
[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 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
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, NoSvnRepositoryPresent
30
31 svn_config = svn.core.svn_config_get_config(None)
32
33
34 def _create_auth_baton(pool):
35     """Create a Subversion authentication baton. """
36     # Give the client context baton a suite of authentication
37     # providers.h
38     providers = [
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),
44         ]
45     return svn.core.svn_auth_open(providers, pool)
46
47
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():
51     return []
52
53
54 def get_svn_ra_transport(bzr_transport):
55     """Obtain corresponding SvnRaTransport for a stock Bazaar transport."""
56     if isinstance(bzr_transport, SvnRaTransport):
57         return bzr_transport
58
59     return SvnRaTransport(bzr_transport.base)
60
61
62 def bzr_to_svn_url(url):
63     """Convert a Bazaar URL to a URL understood by Subversion.
64
65     This will possibly remove the svn+ prefix.
66     """
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+
71
72     # The SVN libraries don't like trailing slashes...
73     return url.rstrip('/')
74
75
76 def needs_busy(unbound):
77     """Decorator that marks a transport as busy before running a methd on it.
78     """
79     def convert(self, *args, **kwargs):
80         self._mark_busy()
81         ret = unbound(self, *args, **kwargs)
82         self._unmark_busy()
83         return ret
84
85     convert.__doc__ = unbound.__doc__
86     convert.__name__ = unbound.__name__
87     return convert
88
89
90 class Editor:
91     """Simple object wrapper around the Subversion delta editor interface."""
92     def __init__(self, transport, (editor, editor_baton)):
93         self.editor = editor
94         self.editor_baton = editor_baton
95         self.recent_baton = []
96         self._transport = transport
97
98     @convert_svn_error
99     def open_root(self, base_revnum):
100         assert self.recent_baton == [], "root already opened"
101         baton = svn.delta.editor_invoke_open_root(self.editor, 
102                 self.editor_baton, base_revnum)
103         self.recent_baton.append(baton)
104         return baton
105
106     @convert_svn_error
107     def close_directory(self, baton, *args, **kwargs):
108         assert self.recent_baton.pop() == baton, \
109                 "only most recently opened baton can be closed"
110         svn.delta.editor_invoke_close_directory(self.editor, baton, *args, **kwargs)
111
112     @convert_svn_error
113     def close(self):
114         assert self.recent_baton == []
115         svn.delta.editor_invoke_close_edit(self.editor, self.editor_baton)
116         self._transport._unmark_busy()
117
118     @convert_svn_error
119     def apply_textdelta(self, baton, *args, **kwargs):
120         assert self.recent_baton[-1] == baton
121         return svn.delta.editor_invoke_apply_textdelta(self.editor, baton,
122                 *args, **kwargs)
123
124     @convert_svn_error
125     def change_dir_prop(self, baton, *args, **kwargs):
126         assert self.recent_baton[-1] == baton
127         return svn.delta.editor_invoke_change_dir_prop(self.editor, baton, 
128                                                        *args, **kwargs)
129
130     @convert_svn_error
131     def delete_entry(self, *args, **kwargs):
132         return svn.delta.editor_invoke_delete_entry(self.editor, *args, **kwargs)
133
134     @convert_svn_error
135     def add_file(self, path, parent_baton, *args, **kwargs):
136         assert self.recent_baton[-1] == parent_baton
137         baton = svn.delta.editor_invoke_add_file(self.editor, path, 
138             parent_baton, *args, **kwargs)
139         self.recent_baton.append(baton)
140         return baton
141
142     @convert_svn_error
143     def open_file(self, path, parent_baton, *args, **kwargs):
144         assert self.recent_baton[-1] == parent_baton
145         baton = svn.delta.editor_invoke_open_file(self.editor, path, 
146                                                  parent_baton, *args, **kwargs)
147         self.recent_baton.append(baton)
148         return baton
149
150     @convert_svn_error
151     def change_file_prop(self, baton, *args, **kwargs):
152         assert self.recent_baton[-1] == baton
153         svn.delta.editor_invoke_change_file_prop(self.editor, baton, *args, 
154                                                  **kwargs)
155
156     @convert_svn_error
157     def close_file(self, baton, *args, **kwargs):
158         assert self.recent_baton.pop() == baton
159         svn.delta.editor_invoke_close_file(self.editor, baton, *args, **kwargs)
160
161     @convert_svn_error
162     def add_directory(self, path, parent_baton, *args, **kwargs):
163         assert self.recent_baton[-1] == parent_baton
164         baton = svn.delta.editor_invoke_add_directory(self.editor, path, 
165             parent_baton, *args, **kwargs)
166         self.recent_baton.append(baton)
167         return baton
168
169     @convert_svn_error
170     def open_directory(self, path, parent_baton, *args, **kwargs):
171         assert self.recent_baton[-1] == parent_baton
172         baton = svn.delta.editor_invoke_open_directory(self.editor, path, 
173             parent_baton, *args, **kwargs)
174         self.recent_baton.append(baton)
175         return baton
176
177
178 class SvnRaTransport(Transport):
179     """Fake transport for Subversion-related namespaces.
180     
181     This implements just as much of Transport as is necessary 
182     to fool Bazaar. """
183     @convert_svn_error
184     def __init__(self, url="", _backing_url=None):
185         self.pool = Pool()
186         bzr_url = url
187         self.svn_url = bzr_to_svn_url(url)
188         self._root = None
189         # _backing_url is an evil hack so the root directory of a repository 
190         # can be accessed on some HTTP repositories. 
191         if _backing_url is None:
192             _backing_url = self.svn_url
193         self._backing_url = _backing_url.rstrip("/")
194         Transport.__init__(self, bzr_url)
195
196         self._client = svn.client.create_context(self.pool)
197         self._client.auth_baton = _create_auth_baton(self.pool)
198         self._client.config = svn_config
199
200         try:
201             self.mutter('opening SVN RA connection to %r' % self._backing_url)
202             self._ra = svn.client.open_ra_session(self._backing_url.encode('utf8'), 
203                     self._client, self.pool)
204         except SubversionException, (_, num):
205             if num in (svn.core.SVN_ERR_RA_SVN_REPOS_NOT_FOUND,):
206                 raise NoSvnRepositoryPresent(url=url)
207             if num == svn.core.SVN_ERR_BAD_URL:
208                 raise InvalidURL(url)
209             raise
210
211         from bzrlib.plugins.svn import lazy_check_versions
212         lazy_check_versions()
213
214         self._busy = False
215
216     def _mark_busy(self):
217         assert not self._busy
218         self._busy = True
219
220     def _unmark_busy(self):
221         assert self._busy
222         self._busy = False
223
224     def mutter(self, text):
225         if 'transport' in debug.debug_flags:
226             mutter(text)
227
228     class Reporter:
229         def __init__(self, transport, (reporter, report_baton)):
230             self._reporter = reporter
231             self._baton = report_baton
232             self._transport = transport
233
234         @convert_svn_error
235         def set_path(self, path, revnum, start_empty, lock_token, pool=None):
236             svn.ra.reporter2_invoke_set_path(self._reporter, self._baton, 
237                         path, revnum, start_empty, lock_token, pool)
238
239         @convert_svn_error
240         def delete_path(self, path, pool=None):
241             svn.ra.reporter2_invoke_delete_path(self._reporter, self._baton,
242                     path, pool)
243
244         @convert_svn_error
245         def link_path(self, path, url, revision, start_empty, lock_token, 
246                       pool=None):
247             svn.ra.reporter2_invoke_link_path(self._reporter, self._baton,
248                     path, url, revision, start_empty, lock_token,
249                     pool)
250
251         @convert_svn_error
252         def finish_report(self, pool=None):
253             svn.ra.reporter2_invoke_finish_report(self._reporter, 
254                     self._baton, pool)
255             self._transport._unmark_busy()
256
257         @convert_svn_error
258         def abort_report(self, pool=None):
259             svn.ra.reporter2_invoke_abort_report(self._reporter, 
260                     self._baton, pool)
261             self._transport._unmark_busy()
262
263     def has(self, relpath):
264         """See Transport.has()."""
265         # TODO: Raise TransportNotPossible here instead and 
266         # catch it in bzrdir.py
267         return False
268
269     def get(self, relpath):
270         """See Transport.get()."""
271         # TODO: Raise TransportNotPossible here instead and 
272         # catch it in bzrdir.py
273         raise NoSuchFile(path=relpath)
274
275     def stat(self, relpath):
276         """See Transport.stat()."""
277         raise TransportNotPossible('stat not supported on Subversion')
278
279     @convert_svn_error
280     @needs_busy
281     def get_uuid(self):
282         self.mutter('svn get-uuid')
283         return svn.ra.get_uuid(self._ra)
284
285     @convert_svn_error
286     @needs_busy
287     def get_repos_root(self):
288         if self._root is None:
289             self.mutter("svn get-repos-root")
290             self._root = svn.ra.get_repos_root(self._ra)
291         return self._root
292
293     @convert_svn_error
294     @needs_busy
295     def get_latest_revnum(self):
296         self.mutter("svn get-latest-revnum")
297         return svn.ra.get_latest_revnum(self._ra)
298
299     @convert_svn_error
300     def do_switch(self, switch_rev, recurse, switch_url, *args, **kwargs):
301         self._open_real_transport()
302         self.mutter('svn switch -r %d -> %r' % (switch_rev, switch_url))
303         self._mark_busy()
304         return self.Reporter(self, svn.ra.do_switch(self._ra, switch_rev, "", recurse, switch_url, *args, **kwargs))
305
306     @convert_svn_error
307     @needs_busy
308     def get_log(self, path, from_revnum, to_revnum, *args, **kwargs):
309         self.mutter('svn log %r:%r %r' % (from_revnum, to_revnum, path))
310         return svn.ra.get_log(self._ra, [self._request_path(path)], from_revnum, to_revnum, *args, **kwargs)
311
312     def _open_real_transport(self):
313         if self._backing_url != self.svn_url:
314             self.reparent(self.svn_url)
315         assert self._backing_url == self.svn_url
316
317     def reparent_root(self):
318         if self._is_http_transport():
319             self.svn_url = self.base = self.get_repos_root()
320         else:
321             self.reparent(self.get_repos_root())
322
323     @convert_svn_error
324     @needs_busy
325     def reparent(self, url):
326         url = url.rstrip("/")
327         self.base = url
328         self.svn_url = url
329         if url == self._backing_url:
330             return
331         if hasattr(svn.ra, 'reparent'):
332             self.mutter('svn reparent %r' % url)
333             svn.ra.reparent(self._ra, url, self.pool)
334         else:
335             self.mutter('svn reparent (reconnect) %r' % url)
336             self._ra = svn.client.open_ra_session(self.svn_url.encode('utf8'), 
337                     self._client, self.pool)
338         self._backing_url = url
339
340     @convert_svn_error
341     @needs_busy
342     def get_dir(self, path, revnum, pool=None, kind=False):
343         self.mutter("svn ls -r %d '%r'" % (revnum, path))
344         assert len(path) == 0 or path[0] != "/"
345         path = self._request_path(path)
346         # ra_dav backends fail with strange errors if the path starts with a 
347         # slash while other backends don't.
348         if hasattr(svn.ra, 'get_dir2'):
349             fields = 0
350             if kind:
351                 fields += svn.core.SVN_DIRENT_KIND
352             return svn.ra.get_dir2(self._ra, path, revnum, fields)
353         else:
354             return svn.ra.get_dir(self._ra, path, revnum)
355
356     def _request_path(self, relpath):
357         if self._backing_url == self.svn_url:
358             return relpath
359         newrelpath = urlutils.join(
360                 urlutils.relative_url(self._backing_url, self.svn_url),
361                 relpath).rstrip("/")
362         self.mutter('request path %r -> %r' % (relpath, newrelpath))
363         return newrelpath
364
365     @convert_svn_error
366     def list_dir(self, relpath):
367         assert len(relpath) == 0 or relpath[0] != "/"
368         if relpath == ".":
369             relpath = ""
370         try:
371             (dirents, _, _) = self.get_dir(self._request_path(relpath),
372                                            self.get_latest_revnum())
373         except SubversionException, (msg, num):
374             if num == svn.core.SVN_ERR_FS_NOT_DIRECTORY:
375                 raise NoSuchFile(relpath)
376             raise
377         return dirents.keys()
378
379     @convert_svn_error
380     @needs_busy
381     def get_lock(self, path):
382         return svn.ra.get_lock(self._ra, path)
383
384     class SvnLock:
385         def __init__(self, transport, tokens):
386             self._tokens = tokens
387             self._transport = transport
388
389         def unlock(self):
390             self.transport.unlock(self.locks)
391
392     @convert_svn_error
393     @needs_busy
394     def unlock(self, locks, break_lock=False):
395         def lock_cb(baton, path, do_lock, lock, ra_err, pool):
396             pass
397         return svn.ra.unlock(self._ra, locks, break_lock, lock_cb)
398
399     @convert_svn_error
400     @needs_busy
401     def lock_write(self, path_revs, comment=None, steal_lock=False):
402         return self.PhonyLock() # FIXME
403         tokens = {}
404         def lock_cb(baton, path, do_lock, lock, ra_err, pool):
405             tokens[path] = lock
406         svn.ra.lock(self._ra, path_revs, comment, steal_lock, lock_cb)
407         return SvnLock(self, tokens)
408
409     @convert_svn_error
410     @needs_busy
411     def check_path(self, path, revnum, *args, **kwargs):
412         assert len(path) == 0 or path[0] != "/"
413         path = self._request_path(path)
414         self.mutter("svn check_path -r%d %s" % (revnum, path))
415         return svn.ra.check_path(self._ra, path.encode('utf-8'), revnum, *args, **kwargs)
416
417     @convert_svn_error
418     @needs_busy
419     def mkdir(self, relpath, mode=None):
420         assert len(relpath) == 0 or relpath[0] != "/"
421         path = urlutils.join(self.svn_url, relpath)
422         try:
423             svn.client.mkdir([path.encode("utf-8")], self._client)
424         except SubversionException, (msg, num):
425             if num == svn.core.SVN_ERR_FS_NOT_FOUND:
426                 raise NoSuchFile(path)
427             if num == svn.core.SVN_ERR_FS_ALREADY_EXISTS:
428                 raise FileExists(path)
429             raise
430
431     @convert_svn_error
432     def do_update(self, revnum, *args, **kwargs):
433         self._open_real_transport()
434         self.mutter('svn update -r %r' % revnum)
435         self._mark_busy()
436         return self.Reporter(self, svn.ra.do_update(self._ra, revnum, "", 
437                              *args, **kwargs))
438
439     @convert_svn_error
440     def get_commit_editor(self, *args, **kwargs):
441         self._open_real_transport()
442         self._mark_busy()
443         return Editor(self, svn.ra.get_commit_editor(self._ra, *args, **kwargs))
444
445     def listable(self):
446         """See Transport.listable().
447         """
448         return True
449
450     # There is no real way to do locking directly on the transport 
451     # nor is there a need to as the remote server will take care of 
452     # locking
453     class PhonyLock:
454         def unlock(self):
455             pass
456
457     def lock_read(self, relpath):
458         """See Transport.lock_read()."""
459         return self.PhonyLock()
460
461     def _is_http_transport(self):
462         return (self.svn_url.startswith("http://") or 
463                 self.svn_url.startswith("https://"))
464
465     def clone_root(self):
466         if self._is_http_transport():
467             return SvnRaTransport(self.get_repos_root(), 
468                                   bzr_to_svn_url(self.base))
469         return SvnRaTransport(self.get_repos_root())
470
471     def clone(self, offset=None):
472         """See Transport.clone()."""
473         if offset is None:
474             return SvnRaTransport(self.base)
475
476         return SvnRaTransport(urlutils.join(self.base, offset))
477
478     def local_abspath(self, relpath):
479         """See Transport.local_abspath()."""
480         absurl = self.abspath(relpath)
481         if self.base.startswith("file:///"):
482             return urlutils.local_path_from_url(absurl)
483         raise NotLocalUrl(absurl)
484
485     def abspath(self, relpath):
486         """See Transport.abspath()."""
487         return urlutils.join(self.base, relpath)