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
7 # or (at your option) any later version of the License.
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,
20 """Git smart network protocol server implementation."""
27 from dulwich.errors import (
30 from dulwich.objects import (
33 from dulwich.protocol import (
38 extract_want_line_capabilities,
43 from dulwich.repo import (
46 from dulwich.pack import (
50 class Backend(object):
54 Get all the refs in the repository
56 :return: dict of name -> sha
58 raise NotImplementedError
60 def apply_pack(self, refs, read):
61 """ Import a set of changes into a repository and update the refs
63 :param refs: list of tuple(name, sha)
64 :param read: callback to read from the incoming pack
66 raise NotImplementedError
68 def fetch_objects(self, determine_wants, graph_walker, progress):
70 Yield the objects required for a list of commits.
72 :param progress: is a callback to send progress messages to the client
74 raise NotImplementedError
77 class GitBackend(Backend):
79 def __init__(self, repo=None):
81 repo = Repo(tmpfile.mkdtemp())
83 self.object_store = self.repo.object_store
84 self.fetch_objects = self.repo.fetch_objects
85 self.get_refs = self.repo.get_refs
87 def apply_pack(self, refs, read):
88 f, commit = self.repo.object_store.add_thin_pack()
94 for oldsha, sha, ref in refs:
96 del self.repo.refs[ref]
98 self.repo.refs[ref] = sha
103 class Handler(object):
104 """Smart protocol command handler base class."""
106 def __init__(self, backend, read, write):
107 self.backend = backend
108 self.proto = Protocol(read, write)
110 def capabilities(self):
111 return " ".join(self.default_capabilities())
114 class UploadPackHandler(Handler):
115 """Protocol handler for uploading a pack to the server."""
117 def __init__(self, backend, read, write):
118 Handler.__init__(self, backend, read, write)
119 self._client_capabilities = None
120 self._graph_walker = None
122 def default_capabilities(self):
123 return ("multi_ack", "side-band-64k", "thin-pack", "ofs-delta")
125 def set_client_capabilities(self, caps):
126 my_caps = self.default_capabilities()
128 if '_ack' in cap and cap not in my_caps:
129 raise GitProtocolError('Client asked for capability %s that '
130 'was not advertised.' % cap)
131 self._client_capabilities = caps
133 def get_client_capabilities(self):
134 return self._client_capabilities
136 client_capabilities = property(get_client_capabilities,
137 set_client_capabilities)
140 def determine_wants(heads):
142 values = set(heads.itervalues())
144 self.proto.write_pkt_line("%s %s\x00%s\n" % ( heads[keys[0]], keys[0], self.capabilities()))
146 self.proto.write_pkt_line("%s %s\n" % (heads[k], k))
149 self.proto.write("0000")
151 # Now client will either send "0000", meaning that it doesnt want to pull.
152 # or it will start sending want want want commands
153 want = self.proto.read_pkt_line()
157 want, self.client_capabilities = extract_want_line_capabilities(want)
158 graph_walker.set_ack_type(ack_type(self.client_capabilities))
161 while want and want[:4] == 'want':
165 except (TypeError, AssertionError), e:
166 raise GitProtocolError(e)
168 if sha not in values:
169 raise GitProtocolError(
170 'Client wants invalid object %s' % sha)
171 want_revs.append(sha)
172 want = self.proto.read_pkt_line()
173 graph_walker.set_wants(want_revs)
176 progress = lambda x: self.proto.write_sideband(2, x)
177 write = lambda x: self.proto.write_sideband(1, x)
179 graph_walker = ProtocolGraphWalker(self.backend.object_store, self.proto)
180 objects_iter = self.backend.fetch_objects(determine_wants, graph_walker, progress)
182 # Do they want any objects?
183 if len(objects_iter) == 0:
186 progress("dul-daemon says what\n")
187 progress("counting objects: %d, done.\n" % len(objects_iter))
188 write_pack_data(ProtocolFile(None, write), objects_iter,
190 progress("how was that, then?\n")
192 self.proto.write("0000")
195 class ProtocolGraphWalker(object):
196 """A graph walker that knows the git protocol.
198 As a graph walker, this class implements ack(), next(), and reset(). It also
199 contains some base methods for interacting with the wire and walking the
202 The work of determining which acks to send is passed on to the
203 implementation instance stored in _impl. The reason for this is that we do
204 not know at object creation time what ack level the protocol requires. A
205 call to set_ack_level() is required to set up the implementation, before any
206 calls to next() or ack() are made.
208 def __init__(self, object_store, proto):
209 self.store = object_store
214 self._cache_index = 0
217 def ack(self, have_ref):
218 return self._impl.ack(have_ref)
222 self._cache_index = 0
226 return self._impl.next()
227 self._cache_index += 1
228 if self._cache_index > len(self._cache):
230 return self._cache[self._cache_index]
232 def read_proto_line(self):
233 """Read a line from the wire.
235 :return: a tuple having one of the following forms:
238 (None, None) (for a flush-pkt)
240 line = self.proto.read_pkt_line()
243 fields = line.rstrip('\n').split(' ', 1)
244 if len(fields) == 1 and fields[0] == 'done':
245 return ('done', None)
246 if len(fields) == 2 and fields[0] == 'have':
248 hex_to_sha(fields[1])
250 except (TypeError, AssertionError), e:
251 raise GitProtocolError(e)
252 raise GitProtocolError('Received invalid line from client:\n%s' % line)
254 def send_ack(self, sha, ack_type=''):
256 ack_type = ' %s' % ack_type
257 self.proto.write_pkt_line('ACK %s%s\n' % (sha, ack_type))
260 self.proto.write_pkt_line('NAK\n')
262 def set_wants(self, wants):
265 def _is_satisfied(self, haves, want, earliest):
266 """Check whether a want is satisfied by a set of haves.
268 A want, typically a branch tip, is "satisfied" only if there exists a
269 path back from that want to one of the haves.
271 :param haves: A set of commits we know the client has.
272 :param want: The want to check satisfaction for.
273 :param earliest: A timestamp beyond which the search for haves will be
274 terminated, presumably because we're searching too far down the
278 pending = collections.deque([o])
280 commit = pending.popleft()
281 if commit.id in haves:
283 if not getattr(commit, 'get_parents', None):
284 # non-commit wants are assumed to be satisfied
286 for parent in commit.get_parents():
287 parent_obj = self.store[parent]
288 # TODO: handle parents with later commit times than children
289 if parent_obj.commit_time >= earliest:
290 pending.append(parent_obj)
293 def all_wants_satisfied(self, haves):
294 """Check whether all the current wants are satisfied by a set of haves.
296 :param haves: A set of commits we know the client has.
297 :note: Wants are specified with set_wants rather than passed in since
298 in the current interface they are determined outside this class.
301 earliest = min([self.store[h].commit_time for h in haves])
302 for want in self._wants:
303 if not self._is_satisfied(haves, want, earliest):
307 def set_ack_type(self, ack_type):
309 MULTI_ACK: MultiAckGraphWalkerImpl,
310 SINGLE_ACK: SingleAckGraphWalkerImpl,
312 self._impl = impl_classes[ack_type](self)
315 class SingleAckGraphWalkerImpl(object):
316 """Graph walker implementation that speaks the single-ack protocol."""
318 def __init__(self, walker):
320 self._sent_ack = False
322 def ack(self, have_ref):
323 if not self._sent_ack:
324 self.walker.send_ack(have_ref)
325 self._sent_ack = True
328 command, sha = self.walker.read_proto_line()
329 if command in (None, 'done'):
330 if not self._sent_ack:
331 self.walker.send_nak()
333 elif command == 'have':
337 class MultiAckGraphWalkerImpl(object):
338 """Graph walker implementation that speaks the multi-ack protocol."""
340 def __init__(self, walker):
342 self._found_base = False
345 def ack(self, have_ref):
346 self._common.append(have_ref)
347 if not self._found_base:
348 self.walker.send_ack(have_ref, 'continue')
349 if self.walker.all_wants_satisfied(self._common):
350 self._found_base = True
351 # else we blind ack within next
354 command, sha = self.walker.read_proto_line()
356 self.walker.send_nak()
358 elif command == 'done':
359 # don't nak unless no common commits were found, even if not
360 # everything is satisfied
362 self.walker.send_ack(self._common[-1])
364 self.walker.send_nak()
366 elif command == 'have':
369 self.walker.send_ack(sha, 'continue')
373 class ReceivePackHandler(Handler):
374 """Protocol handler for downloading a pack to the client."""
376 def default_capabilities(self):
377 return ("report-status", "delete-refs")
380 refs = self.backend.get_refs().items()
383 self.proto.write_pkt_line("%s %s\x00%s\n" % (refs[0][1], refs[0][0], self.capabilities()))
384 for i in range(1, len(refs)):
386 self.proto.write_pkt_line("%s %s\n" % (ref[1], ref[0]))
388 self.proto.write_pkt_line("0000000000000000000000000000000000000000 capabilities^{} %s" % self.capabilities())
390 self.proto.write("0000")
393 ref = self.proto.read_pkt_line()
395 # if ref is none then client doesnt want to send us anything..
399 ref, client_capabilities = extract_capabilities(ref)
401 # client will now send us a list of (oldsha, newsha, ref)
403 client_refs.append(ref.split())
404 ref = self.proto.read_pkt_line()
406 # backend can now deal with this refs and read a pack using self.read
407 self.backend.apply_pack(client_refs, self.proto.read)
409 # when we have read all the pack from the client, it assumes
410 # everything worked OK.
411 # there is NO ack from the server before it reports victory.
414 class TCPGitRequestHandler(SocketServer.StreamRequestHandler):
417 proto = Protocol(self.rfile.read, self.wfile.write)
418 command, args = proto.read_cmd()
420 # switch case to handle the specific git command
421 if command == 'git-upload-pack':
422 cls = UploadPackHandler
423 elif command == 'git-receive-pack':
424 cls = ReceivePackHandler
428 h = cls(self.server.backend, self.rfile.read, self.wfile.write)
432 class TCPGitServer(SocketServer.TCPServer):
434 allow_reuse_address = True
435 serve = SocketServer.TCPServer.serve_forever
437 def __init__(self, backend, listen_addr, port=TCP_GIT_PORT):
438 self.backend = backend
439 SocketServer.TCPServer.__init__(self, (listen_addr, port), TCPGitRequestHandler)