Rework server protocol to be smarter and interoperate with cgit client.
[jelmer/dulwich-libgit2.git] / dulwich / protocol.py
1 # protocol.py -- Shared parts of the git protocols
2 # Copryight (C) 2008 John Carr <john.carr@unrouted.co.uk>
3 # Copyright (C) 2008 Jelmer Vernooij <jelmer@samba.org>
4 #
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; version 2
8 # or (at your option) any later version of the License.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
18 # MA  02110-1301, USA.
19
20 """Generic functions for talking the git smart server protocol."""
21
22 import socket
23
24 from dulwich.errors import (
25     HangupException,
26     GitProtocolError,
27     )
28
29 TCP_GIT_PORT = 9418
30
31 SINGLE_ACK = 0
32 MULTI_ACK = 1
33
34 class ProtocolFile(object):
35     """
36     Some network ops are like file ops. The file ops expect to operate on
37     file objects, so provide them with a dummy file.
38     """
39
40     def __init__(self, read, write):
41         self.read = read
42         self.write = write
43
44     def tell(self):
45         pass
46
47     def close(self):
48         pass
49
50
51 class Protocol(object):
52
53     def __init__(self, read, write, report_activity=None):
54         self.read = read
55         self.write = write
56         self.report_activity = report_activity
57
58     def read_pkt_line(self):
59         """
60         Reads a 'pkt line' from the remote git process
61
62         :return: The next string from the stream
63         """
64         try:
65             sizestr = self.read(4)
66             if not sizestr:
67                 raise HangupException()
68             size = int(sizestr, 16)
69             if size == 0:
70                 if self.report_activity:
71                     self.report_activity(4, 'read')
72                 return None
73             if self.report_activity:
74                 self.report_activity(size, 'read')
75             return self.read(size-4)
76         except socket.error, e:
77             raise GitProtocolError(e)
78
79     def read_pkt_seq(self):
80         pkt = self.read_pkt_line()
81         while pkt:
82             yield pkt
83             pkt = self.read_pkt_line()
84
85     def write_pkt_line(self, line):
86         """
87         Sends a 'pkt line' to the remote git process
88
89         :param line: A string containing the data to send
90         """
91         try:
92             if line is None:
93                 self.write("0000")
94                 if self.report_activity:
95                     self.report_activity(4, 'write')
96             else:
97                 self.write("%04x%s" % (len(line)+4, line))
98                 if self.report_activity:
99                     self.report_activity(4+len(line), 'write')
100         except socket.error, e:
101             raise GitProtocolError(e)
102
103     def write_file(self):
104         class ProtocolFile(object):
105
106             def __init__(self, proto):
107                 self._proto = proto
108                 self._offset = 0
109
110             def write(self, data):
111                 self._proto.write(data)
112                 self._offset += len(data)
113
114             def tell(self):
115                 return self._offset
116
117             def close(self):
118                 pass
119
120         return ProtocolFile(self)
121
122     def write_sideband(self, channel, blob):
123         """
124         Write data to the sideband (a git multiplexing method)
125
126         :param channel: int specifying which channel to write to
127         :param blob: a blob of data (as a string) to send on this channel
128         """
129         # a pktline can be a max of 65520. a sideband line can therefore be
130         # 65520-5 = 65515
131         # WTF: Why have the len in ASCII, but the channel in binary.
132         while blob:
133             self.write_pkt_line("%s%s" % (chr(channel), blob[:65515]))
134             blob = blob[65515:]
135
136     def send_cmd(self, cmd, *args):
137         """
138         Send a command and some arguments to a git server
139
140         Only used for git://
141
142         :param cmd: The remote service to access
143         :param args: List of arguments to send to remove service
144         """
145         self.write_pkt_line("%s %s" % (cmd, "".join(["%s\0" % a for a in args])))
146
147     def read_cmd(self):
148         """
149         Read a command and some arguments from the git client
150
151         Only used for git://
152
153         :return: A tuple of (command, [list of arguments])
154         """
155         line = self.read_pkt_line()
156         splice_at = line.find(" ")
157         cmd, args = line[:splice_at], line[splice_at+1:]
158         assert args[-1] == "\x00"
159         return cmd, args[:-1].split(chr(0))
160
161
162 def extract_capabilities(text):
163     """Extract a capabilities list from a string, if present.
164
165     :param text: String to extract from
166     :return: Tuple with text with capabilities removed and list of capabilities
167     """
168     if not "\0" in text:
169         return text, []
170     text, capabilities = text.rstrip().split("\0")
171     return (text, capabilities.split(" "))
172
173
174 def extract_want_line_capabilities(text):
175     """Extract a capabilities list from a want line, if present.
176
177     Note that want lines have capabilities separated from the rest of the line
178     by a space instead of a null byte. Thus want lines have the form:
179
180         want obj-id cap1 cap2 ...
181
182     :param text: Want line to extract from
183     :return: Tuple with text with capabilities removed and list of capabilities
184     """
185     split_text = text.rstrip().split(" ")
186     if len(split_text) < 3:
187         return text, []
188     return (" ".join(split_text[:2]), split_text[2:])
189
190
191 def ack_type(capabilities):
192     """Extract the ack type from a capabilities list."""
193     if 'multi_ack' in capabilities:
194         return MULTI_ACK
195     return SINGLE_ACK