1 # server.py -- Implementation of the server side git protocols
2 # Copryight (C) 2008 John Carr <john.carr@unrouted.co.uk>
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
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.
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,
21 class Backend(object):
25 Get all the refs in the repository
27 :return: list of tuple(name, sha)
29 raise NotImplementedError
31 def has_revision(self, sha):
33 Is a given sha in this repository?
35 :return: True or False
37 raise NotImplementedError
39 def apply_pack(self, refs, read):
40 """ Import a set of changes into a repository and update the refs
42 :param refs: list of tuple(name, sha)
43 :param read: callback to read from the incoming pack
45 raise NotImplementedError
47 def generate_pack(self, want, have, write, progress):
49 Generate a pack containing all commits a client is missing
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
56 raise NotImplementedError
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
63 class PackWriteWrapper(object):
65 def __init__(self, write):
69 def write(self, blob):
78 return self.sha.digest()
80 class GitBackend(Backend):
82 def __init__(self, gitdir=None):
86 self.gitdir = tempfile.mkdtemp()
87 Repo.create(self.gitdir)
89 self.repo = Repo(self.gitdir)
94 refs.append(('HEAD', self.repo.head()))
95 for ref, sha in self.repo.heads().items():
96 refs.append(('refs/heads/'+ref,sha))
99 def has_revision(self, sha):
100 return self.repo.get_object(sha) != None
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())
108 # strip '.pack' off our filename
111 # generate an index for it
113 pd.create_index_v2(basename+".idx")
115 for oldsha, sha, ref in refs:
117 self.repo.remove_ref(ref)
119 self.repo.set_ref(ref, sha)
123 def generate_pack(self, want, have, write, progress):
124 progress("dul-daemon says what\n")
128 commits_to_send = want[:]
129 for sha in commits_to_send:
133 sha_queue.append((1,sha))
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)
140 def parse_tree(tree, sha_queue):
141 for mode, name, x in tree.entries():
142 if not x in sha_queue:
144 t = self.repo.get_tree(x)
145 sha_queue.append((2, x))
146 parse_tree(t, sha_queue)
148 sha_queue.append((3, x))
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)
156 progress("counting objects: %d\r" % len(sha_queue))
158 progress("counting objects: %d, done.\n" % len(sha_queue))
160 write_pack_data(write, (self.repo.get_object(sha).as_raw_string() for sha in sha_queue))
162 progress("how was that, then?\n")
165 class Handler(object):
167 def __init__(self, backend, read, write):
168 self.backend = backend
172 def read_pkt_line(self):
174 Reads a 'pkt line' from the remote git process
176 :return: The next string from the stream
178 sizestr = self.read(4)
181 size = int(sizestr, 16)
184 return self.read(size-4)
186 def write_pkt_line(self, line):
188 Sends a 'pkt line' to the remote git process
190 :param line: A string containing the data to send
192 self.write("%04x%s" % (len(line)+4, line))
194 def write_sideband(self, channel, blob):
196 Write data to the sideband (a git multiplexing method)
198 :param channel: int specifying which channel to write to
199 :param blob: a blob of data (as a string) to send on this channel
201 # a pktline can be a max of 65535. a sideband line can therefore be
203 # WTF: Why have the len in ASCII, but the channel in binary.
205 self.write_pkt_line("%s%s" % (chr(channel), blob[:65530]))
208 def capabilities(self):
209 return " ".join(self.default_capabilities())
211 def handshake(self, blob):
213 Compare remote capabilites with our own and alter protocol accordingly
215 :param blob: space seperated list of capabilities (i.e. wire format)
217 if not "\x00" in blob:
219 blob, caps = blob.split("\x00")
221 # FIXME: Do something with this..
228 Deal with the request
230 raise NotImplementedError
233 class UploadPackHandler(Handler):
235 def default_capabilities(self):
236 return ("multi_ack", "side-band-64k", "thin-pack", "ofs-delta")
239 refs = self.backend.get_refs()
242 self.write_pkt_line("%s %s\x00%s\n" % (refs[0][1], refs[0][0], self.capabilities()))
243 for i in range(1, len(refs)):
245 self.write_pkt_line("%s %s\n" % (ref[1], ref[0]))
250 # Now client will either send "0000", meaning that it doesnt want to pull.
251 # or it will start sending want want want commands
252 want = self.read_pkt_line()
256 want = self.handshake(want)
258 # Keep reading the list of demands until we hit another "0000"
260 while want and want[:4] == 'want':
261 want_rev = want[5:45]
262 # FIXME: This check probably isnt needed?
263 if self.backend.has_revision(want_rev):
264 want_revs.append(want_rev)
265 want = self.read_pkt_line()
267 # Client will now tell us which commits it already has - if we have them we ACK them
268 # this allows client to stop looking at that commits parents (main reason why git pull is fast)
271 have = self.read_pkt_line()
272 while have and have[:4] == 'have':
273 have_ref = have[6:46]
274 if self.backend.has_revision(have_ref):
275 self.write_pkt_line("ACK %s continue\n" % have_ref)
277 have_revs.append(rev_id)
278 have = self.read_pkt_line()
280 # At some point client will stop sending commits and will tell us it is done
281 assert(have[:4] == "done")
283 # Oddness: Git seems to resend the last ACK, without the "continue" statement
285 self.write_pkt_line("ACK %s\n" % last_sha)
287 # The exchange finishes with a NAK
288 self.write_pkt_line("NAK\n")
290 self.backend.generate_pack(want_revs, have_revs, lambda x: self.write_sideband(1, x), lambda x: self.write_sideband(2, x))
296 class ReceivePackHandler(Handler):
298 def default_capabilities(self):
299 return ("report-status", "delete-refs")
302 refs = self.backend.get_refs()
305 self.write_pkt_line("%s %s\x00%s\n" % (refs[0][1], refs[0][0], self.capabilities()))
306 for i in range(1, len(refs)):
308 self.write_pkt_line("%s %s\n" % (ref[1], ref[0]))
310 self.write_pkt_line("0000000000000000000000000000000000000000 capabilities^{} %s" % self.capabilities())
315 ref = self.read_pkt_line()
317 # if ref is none then client doesnt want to send us anything..
321 ref = self.handshake(ref)
323 # client will now send us a list of (oldsha, newsha, ref)
325 client_refs.append(ref.split())
326 ref = self.read_pkt_line()
328 # backend can now deal with this refs and read a pack using self.read
329 self.backend.apply_pack(client_refs, self.read)
331 # when we have read all the pack from the client, it assumes everything worked OK
332 # there is NO ack from the server before it reports victory.
335 class TCPGitRequestHandler(SocketServer.StreamRequestHandler, Handler):
337 def __init__(self, request, client_address, server):
338 SocketServer.StreamRequestHandler.__init__(self, request, client_address, server)
341 #FIXME: StreamRequestHandler seems to be the thing that calls handle(),
342 #so we can't call this in a sane place??
343 Handler.__init__(self, self.server.backend, self.rfile.read, self.wfile.write)
345 request = self.read_pkt_line()
347 # up until the space is the command to run, everything after is parameters
348 splice_point = request.find(' ')
349 command, params = request[:splice_point], request[splice_point+1:]
351 # params are null seperated
352 params = params.split(chr(0))
354 # switch case to handle the specific git command
355 if command == 'git-upload-pack':
356 cls = UploadPackHandler
357 elif command == 'git-receive-pack':
358 cls = ReceivePackHandler
362 h = cls(self.backend, self.read, self.write)
366 class TCPGitServer(SocketServer.TCPServer):
368 allow_reuse_address = True
369 serve = SocketServer.TCPServer.serve_forever
371 def __init__(self, backend, addr):
372 self.backend = backend
373 SocketServer.TCPServer.__init__(self, addr, TCPGitRequestHandler)