Merge upstream
[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 from dulwich.repo import Repo
59 from dulwich.pack import PackData, Pack
60 import sha, tempfile, os
61 from dulwich.pack import write_pack_object
62
63 class PackWriteWrapper(object):
64
65     def __init__(self, write):
66         self.writefn = write
67         self.sha = sha.sha()
68
69     def write(self, blob):
70         self.sha.update(blob)
71         self.writefn(blob)
72
73     def tell(self):
74         pass
75
76     @property
77     def digest(self):
78         return self.sha.digest()
79
80 class GitBackend(Backend):
81
82     def __init__(self, gitdir=None):
83         self.gitdir = gitdir
84
85         if not self.gitdir:
86             self.gitdir = tempfile.mkdtemp()
87             Repo.create(self.gitdir)
88
89         self.repo = Repo(self.gitdir)
90
91     def get_refs(self):
92         refs = []
93         if self.repo.head():
94             refs.append(('HEAD', self.repo.head()))
95         for ref, sha in self.repo.heads().items():
96             refs.append(('refs/heads/'+ref,sha))
97         return refs
98
99     def has_revision(self, sha):
100         return self.repo.get_object(sha) != None
101
102     def apply_pack(self, refs, read):
103         # store the incoming pack in the repository
104         fd, name = tempfile.mkstemp(suffix='.pack', prefix='', dir=self.repo.pack_dir())
105         os.write(fd, read())
106         os.close(fd)
107
108         # strip '.pack' off our filename
109         basename = name[:-5]
110
111         # generate an index for it
112         pd = PackData(name)
113         pd.create_index_v2(basename+".idx")
114
115         for oldsha, sha, ref in refs:
116             if ref == "0" * 40:
117                 self.repo.remove_ref(ref)
118             else:
119                 self.repo.set_ref(ref, sha)
120
121         print "pack applied"
122
123     def generate_pack(self, want, have, write, progress):
124         progress("dul-daemon says what\n")
125
126         sha_queue = []
127
128         commits_to_send = want[:]
129         for sha in commits_to_send:
130             if sha in sha_queue:
131                 continue
132
133             sha_queue.append((1,sha))
134
135             c = self.repo.commit(sha)
136             for p in c.parents():
137                 if not p in commits_to_send:
138                     commits_to_send.append(p)
139
140             def parse_tree(tree, sha_queue):
141                 for mode, name, x in tree.entries():
142                     if not x in sha_queue:
143                         try:
144                             t = self.repo.get_tree(x)
145                             sha_queue.append((2, x))
146                             parse_tree(t, sha_queue)
147                         except:
148                             sha_queue.append((3, x))
149
150             treesha = c.tree()
151             if treesha not in sha_queue:
152                 sha_queue.append((2, treesha))
153                 t = self.repo.get_tree(treesha)
154                 parse_tree(t, sha_queue)
155
156             progress("counting objects: %d\r" % len(sha_queue))
157
158         progress("counting objects: %d, done.\n" % len(sha_queue))
159
160         w = PackWriteWrapper(write)
161         w.write("PACK")
162         w.write(struct.pack(">L", 2))
163         w.write(struct.pack(">L", len(sha_queue)))
164
165         for t, sha in sha_queue:
166             ty, obj = self.repo.get_object(sha).as_raw_string()
167             write_pack_object(w, t, obj)
168
169         # send sha1 of pack
170         write(w.digest)
171
172         progress("how was that, then?\n")
173
174
175 class Handler(object):
176
177     def __init__(self, backend, read, write):
178         self.backend = backend
179         self.read = read
180         self.write = write
181
182     def read_pkt_line(self):
183         """
184         Reads a 'pkt line' from the remote git process
185
186         :return: The next string from the stream
187         """
188         sizestr = self.read(4)
189         if not sizestr:
190             return None
191         size = int(sizestr, 16)
192         if size == 0:
193             return None
194         return self.read(size-4)
195
196     def write_pkt_line(self, line):
197         """
198         Sends a 'pkt line' to the remote git process
199
200         :param line: A string containing the data to send
201         """
202         self.write("%04x%s" % (len(line)+4, line))
203
204     def write_sideband(self, channel, blob):
205         """
206         Write data to the sideband (a git multiplexing method)
207
208         :param channel: int specifying which channel to write to
209         :param blob: a blob of data (as a string) to send on this channel
210         """
211         # a pktline can be a max of 65535. a sideband line can therefore be
212         # 65535-5 = 65530
213         # WTF: Why have the len in ASCII, but the channel in binary.
214         while blob:
215             self.write_pkt_line("%s%s" % (chr(channel), blob[:65530]))
216             blob = blob[65530:]
217
218     def capabilities(self):
219         return " ".join(self.default_capabilities())
220
221     def handshake(self, blob):
222         """
223         Compare remote capabilites with our own and alter protocol accordingly
224
225         :param blob: space seperated list of capabilities (i.e. wire format)
226         """
227         if not "\x00" in blob:
228             return blob
229         blob, caps = blob.split("\x00")
230
231         # FIXME: Do something with this..
232         caps = caps.split()
233
234         return blob
235
236     def handle(self):
237         """
238         Deal with the request
239         """
240         raise NotImplementedError
241
242
243 class UploadPackHandler(Handler):
244
245     def default_capabilities(self):
246         return ("multi_ack", "side-band-64k", "thin-pack", "ofs-delta")
247
248     def handle(self):
249         refs = self.backend.get_refs()
250
251         if refs:
252             self.write_pkt_line("%s %s\x00%s\n" % (refs[0][1], refs[0][0], self.capabilities()))
253             for i in range(1, len(refs)):
254                 ref = refs[i]
255                 self.write_pkt_line("%s %s\n" % (ref[1], ref[0]))
256
257         # i'm done...
258         self.write("0000")
259
260         # Now client will either send "0000", meaning that it doesnt want to pull.
261         # or it will start sending want want want commands
262         want = self.read_pkt_line()
263         if want == None:
264             return
265
266         want = self.handshake(want)
267
268         # Keep reading the list of demands until we hit another "0000" 
269         want_revs = []
270         while want and want[:4] == 'want':
271             want_rev = want[5:45]
272             # FIXME: This check probably isnt needed?
273             if self.backend.has_revision(want_rev):
274                want_revs.append(want_rev)
275             want = self.read_pkt_line()
276         
277         # Client will now tell us which commits it already has - if we have them we ACK them
278         # this allows client to stop looking at that commits parents (main reason why git pull is fast)
279         last_sha = None
280         have_revs = []
281         have = self.read_pkt_line()
282         while have and have[:4] == 'have':
283             have_ref = have[6:46]
284             if self.backend.has_revision(hav_rev):
285                 self.write_pkt_line("ACK %s continue\n" % sha)
286                 last_sha = sha
287                 have_revs.append(rev_id)
288             have = self.read_pkt_line()
289
290         # At some point client will stop sending commits and will tell us it is done
291         assert(have[:4] == "done")
292
293         # Oddness: Git seems to resend the last ACK, without the "continue" statement
294         if last_sha:
295             self.write_pkt_line("ACK %s\n" % last_sha)
296
297         # The exchange finishes with a NAK
298         self.write_pkt_line("NAK\n")
299       
300         self.backend.generate_pack(want_revs, have_revs, lambda x: self.write_sideband(1, x), lambda x: self.write_sideband(2, x))
301
302         # we are done
303         self.write("0000")
304
305
306 class ReceivePackHandler(Handler):
307
308     def default_capabilities(self):
309         return ("report-status", "delete-refs")
310
311     def handle(self):
312         refs = self.backend.get_refs()
313
314         if refs:
315             self.write_pkt_line("%s %s\x00%s\n" % (refs[0][1], refs[0][0], self.capabilities()))
316             for i in range(1, len(refs)):
317                 ref = refs[i]
318                 self.write_pkt_line("%s %s\n" % (ref[1], ref[0]))
319         else:
320             self.write_pkt_line("0000000000000000000000000000000000000000 capabilities^{} %s" % self.capabilities())
321
322         self.write("0000")
323
324         client_refs = []
325         ref = self.read_pkt_line()
326
327         # if ref is none then client doesnt want to send us anything..
328         if ref is None:
329             return
330
331         ref = self.handshake(ref)
332
333         # client will now send us a list of (oldsha, newsha, ref)
334         while ref:
335             client_refs.append(ref.split())
336             ref = self.read_pkt_line()
337
338         # backend can now deal with this refs and read a pack using self.read
339         self.backend.apply_pack(client_refs, self.read)
340
341         # when we have read all the pack from the client, it assumes everything worked OK
342         # there is NO ack from the server before it reports victory.
343
344
345 class TCPGitRequestHandler(SocketServer.StreamRequestHandler, Handler):
346
347     def __init__(self, request, client_address, server):
348         SocketServer.StreamRequestHandler.__init__(self, request, client_address, server)
349
350     def handle(self):
351         #FIXME: StreamRequestHandler seems to be the thing that calls handle(),
352         #so we can't call this in a sane place??
353         Handler.__init__(self, self.server.backend, self.rfile.read, self.wfile.write)
354
355         request = self.read_pkt_line()
356
357         # up until the space is the command to run, everything after is parameters
358         splice_point = request.find(' ')
359         command, params = request[:splice_point], request[splice_point+1:]
360
361         # params are null seperated
362         params = params.split(chr(0))
363
364         # switch case to handle the specific git command
365         if command == 'git-upload-pack':
366             cls = UploadPackHandler
367         elif command == 'git-receive-pack':
368             cls = ReceivePackHandler
369         else:
370             return
371
372         h = cls(self.backend, self.read, self.write)
373         h.handle()
374
375
376 class TCPGitServer(SocketServer.TCPServer):
377
378     allow_reuse_address = True
379     serve = SocketServer.TCPServer.serve_forever
380
381     def __init__(self, backend, addr):
382         self.backend = backend
383         SocketServer.TCPServer.__init__(self, addr, TCPGitRequestHandler)
384
385