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