After sending pack, close off.
[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:45]
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:46]
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         self.backend.generate_pack(want_revs, have_revs, lambda x: self.write_sideband(1, x), lambda x: self.write_sideband(2, x))
162
163         # we are done
164         self.write("0000")
165
166
167 class ReceivePackHandler(Handler):
168
169     def handle(self):
170         refs = self.backend.get_refs()
171
172         if refs:
173             self.write_pkt_line("%s %s\x00multi_ack side-band-64k thin-pack ofs-delta\n" % (refs[0][1], refs[0][0]))
174             for i in range(1, len(refs)):
175                 ref = refs[i]
176                 self.write_pkt_line("%s %s\n" % (ref[1], ref[0]))
177
178         self.write("0000")
179
180         client_refs = []
181         ref = self.read_pkt_line()
182         while ref:
183             client_refs.append(ref.split())
184             ref = self.read_pkt_line()
185
186         # client might hang up without sending us any refs
187         if len(client_refs) == 0:
188             return None
189
190         # backend can now deal with this refs and read a pack using self.read
191         self.backend.apply_pack(client_refs, self.read)
192
193
194 class TCPGitRequestHandler(SocketServer.StreamRequestHandler, Handler):
195
196     def __init__(self, request, client_address, server):
197         SocketServer.StreamRequestHandler.__init__(self, request, client_address, server)
198
199     def handle(self):
200         #FIXME: StreamRequestHandler seems to be the thing that calls handle(),
201         #so we can't call this in a sane place??
202         Handler.__init__(self, self.server.backend, self.rfile.read, self.wfile.write)
203
204         request = self.read_pkt_line()
205
206         # up until the space is the command to run, everything after is parameters
207         splice_point = request.find(' ')
208         command, params = request[:splice_point], request[splice_point+1:]
209
210         # params are null seperated
211         params = params.split(chr(0))
212
213         # switch case to handle the specific git command
214         if command == 'git-upload-pack':
215             cls = UploadPackHandler
216         elif command == 'git-receive-pack':
217             cls = ReceivePackHandler
218         else:
219             return
220
221         h = cls(self.backend, self.read, self.write)
222         h.handle()
223
224
225 class TCPGitServer(SocketServer.TCPServer):
226
227     allow_reuse_address = True
228     serve = SocketServer.TCPServer.serve_forever
229
230     def __init__(self, backend, addr):
231         self.backend = backend
232         SocketServer.TCPServer.__init__(self, addr, TCPGitRequestHandler)
233
234