Send caps when no refs
[jelmer/dulwich-libgit2.git] / dulwich / server.py
1 # server.py -- Implementation of the server side git protocols
2 # Copryight (C) 2008 John Carr <john.carr@unrouted.co.uk>
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; version 2
7 # of the License.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
17 # MA  02110-1301, USA.
18
19 import SocketServer
20
21 class Backend(object):
22
23     def get_refs(self):
24         """
25         Get all the refs in the repository
26
27         :return: list of tuple(name, sha)
28         """
29         raise NotImplementedError
30
31     def has_revision(self, sha):
32         """
33         Is a given sha in this repository?
34
35         :return: True or False
36         """
37         raise NotImplementedError
38
39     def apply_pack(self, refs, read):
40         """ Import a set of changes into a repository and update the refs
41
42         :param refs: list of tuple(name, sha)
43         :param read: callback to read from the incoming pack
44         """
45         raise NotImplementedError
46
47     def generate_pack(self, want, have, write, progress):
48         """
49         Generate a pack containing all commits a client is missing
50
51         :param want: is a list of sha's the client desires
52         :param have: is a list of sha's the client has (allowing us to send the minimal pack)
53         :param write: is a callback to write pack data to the client
54         :param progress: is a callback to send progress messages to the client
55         """
56         raise NotImplementedError
57
58
59 class Handler(object):
60
61     def __init__(self, backend, read, write):
62         self.backend = backend
63         self.read = read
64         self.write = write
65
66     def read_pkt_line(self):
67         """
68         Reads a 'pkt line' from the remote git process
69
70         :return: The next string from the stream
71         """
72         sizestr = self.read(4)
73         if not sizestr:
74             return None
75         size = int(sizestr, 16)
76         if size == 0:
77             return None
78         return self.read(size-4)
79
80     def write_pkt_line(self, line):
81         """
82         Sends a 'pkt line' to the remote git process
83
84         :param line: A string containing the data to send
85         """
86         self.write("%04x%s" % (len(line)+4, line))
87
88     def write_sideband(self, channel, blob):
89         """
90         Write data to the sideband (a git multiplexing method)
91
92         :param channel: int specifying which channel to write to
93         :param blob: a blob of data (as a string) to send on this channel
94         """
95         # a pktline can be a max of 65535. a sideband line can therefore be
96         # 65535-5 = 65530
97         # WTF: Why have the len in ASCII, but the channel in binary.
98         while blob:
99             self.write_pkt_line("%s%s" % (chr(channel), blob[:65530]))
100             blob = blob[65530:]
101
102     def capabilities(self):
103         # FIXME: Capabilities are different for pushing...
104         return "multi_ack side-band-64k thin-pack ofs-delta"
105
106     def handshake(self, blob):
107         """
108         Compare remote capabilites with our own and alter protocol accordingly
109
110         :param blob: space seperated list of capabilities (i.e. wire format)
111         """
112         if not "\x00" in blob:
113             return blob
114         blob, caps = blob.split("\x00")
115
116         # FIXME: Do something with this..
117         caps = caps.split()
118
119         return blob
120
121     def handle(self):
122         """
123         Deal with the request
124         """
125         raise NotImplementedError
126
127
128 class UploadPackHandler(Handler):
129
130     def handle(self):
131         refs = self.backend.get_refs()
132
133         if refs:
134             self.write_pkt_line("%s %s\x00%s\n" % (refs[0][1], refs[0][0], self.capabilities()))
135             for i in range(1, len(refs)):
136                 ref = refs[i]
137                 self.write_pkt_line("%s %s\n" % (ref[1], ref[0]))
138
139         # i'm done...
140         self.write("0000")
141
142         # Now client will either send "0000", meaning that it doesnt want to pull.
143         # or it will start sending want want want commands
144         want = self.read_pkt_line()
145         if want == None:
146             return
147
148         want = self.handshake(want)
149
150         # Keep reading the list of demands until we hit another "0000" 
151         want_revs = []
152         while want and want[:4] == 'want':
153             want_rev = want[5:45]
154             # FIXME: This check probably isnt needed?
155             if self.backend.has_revision(want_rev):
156                want_revs.append(want_rev)
157             want = self.read_pkt_line()
158         
159         # Client will now tell us which commits it already has - if we have them we ACK them
160         # this allows client to stop looking at that commits parents (main reason why git pull is fast)
161         last_sha = None
162         have_revs = []
163         have = self.read_pkt_line()
164         while have and have[:4] == 'have':
165             have_ref = have[6:46]
166             if self.backend.has_revision(hav_rev):
167                 self.write_pkt_line("ACK %s continue\n" % sha)
168                 last_sha = sha
169                 have_revs.append(rev_id)
170             have = self.read_pkt_line()
171
172         # At some point client will stop sending commits and will tell us it is done
173         assert(have[:4] == "done")
174
175         # Oddness: Git seems to resend the last ACK, without the "continue" statement
176         if last_sha:
177             self.write_pkt_line("ACK %s\n" % last_sha)
178
179         # The exchange finishes with a NAK
180         self.write_pkt_line("NAK\n")
181       
182         self.backend.generate_pack(want_revs, have_revs, lambda x: self.write_sideband(1, x), lambda x: self.write_sideband(2, x))
183
184         # we are done
185         self.write("0000")
186
187
188 class ReceivePackHandler(Handler):
189
190     def handle(self):
191         refs = self.backend.get_refs()
192
193         if refs:
194             self.write_pkt_line("%s %s\x00%s\n" % (refs[0][1], refs[0][0], self.capabilities()))
195             for i in range(1, len(refs)):
196                 ref = refs[i]
197                 self.write_pkt_line("%s %s\n" % (ref[1], ref[0]))
198         else:
199             self.write_pkt_line("0000000000000000000000000000000000000000 capabilities^{} %s" % self.capabilities())
200
201         self.write("0000")
202
203         client_refs = []
204         ref = self.read_pkt_line()
205
206         # if ref is none then client doesnt want to send us anything..
207         if ref is None:
208             return
209
210         ref = self.handshake(ref)
211
212         # client will now send us a list of (oldsha, newsha, ref)
213         while ref:
214             client_refs.append(ref.split())
215             ref = self.read_pkt_line()
216
217         # backend can now deal with this refs and read a pack using self.read
218         self.backend.apply_pack(client_refs, self.read)
219
220         # when we have read all the pack from the client, it assumes everything worked OK
221         # there is NO ack from the server before it reports victory.
222
223
224 class TCPGitRequestHandler(SocketServer.StreamRequestHandler, Handler):
225
226     def __init__(self, request, client_address, server):
227         SocketServer.StreamRequestHandler.__init__(self, request, client_address, server)
228
229     def handle(self):
230         #FIXME: StreamRequestHandler seems to be the thing that calls handle(),
231         #so we can't call this in a sane place??
232         Handler.__init__(self, self.server.backend, self.rfile.read, self.wfile.write)
233
234         request = self.read_pkt_line()
235
236         # up until the space is the command to run, everything after is parameters
237         splice_point = request.find(' ')
238         command, params = request[:splice_point], request[splice_point+1:]
239
240         # params are null seperated
241         params = params.split(chr(0))
242
243         # switch case to handle the specific git command
244         if command == 'git-upload-pack':
245             cls = UploadPackHandler
246         elif command == 'git-receive-pack':
247             cls = ReceivePackHandler
248         else:
249             return
250
251         h = cls(self.backend, self.read, self.write)
252         h.handle()
253
254
255 class TCPGitServer(SocketServer.TCPServer):
256
257     allow_reuse_address = True
258     serve = SocketServer.TCPServer.serve_forever
259
260     def __init__(self, backend, addr):
261         self.backend = backend
262         SocketServer.TCPServer.__init__(self, addr, TCPGitRequestHandler)
263
264