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 size = int(self.read(4), 16)
75 return self.read(size-4)
77 def write_pkt_line(self, line):
79 Sends a 'pkt line' to the remote git process
81 :param line: A string containing the data to send
83 self.write("%04x%s" % (len(line)+4, line))
85 def write_sideband(self, channel, blob):
87 Write data to the sideband (a git multiplexing method)
89 :param channel: int specifying which channel to write to
90 :param blob: a blob of data (as a string) to send on this channel
92 # a pktline can be a max of 65535. a sideband line can therefore be
94 # WTF: Why have the len in ASCII, but the channel in binary.
96 self.write_pkt_line("%s%s" % (chr(channel), blob[:65530]))
101 Deal with the request
103 raise NotImplementedError
106 class UploadPackHandler(Handler):
109 refs = self.backend.get_refs()
111 self.write_pkt_line("%s %s\x00multi_ack side-band-64k thin-pack ofs-delta\n" % (refs[0][1], refs[0][0]))
112 for i in range(1, len(refs)):
114 self.write_pkt_line("%s %s\n" % (ref[1], ref[0]))
119 # Now client will either send "0000", meaning that it doesnt want to pull.
120 # or it will start sending want want want commands
121 want = self.read_pkt_line()
125 # Keep reading the list of demands until we hit another "0000"
127 while want and want[:4] == 'want':
128 want_rev = want[5:40]
129 # FIXME: This check probably isnt needed?
130 if self.backend.has_revision(want_rev):
131 want_revs.append(want_rev)
132 want = self.read_pkt_line()
134 # Client will now tell us which commits it already has - if we have them we ACK them
135 # this allows client to stop looking at that commits parents (main reason why git pull is fast)
138 have = self.read_pkt_line()
139 while have and have[:4] == 'have':
140 have_ref = have[6:40]
141 if self.backend.has_revision(hav_rev):
142 self.write_pkt_line("ACK %s continue\n" % sha)
144 have_revs.append(rev_id)
145 have = self.read_pkt_line()
147 # At some point client will stop sending commits and will tell us it is done
148 assert(have[:4] == "done")
150 # Oddness: Git seems to resend the last ACK, without the "continue" statement
152 self.write_pkt_line("ACK %s\n" % last_sha)
154 # The exchange finishes with a NAK
155 self.write_pkt_line("NAK\n")
157 #if True: # False: #self.no_progress == False:
158 # self.write_sideband(2, "Bazaar is preparing your pack, plz hold.\n")
160 # for x in range(1,200)
161 # self.write_sideband(2, "Counting objects: %d\x0d" % x*2)
162 # self.write_sideband(2, "Counting objects: 200, done.\n")
164 # for x in range(1,100):
165 # self.write_sideband(2, "Compressiong objects: %d (%d/%d)\x0d" % (x, x*2, 200))
166 # self.write_sideband(2, "Compressing objects: 100% (200/200), done.\n")
168 self.backend.generate_pack(want_revs, have_revs, self.write, None)
171 class ReceivePackHandler(Handler):
174 refs = self.backend.get_refs()
176 self.write_pkt_line("%s %s\x00multi_ack side-band-64k thin-pack ofs-delta\n" % (refs[0][1], refs[0][0]))
177 for i in range(1, len(refs)):
179 self.write_pkt_line("%s %s\n" % (ref[1], ref[0]))
184 ref = self.read_pkt_line()
186 client_refs.append(ref.split())
187 ref = self.read_pkt_line()
189 self.backend.apply_pack(client_refs, self.read)
192 class TCPGitRequestHandler(SocketServer.StreamRequestHandler, Handler):
194 def __init__(self, request, client_address, server):
195 SocketServer.StreamRequestHandler.__init__(self, request, client_address, server)
198 #FIXME: StreamRequestHandler seems to be the thing that calls handle(),
199 #so we can't call this in a sane place??
200 Handler.__init__(self, self.server.backend, self.rfile.read, self.wfile.write)
202 request = self.read_pkt_line()
204 # up until the space is the command to run, everything after is parameters
205 splice_point = request.find(' ')
206 command, params = request[:splice_point], request[splice_point+1:]
208 # params are null seperated
209 params = params.split(chr(0))
211 # switch case to handle the specific git command
212 if command == 'git-upload-pack':
213 cls = UploadPackHandler
214 elif command == 'git-receive-pack':
215 cls = ReceivePackHandler
219 h = cls(self.backend, self.read, self.write)
223 class TCPGitServer(SocketServer.TCPServer):
225 allow_reuse_address = True
226 serve = SocketServer.TCPServer.serve_forever
228 def __init__(self, backend, addr):
229 self.backend = backend
230 SocketServer.TCPServer.__init__(self, addr, TCPGitRequestHandler)