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