af6f2f1416db623542dc5ff673bd08081ecefd0f
[jelmer/subvertpy.git] / subvertpy / ra_svn.py
1 # Copyright (C) 2006-2008 Jelmer Vernooij <jelmer@jelmer.uk>
2
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.
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 Lesser General Public License for more details.
12
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."""
17
18 __author__ = "Jelmer Vernooij <jelmer@jelmer.uk>"
19
20 try:
21     from SocketServer import StreamRequestHandler, TCPServer
22 except ImportError:
23     from socketserver import StreamRequestHandler, TCPServer
24 import base64
25 import os
26 import socket
27 import subprocess
28 from errno import EPIPE
29 try:
30     import urlparse
31 except ImportError:
32     import urllib.parse as urlparse
33
34 from subvertpy import (
35     ERR_RA_SVN_UNKNOWN_CMD,
36     ERR_UNSUPPORTED_FEATURE,
37     NODE_DIR,
38     NODE_FILE,
39     NODE_UNKNOWN,
40     NODE_NONE,
41     SubversionException,
42     properties,
43     )
44 from subvertpy.delta import (
45     pack_svndiff0_window,
46     unpack_svndiff0,
47     SVNDIFF0_HEADER,
48     )
49 from subvertpy.marshall import (
50     NeedMoreData,
51     literal,
52     marshall,
53     unmarshall,
54     )
55 from subvertpy.ra import (
56     DIRENT_CREATED_REV,
57     DIRENT_HAS_PROPS,
58     DIRENT_KIND,
59     DIRENT_LAST_AUTHOR,
60     DIRENT_SIZE,
61     DIRENT_TIME,
62     )
63 from subvertpy.server import (
64     generate_random_id,
65     )
66
67
68 class SSHSubprocess(object):
69     """A socket-like object that talks to an ssh subprocess via pipes."""
70
71     __slots__ = ('proc')
72
73     def __init__(self, proc):
74         self.proc = proc
75
76     def send(self, data):
77         return os.write(self.proc.stdin.fileno(), data)
78
79     def recv(self, count):
80         return os.read(self.proc.stdout.fileno(), count)
81
82     def close(self):
83         self.proc.stdin.close()
84         self.proc.stdout.close()
85         self.proc.wait()
86
87     def get_filelike_channels(self):
88         return (self.proc.stdout, self.proc.stdin)
89
90
91 class SSHVendor(object):
92
93     def connect_ssh(self, username, password, host, port, command):
94         args = ['ssh', '-x']
95         if port is not None:
96             args.extend(['-p', str(port)])
97         if username is not None:
98             host = "%s@%s" % (username, host)
99         args.append(host)
100         proc = subprocess.Popen(args + command,
101                                 stdin=subprocess.PIPE,
102                                 stdout=subprocess.PIPE)
103         return SSHSubprocess(proc)
104
105
106 # Can be overridden by users
107 get_ssh_vendor = SSHVendor
108
109
110 class SVNConnection(object):
111
112     def __init__(self, recv_fn, send_fn):
113         self.inbuffer = ""
114         self.recv_fn = recv_fn
115         self.send_fn = send_fn
116
117     def recv_msg(self):
118         while True:
119             try:
120                 (self.inbuffer, ret) = unmarshall(self.inbuffer)
121                 return ret
122             except NeedMoreData:
123                 newdata = self.recv_fn(1)
124                 if newdata != "":
125                     # self.mutter("IN: %r" % newdata)
126                     self.inbuffer += newdata
127
128     def send_msg(self, data):
129         marshalled_data = marshall(data)
130         # self.mutter("OUT: %r" % marshalled_data)
131         self.send_fn(marshalled_data)
132
133     def send_success(self, *contents):
134         self.send_msg([literal("success"), list(contents)])
135
136
137 SVN_PORT = 3690
138
139
140 def feed_editor(conn, editor):
141     tokens = {}
142     diff = {}
143     txdelta_handler = {}
144     # Process commands
145     while True:
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()
152             else:
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])
160             else:
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)
169             else:
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])
178             else:
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(
187                     None)
188             else:
189                 txdelta_handler[args[0]] = tokens[args[0]].apply_textdelta(
190                     args[1][0])
191             diff[args[0]] = ""
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)
201             else:
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()
206             else:
207                 tokens[args[0]].close(args[1][0])
208         elif command == "close-edit":
209             editor.close()
210             break
211         elif command == "abort-edit":
212             editor.abort()
213             break
214
215     conn.send_success()
216     conn._unpack()
217
218
219 class Reporter(object):
220
221     __slots__ = ('conn', 'editor')
222
223     def __init__(self, conn, editor):
224         self.conn = conn
225         self.editor = editor
226
227     def set_path(self, path, rev, start_empty=False, lock_token=None,
228                  depth=None):
229         args = [path, rev, start_empty]
230         if lock_token is not None:
231             args.append([lock_token])
232         else:
233             args.append([])
234         if depth is not None:
235             args.append(depth)
236
237         self.conn.send_msg([literal("set-path"), args])
238
239     def delete_path(self, path):
240         self.conn.send_msg([literal("delete-path"), [path]])
241
242     def link_path(self, path, url, rev, start_empty=False, lock_token=None,
243                   depth=None):
244         args = [path, url, rev, start_empty]
245         if lock_token is not None:
246             args.append([lock_token])
247         else:
248             args.append([])
249         if depth is not None:
250             args.append(depth)
251
252         self.conn.send_msg([literal("link-path"), args])
253
254     def finish(self):
255         self.conn.send_msg([literal("finish-report"), []])
256         self.conn.recv_msg()
257         feed_editor(self.conn, self.editor)
258         self.conn.busy = False
259
260     def abort(self):
261         self.conn.send_msg([literal("abort-report"), []])
262         self.conn.busy = False
263
264
265 class Editor(object):
266
267     __slots__ = ('conn')
268
269     def __init__(self, conn):
270         self.conn = conn
271
272     def set_target_revision(self, revnum):
273         self.conn.send_msg([literal("target-rev"), [revnum]])
274
275     def open_root(self, base_revision=None):
276         id = generate_random_id()
277         if base_revision is None:
278             baserev = []
279         else:
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)
284
285     def close(self):
286         self.conn.send_msg([literal("close-edit"), []])
287
288     def abort(self):
289         self.conn.send_msg([literal("abort-edit"), []])
290
291
292 class DirectoryEditor(object):
293
294     __slots__ = ('conn', 'id')
295
296     def __init__(self, conn, id):
297         self.conn = conn
298         self.id = id
299         self.conn._open_ids.append(id)
300
301     def add_file(self, path, copyfrom_path=None, copyfrom_rev=-1):
302         self._is_last_open()
303         child = generate_random_id()
304         if copyfrom_path is not None:
305             copyfrom_data = [copyfrom_path, copyfrom_rev]
306         else:
307             copyfrom_data = []
308         self.conn.send_msg([literal("add-file"),
309                            [path, self.id, child, copyfrom_data]])
310         return FileEditor(self.conn, child)
311
312     def open_file(self, path, base_revnum):
313         self._is_last_open()
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)
318
319     def delete_entry(self, path, base_revnum):
320         self._is_last_open()
321         self.conn.send_msg([literal("delete-entry"),
322                            [path, base_revnum, self.id]])
323
324     def add_directory(self, path, copyfrom_path=None, copyfrom_rev=-1):
325         self._is_last_open()
326         child = generate_random_id()
327         if copyfrom_path is not None:
328             copyfrom_data = [copyfrom_path, copyfrom_rev]
329         else:
330             copyfrom_data = []
331         self.conn.send_msg([literal("add-dir"),
332                            [path, self.id, child, copyfrom_data]])
333         return DirectoryEditor(self.conn, child)
334
335     def open_directory(self, path, base_revnum):
336         self._is_last_open()
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)
341
342     def change_prop(self, name, value):
343         self._is_last_open()
344         if value is None:
345             value = []
346         else:
347             value = [value]
348         self.conn.send_msg([literal("change-dir-prop"),
349                            [self.id, name, value]])
350
351     def _is_last_open(self):
352         assert self.conn._open_ids[-1] == self.id
353
354     def close(self):
355         self._is_last_open()
356         self.conn._open_ids.pop()
357         self.conn.send_msg([literal("close-dir"), [self.id]])
358
359
360 class FileEditor(object):
361
362     __slots__ = ('conn', 'id')
363
364     def __init__(self, conn, id):
365         self.conn = conn
366         self.id = id
367         self.conn._open_ids.append(id)
368
369     def _is_last_open(self):
370         assert self.conn._open_ids[-1] == self.id
371
372     def close(self, checksum=None):
373         self._is_last_open()
374         self.conn._open_ids.pop()
375         if checksum is None:
376             checksum = []
377         else:
378             checksum = [checksum]
379         self.conn.send_msg([literal("close-file"), [self.id, checksum]])
380
381     def apply_textdelta(self, base_checksum=None):
382         self._is_last_open()
383         if base_checksum is None:
384             base_check = []
385         else:
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]])
391
392         def send_textdelta(delta):
393             if delta is None:
394                 self.conn.send_msg([literal("textdelta-end"), [self.id]])
395             else:
396                 self.conn.send_msg([literal("textdelta-chunk"),
397                                    [self.id, pack_svndiff0_window(delta)]])
398         return send_textdelta
399
400     def change_prop(self, name, value):
401         self._is_last_open()
402         if value is None:
403             value = []
404         else:
405             value = [value]
406         self.conn.send_msg([literal("change-file-prop"),
407                            [self.id, name, value]])
408
409
410 def mark_busy(unbound):
411
412     def convert(self, *args, **kwargs):
413         self.busy = True
414         try:
415             ret = unbound(self, *args, **kwargs)
416         finally:
417             self.busy = False
418         return ret
419
420     convert.__doc__ = unbound.__doc__
421     convert.__name__ = unbound.__name__
422     return convert
423
424
425 def unmarshall_dirent(d):
426     ret = {
427         "name": d[0],
428         "kind": d[1],
429         "size": d[2],
430         "has-props": bool(d[3]),
431         "created-rev": d[4],
432         }
433     if d[5] != []:
434         ret["created-date"] = d[5]
435     if d[6] != []:
436         ret["last-author"] = d[6]
437     return ret
438
439
440 class SVNClient(SVNConnection):
441
442     def __init__(self, url, progress_cb=None, auth=None, config=None,
443                  client_string_func=None, open_tmp_file_func=None):
444         self.url = url
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
449         self._auth = auth
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://
453         if type == "svn":
454             (recv_func, send_func) = self._connect(host)
455         else:
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())
460         self.send_msg(
461             [max_version,
462              [literal(x) for x in CAPABILITIES
463                  if x in self._server_capabilities],
464              self.url])
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"),
469                           [base64.b64encode(
470                               "anonymous@%s" % socket.gethostname())]])
471             self.recv_msg()
472         msg = self._unpack()
473         if len(msg) > 2:
474             self._server_capabilities += msg[2]
475         (self._uuid, self._root_url) = msg[0:2]
476         self.busy = False
477
478     def _unpack(self):
479         msg = self.recv_msg()
480         if msg[0] == "failure":
481             if isinstance(msg[1], str):
482                 raise SubversionException(*msg[1])
483             num = msg[1][0][0]
484             msg = msg[1][0][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
489         assert len(msg) == 2
490         return msg[1]
491
492     def _recv_greeting(self):
493         greeting = self._unpack()
494         assert len(greeting) == 4
495         return greeting
496
497     _recv_ack = _unpack
498
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)
504         self._socket = None
505         err = RuntimeError('no addresses for %s:%s' % (host, port))
506         for (family, socktype, proto, canonname, sockaddr) in sockaddrs:
507             try:
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:
512                     self._socket.close()
513                 self._socket = None
514                 continue
515             break
516         if self._socket is None:
517             raise err
518         self._socket.setblocking(True)
519         return (self._socket.recv, self._socket.send)
520
521     def _connect_ssh(self, host):
522         (user, host) = urlparse.splituser(host)
523         if user is not None:
524             (user, password) = urlparse.splitpassword(user)
525         else:
526             password = None
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)
531
532     def get_file_revs(self, path, start, end, file_rev_handler):
533         raise NotImplementedError(self.get_file_revs)
534
535     @mark_busy
536     def get_locations(self, path, peg_revision, location_revisions):
537         self.send_msg([literal("get-locations"), [path, peg_revision,
538                       location_revisions]])
539         self._recv_ack()
540         ret = {}
541         while True:
542             msg = self.recv_msg()
543             if msg == "done":
544                 break
545             ret[msg[0]] = msg[1]
546         self._unparse()
547         return ret
548
549     def get_locks(self, path):
550         self.send_msg([literal("get-lock"), [path]])
551         self._recv_ack()
552         return self._unpack()
553
554     def lock(self, path_revs, comment, steal_lock, lock_func):
555         raise NotImplementedError(self.lock)
556
557     def unlock(self, path_tokens, break_lock, lock_func):
558         raise NotImplementedError(self.unlock)
559
560     def mergeinfo(self, paths, revision=-1, inherit=None,
561                   include_descendants=False):
562         raise NotImplementedError(self.mergeinfo)
563
564     def location_segments(self, path, start_revision, end_revision,
565                           include_merged_revisions=False):
566         args = [path]
567         if start_revision is None or start_revision == -1:
568             args.append([])
569         else:
570             args.append([start_revision])
571         if end_revision is None or end_revision == -1:
572             args.append([])
573         else:
574             args.append([end_revision])
575         args.append(include_merged_revisions)
576         self.send_msg([literal("get-location-segments"), args])
577         self._recv_ack()
578         while True:
579             msg = self.recv_msg()
580             if msg == "done":
581                 break
582             yield msg
583         self._unpack()
584
585     def get_location_segments(self, path, start_revision, end_revision, rcvr):
586         for msg in self.location_segments(path, start_revision, end_revision):
587             rcvr(*msg)
588
589     def has_capability(self, capability):
590         return capability in self._server_capabilities
591
592     @mark_busy
593     def check_path(self, path, revision=None):
594         args = [path]
595         if revision is None or revision == -1:
596             args.append([])
597         else:
598             args.append([revision])
599         self.send_msg([literal("check-path"), args])
600         self._recv_ack()
601         ret = self._unpack()[0]
602         return {"dir": NODE_DIR, "file": NODE_FILE, "unknown": NODE_UNKNOWN,
603                 "none": NODE_NONE}[ret]
604
605     def get_lock(self, path):
606         self.send_msg([literal("get-lock"), [path]])
607         self._recv_ack()
608         ret = self._unpack()
609         if len(ret) == 0:
610             return None
611         else:
612             return ret[0]
613
614     @mark_busy
615     def get_dir(self, path, revision=-1, dirent_fields=0, want_props=True,
616                 want_contents=True):
617         args = [path]
618         if revision is None or revision == -1:
619             args.append([])
620         else:
621             args.append([revision])
622
623         args += [want_props, want_contents]
624
625         fields = []
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"))
638         args.append(fields)
639
640         self.send_msg([literal("get-dir"), args])
641         self._recv_ack()
642         ret = self._unpack()
643         fetch_rev = ret[0]
644         props = dict(ret[1])
645         dirents = {}
646         for d in ret[2]:
647             entry = unmarshall_dirent(d)
648             dirents[entry["name"]] = entry
649
650         return (dirents, fetch_rev, props)
651
652     @mark_busy
653     def stat(self, path, revision=-1):
654         args = [path]
655         if revision is None or revision == -1:
656             args.append([revision])
657         else:
658             args.append([])
659
660         self.send_msg([literal("stat"), args])
661         self._recv_ack()
662         ret = self._unpack()
663         if len(ret) == 0:
664             return None
665         return unmarshall_dirent(ret[0])
666
667     @mark_busy
668     def get_file(self, path, stream, revision=-1):
669         raise NotImplementedError(self.get_file)
670
671     def change_rev_prop(self, rev, name, value):
672         args = [rev, name]
673         if value is not None:
674             args.append(value)
675         self.send_msg([literal("change-rev-prop"), args])
676         self._recv_ack()
677         self._unparse()
678
679     def get_commit_editor(self, revprops, callback=None, lock_tokens=None,
680                           keep_locks=False):
681         args = [revprops[properties.PROP_REVISION_LOG]]
682         if lock_tokens is not None:
683             args.append(list(lock_tokens.items()))
684         else:
685             args.append([])
686         args.append(keep_locks)
687         if len(revprops) > 1:
688             args.append(list(revprops.items()))
689         self.send_msg([literal("commit"), args])
690         self._recv_ack()
691         raise NotImplementedError(self.get_commit_editor)
692
693     def rev_proplist(self, revision):
694         self.send_msg([literal("rev-proplist"), [revision]])
695         self._recv_ack()
696         return dict(self._unpack()[0])
697
698     def rev_prop(self, revision, name):
699         self.send_msg([literal("rev-prop"), [revision, name]])
700         self._recv_ack()
701         ret = self._unpack()
702         if len(ret) == 0:
703             return None
704         else:
705             return ret[0]
706
707     @mark_busy
708     def replay(self, revision, low_water_mark, update_editor,
709                send_deltas=True):
710         self.send_msg([literal("replay"), [revision, low_water_mark,
711                       send_deltas]])
712         self._recv_ack()
713         feed_editor(self, update_editor)
714         self._unpack()
715
716     @mark_busy
717     def replay_range(self, start_revision, end_revision, low_water_mark, cbs,
718                      send_deltas=True):
719         self.send_msg([literal("replay-range"), [start_revision, end_revision,
720                       low_water_mark, send_deltas]])
721         self._recv_ack()
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)
728         self._unpack()
729
730     def do_switch(self, revision_to_update_to, update_target, recurse,
731                   switch_url, update_editor, depth=None):
732         args = []
733         if revision_to_update_to is None or revision_to_update_to == -1:
734             args.append([])
735         else:
736             args.append([revision_to_update_to])
737         args.append(update_target)
738         args.append(recurse)
739         args.append(switch_url)
740         if depth is not None:
741             args.append(literal(depth))
742
743         self.busy = True
744         try:
745             self.send_msg([literal("switch"), args])
746             self._recv_ack()
747             return Reporter(self, update_editor)
748         except BaseException:
749             self.busy = False
750             raise
751
752     def do_update(self, revision_to_update_to, update_target, recurse,
753                   update_editor, depth=None):
754         args = []
755         if revision_to_update_to is None or revision_to_update_to == -1:
756             args.append([])
757         else:
758             args.append([revision_to_update_to])
759         args.append(update_target)
760         args.append(recurse)
761         if depth is not None:
762             args.append(literal(depth))
763
764         self.busy = True
765         try:
766             self.send_msg([literal("update"), args])
767             self._recv_ack()
768             return Reporter(self, update_editor)
769         except BaseException:
770             self.busy = False
771             raise
772
773     def do_diff(self, revision_to_update, diff_target, versus_url, diff_editor,
774                 recurse=True, ignore_ancestry=False, text_deltas=False,
775                 depth=None):
776         args = []
777         if revision_to_update is None or revision_to_update == -1:
778             args.append([])
779         else:
780             args.append([revision_to_update])
781         args += [diff_target, recurse, ignore_ancestry, versus_url,
782                  text_deltas]
783         if depth is not None:
784             args.append(literal(depth))
785         self.busy = True
786         try:
787             self.send_msg([literal("diff"), args])
788             self._recv_ack()
789             return Reporter(self, diff_editor)
790         except BaseException:
791             self.busy = False
792             raise
793
794     def get_repos_root(self):
795         return self._root_url
796
797     @mark_busy
798     def get_latest_revnum(self):
799         self.send_msg([literal("get-latest-rev"), []])
800         self._recv_ack()
801         return self._unpack()[0]
802
803     @mark_busy
804     def get_dated_rev(self, date):
805         self.send_msg([literal("get-dated-rev"), [date]])
806         self._recv_ack()
807         return self._unpack()[0]
808
809     @mark_busy
810     def reparent(self, url):
811         self.send_msg([literal("reparent"), [url]])
812         self._recv_ack()
813         self._unpack()
814         self.url = url
815
816     def get_uuid(self):
817         return self._uuid
818
819     @mark_busy
820     def log(self, paths, start, end, limit=0, discover_changed_paths=True,
821             strict_node_history=True, include_merged_revisions=True,
822             revprops=None):
823         args = [paths]
824         if start is None or start == -1:
825             args.append([])
826         else:
827             args.append([start])
828         if end is None or end == -1:
829             args.append([])
830         else:
831             args.append([end])
832         args.append(discover_changed_paths)
833         args.append(strict_node_history)
834         args.append(limit)
835         args.append(include_merged_revisions)
836         if revprops is None:
837             args.append(literal("all-revprops"))
838             args.append([])
839         else:
840             args.append(literal("revprops"))
841             args.append(revprops)
842
843         self.send_msg([literal("log"), args])
844         self._recv_ack()
845         while True:
846             msg = self.recv_msg()
847             if msg == "done":
848                 break
849             paths = {}
850             for p, action, cfd in msg[0]:
851                 if len(cfd) == 0:
852                     paths[p] = (str(action), None, -1)
853                 else:
854                     paths[p] = (str(action), cfd[0], cfd[1])
855
856             if len(msg) > 5:
857                 has_children = msg[5]
858             else:
859                 has_children = None
860             if len(msg) > 6 and msg[6]:
861                 revno = None
862             else:
863                 revno = msg[1]  # noqa: F841
864                 # TODO(jelmer): Do something with revno
865             revprops = {}
866             if len(msg[2]) != 0:
867                 revprops[properties.PROP_REVISION_AUTHOR] = msg[2][0]
868             if len(msg[3]) != 0:
869                 revprops[properties.PROP_REVISION_DATE] = msg[3][0]
870             if len(msg[4]) != 0:
871                 revprops[properties.PROP_REVISION_LOG] = msg[4][0]
872             if len(msg) > 8:
873                 revprops.update(dict(msg[8]))
874             yield paths, msg[1], revprops, has_children
875
876         self._unpack()
877
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)
882             else:
883                 callback(paths, rev, props, has_children)
884
885
886 MIN_VERSION = 2
887 MAX_VERSION = 2
888 CAPABILITIES = ["edit-pipeline", "bazaar", "log-revprops"]
889 MECHANISMS = ["ANONYMOUS"]
890
891
892 class SVNServer(SVNConnection):
893
894     def __init__(self, backend, recv_fn, send_fn, logf=None):
895         self.backend = backend
896         self._stop = False
897         self._logf = logf
898         super(SVNServer, self).__init__(recv_fn, send_fn)
899
900         self.send_success(
901             MIN_VERSION, MAX_VERSION, [literal(x) for x in MECHANISMS],
902             [literal(x) for x in CAPABILITIES])
903
904     def send_mechs(self):
905         self.send_success([literal(x) for x in MECHANISMS], "")
906
907     def send_failure(self, *contents):
908         self.send_msg([literal("failure"), list(contents)])
909
910     def send_ack(self):
911         self.send_success([], "")
912
913     def send_unknown(self, cmd):
914         self.send_failure(
915             [ERR_RA_SVN_UNKNOWN_CMD,
916              "Unknown command '%s'" % cmd, __file__, 52])
917
918     def get_latest_rev(self):
919         self.send_ack()
920         self.send_success(self.repo_backend.get_latest_revnum())
921
922     def check_path(self, path, rev):
923         if len(rev) == 0:
924             revnum = None
925         else:
926             revnum = rev[0]
927         kind = self.repo_backend.check_path(path, revnum)
928         self.send_ack()
929         self.send_success(literal({
930             NODE_NONE: "none",
931             NODE_DIR: "dir",
932             NODE_FILE: "file",
933             NODE_UNKNOWN: "unknown"}[kind]))
934
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):
939             changes = []
940             if changed_paths is not None:
941                 for p, (action, cf, cr) in changed_paths.items():
942                     if cf is not None:
943                         changes.append((p, literal(action), (cf, cr)))
944                     else:
945                         changes.append((p, literal(action), ()))
946             self.send_msg([changes, revno, [author], [date], [message]])
947         self.send_ack()
948         if len(start_rev) == 0:
949             start_revnum = None
950         else:
951             start_revnum = start_rev[0]
952         if len(end_rev) == 0:
953             end_revnum = None
954         else:
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"))
959         self.send_success()
960
961     def open_backend(self, url):
962         (rooturl, location) = urlparse.splithost(url)
963         self.repo_backend, self.relpath = self.backend.open_repository(
964              location)
965
966     def reparent(self, parent):
967         self.open_backend(parent)
968         self.send_ack()
969         self.send_success()
970
971     def stat(self, path, rev):
972         if len(rev) == 0:
973             revnum = None
974         else:
975             revnum = rev[0]
976         self.send_ack()
977         dirent = self.repo_backend.stat(path, revnum)
978         if dirent is None:
979             self.send_success([])
980         else:
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"]])
985             else:
986                 args.append([])
987             if "last-author" in dirent:
988                 args.append([dirent["last-author"]])
989             else:
990                 args.append([])
991             self.send_success([args])
992
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])
996
997     def rev_proplist(self, revnum):
998         self.send_ack()
999         revprops = self.repo_backend.rev_proplist(revnum)
1000         self.send_success(list(revprops.items()))
1001
1002     def rev_prop(self, revnum, name):
1003         self.send_ack()
1004         revprops = self.repo_backend.rev_proplist(revnum)
1005         if name in revprops:
1006             self.send_success([revprops[name]])
1007         else:
1008             self.send_success()
1009
1010     def get_locations(self, path, peg_revnum, revnums):
1011         self.send_ack()
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"))
1016         self.send_success()
1017
1018     def update(self, rev, target, recurse, depth=None,
1019                send_copyfrom_param=True):
1020         self.send_ack()
1021         while True:
1022             msg = self.recv_msg()
1023             assert msg[0] in ["set-path", "finish-report"]
1024             if msg[0] == "finish-report":
1025                 break
1026
1027         self.send_ack()
1028
1029         if len(rev) == 0:
1030             revnum = None
1031         else:
1032             revnum = rev[0]
1033         self.repo_backend.update(Editor(self), revnum, target, recurse)
1034         self.send_success()
1035         client_result = self.recv_msg()
1036         if client_result[0] == "success":
1037             return
1038         else:
1039             self.mutter("Client reported error during update: %r" %
1040                         client_result)
1041             # Needs to be sent back to the client to display
1042             self.send_failure(client_result[1][0])
1043
1044     commands = {
1045             "get-latest-rev": get_latest_rev,
1046             "log": log,
1047             "update": update,
1048             "check-path": check_path,
1049             "reparent": reparent,
1050             "stat": stat,
1051             "commit": commit,
1052             "rev-proplist": rev_proplist,
1053             "rev-prop": rev_prop,
1054             "get-locations": get_locations,
1055             # FIXME: get-dated-rev
1056             # FIXME: get-file
1057             # FIXME: get-dir
1058             # FIXME: check-path
1059             # FIXME: switch
1060             # FIXME: status
1061             # FIXME: diff
1062             # FIXME: get-file-revs
1063             # FIXME: replay
1064     }
1065
1066     def send_auth_request(self):
1067         pass
1068
1069     def serve(self):
1070         self.send_greeting()
1071         msg = self.recv_msg()
1072         version = msg[0]
1073         capabilities = msg[1]
1074         url = msg[2]
1075         if len(msg) > 3:
1076             self.client_user_agent = msg[3]
1077         else:
1078             self.client_user_agent = None
1079         self.capabilities = capabilities
1080         self.version = version
1081         self.url = url
1082         self.mutter("client supports:")
1083         self.mutter("  version %r" % version)
1084         self.mutter("  capabilities %r " % capabilities)
1085         self.send_mechs()
1086
1087         (mech, args) = self.recv_msg()
1088         # TODO: Proper authentication
1089         self.send_success()
1090
1091         self.open_backend(url)
1092         self.send_success(self.repo_backend.get_uuid(), url)
1093
1094         # Expect:
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)
1100                 return
1101             else:
1102                 self.commands[cmd](self, *args)
1103
1104     def close(self):
1105         self._stop = True
1106
1107     def mutter(self, text):
1108         if self._logf is not None:
1109             self._logf.write("%s\n" % text)
1110
1111
1112 class TCPSVNRequestHandler(StreamRequestHandler):
1113
1114     def __init__(self, request, client_address, server):
1115         self._server = server
1116         StreamRequestHandler.__init__(
1117             self, request, client_address, server)
1118
1119     def handle(self):
1120         server = SVNServer(
1121             self._server._backend, self.rfile.read,
1122             self.wfile.write, self._server._logf)
1123         try:
1124             server.serve()
1125         except socket.error as e:
1126             if e.args[0] == EPIPE:
1127                 return
1128             raise
1129
1130
1131 class TCPSVNServer(TCPServer):
1132
1133     allow_reuse_address = True
1134     serve = TCPServer.serve_forever
1135
1136     def __init__(self, backend, addr, logf=None):
1137         self._logf = logf
1138         self._backend = backend
1139         TCPServer.__init__(self, addr, TCPSVNRequestHandler)