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