Start refactoring to handle the capability exchange
[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         return "multi_ack side-band-64k thin-pack ofs-delta"
104
105     def handshake(self, blob):
106         """
107         Compare remote capabilites with our own and alter protocol accordingly
108
109         :param blob: space seperated list of capabilities (i.e. wire format)
110         """
111         if not "\x00" in blob:
112             return blob
113         blob, caps = blob.split("\x00")
114
115         caps = caps.split()
116
117         return blob
118
119     def handle(self):
120         """
121         Deal with the request
122         """
123         raise NotImplementedError
124
125
126 class UploadPackHandler(Handler):
127
128     def handle(self):
129         refs = self.backend.get_refs()
130
131         if refs:
132             self.write_pkt_line("%s %s\x00%s\n" % (refs[0][1], refs[0][0], self.capabilities()))
133             for i in range(1, len(refs)):
134                 ref = refs[i]
135                 self.write_pkt_line("%s %s\n" % (ref[1], ref[0]))
136
137         # i'm done...
138         self.write("0000")
139
140         # Now client will either send "0000", meaning that it doesnt want to pull.
141         # or it will start sending want want want commands
142         want = self.read_pkt_line()
143         if want == None:
144             return
145
146         want = self.handshake(want)
147
148         # Keep reading the list of demands until we hit another "0000" 
149         want_revs = []
150         while want and want[:4] == 'want':
151             want_rev = want[5:45]
152             # FIXME: This check probably isnt needed?
153             if self.backend.has_revision(want_rev):
154                want_revs.append(want_rev)
155             want = self.read_pkt_line()
156         
157         # Client will now tell us which commits it already has - if we have them we ACK them
158         # this allows client to stop looking at that commits parents (main reason why git pull is fast)
159         last_sha = None
160         have_revs = []
161         have = self.read_pkt_line()
162         while have and have[:4] == 'have':
163             have_ref = have[6:46]
164             if self.backend.has_revision(hav_rev):
165                 self.write_pkt_line("ACK %s continue\n" % sha)
166                 last_sha = sha
167                 have_revs.append(rev_id)
168             have = self.read_pkt_line()
169
170         # At some point client will stop sending commits and will tell us it is done
171         assert(have[:4] == "done")
172
173         # Oddness: Git seems to resend the last ACK, without the "continue" statement
174         if last_sha:
175             self.write_pkt_line("ACK %s\n" % last_sha)
176
177         # The exchange finishes with a NAK
178         self.write_pkt_line("NAK\n")
179       
180         self.backend.generate_pack(want_revs, have_revs, lambda x: self.write_sideband(1, x), lambda x: self.write_sideband(2, x))
181
182         # we are done
183         self.write("0000")
184
185
186 class ReceivePackHandler(Handler):
187
188     def handle(self):
189         refs = self.backend.get_refs()
190
191         if refs:
192             self.write_pkt_line("%s %s\x00%s\n" % (refs[0][1], refs[0][0], self.capabilities()))
193             for i in range(1, len(refs)):
194                 ref = refs[i]
195                 self.write_pkt_line("%s %s\n" % (ref[1], ref[0]))
196
197         self.write("0000")
198
199         client_refs = []
200         ref = self.read_pkt_line()
201
202         # if ref is none then client doesnt want to send us anything..
203         if ref is None:
204             return
205
206         ref = self.handshake(ref)
207
208         # client will now send us a list of (oldsha, newsha, ref)
209         while ref:
210             client_refs.append(ref.split())
211             ref = self.read_pkt_line()
212
213         # backend can now deal with this refs and read a pack using self.read
214         self.backend.apply_pack(client_refs, self.read)
215
216         # when we have read all the pack from the client, it assumes everything worked OK
217         # there is NO ack from the server before it reports victory.
218
219
220 class TCPGitRequestHandler(SocketServer.StreamRequestHandler, Handler):
221
222     def __init__(self, request, client_address, server):
223         SocketServer.StreamRequestHandler.__init__(self, request, client_address, server)
224
225     def handle(self):
226         #FIXME: StreamRequestHandler seems to be the thing that calls handle(),
227         #so we can't call this in a sane place??
228         Handler.__init__(self, self.server.backend, self.rfile.read, self.wfile.write)
229
230         request = self.read_pkt_line()
231
232         # up until the space is the command to run, everything after is parameters
233         splice_point = request.find(' ')
234         command, params = request[:splice_point], request[splice_point+1:]
235
236         # params are null seperated
237         params = params.split(chr(0))
238
239         # switch case to handle the specific git command
240         if command == 'git-upload-pack':
241             cls = UploadPackHandler
242         elif command == 'git-receive-pack':
243             cls = ReceivePackHandler
244         else:
245             return
246
247         h = cls(self.backend, self.read, self.write)
248         h.handle()
249
250
251 class TCPGitServer(SocketServer.TCPServer):
252
253     allow_reuse_address = True
254     serve = SocketServer.TCPServer.serve_forever
255
256     def __init__(self, backend, addr):
257         self.backend = backend
258         SocketServer.TCPServer.__init__(self, addr, TCPGitRequestHandler)
259
260