1 # Copyright (C) 2006-2008 Jelmer Vernooij <jelmer@jelmer.uk>
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU Lesser General Public License as published by
5 # the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details.
13 # You should have received a copy of the GNU Lesser General Public License
14 # along with this program; if not, write to the Free Software
15 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
16 """Python bindings for Subversion."""
18 __author__ = "Jelmer Vernooij <jelmer@jelmer.uk>"
21 from SocketServer import StreamRequestHandler, TCPServer
23 from socketserver import StreamRequestHandler, TCPServer
28 from errno import EPIPE
32 import urllib.parse as urlparse
34 from subvertpy import (
35 ERR_RA_SVN_UNKNOWN_CMD,
36 ERR_UNSUPPORTED_FEATURE,
44 from subvertpy.delta import (
49 from subvertpy.marshall import (
55 from subvertpy.ra import (
63 from subvertpy.server import (
68 class SSHSubprocess(object):
69 """A socket-like object that talks to an ssh subprocess via pipes."""
73 def __init__(self, proc):
77 return os.write(self.proc.stdin.fileno(), data)
79 def recv(self, count):
80 return os.read(self.proc.stdout.fileno(), count)
83 self.proc.stdin.close()
84 self.proc.stdout.close()
87 def get_filelike_channels(self):
88 return (self.proc.stdout, self.proc.stdin)
91 class SSHVendor(object):
93 def connect_ssh(self, username, password, host, port, command):
96 args.extend(['-p', str(port)])
97 if username is not None:
98 host = "%s@%s" % (username, host)
100 proc = subprocess.Popen(args + command,
101 stdin=subprocess.PIPE,
102 stdout=subprocess.PIPE)
103 return SSHSubprocess(proc)
106 # Can be overridden by users
107 get_ssh_vendor = SSHVendor
110 class SVNConnection(object):
112 def __init__(self, recv_fn, send_fn):
114 self.recv_fn = recv_fn
115 self.send_fn = send_fn
120 (self.inbuffer, ret) = unmarshall(self.inbuffer)
123 newdata = self.recv_fn(1)
125 # self.mutter("IN: %r" % newdata)
126 self.inbuffer += newdata
128 def send_msg(self, data):
129 marshalled_data = marshall(data)
130 # self.mutter("OUT: %r" % marshalled_data)
131 self.send_fn(marshalled_data)
133 def send_success(self, *contents):
134 self.send_msg([literal("success"), list(contents)])
140 def feed_editor(conn, editor):
146 command, args = conn.recv_msg()
147 if command == "target-rev":
148 editor.set_target_revision(args[0])
149 elif command == "open-root":
150 if len(args[0]) == 0:
151 token = editor.open_root()
153 token = editor.open_root(args[0][0])
154 tokens[args[1]] = token
155 elif command == "delete-entry":
156 tokens[args[2]].delete_entry(args[0], args[1])
157 elif command == "add-dir":
158 if len(args[3]) == 0:
159 token = tokens[args[1]].add_directory(args[0])
161 token = tokens[args[1]].add_directory(
162 args[0], args[3][0], args[4][0])
163 tokens[args[2]] = token
164 elif command == "open-dir":
165 tokens[args[2]] = tokens[args[1]].open_directory(args[0], args[3])
166 elif command == "change-dir-prop":
167 if len(args[2]) == 0:
168 tokens[args[0]].change_prop(args[1], None)
170 tokens[args[0]].change_prop(args[1], args[2][0])
171 elif command == "close-dir":
172 tokens[args[0]].close()
173 elif command == "absent-dir":
174 tokens[args[1]].absent(args[0])
175 elif command == "add-file":
176 if len(args[3]) == 0:
177 token = tokens[args[1]].add_file(args[0])
179 token = tokens[args[1]].add_file(
180 args[0], args[3][0], args[4][0])
181 tokens[args[2]] = token
182 elif command == "open-file":
183 tokens[args[2]] = tokens[args[1]].open_file(args[0], args[3])
184 elif command == "apply-textdelta":
185 if len(args[1]) == 0:
186 txdelta_handler[args[0]] = tokens[args[0]].apply_textdelta(
189 txdelta_handler[args[0]] = tokens[args[0]].apply_textdelta(
192 elif command == "textdelta-chunk":
193 diff[args[0]] += args[1]
194 elif command == "textdelta-end":
195 for w in unpack_svndiff0(diff[args[0]]):
196 txdelta_handler[args[0]](w)
197 txdelta_handler[args[0]](None)
198 elif command == "change-file-prop":
199 if len(args[2]) == 0:
200 tokens[args[0]].change_prop(args[1], None)
202 tokens[args[0]].change_prop(args[1], args[2][0])
203 elif command == "close-file":
204 if len(args[1]) == 0:
205 tokens[args[0]].close()
207 tokens[args[0]].close(args[1][0])
208 elif command == "close-edit":
211 elif command == "abort-edit":
219 class Reporter(object):
221 __slots__ = ('conn', 'editor')
223 def __init__(self, conn, editor):
227 def set_path(self, path, rev, start_empty=False, lock_token=None,
229 args = [path, rev, start_empty]
230 if lock_token is not None:
231 args.append([lock_token])
234 if depth is not None:
237 self.conn.send_msg([literal("set-path"), args])
239 def delete_path(self, path):
240 self.conn.send_msg([literal("delete-path"), [path]])
242 def link_path(self, path, url, rev, start_empty=False, lock_token=None,
244 args = [path, url, rev, start_empty]
245 if lock_token is not None:
246 args.append([lock_token])
249 if depth is not None:
252 self.conn.send_msg([literal("link-path"), args])
255 self.conn.send_msg([literal("finish-report"), []])
257 feed_editor(self.conn, self.editor)
258 self.conn.busy = False
261 self.conn.send_msg([literal("abort-report"), []])
262 self.conn.busy = False
265 class Editor(object):
269 def __init__(self, conn):
272 def set_target_revision(self, revnum):
273 self.conn.send_msg([literal("target-rev"), [revnum]])
275 def open_root(self, base_revision=None):
276 id = generate_random_id()
277 if base_revision is None:
280 baserev = [base_revision]
281 self.conn.send_msg([literal("open-root"), [baserev, id]])
282 self.conn._open_ids = []
283 return DirectoryEditor(self.conn, id)
286 self.conn.send_msg([literal("close-edit"), []])
289 self.conn.send_msg([literal("abort-edit"), []])
292 class DirectoryEditor(object):
294 __slots__ = ('conn', 'id')
296 def __init__(self, conn, id):
299 self.conn._open_ids.append(id)
301 def add_file(self, path, copyfrom_path=None, copyfrom_rev=-1):
303 child = generate_random_id()
304 if copyfrom_path is not None:
305 copyfrom_data = [copyfrom_path, copyfrom_rev]
308 self.conn.send_msg([literal("add-file"),
309 [path, self.id, child, copyfrom_data]])
310 return FileEditor(self.conn, child)
312 def open_file(self, path, base_revnum):
314 child = generate_random_id()
315 self.conn.send_msg([literal("open-file"),
316 [path, self.id, child, base_revnum]])
317 return FileEditor(self.conn, child)
319 def delete_entry(self, path, base_revnum):
321 self.conn.send_msg([literal("delete-entry"),
322 [path, base_revnum, self.id]])
324 def add_directory(self, path, copyfrom_path=None, copyfrom_rev=-1):
326 child = generate_random_id()
327 if copyfrom_path is not None:
328 copyfrom_data = [copyfrom_path, copyfrom_rev]
331 self.conn.send_msg([literal("add-dir"),
332 [path, self.id, child, copyfrom_data]])
333 return DirectoryEditor(self.conn, child)
335 def open_directory(self, path, base_revnum):
337 child = generate_random_id()
338 self.conn.send_msg([literal("open-dir"),
339 [path, self.id, child, base_revnum]])
340 return DirectoryEditor(self.conn, child)
342 def change_prop(self, name, value):
348 self.conn.send_msg([literal("change-dir-prop"),
349 [self.id, name, value]])
351 def _is_last_open(self):
352 assert self.conn._open_ids[-1] == self.id
356 self.conn._open_ids.pop()
357 self.conn.send_msg([literal("close-dir"), [self.id]])
360 class FileEditor(object):
362 __slots__ = ('conn', 'id')
364 def __init__(self, conn, id):
367 self.conn._open_ids.append(id)
369 def _is_last_open(self):
370 assert self.conn._open_ids[-1] == self.id
372 def close(self, checksum=None):
374 self.conn._open_ids.pop()
378 checksum = [checksum]
379 self.conn.send_msg([literal("close-file"), [self.id, checksum]])
381 def apply_textdelta(self, base_checksum=None):
383 if base_checksum is None:
386 base_check = [base_checksum]
387 self.conn.send_msg([literal("apply-textdelta"),
388 [self.id, base_check]])
389 self.conn.send_msg([literal("textdelta-chunk"),
390 [self.id, SVNDIFF0_HEADER]])
392 def send_textdelta(delta):
394 self.conn.send_msg([literal("textdelta-end"), [self.id]])
396 self.conn.send_msg([literal("textdelta-chunk"),
397 [self.id, pack_svndiff0_window(delta)]])
398 return send_textdelta
400 def change_prop(self, name, value):
406 self.conn.send_msg([literal("change-file-prop"),
407 [self.id, name, value]])
410 def mark_busy(unbound):
412 def convert(self, *args, **kwargs):
415 ret = unbound(self, *args, **kwargs)
420 convert.__doc__ = unbound.__doc__
421 convert.__name__ = unbound.__name__
425 def unmarshall_dirent(d):
430 "has-props": bool(d[3]),
434 ret["created-date"] = d[5]
436 ret["last-author"] = d[6]
440 class SVNClient(SVNConnection):
442 def __init__(self, url, progress_cb=None, auth=None, config=None,
443 client_string_func=None, open_tmp_file_func=None):
445 (type, opaque) = urlparse.splittype(url)
446 assert type in ("svn", "svn+ssh")
447 (host, path) = urlparse.splithost(opaque)
448 self._progress_cb = progress_cb
450 self._config = config
451 self._client_string_func = client_string_func
452 # open_tmp_file_func is ignored, as it is not needed for svn://
454 (recv_func, send_func) = self._connect(host)
456 (recv_func, send_func) = self._connect_ssh(host)
457 super(SVNClient, self).__init__(recv_func, send_func)
458 (min_version, max_version, _, self._server_capabilities) = (
459 self._recv_greeting())
462 [literal(x) for x in CAPABILITIES
463 if x in self._server_capabilities],
465 (self._server_mechanisms, mech_arg) = self._unpack()
466 if self._server_mechanisms != []:
467 # FIXME: Support other mechanisms as well
468 self.send_msg([literal("ANONYMOUS"),
470 "anonymous@%s" % socket.gethostname())]])
474 self._server_capabilities += msg[2]
475 (self._uuid, self._root_url) = msg[0:2]
479 msg = self.recv_msg()
480 if msg[0] == "failure":
481 if isinstance(msg[1], str):
482 raise SubversionException(*msg[1])
485 if num == ERR_RA_SVN_UNKNOWN_CMD:
486 raise NotImplementedError(msg)
487 raise SubversionException(msg, num)
488 assert msg[0] == "success", "Got: %r" % msg
492 def _recv_greeting(self):
493 greeting = self._unpack()
494 assert len(greeting) == 4
499 def _connect(self, host):
500 (host, port) = urlparse.splitnport(host, SVN_PORT)
501 sockaddrs = socket.getaddrinfo(
502 host, port, socket.AF_UNSPEC,
503 socket.SOCK_STREAM, 0, 0)
505 err = RuntimeError('no addresses for %s:%s' % (host, port))
506 for (family, socktype, proto, canonname, sockaddr) in sockaddrs:
508 self._socket = socket.socket(family, socktype, proto)
509 self._socket.connect(sockaddr)
510 except socket.error as err:
511 if self._socket is not None:
516 if self._socket is None:
518 self._socket.setblocking(True)
519 return (self._socket.recv, self._socket.send)
521 def _connect_ssh(self, host):
522 (user, host) = urlparse.splituser(host)
524 (user, password) = urlparse.splitpassword(user)
527 (host, port) = urlparse.splitnport(host, 22)
528 self._tunnel = get_ssh_vendor().connect_ssh(
529 user, password, host, port, ["svnserve", "-t"])
530 return (self._tunnel.recv, self._tunnel.send)
532 def get_file_revs(self, path, start, end, file_rev_handler):
533 raise NotImplementedError(self.get_file_revs)
536 def get_locations(self, path, peg_revision, location_revisions):
537 self.send_msg([literal("get-locations"), [path, peg_revision,
538 location_revisions]])
542 msg = self.recv_msg()
549 def get_locks(self, path):
550 self.send_msg([literal("get-lock"), [path]])
552 return self._unpack()
554 def lock(self, path_revs, comment, steal_lock, lock_func):
555 raise NotImplementedError(self.lock)
557 def unlock(self, path_tokens, break_lock, lock_func):
558 raise NotImplementedError(self.unlock)
560 def mergeinfo(self, paths, revision=-1, inherit=None,
561 include_descendants=False):
562 raise NotImplementedError(self.mergeinfo)
564 def location_segments(self, path, start_revision, end_revision,
565 include_merged_revisions=False):
567 if start_revision is None or start_revision == -1:
570 args.append([start_revision])
571 if end_revision is None or end_revision == -1:
574 args.append([end_revision])
575 args.append(include_merged_revisions)
576 self.send_msg([literal("get-location-segments"), args])
579 msg = self.recv_msg()
585 def get_location_segments(self, path, start_revision, end_revision, rcvr):
586 for msg in self.location_segments(path, start_revision, end_revision):
589 def has_capability(self, capability):
590 return capability in self._server_capabilities
593 def check_path(self, path, revision=None):
595 if revision is None or revision == -1:
598 args.append([revision])
599 self.send_msg([literal("check-path"), args])
601 ret = self._unpack()[0]
602 return {"dir": NODE_DIR, "file": NODE_FILE, "unknown": NODE_UNKNOWN,
603 "none": NODE_NONE}[ret]
605 def get_lock(self, path):
606 self.send_msg([literal("get-lock"), [path]])
615 def get_dir(self, path, revision=-1, dirent_fields=0, want_props=True,
618 if revision is None or revision == -1:
621 args.append([revision])
623 args += [want_props, want_contents]
626 if dirent_fields & DIRENT_KIND:
627 fields.append(literal("kind"))
628 if dirent_fields & DIRENT_SIZE:
629 fields.append(literal("size"))
630 if dirent_fields & DIRENT_HAS_PROPS:
631 fields.append(literal("has-props"))
632 if dirent_fields & DIRENT_CREATED_REV:
633 fields.append(literal("created-rev"))
634 if dirent_fields & DIRENT_TIME:
635 fields.append(literal("time"))
636 if dirent_fields & DIRENT_LAST_AUTHOR:
637 fields.append(literal("last-author"))
640 self.send_msg([literal("get-dir"), args])
647 entry = unmarshall_dirent(d)
648 dirents[entry["name"]] = entry
650 return (dirents, fetch_rev, props)
653 def stat(self, path, revision=-1):
655 if revision is None or revision == -1:
656 args.append([revision])
660 self.send_msg([literal("stat"), args])
665 return unmarshall_dirent(ret[0])
668 def get_file(self, path, stream, revision=-1):
669 raise NotImplementedError(self.get_file)
671 def change_rev_prop(self, rev, name, value):
673 if value is not None:
675 self.send_msg([literal("change-rev-prop"), args])
679 def get_commit_editor(self, revprops, callback=None, lock_tokens=None,
681 args = [revprops[properties.PROP_REVISION_LOG]]
682 if lock_tokens is not None:
683 args.append(list(lock_tokens.items()))
686 args.append(keep_locks)
687 if len(revprops) > 1:
688 args.append(list(revprops.items()))
689 self.send_msg([literal("commit"), args])
691 raise NotImplementedError(self.get_commit_editor)
693 def rev_proplist(self, revision):
694 self.send_msg([literal("rev-proplist"), [revision]])
696 return dict(self._unpack()[0])
698 def rev_prop(self, revision, name):
699 self.send_msg([literal("rev-prop"), [revision, name]])
708 def replay(self, revision, low_water_mark, update_editor,
710 self.send_msg([literal("replay"), [revision, low_water_mark,
713 feed_editor(self, update_editor)
717 def replay_range(self, start_revision, end_revision, low_water_mark, cbs,
719 self.send_msg([literal("replay-range"), [start_revision, end_revision,
720 low_water_mark, send_deltas]])
722 for i in range(start_revision, end_revision+1):
723 msg = self.recv_msg()
724 assert msg[0] == "revprops"
725 edit = cbs[0](i, dict(msg[1]))
726 feed_editor(self, edit)
727 cbs[1](i, dict(msg[1]), edit)
730 def do_switch(self, revision_to_update_to, update_target, recurse,
731 switch_url, update_editor, depth=None):
733 if revision_to_update_to is None or revision_to_update_to == -1:
736 args.append([revision_to_update_to])
737 args.append(update_target)
739 args.append(switch_url)
740 if depth is not None:
741 args.append(literal(depth))
745 self.send_msg([literal("switch"), args])
747 return Reporter(self, update_editor)
748 except BaseException:
752 def do_update(self, revision_to_update_to, update_target, recurse,
753 update_editor, depth=None):
755 if revision_to_update_to is None or revision_to_update_to == -1:
758 args.append([revision_to_update_to])
759 args.append(update_target)
761 if depth is not None:
762 args.append(literal(depth))
766 self.send_msg([literal("update"), args])
768 return Reporter(self, update_editor)
769 except BaseException:
773 def do_diff(self, revision_to_update, diff_target, versus_url, diff_editor,
774 recurse=True, ignore_ancestry=False, text_deltas=False,
777 if revision_to_update is None or revision_to_update == -1:
780 args.append([revision_to_update])
781 args += [diff_target, recurse, ignore_ancestry, versus_url,
783 if depth is not None:
784 args.append(literal(depth))
787 self.send_msg([literal("diff"), args])
789 return Reporter(self, diff_editor)
790 except BaseException:
794 def get_repos_root(self):
795 return self._root_url
798 def get_latest_revnum(self):
799 self.send_msg([literal("get-latest-rev"), []])
801 return self._unpack()[0]
804 def get_dated_rev(self, date):
805 self.send_msg([literal("get-dated-rev"), [date]])
807 return self._unpack()[0]
810 def reparent(self, url):
811 self.send_msg([literal("reparent"), [url]])
820 def log(self, paths, start, end, limit=0, discover_changed_paths=True,
821 strict_node_history=True, include_merged_revisions=True,
824 if start is None or start == -1:
828 if end is None or end == -1:
832 args.append(discover_changed_paths)
833 args.append(strict_node_history)
835 args.append(include_merged_revisions)
837 args.append(literal("all-revprops"))
840 args.append(literal("revprops"))
841 args.append(revprops)
843 self.send_msg([literal("log"), args])
846 msg = self.recv_msg()
850 for p, action, cfd in msg[0]:
852 paths[p] = (str(action), None, -1)
854 paths[p] = (str(action), cfd[0], cfd[1])
857 has_children = msg[5]
860 if len(msg) > 6 and msg[6]:
863 revno = msg[1] # noqa: F841
864 # TODO(jelmer): Do something with revno
867 revprops[properties.PROP_REVISION_AUTHOR] = msg[2][0]
869 revprops[properties.PROP_REVISION_DATE] = msg[3][0]
871 revprops[properties.PROP_REVISION_LOG] = msg[4][0]
873 revprops.update(dict(msg[8]))
874 yield paths, msg[1], revprops, has_children
878 def get_log(self, callback, *args, **kwargs):
879 for (paths, rev, props, has_children) in self.log(*args, **kwargs):
880 if has_children is None:
881 callback(paths, rev, props)
883 callback(paths, rev, props, has_children)
888 CAPABILITIES = ["edit-pipeline", "bazaar", "log-revprops"]
889 MECHANISMS = ["ANONYMOUS"]
892 class SVNServer(SVNConnection):
894 def __init__(self, backend, recv_fn, send_fn, logf=None):
895 self.backend = backend
898 super(SVNServer, self).__init__(recv_fn, send_fn)
901 MIN_VERSION, MAX_VERSION, [literal(x) for x in MECHANISMS],
902 [literal(x) for x in CAPABILITIES])
904 def send_mechs(self):
905 self.send_success([literal(x) for x in MECHANISMS], "")
907 def send_failure(self, *contents):
908 self.send_msg([literal("failure"), list(contents)])
911 self.send_success([], "")
913 def send_unknown(self, cmd):
915 [ERR_RA_SVN_UNKNOWN_CMD,
916 "Unknown command '%s'" % cmd, __file__, 52])
918 def get_latest_rev(self):
920 self.send_success(self.repo_backend.get_latest_revnum())
922 def check_path(self, path, rev):
927 kind = self.repo_backend.check_path(path, revnum)
929 self.send_success(literal({
933 NODE_UNKNOWN: "unknown"}[kind]))
935 def log(self, target_path, start_rev, end_rev, changed_paths,
936 strict_node, limit=None, include_merged_revisions=False,
937 all_revprops=None, revprops=None):
938 def send_revision(revno, author, date, message, changed_paths=None):
940 if changed_paths is not None:
941 for p, (action, cf, cr) in changed_paths.items():
943 changes.append((p, literal(action), (cf, cr)))
945 changes.append((p, literal(action), ()))
946 self.send_msg([changes, revno, [author], [date], [message]])
948 if len(start_rev) == 0:
951 start_revnum = start_rev[0]
952 if len(end_rev) == 0:
955 end_revnum = end_rev[0]
956 self.repo_backend.log(send_revision, target_path, start_revnum,
957 end_revnum, changed_paths, strict_node, limit)
958 self.send_msg(literal("done"))
961 def open_backend(self, url):
962 (rooturl, location) = urlparse.splithost(url)
963 self.repo_backend, self.relpath = self.backend.open_repository(
966 def reparent(self, parent):
967 self.open_backend(parent)
971 def stat(self, path, rev):
977 dirent = self.repo_backend.stat(path, revnum)
979 self.send_success([])
981 args = [dirent["name"], dirent["kind"], dirent["size"],
982 dirent["has-props"], dirent["created-rev"]]
983 if "created-date" in dirent:
984 args.append([dirent["created-date"]])
987 if "last-author" in dirent:
988 args.append([dirent["last-author"]])
991 self.send_success([args])
993 def commit(self, logmsg, locks, keep_locks=False, rev_props=None):
994 self.send_failure([ERR_UNSUPPORTED_FEATURE,
995 "commit not yet supported", __file__, 42])
997 def rev_proplist(self, revnum):
999 revprops = self.repo_backend.rev_proplist(revnum)
1000 self.send_success(list(revprops.items()))
1002 def rev_prop(self, revnum, name):
1004 revprops = self.repo_backend.rev_proplist(revnum)
1005 if name in revprops:
1006 self.send_success([revprops[name]])
1010 def get_locations(self, path, peg_revnum, revnums):
1012 locations = self.repo_backend.get_locations(path, peg_revnum, revnums)
1013 for rev, path in locations.items():
1014 self.send_msg([rev, path])
1015 self.send_msg(literal("done"))
1018 def update(self, rev, target, recurse, depth=None,
1019 send_copyfrom_param=True):
1022 msg = self.recv_msg()
1023 assert msg[0] in ["set-path", "finish-report"]
1024 if msg[0] == "finish-report":
1033 self.repo_backend.update(Editor(self), revnum, target, recurse)
1035 client_result = self.recv_msg()
1036 if client_result[0] == "success":
1039 self.mutter("Client reported error during update: %r" %
1041 # Needs to be sent back to the client to display
1042 self.send_failure(client_result[1][0])
1045 "get-latest-rev": get_latest_rev,
1048 "check-path": check_path,
1049 "reparent": reparent,
1052 "rev-proplist": rev_proplist,
1053 "rev-prop": rev_prop,
1054 "get-locations": get_locations,
1055 # FIXME: get-dated-rev
1062 # FIXME: get-file-revs
1066 def send_auth_request(self):
1070 self.send_greeting()
1071 msg = self.recv_msg()
1073 capabilities = msg[1]
1076 self.client_user_agent = msg[3]
1078 self.client_user_agent = None
1079 self.capabilities = capabilities
1080 self.version = version
1082 self.mutter("client supports:")
1083 self.mutter(" version %r" % version)
1084 self.mutter(" capabilities %r " % capabilities)
1087 (mech, args) = self.recv_msg()
1088 # TODO: Proper authentication
1091 self.open_backend(url)
1092 self.send_success(self.repo_backend.get_uuid(), url)
1095 while not self._stop:
1096 (cmd, args) = self.recv_msg()
1097 if cmd not in self.commands:
1098 self.mutter("client used unknown command %r" % cmd)
1099 self.send_unknown(cmd)
1102 self.commands[cmd](self, *args)
1107 def mutter(self, text):
1108 if self._logf is not None:
1109 self._logf.write("%s\n" % text)
1112 class TCPSVNRequestHandler(StreamRequestHandler):
1114 def __init__(self, request, client_address, server):
1115 self._server = server
1116 StreamRequestHandler.__init__(
1117 self, request, client_address, server)
1121 self._server._backend, self.rfile.read,
1122 self.wfile.write, self._server._logf)
1125 except socket.error as e:
1126 if e.args[0] == EPIPE:
1131 class TCPSVNServer(TCPServer):
1133 allow_reuse_address = True
1134 serve = TCPServer.serve_forever
1136 def __init__(self, backend, addr, logf=None):
1138 self._backend = backend
1139 TCPServer.__init__(self, addr, TCPSVNRequestHandler)