Add comments
[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 handle(self):
103         """
104         Deal with the request
105         """
106         raise NotImplementedError
107
108
109 class UploadPackHandler(Handler):
110
111     def handle(self):
112         refs = self.backend.get_refs()
113
114         if 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)):
117                 ref = refs[i]
118                 self.write_pkt_line("%s %s\n" % (ref[1], ref[0]))
119
120         # i'm done...
121         self.write("0000")
122
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()
126         if want == None:
127             return
128        
129         # Keep reading the list of demands until we hit another "0000" 
130         want_revs = []
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()
137         
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)
140         last_sha = None
141         have_revs = []
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)
147                 last_sha = sha
148                 have_revs.append(rev_id)
149             have = self.read_pkt_line()
150
151         # At some point client will stop sending commits and will tell us it is done
152         assert(have[:4] == "done")
153
154         # Oddness: Git seems to resend the last ACK, without the "continue" statement
155         if last_sha:
156             self.write_pkt_line("ACK %s\n" % last_sha)
157
158         # The exchange finishes with a NAK
159         self.write_pkt_line("NAK\n")
160       
161         #if True: # False: #self.no_progress == False:
162         #    self.write_sideband(2, "Bazaar is preparing your pack, plz hold.\n")
163
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")
167
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")
171
172         self.backend.generate_pack(want_revs, have_revs, self.write, None)
173
174
175 class ReceivePackHandler(Handler):
176
177     def handle(self):
178         refs = self.backend.get_refs()
179
180         if 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)):
183                 ref = refs[i]
184                 self.write_pkt_line("%s %s\n" % (ref[1], ref[0]))
185
186         self.write("0000")
187
188         client_refs = []
189         ref = self.read_pkt_line()
190         while ref:
191             client_refs.append(ref.split())
192             ref = self.read_pkt_line()
193
194         # client might hang up without sending us any refs
195         if len(client_refs) == 0:
196             return None
197
198         # backend can now deal with this refs and read a pack using self.read
199         self.backend.apply_pack(client_refs, self.read)
200
201
202 class TCPGitRequestHandler(SocketServer.StreamRequestHandler, Handler):
203
204     def __init__(self, request, client_address, server):
205         SocketServer.StreamRequestHandler.__init__(self, request, client_address, server)
206
207     def handle(self):
208         #FIXME: StreamRequestHandler seems to be the thing that calls handle(),
209         #so we can't call this in a sane place??
210         Handler.__init__(self, self.server.backend, self.rfile.read, self.wfile.write)
211
212         request = self.read_pkt_line()
213
214         # up until the space is the command to run, everything after is parameters
215         splice_point = request.find(' ')
216         command, params = request[:splice_point], request[splice_point+1:]
217
218         # params are null seperated
219         params = params.split(chr(0))
220
221         # switch case to handle the specific git command
222         if command == 'git-upload-pack':
223             cls = UploadPackHandler
224         elif command == 'git-receive-pack':
225             cls = ReceivePackHandler
226         else:
227             return
228
229         h = cls(self.backend, self.read, self.write)
230         h.handle()
231
232
233 class TCPGitServer(SocketServer.TCPServer):
234
235     allow_reuse_address = True
236     serve = SocketServer.TCPServer.serve_forever
237
238     def __init__(self, backend, addr):
239         self.backend = backend
240         SocketServer.TCPServer.__init__(self, addr, TCPGitRequestHandler)
241
242