Implemented SvnWorkingTreeDir.needs_format_conversion().
[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, "", 
305                              recurse, switch_url, *args, **kwargs))
306
307     @convert_svn_error
308     @needs_busy
309     def get_log(self, path, from_revnum, to_revnum, *args, **kwargs):
310         self.mutter('svn log %r:%r %r' % (from_revnum, to_revnum, path))
311         return svn.ra.get_log(self._ra, [self._request_path(path)], 
312                               from_revnum, to_revnum, *args, **kwargs)
313
314     def _open_real_transport(self):
315         if self._backing_url != self.svn_url:
316             self.reparent(self.svn_url)
317         assert self._backing_url == self.svn_url
318
319     def reparent_root(self):
320         if self._is_http_transport():
321             self.svn_url = self.base = self.get_repos_root()
322         else:
323             self.reparent(self.get_repos_root())
324
325     @convert_svn_error
326     @needs_busy
327     def reparent(self, url):
328         url = url.rstrip("/")
329         self.base = url
330         self.svn_url = url
331         if url == self._backing_url:
332             return
333         if hasattr(svn.ra, 'reparent'):
334             self.mutter('svn reparent %r' % url)
335             svn.ra.reparent(self._ra, url, self.pool)
336         else:
337             self.mutter('svn reparent (reconnect) %r' % url)
338             self._ra = svn.client.open_ra_session(self.svn_url.encode('utf8'), 
339                     self._client, self.pool)
340         self._backing_url = url
341
342     @convert_svn_error
343     @needs_busy
344     def get_dir(self, path, revnum, pool=None, kind=False):
345         self.mutter("svn ls -r %d '%r'" % (revnum, path))
346         assert len(path) == 0 or path[0] != "/"
347         path = self._request_path(path)
348         # ra_dav backends fail with strange errors if the path starts with a 
349         # slash while other backends don't.
350         if hasattr(svn.ra, 'get_dir2'):
351             fields = 0
352             if kind:
353                 fields += svn.core.SVN_DIRENT_KIND
354             return svn.ra.get_dir2(self._ra, path, revnum, fields)
355         else:
356             return svn.ra.get_dir(self._ra, path, revnum)
357
358     def _request_path(self, relpath):
359         if self._backing_url == self.svn_url:
360             return relpath
361         newrelpath = urlutils.join(
362                 urlutils.relative_url(self._backing_url+"/", self.svn_url+"/"),
363                 relpath).rstrip("/")
364         self.mutter('request path %r -> %r' % (relpath, newrelpath))
365         return newrelpath
366
367     @convert_svn_error
368     def list_dir(self, relpath):
369         assert len(relpath) == 0 or relpath[0] != "/"
370         if relpath == ".":
371             relpath = ""
372         try:
373             (dirents, _, _) = self.get_dir(self._request_path(relpath),
374                                            self.get_latest_revnum())
375         except SubversionException, (msg, num):
376             if num == svn.core.SVN_ERR_FS_NOT_DIRECTORY:
377                 raise NoSuchFile(relpath)
378             raise
379         return dirents.keys()
380
381     @convert_svn_error
382     @needs_busy
383     def get_lock(self, path):
384         return svn.ra.get_lock(self._ra, path)
385
386     class SvnLock:
387         def __init__(self, transport, tokens):
388             self._tokens = tokens
389             self._transport = transport
390
391         def unlock(self):
392             self.transport.unlock(self.locks)
393
394     @convert_svn_error
395     @needs_busy
396     def unlock(self, locks, break_lock=False):
397         def lock_cb(baton, path, do_lock, lock, ra_err, pool):
398             pass
399         return svn.ra.unlock(self._ra, locks, break_lock, lock_cb)
400
401     @convert_svn_error
402     @needs_busy
403     def lock_write(self, path_revs, comment=None, steal_lock=False):
404         return self.PhonyLock() # FIXME
405         tokens = {}
406         def lock_cb(baton, path, do_lock, lock, ra_err, pool):
407             tokens[path] = lock
408         svn.ra.lock(self._ra, path_revs, comment, steal_lock, lock_cb)
409         return SvnLock(self, tokens)
410
411     @convert_svn_error
412     @needs_busy
413     def check_path(self, path, revnum, *args, **kwargs):
414         assert len(path) == 0 or path[0] != "/"
415         path = self._request_path(path)
416         self.mutter("svn check_path -r%d %s" % (revnum, path))
417         return svn.ra.check_path(self._ra, path.encode('utf-8'), revnum, *args, **kwargs)
418
419     @convert_svn_error
420     @needs_busy
421     def mkdir(self, relpath, mode=None):
422         assert len(relpath) == 0 or relpath[0] != "/"
423         path = urlutils.join(self.svn_url, relpath)
424         try:
425             svn.client.mkdir([path.encode("utf-8")], self._client)
426         except SubversionException, (msg, num):
427             if num == svn.core.SVN_ERR_FS_NOT_FOUND:
428                 raise NoSuchFile(path)
429             if num == svn.core.SVN_ERR_FS_ALREADY_EXISTS:
430                 raise FileExists(path)
431             raise
432
433     @convert_svn_error
434     def do_update(self, revnum, *args, **kwargs):
435         self._open_real_transport()
436         self.mutter('svn update -r %r' % revnum)
437         self._mark_busy()
438         return self.Reporter(self, svn.ra.do_update(self._ra, revnum, "", 
439                              *args, **kwargs))
440
441     @convert_svn_error
442     def get_commit_editor(self, *args, **kwargs):
443         self._open_real_transport()
444         self._mark_busy()
445         return Editor(self, svn.ra.get_commit_editor(self._ra, *args, **kwargs))
446
447     def listable(self):
448         """See Transport.listable().
449         """
450         return True
451
452     # There is no real way to do locking directly on the transport 
453     # nor is there a need to as the remote server will take care of 
454     # locking
455     class PhonyLock:
456         def unlock(self):
457             pass
458
459     def lock_read(self, relpath):
460         """See Transport.lock_read()."""
461         return self.PhonyLock()
462
463     def _is_http_transport(self):
464         return (self.svn_url.startswith("http://") or 
465                 self.svn_url.startswith("https://"))
466
467     def clone_root(self):
468         if self._is_http_transport():
469             return SvnRaTransport(self.get_repos_root(), 
470                                   bzr_to_svn_url(self.base))
471         return SvnRaTransport(self.get_repos_root())
472
473     def clone(self, offset=None):
474         """See Transport.clone()."""
475         if offset is None:
476             return SvnRaTransport(self.base)
477
478         return SvnRaTransport(urlutils.join(self.base, offset))
479
480     def local_abspath(self, relpath):
481         """See Transport.local_abspath()."""
482         absurl = self.abspath(relpath)
483         if self.base.startswith("file:///"):
484             return urlutils.local_path_from_url(absurl)
485         raise NotLocalUrl(absurl)
486
487     def abspath(self, relpath):
488         """See Transport.abspath()."""
489         return urlutils.join(self.base, relpath)