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
59 class Handler(object):
61 def __init__(self, backend, read, write):
62 self.backend = backend
66 def read_pkt_line(self):
68 Reads a 'pkt line' from the remote git process
70 :return: The next string from the stream
72 sizestr = self.read(4)
75 size = int(sizestr, 16)
78 return self.read(size-4)
80 def write_pkt_line(self, line):
82 Sends a 'pkt line' to the remote git process
84 :param line: A string containing the data to send
86 self.write("%04x%s" % (len(line)+4, line))
88 def write_sideband(self, channel, blob):
90 Write data to the sideband (a git multiplexing method)
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
95 # a pktline can be a max of 65535. a sideband line can therefore be
97 # WTF: Why have the len in ASCII, but the channel in binary.
99 self.write_pkt_line("%s%s" % (chr(channel), blob[:65530]))
104 Deal with the request
106 raise NotImplementedError
109 class UploadPackHandler(Handler):
112 refs = self.backend.get_refs()
115 self.write_pkt_line("%s %s\x00multi_ack side-band-64k thin-pack ofs-delta\n" % (refs[0][1], refs[0][0]))
116 for i in range(1, len(refs)):
118 self.write_pkt_line("%s %s\n" % (ref[1], ref[0]))
123 # Now client will either send "0000", meaning that it doesnt want to pull.
124 # or it will start sending want want want commands
125 want = self.read_pkt_line()
129 # Keep reading the list of demands until we hit another "0000"
131 while want and want[:4] == 'want':
132 want_rev = want[5:40]
133 # FIXME: This check probably isnt needed?
134 if self.backend.has_revision(want_rev):
135 want_revs.append(want_rev)
136 want = self.read_pkt_line()
138 # Client will now tell us which commits it already has - if we have them we ACK them
139 # this allows client to stop looking at that commits parents (main reason why git pull is fast)
142 have = self.read_pkt_line()
143 while have and have[:4] == 'have':
144 have_ref = have[6:40]
145 if self.backend.has_revision(hav_rev):
146 self.write_pkt_line("ACK %s continue\n" % sha)
148 have_revs.append(rev_id)
149 have = self.read_pkt_line()
151 # At some point client will stop sending commits and will tell us it is done
152 assert(have[:4] == "done")
154 # Oddness: Git seems to resend the last ACK, without the "continue" statement
156 self.write_pkt_line("ACK %s\n" % last_sha)
158 # The exchange finishes with a NAK
159 self.write_pkt_line("NAK\n")
161 #if True: # False: #self.no_progress == False:
162 # self.write_sideband(2, "Bazaar is preparing your pack, plz hold.\n")
164 # for x in range(1,200)
165 # self.write_sideband(2, "Counting objects: %d\x0d" % x*2)
166 # self.write_sideband(2, "Counting objects: 200, done.\n")
168 # for x in range(1,100):
169 # self.write_sideband(2, "Compressiong objects: %d (%d/%d)\x0d" % (x, x*2, 200))
170 # self.write_sideband(2, "Compressing objects: 100% (200/200), done.\n")
172 self.backend.generate_pack(want_revs, have_revs, self.write, None)
175 class ReceivePackHandler(Handler):
178 refs = self.backend.get_refs()
181 self.write_pkt_line("%s %s\x00multi_ack side-band-64k thin-pack ofs-delta\n" % (refs[0][1], refs[0][0]))
182 for i in range(1, len(refs)):
184 self.write_pkt_line("%s %s\n" % (ref[1], ref[0]))
189 ref = self.read_pkt_line()
191 client_refs.append(ref.split())
192 ref = self.read_pkt_line()
194 if len(client_refs) == 0:
197 self.backend.apply_pack(client_refs, self.read)
200 class TCPGitRequestHandler(SocketServer.StreamRequestHandler, Handler):
202 def __init__(self, request, client_address, server):
203 SocketServer.StreamRequestHandler.__init__(self, request, client_address, server)
206 #FIXME: StreamRequestHandler seems to be the thing that calls handle(),
207 #so we can't call this in a sane place??
208 Handler.__init__(self, self.server.backend, self.rfile.read, self.wfile.write)
210 request = self.read_pkt_line()
212 # up until the space is the command to run, everything after is parameters
213 splice_point = request.find(' ')
214 command, params = request[:splice_point], request[splice_point+1:]
216 # params are null seperated
217 params = params.split(chr(0))
219 # switch case to handle the specific git command
220 if command == 'git-upload-pack':
221 cls = UploadPackHandler
222 elif command == 'git-receive-pack':
223 cls = ReceivePackHandler
227 h = cls(self.backend, self.read, self.write)
231 class TCPGitServer(SocketServer.TCPServer):
233 allow_reuse_address = True
234 serve = SocketServer.TCPServer.serve_forever
236 def __init__(self, backend, addr):
237 self.backend = backend
238 SocketServer.TCPServer.__init__(self, addr, TCPGitRequestHandler)