1 # Copyright (C) 2006 Jelmer Vernooij <jelmer@samba.org>
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.
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.
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."""
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
24 from svn.core import SubversionException, Pool
29 from bzrlib.plugins.svn import properties
30 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
34 svn_config = svn.core.svn_config_get_config(None)
36 def get_client_string():
37 """Return a string that can be send as part of the User Agent string."""
38 return "bzr%s+bzr-svn%s" % (bzrlib.__version__, bzrlib.plugins.svn.__version__)
41 def create_svn_client(url):
42 from auth import create_auth_baton
43 client = svn.client.create_context()
44 client.auth_baton = create_auth_baton(url)
45 client.config = svn_config
49 # Don't run any tests on SvnTransport as it is not intended to be
50 # a full implementation of Transport
51 def get_test_permutations():
55 def get_svn_ra_transport(bzr_transport):
56 """Obtain corresponding SvnRaTransport for a stock Bazaar transport."""
57 if isinstance(bzr_transport, SvnRaTransport):
60 return SvnRaTransport(bzr_transport.base)
63 def _url_unescape_uri(url):
64 (scheme, netloc, path, query, fragment) = urlparse.urlsplit(url)
65 path = urllib.unquote(path)
66 return urlparse.urlunsplit((scheme, netloc, path, query, fragment))
69 def bzr_to_svn_url(url):
70 """Convert a Bazaar URL to a URL understood by Subversion.
72 This will possibly remove the svn+ prefix.
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+
79 if url.startswith("http"):
80 # Without this, URLs with + in them break
81 url = _url_unescape_uri(url)
83 # The SVN libraries don't like trailing slashes...
89 def needs_busy(unbound):
90 """Decorator that marks a connection as busy before running a methd on it.
92 def convert(self, *args, **kwargs):
95 return unbound(self, *args, **kwargs)
99 convert.__doc__ = unbound.__doc__
100 convert.__name__ = unbound.__name__
104 class Editor(object):
105 """Simple object wrapper around the Subversion delta editor interface."""
106 def __init__(self, connection, (editor, editor_baton)):
108 self.editor_baton = editor_baton
109 self.recent_baton = []
110 self._connection = connection
113 def open_root(self, base_revnum):
114 assert self.recent_baton == [], "root already opened"
115 baton = svn.delta.editor_invoke_open_root(self.editor,
116 self.editor_baton, base_revnum)
117 self.recent_baton.append(baton)
121 def close_directory(self, baton, *args, **kwargs):
122 assert self.recent_baton.pop() == baton, \
123 "only most recently opened baton can be closed"
124 svn.delta.editor_invoke_close_directory(self.editor, baton, *args, **kwargs)
128 assert self.recent_baton == []
129 svn.delta.editor_invoke_close_edit(self.editor, self.editor_baton)
130 self._connection._unmark_busy()
133 def apply_textdelta(self, baton, *args, **kwargs):
134 assert self.recent_baton[-1] == baton
135 return svn.delta.editor_invoke_apply_textdelta(self.editor, baton,
139 def change_dir_prop(self, baton, name, value, pool=None):
140 assert self.recent_baton[-1] == baton
141 return svn.delta.editor_invoke_change_dir_prop(self.editor, baton,
145 def delete_entry(self, *args, **kwargs):
146 return svn.delta.editor_invoke_delete_entry(self.editor, *args, **kwargs)
149 def add_file(self, path, parent_baton, *args, **kwargs):
150 assert self.recent_baton[-1] == parent_baton
151 baton = svn.delta.editor_invoke_add_file(self.editor, path,
152 parent_baton, *args, **kwargs)
153 self.recent_baton.append(baton)
157 def open_file(self, path, parent_baton, *args, **kwargs):
158 assert self.recent_baton[-1] == parent_baton
159 baton = svn.delta.editor_invoke_open_file(self.editor, path,
160 parent_baton, *args, **kwargs)
161 self.recent_baton.append(baton)
165 def change_file_prop(self, baton, name, value, pool=None):
166 assert self.recent_baton[-1] == baton
167 svn.delta.editor_invoke_change_file_prop(self.editor, baton, name,
171 def close_file(self, baton, *args, **kwargs):
172 assert self.recent_baton.pop() == baton
173 svn.delta.editor_invoke_close_file(self.editor, baton, *args, **kwargs)
176 def add_directory(self, path, parent_baton, *args, **kwargs):
177 assert self.recent_baton[-1] == parent_baton
178 baton = svn.delta.editor_invoke_add_directory(self.editor, path,
179 parent_baton, *args, **kwargs)
180 self.recent_baton.append(baton)
184 def open_directory(self, path, parent_baton, *args, **kwargs):
185 assert self.recent_baton[-1] == parent_baton
186 baton = svn.delta.editor_invoke_open_directory(self.editor, path,
187 parent_baton, *args, **kwargs)
188 self.recent_baton.append(baton)
192 class Connection(object):
193 """An single connection to a Subversion repository. This usually can
194 only do one operation at a time."""
195 def __init__(self, url):
198 self._client = create_svn_client(url)
199 self._unbusy_handler = None
201 self.mutter('opening SVN RA connection to %r' % url)
202 self._ra = svn.client.open_ra_session(url.encode('utf8'),
204 except SubversionException, (_, num):
205 if num == ERR_RA_SVN_REPOS_NOT_FOUND:
206 raise NoSvnRepositoryPresent(url=url)
207 if num == ERR_BAD_URL:
208 raise InvalidURL(url)
212 class Reporter(object):
213 def __init__(self, connection, (reporter, report_baton)):
214 self._reporter = reporter
215 self._baton = report_baton
216 self._connection = connection
219 def set_path(self, path, revnum, start_empty, lock_token, pool=None):
220 svn.ra.reporter2_invoke_set_path(self._reporter, self._baton,
221 path, revnum, start_empty, lock_token, pool)
224 def delete_path(self, path, pool=None):
225 svn.ra.reporter2_invoke_delete_path(self._reporter, self._baton,
229 def link_path(self, path, url, revision, start_empty, lock_token,
231 svn.ra.reporter2_invoke_link_path(self._reporter, self._baton,
232 path, url, revision, start_empty, lock_token,
236 def finish_report(self, pool=None):
238 svn.ra.reporter2_invoke_finish_report(self._reporter,
241 self._connection._unmark_busy()
244 def abort_report(self, pool=None):
246 svn.ra.reporter2_invoke_abort_report(self._reporter,
249 self._connection._unmark_busy()
254 def _mark_busy(self):
255 assert not self._busy, "already busy"
258 def set_unbusy_handler(self, handler):
259 self._unbusy_handler = handler
261 def _unmark_busy(self):
262 assert self._busy, "not busy"
264 if self._unbusy_handler is not None:
265 self._unbusy_handler()
266 self._unbusy_handler = None
268 def mutter(self, text):
269 if 'transport' in debug.debug_flags:
275 self.mutter('svn get-uuid')
276 return svn.ra.get_uuid(self._ra)
280 def get_repos_root(self):
281 if self._root is None:
282 self.mutter("svn get-repos-root")
283 self._root = svn.ra.get_repos_root(self._ra)
288 def get_latest_revnum(self):
289 self.mutter("svn get-latest-revnum")
290 return svn.ra.get_latest_revnum(self._ra)
292 def _make_editor(self, editor, pool=None):
293 edit, edit_baton = svn.delta.make_editor(editor, pool)
295 self._edit_baton = edit_baton
296 return self._edit, self._edit_baton
299 def do_switch(self, switch_rev, recurse, switch_url, editor, pool=None):
300 self.mutter('svn switch -r %d -> %r' % (switch_rev, switch_url))
302 edit, edit_baton = self._make_editor(editor, pool)
303 return self.Reporter(self, svn.ra.do_switch(self._ra, switch_rev, "",
304 recurse, switch_url, edit, edit_baton, pool))
307 def change_rev_prop(self, revnum, name, value, pool=None):
308 self.mutter('svn revprop -r%d --set %s=%s' % (revnum, name, value))
309 svn.ra.change_rev_prop(self._ra, revnum, name, value)
313 def get_lock(self, path):
314 return svn.ra.get_lock(self._ra, path)
318 def unlock(self, locks, break_lock=False):
319 def lock_cb(baton, path, do_lock, lock, ra_err, pool):
321 return svn.ra.unlock(self._ra, locks, break_lock, lock_cb)
325 def get_dir(self, path, revnum, pool=None, kind=False):
326 self.mutter("svn ls -r %d '%r'" % (revnum, path))
327 assert len(path) == 0 or path[0] != "/"
328 # ra_dav backends fail with strange errors if the path starts with a
329 # slash while other backends don't.
330 if hasattr(svn.ra, 'get_dir2'):
333 fields += svn.core.SVN_DIRENT_KIND
334 return svn.ra.get_dir2(self._ra, path, revnum, fields)
336 return svn.ra.get_dir(self._ra, path, revnum)
340 def check_path(self, path, revnum):
341 assert len(path) == 0 or path[0] != "/"
342 self.mutter("svn check_path -r%d %s" % (revnum, path))
343 return svn.ra.check_path(self._ra, path.encode('utf-8'), revnum)
347 def mkdir(self, relpath, mode=None):
348 assert len(relpath) == 0 or relpath[0] != "/"
349 path = urlutils.join(self.url, relpath)
351 svn.client.mkdir([path.encode("utf-8")], self._client)
352 except SubversionException, (msg, num):
353 if num == ERR_FS_NOT_FOUND:
354 raise NoSuchFile(path)
355 if num == ERR_FS_ALREADY_EXISTS:
356 raise FileExists(path)
360 def replay(self, revision, low_water_mark, send_deltas, editor, pool=None):
361 self.mutter('svn replay -r%r:%r' % (low_water_mark, revision))
363 edit, edit_baton = self._make_editor(editor, pool)
364 svn.ra.replay(self._ra, revision, low_water_mark, send_deltas,
365 edit, edit_baton, pool)
368 def do_update(self, revnum, recurse, editor, pool=None):
369 self.mutter('svn update -r %r' % revnum)
371 edit, edit_baton = self._make_editor(editor, pool)
372 return self.Reporter(self, svn.ra.do_update(self._ra, revnum, "",
373 recurse, edit, edit_baton, pool))
376 def has_capability(self, cap):
377 return svn.ra.has_capability(self._ra, cap)
380 def revprop_list(self, revnum, pool=None):
381 self.mutter('svn revprop-list -r %r' % revnum)
382 return svn.ra.rev_proplist(self._ra, revnum, pool)
385 def get_commit_editor(self, revprops, done_cb, lock_token, keep_locks):
388 if hasattr(svn.ra, 'get_commit_editor3'):
389 editor = svn.ra.get_commit_editor3(self._ra, revprops, done_cb,
390 lock_token, keep_locks)
391 elif revprops.keys() != [properties.PROP_REVISION_LOG]:
392 raise NotImplementedError()
394 editor = svn.ra.get_commit_editor2(self._ra,
395 revprops[properties.PROP_REVISION_LOG],
396 done_cb, lock_token, keep_locks)
398 return Editor(self, editor)
403 class SvnLock(object):
404 def __init__(self, connection, tokens):
405 self._tokens = tokens
406 self._connection = connection
409 self._connection.unlock(self.locks)
413 def lock_write(self, path_revs, comment=None, steal_lock=False):
415 def lock_cb(baton, path, do_lock, lock, ra_err, pool):
417 svn.ra.lock(self._ra, path_revs, comment, steal_lock, lock_cb)
418 return SvnLock(self, tokens)
422 def get_log(self, paths, from_revnum, to_revnum, limit,
423 discover_changed_paths, strict_node_history, revprops, rcvr,
425 # No paths starting with slash, please
426 assert paths is None or all([not p.startswith("/") for p in paths])
427 if (paths is None and
428 (svn.core.SVN_VER_MINOR < 6 or (
429 svn.core.SVN_VER_REVISION < 31470 and svn.core.SVN_VER_REVISION != 0))):
431 self.mutter('svn log %r:%r %r (limit: %r)' % (from_revnum, to_revnum, paths, limit))
432 if hasattr(svn.ra, 'get_log2'):
433 return svn.ra.get_log2(self._ra, paths,
434 from_revnum, to_revnum, limit,
435 discover_changed_paths, strict_node_history, False,
436 revprops, rcvr, pool)
438 class LogEntry(object):
439 def __init__(self, changed_paths, rev, author, date, message):
440 self.changed_paths = changed_paths
442 if properties.PROP_REVISION_AUTHOR in revprops:
443 self.revprops[properties.PROP_REVISION_AUTHOR] = author
444 if properties.PROP_REVISION_LOG in revprops:
445 self.revprops[properties.PROP_REVISION_LOG] = message
446 if properties.PROP_REVISION_DATE in revprops:
447 self.revprops[properties.PROP_REVISION_DATE] = date
448 # FIXME: Check other revprops
449 # FIXME: Handle revprops is None
451 self.has_children = None
453 def rcvr_convert(orig_paths, rev, author, date, message, pool):
454 rcvr(LogEntry(orig_paths, rev, author, date, message), pool)
456 return svn.ra.get_log(self._ra, paths,
457 from_revnum, to_revnum, limit, discover_changed_paths,
458 strict_node_history, rcvr_convert, pool)
462 def reparent(self, url):
465 if hasattr(svn.ra, 'reparent'):
466 self.mutter('svn reparent %r' % url)
467 svn.ra.reparent(self._ra, url)
470 raise NotImplementedError(self.reparent)
473 class ConnectionPool(object):
474 """Collection of connections to a Subversion repository."""
476 self.connections = set()
479 # Check if there is an existing connection we can use
480 for c in self.connections:
481 assert not c.is_busy(), "busy connection in pool"
483 self.connections.remove(c)
485 # Nothing available? Just pick an existing one and reparent:
486 if len(self.connections) == 0:
487 return Connection(url)
488 c = self.connections.pop()
492 except NotImplementedError:
493 self.connections.add(c)
494 return Connection(url)
496 self.connections.add(c)
499 def add(self, connection):
500 assert not connection.is_busy(), "adding busy connection in pool"
501 self.connections.add(connection)
504 class SvnRaTransport(Transport):
505 """Fake transport for Subversion-related namespaces.
507 This implements just as much of Transport as is necessary
510 def __init__(self, url="", _backing_url=None, pool=None):
513 self.svn_url = bzr_to_svn_url(url)
514 # _backing_url is an evil hack so the root directory of a repository
515 # can be accessed on some HTTP repositories.
516 if _backing_url is None:
517 _backing_url = self.svn_url
518 self._backing_url = _backing_url.rstrip("/")
519 Transport.__init__(self, bzr_url)
522 self.connections = ConnectionPool()
524 # Make sure that the URL is valid by connecting to it.
525 self.connections.add(self.connections.get(self._backing_url))
527 self.connections = pool
529 from bzrlib.plugins.svn import lazy_check_versions
530 lazy_check_versions()
532 def get_connection(self):
533 return self.connections.get(self._backing_url)
535 def add_connection(self, conn):
536 self.connections.add(conn)
538 def has(self, relpath):
539 """See Transport.has()."""
540 # TODO: Raise TransportNotPossible here instead and
541 # catch it in bzrdir.py
544 def get(self, relpath):
545 """See Transport.get()."""
546 # TODO: Raise TransportNotPossible here instead and
547 # catch it in bzrdir.py
548 raise NoSuchFile(path=relpath)
550 def stat(self, relpath):
551 """See Transport.stat()."""
552 raise TransportNotPossible('stat not supported on Subversion')
555 conn = self.get_connection()
557 return conn.get_uuid()
559 self.add_connection(conn)
561 def get_repos_root(self):
562 root = self.get_svn_repos_root()
563 if (self.base.startswith("svn+http:") or
564 self.base.startswith("svn+https:")):
565 return "svn+%s" % root
568 def get_svn_repos_root(self):
569 conn = self.get_connection()
571 return conn.get_repos_root()
573 self.add_connection(conn)
575 def get_latest_revnum(self):
576 conn = self.get_connection()
578 return conn.get_latest_revnum()
580 self.add_connection(conn)
582 def do_switch(self, switch_rev, recurse, switch_url, editor, pool=None):
583 conn = self._open_real_transport()
584 conn.set_unbusy_handler(lambda: self.add_connection(conn))
585 return conn.do_switch(switch_rev, recurse, switch_url, editor, pool)
587 def iter_log(self, paths, from_revnum, to_revnum, limit, discover_changed_paths,
588 strict_node_history, revprops):
590 assert paths is None or isinstance(paths, list)
591 assert paths is None or all([isinstance(x, str) for x in paths])
592 assert isinstance(from_revnum, int) and isinstance(to_revnum, int)
593 assert isinstance(limit, int)
594 from threading import Thread, Semaphore
596 class logfetcher(Thread):
597 def __init__(self, transport, **kwargs):
598 Thread.__init__(self)
600 self.transport = transport
604 self.semaphore = Semaphore(0)
607 self.semaphore.acquire()
608 ret = self.pending.pop(0)
610 self.transport.add_connection(self.conn)
611 elif isinstance(ret, Exception):
612 self.transport.add_connection(self.conn)
617 assert self.conn is None, "already running"
618 def rcvr(log_entry, pool):
619 self.pending.append((log_entry.changed_paths, log_entry.revision, log_entry.revprops))
620 self.semaphore.release()
621 self.conn = self.transport.get_connection()
623 self.conn.get_log(rcvr=rcvr, **self.kwargs)
624 self.pending.append(None)
626 self.pending.append(e)
627 self.semaphore.release()
632 newpaths = [self._request_path(path) for path in paths]
634 fetcher = logfetcher(self, paths=newpaths, from_revnum=from_revnum, to_revnum=to_revnum, limit=limit, discover_changed_paths=discover_changed_paths, strict_node_history=strict_node_history, revprops=revprops)
636 return iter(fetcher.next, None)
638 def get_log(self, paths, from_revnum, to_revnum, limit, discover_changed_paths,
639 strict_node_history, revprops, rcvr, pool=None):
640 assert paths is None or isinstance(paths, list), "Invalid paths"
641 assert paths is None or all([isinstance(x, str) for x in paths])
646 newpaths = [self._request_path(path) for path in paths]
648 conn = self.get_connection()
650 return conn.get_log(newpaths,
651 from_revnum, to_revnum,
652 limit, discover_changed_paths, strict_node_history,
653 revprops, rcvr, pool)
655 self.add_connection(conn)
657 def _open_real_transport(self):
658 if self._backing_url != self.svn_url:
659 return self.connections.get(self.svn_url)
660 return self.get_connection()
662 def change_rev_prop(self, revnum, name, value, pool=None):
663 conn = self.get_connection()
665 return conn.change_rev_prop(revnum, name, value, pool)
667 self.add_connection(conn)
669 def get_dir(self, path, revnum, pool=None, kind=False):
670 path = self._request_path(path)
671 conn = self.get_connection()
673 return conn.get_dir(path, revnum, pool, kind)
675 self.add_connection(conn)
677 def mutter(self, text):
678 if 'transport' in debug.debug_flags:
681 def _request_path(self, relpath):
682 if self._backing_url == self.svn_url:
683 return relpath.strip("/")
684 newsvnurl = urlutils.join(self.svn_url, relpath)
685 if newsvnurl == self._backing_url:
687 newrelpath = urlutils.relative_url(self._backing_url+"/", newsvnurl+"/").strip("/")
688 self.mutter('request path %r -> %r' % (relpath, newrelpath))
691 def list_dir(self, relpath):
692 assert len(relpath) == 0 or relpath[0] != "/"
696 (dirents, _, _) = self.get_dir(relpath, self.get_latest_revnum())
697 except SubversionException, (msg, num):
698 if num == ERR_FS_NOT_DIRECTORY:
699 raise NoSuchFile(relpath)
701 return dirents.keys()
703 def check_path(self, path, revnum):
704 path = self._request_path(path)
705 conn = self.get_connection()
707 return conn.check_path(path, revnum)
709 self.add_connection(conn)
711 def mkdir(self, relpath, mode=None):
712 conn = self.get_connection()
714 return conn.mkdir(relpath, mode)
716 self.add_connection(conn)
718 def replay(self, revision, low_water_mark, send_deltas, editor, pool=None):
719 conn = self._open_real_transport()
721 return conn.replay(revision, low_water_mark,
722 send_deltas, editor, pool)
724 self.add_connection(conn)
726 def do_update(self, revnum, recurse, editor, pool=None):
727 conn = self._open_real_transport()
728 conn.set_unbusy_handler(lambda: self.add_connection(conn))
729 return conn.do_update(revnum, recurse, editor, pool)
731 def has_capability(self, cap):
732 conn = self.get_connection()
734 return conn.has_capability(cap)
736 self.add_connection(conn)
738 def revprop_list(self, revnum, pool=None):
739 conn = self.get_connection()
741 return conn.revprop_list(revnum, pool)
743 self.add_connection(conn)
745 def get_commit_editor(self, revprops, done_cb, lock_token, keep_locks):
746 conn = self._open_real_transport()
747 conn.set_unbusy_handler(lambda: self.add_connection(conn))
748 return conn.get_commit_editor(revprops, done_cb,
749 lock_token, keep_locks)
752 """See Transport.listable().
756 # There is no real way to do locking directly on the transport
757 # nor is there a need to as the remote server will take care of
759 class PhonyLock(object):
763 def lock_read(self, relpath):
764 """See Transport.lock_read()."""
765 return self.PhonyLock()
767 def lock_write(self, path_revs, comment=None, steal_lock=False):
768 return self.PhonyLock() # FIXME
770 def _is_http_transport(self):
771 return (self.svn_url.startswith("http://") or
772 self.svn_url.startswith("https://"))
774 def clone_root(self):
775 if self._is_http_transport():
776 return SvnRaTransport(self.get_repos_root(),
777 bzr_to_svn_url(self.base),
778 pool=self.connections)
779 return SvnRaTransport(self.get_repos_root(),
780 pool=self.connections)
782 def clone(self, offset=None):
783 """See Transport.clone()."""
785 return SvnRaTransport(self.base, pool=self.connections)
787 return SvnRaTransport(urlutils.join(self.base, offset), pool=self.connections)
789 def local_abspath(self, relpath):
790 """See Transport.local_abspath()."""
791 absurl = self.abspath(relpath)
792 if self.base.startswith("file:///"):
793 return urlutils.local_path_from_url(absurl)
794 raise NotLocalUrl(absurl)
796 def abspath(self, relpath):
797 """See Transport.abspath()."""
798 return urlutils.join(self.base, relpath)