Strip excess whitespace from capabilities lines.
[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 ZERO_SHA = "0" * 40
32
33 SINGLE_ACK = 0
34 MULTI_ACK = 1
35 MULTI_ACK_DETAILED = 2
36
37 class ProtocolFile(object):
38     """
39     Some network ops are like file ops. The file ops expect to operate on
40     file objects, so provide them with a dummy file.
41     """
42
43     def __init__(self, read, write):
44         self.read = read
45         self.write = write
46
47     def tell(self):
48         pass
49
50     def close(self):
51         pass
52
53
54 class Protocol(object):
55
56     def __init__(self, read, write, report_activity=None):
57         self.read = read
58         self.write = write
59         self.report_activity = report_activity
60
61     def read_pkt_line(self):
62         """
63         Reads a 'pkt line' from the remote git process
64
65         :return: The next string from the stream
66         """
67         try:
68             sizestr = self.read(4)
69             if not sizestr:
70                 raise HangupException()
71             size = int(sizestr, 16)
72             if size == 0:
73                 if self.report_activity:
74                     self.report_activity(4, 'read')
75                 return None
76             if self.report_activity:
77                 self.report_activity(size, 'read')
78             return self.read(size-4)
79         except socket.error, e:
80             raise GitProtocolError(e)
81
82     def read_pkt_seq(self):
83         pkt = self.read_pkt_line()
84         while pkt:
85             yield pkt
86             pkt = self.read_pkt_line()
87
88     def write_pkt_line(self, line):
89         """
90         Sends a 'pkt line' to the remote git process
91
92         :param line: A string containing the data to send
93         """
94         try:
95             if line is None:
96                 self.write("0000")
97                 if self.report_activity:
98                     self.report_activity(4, 'write')
99             else:
100                 self.write("%04x%s" % (len(line)+4, line))
101                 if self.report_activity:
102                     self.report_activity(4+len(line), 'write')
103         except socket.error, e:
104             raise GitProtocolError(e)
105
106     def write_file(self):
107         class ProtocolFile(object):
108
109             def __init__(self, proto):
110                 self._proto = proto
111                 self._offset = 0
112
113             def write(self, data):
114                 self._proto.write(data)
115                 self._offset += len(data)
116
117             def tell(self):
118                 return self._offset
119
120             def close(self):
121                 pass
122
123         return ProtocolFile(self)
124
125     def write_sideband(self, channel, blob):
126         """
127         Write data to the sideband (a git multiplexing method)
128
129         :param channel: int specifying which channel to write to
130         :param blob: a blob of data (as a string) to send on this channel
131         """
132         # a pktline can be a max of 65520. a sideband line can therefore be
133         # 65520-5 = 65515
134         # WTF: Why have the len in ASCII, but the channel in binary.
135         while blob:
136             self.write_pkt_line("%s%s" % (chr(channel), blob[:65515]))
137             blob = blob[65515:]
138
139     def send_cmd(self, cmd, *args):
140         """
141         Send a command and some arguments to a git server
142
143         Only used for git://
144
145         :param cmd: The remote service to access
146         :param args: List of arguments to send to remove service
147         """
148         self.write_pkt_line("%s %s" % (cmd, "".join(["%s\0" % a for a in args])))
149
150     def read_cmd(self):
151         """
152         Read a command and some arguments from the git client
153
154         Only used for git://
155
156         :return: A tuple of (command, [list of arguments])
157         """
158         line = self.read_pkt_line()
159         splice_at = line.find(" ")
160         cmd, args = line[:splice_at], line[splice_at+1:]
161         assert args[-1] == "\x00"
162         return cmd, args[:-1].split(chr(0))
163
164
165 def extract_capabilities(text):
166     """Extract a capabilities list from a string, if present.
167
168     :param text: String to extract from
169     :return: Tuple with text with capabilities removed and list of capabilities
170     """
171     if not "\0" in text:
172         return text, []
173     text, capabilities = text.rstrip().split("\0")
174     return (text, capabilities.strip().split(" "))
175
176
177 def extract_want_line_capabilities(text):
178     """Extract a capabilities list from a want line, if present.
179
180     Note that want lines have capabilities separated from the rest of the line
181     by a space instead of a null byte. Thus want lines have the form:
182
183         want obj-id cap1 cap2 ...
184
185     :param text: Want line to extract from
186     :return: Tuple with text with capabilities removed and list of capabilities
187     """
188     split_text = text.rstrip().split(" ")
189     if len(split_text) < 3:
190         return text, []
191     return (" ".join(split_text[:2]), split_text[2:])
192
193
194 def ack_type(capabilities):
195     """Extract the ack type from a capabilities list."""
196     if 'multi_ack_detailed' in capabilities:
197       return MULTI_ACK_DETAILED
198     elif 'multi_ack' in capabilities:
199         return MULTI_ACK
200     return SINGLE_ACK