Merge 0.4.
[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 3 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 bzrlib.plugins.svn import core, properties, ra
25 from bzrlib.plugins.svn import properties
26 from bzrlib.plugins.svn.auth import create_auth_baton
27 from bzrlib.plugins.svn.core import SubversionException, get_config
28 from bzrlib.plugins.svn.errors import convert_svn_error, NoSvnRepositoryPresent, ERR_BAD_URL, ERR_RA_SVN_REPOS_NOT_FOUND, ERR_FS_ALREADY_EXISTS, ERR_FS_NOT_FOUND, ERR_FS_NOT_DIRECTORY
29 from bzrlib.plugins.svn.ra import DIRENT_KIND, RemoteAccess
30 import urlparse
31 import urllib
32
33 svn_config = get_config()
34
35 def get_client_string():
36     """Return a string that can be send as part of the User Agent string."""
37     return "bzr%s+bzr-svn%s" % (bzrlib.__version__, bzrlib.plugins.svn.__version__)
38
39  
40 # Don't run any tests on SvnTransport as it is not intended to be 
41 # a full implementation of Transport
42 def get_test_permutations():
43     return []
44
45
46 def get_svn_ra_transport(bzr_transport):
47     """Obtain corresponding SvnRaTransport for a stock Bazaar transport."""
48     if isinstance(bzr_transport, SvnRaTransport):
49         return bzr_transport
50
51     return SvnRaTransport(bzr_transport.base)
52
53
54 def _url_unescape_uri(url):
55     (scheme, netloc, path, query, fragment) = urlparse.urlsplit(url)
56     path = urllib.unquote(path)
57     return urlparse.urlunsplit((scheme, netloc, path, query, fragment))
58
59
60 def bzr_to_svn_url(url):
61     """Convert a Bazaar URL to a URL understood by Subversion.
62
63     This will possibly remove the svn+ prefix.
64     """
65     if (url.startswith("svn+http://") or 
66         url.startswith("svn+file://") or
67         url.startswith("svn+https://")):
68         url = url[len("svn+"):] # Skip svn+
69
70     if url.startswith("http"):
71         # Without this, URLs with + in them break
72         url = _url_unescape_uri(url)
73
74     # The SVN libraries don't like trailing slashes...
75     url = url.rstrip('/')
76
77     return url
78
79
80 def Connection(url):
81     try:
82         mutter('opening SVN RA connection to %r' % url)
83         ret = ra.RemoteAccess(url.encode('utf8'), 
84                 auth=create_auth_baton(url))
85         # FIXME: Callbacks
86     except SubversionException, (_, num):
87         if num in (ERR_RA_SVN_REPOS_NOT_FOUND,):
88             raise NoSvnRepositoryPresent(url=url)
89         if num == ERR_BAD_URL:
90             raise InvalidURL(url)
91         raise
92
93     from bzrlib.plugins.svn import lazy_check_versions
94     lazy_check_versions()
95
96     return ret
97
98
99 class ConnectionPool(object):
100     """Collection of connections to a Subversion repository."""
101     def __init__(self):
102         self.connections = set()
103
104     def get(self, url):
105         # Check if there is an existing connection we can use
106         for c in self.connections:
107             assert not c.busy, "busy connection in pool"
108             if c.url == url:
109                 self.connections.remove(c)
110                 return c
111         # Nothing available? Just pick an existing one and reparent:
112         if len(self.connections) == 0:
113             return RemoteAccess(url)
114         c = self.connections.pop()
115         try:
116             c.reparent(url)
117             return c
118         except NotImplementedError:
119             self.connections.add(c)
120             return RemoteAccess(url)
121         except:
122             self.connections.add(c)
123             raise
124
125     def add(self, connection):
126         assert not connection.busy, "adding busy connection in pool"
127         self.connections.add(connection)
128     
129
130 class SvnRaTransport(Transport):
131     """Fake transport for Subversion-related namespaces.
132     
133     This implements just as much of Transport as is necessary 
134     to fool Bazaar. """
135     @convert_svn_error
136     def __init__(self, url="", _backing_url=None, pool=None):
137         bzr_url = url
138         self.svn_url = bzr_to_svn_url(url)
139         # _backing_url is an evil hack so the root directory of a repository 
140         # can be accessed on some HTTP repositories. 
141         if _backing_url is None:
142             _backing_url = self.svn_url
143         self._backing_url = _backing_url.rstrip("/")
144         Transport.__init__(self, bzr_url)
145
146         if pool is None:
147             self.connections = ConnectionPool()
148
149             # Make sure that the URL is valid by connecting to it.
150             self.connections.add(self.connections.get(self._backing_url))
151         else:
152             self.connections = pool
153
154         from bzrlib.plugins.svn import lazy_check_versions
155         lazy_check_versions()
156
157     def get_connection(self):
158         return self.connections.get(self._backing_url)
159
160     def add_connection(self, conn):
161         self.connections.add(conn)
162
163     def has(self, relpath):
164         """See Transport.has()."""
165         # TODO: Raise TransportNotPossible here instead and 
166         # catch it in bzrdir.py
167         return False
168
169     def get(self, relpath):
170         """See Transport.get()."""
171         # TODO: Raise TransportNotPossible here instead and 
172         # catch it in bzrdir.py
173         raise NoSuchFile(path=relpath)
174
175     def stat(self, relpath):
176         """See Transport.stat()."""
177         raise TransportNotPossible('stat not supported on Subversion')
178
179     def get_uuid(self):
180         conn = self.get_connection()
181         self.mutter('svn get-uuid')
182         try:
183             return conn.get_uuid()
184         finally:
185             self.add_connection(conn)
186
187     def get_repos_root(self):
188         root = self.get_svn_repos_root()
189         if (self.base.startswith("svn+http:") or 
190             self.base.startswith("svn+https:")):
191             return "svn+%s" % root
192         return root
193
194     def get_svn_repos_root(self):
195         conn = self.get_connection()
196         self.mutter('svn get-repos-root')
197         try:
198             return conn.get_repos_root()
199         finally:
200             self.add_connection(conn)
201
202     def get_latest_revnum(self):
203         conn = self.get_connection()
204         self.mutter('svn get-latest-revnum')
205         try:
206             return conn.get_latest_revnum()
207         finally:
208             self.add_connection(conn)
209
210     def do_switch(self, switch_rev, recurse, switch_url, editor):
211         conn = self._open_real_transport()
212         self.mutter('svn do-switch -r%d %s' % (switch_rev, switch_url))
213         return conn.do_switch(switch_rev, "", recurse, switch_url, editor)
214
215     def iter_log(self, paths, from_revnum, to_revnum, limit, discover_changed_paths, 
216                  strict_node_history, revprops):
217         assert paths is None or isinstance(paths, list)
218         assert paths is None or all([isinstance(x, str) for x in paths])
219         assert isinstance(from_revnum, int) and isinstance(to_revnum, int)
220         assert isinstance(limit, int)
221         from threading import Thread, Semaphore
222
223         class logfetcher(Thread):
224             def __init__(self, transport, *args, **kwargs):
225                 Thread.__init__(self)
226                 self.setDaemon(True)
227                 self.transport = transport
228                 self.args = args
229                 self.kwargs = kwargs
230                 self.pending = []
231                 self.conn = None
232                 self.semaphore = Semaphore(0)
233
234             def next(self):
235                 self.semaphore.acquire()
236                 ret = self.pending.pop(0)
237                 if ret is None:
238                     self.transport.add_connection(self.conn)
239                 elif isinstance(ret, Exception):
240                     self.transport.add_connection(self.conn)
241                     raise ret
242                 return ret
243
244             def run(self):
245                 assert self.conn is None, "already running"
246                 def rcvr(*args):
247                     self.pending.append(args)
248                     self.semaphore.release()
249                 self.conn = self.transport.get_connection()
250                 try:
251                     self.conn.get_log(callback=rcvr, *self.args, **self.kwargs)
252                     self.pending.append(None)
253                 except Exception, e:
254                     self.pending.append(e)
255                 self.semaphore.release()
256
257         if paths is None:
258             newpaths = None
259         else:
260             newpaths = [self._request_path(path) for path in paths]
261         
262         fetcher = logfetcher(self, paths=newpaths, start=from_revnum, end=to_revnum, limit=limit, discover_changed_paths=discover_changed_paths, strict_node_history=strict_node_history, revprops=revprops)
263         fetcher.start()
264         return iter(fetcher.next, None)
265
266     def get_log(self, rcvr, paths, from_revnum, to_revnum, limit, discover_changed_paths, 
267                 strict_node_history, revprops):
268         assert paths is None or isinstance(paths, list), "Invalid paths"
269         assert paths is None or all([isinstance(x, str) for x in paths])
270
271         self.mutter('svn log -r%d:%d %r' % (from_revnum, to_revnum, paths))
272
273         if paths is None:
274             newpaths = None
275         else:
276             newpaths = [self._request_path(path) for path in paths]
277
278         conn = self.get_connection()
279         try:
280             return conn.get_log(rcvr, newpaths, 
281                     from_revnum, to_revnum,
282                     limit, discover_changed_paths, strict_node_history, 
283                     revprops)
284         finally:
285             self.add_connection(conn)
286
287     def _open_real_transport(self):
288         if self._backing_url != self.svn_url:
289             return self.connections.get(self.svn_url)
290         return self.get_connection()
291
292     def change_rev_prop(self, revnum, name, value):
293         conn = self.get_connection()
294         self.mutter('svn change-revprop -r%d %s=%s' % (revnum, name, value))
295         try:
296             return conn.change_rev_prop(revnum, name, value)
297         finally:
298             self.add_connection(conn)
299
300     def get_dir(self, path, revnum, kind=False):
301         path = self._request_path(path)
302         conn = self.get_connection()
303         self.mutter('svn get-dir -r%d %s' % (revnum, path))
304         try:
305             return conn.get_dir(path, revnum, kind)
306         finally:
307             self.add_connection(conn)
308
309     def mutter(self, text, *args):
310         if 'transport' in debug.debug_flags:
311             mutter(text, *args)
312
313     def _request_path(self, relpath):
314         if self._backing_url == self.svn_url:
315             return relpath.strip("/")
316         newsvnurl = urlutils.join(self.svn_url, relpath)
317         if newsvnurl == self._backing_url:
318             return ""
319         newrelpath = urlutils.relative_url(self._backing_url+"/", newsvnurl+"/").strip("/")
320         self.mutter('request path %r -> %r', relpath, newrelpath)
321         return newrelpath
322
323     def list_dir(self, relpath):
324         assert len(relpath) == 0 or relpath[0] != "/"
325         if relpath == ".":
326             relpath = ""
327         try:
328             (dirents, _, _) = self.get_dir(relpath, self.get_latest_revnum())
329         except SubversionException, (msg, num):
330             if num == ERR_FS_NOT_DIRECTORY:
331                 raise NoSuchFile(relpath)
332             raise
333         return dirents.keys()
334
335     def check_path(self, path, revnum):
336         path = self._request_path(path)
337         conn = self.get_connection()
338         self.mutter('svn check-path -r%d %s' % (revnum, path))
339         try:
340             return conn.check_path(path, revnum)
341         finally:
342             self.add_connection(conn)
343
344     def mkdir(self, relpath, message="Creating directory"):
345         conn = self.get_connection()
346         self.mutter('svn mkdir %s' % (relpath,))
347         try:
348             ce = conn.get_commit_editor({"svn:log": message})
349             node = ce.open_root(-1)
350             batons = relpath.split("/")
351             toclose = [node]
352             for i in range(len(batons)):
353                 node = node.open_directory("/".join(batons[:i]), -1)
354                 toclose.append(node)
355             toclose.append(node.add_directory(relpath, None, -1))
356             for c in reversed(toclose):
357                 c.close()
358             ce.close()
359         finally:
360             self.add_connection(conn)
361
362     def replay(self, revision, low_water_mark, send_deltas, editor):
363         conn = self._open_real_transport()
364         self.mutter('svn replay -r%d:%d' % (low_water_mark,revision))
365         try:
366             return conn.replay(revision, low_water_mark, 
367                                              send_deltas, editor)
368         finally:
369             self.add_connection(conn)
370
371     def do_update(self, revnum, recurse, editor):
372         conn = self._open_real_transport()
373         self.mutter('svn do-update -r%d' % (revnum,))
374         return conn.do_update(revnum, "", recurse, editor)
375
376     def has_capability(self, cap):
377         conn = self.get_connection()
378         self.mutter('svn has-capability %s' % (cap,))
379         try:
380             return conn.has_capability(cap)
381         finally:
382             self.add_connection(conn)
383
384     def revprop_list(self, revnum):
385         conn = self.get_connection()
386         self.mutter('svn revprop-list -r%d' % (revnum,))
387         try:
388             return conn.rev_proplist(revnum)
389         finally:
390             self.add_connection(conn)
391
392     def get_commit_editor(self, revprops, done_cb=None, 
393                           lock_token=None, keep_locks=False):
394         conn = self._open_real_transport()
395         self.mutter('svn get-commit-editor %r' % (revprops,))
396         return conn.get_commit_editor(revprops, done_cb, lock_token, keep_locks)
397
398     def listable(self):
399         """See Transport.listable().
400         """
401         return True
402
403     # There is no real way to do locking directly on the transport 
404     # nor is there a need to as the remote server will take care of 
405     # locking
406     class PhonyLock(object):
407         def unlock(self):
408             pass
409
410     def lock_read(self, relpath):
411         """See Transport.lock_read()."""
412         return self.PhonyLock()
413
414     def lock_write(self, path_revs, comment=None, steal_lock=False):
415         return self.PhonyLock() # FIXME
416
417     def _is_http_transport(self):
418         return (self.svn_url.startswith("http://") or 
419                 self.svn_url.startswith("https://"))
420
421     def clone_root(self):
422         if self._is_http_transport():
423             return SvnRaTransport(self.get_repos_root(), 
424                                   bzr_to_svn_url(self.base),
425                                   pool=self.connections)
426         return SvnRaTransport(self.get_repos_root(),
427                               pool=self.connections)
428
429     def clone(self, offset=None):
430         """See Transport.clone()."""
431         if offset is None:
432             return SvnRaTransport(self.base, pool=self.connections)
433
434         return SvnRaTransport(urlutils.join(self.base, offset), pool=self.connections)
435
436     def local_abspath(self, relpath):
437         """See Transport.local_abspath()."""
438         absurl = self.abspath(relpath)
439         if self.base.startswith("file:///"):
440             return urlutils.local_path_from_url(absurl)
441         raise NotLocalUrl(absurl)
442
443     def abspath(self, relpath):
444         """See Transport.abspath()."""
445         return urlutils.join(self.base, relpath)