server.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  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. import SocketServer
  19. from dulwich.protocol import Protocol
  20. class Backend(object):
  21. def get_refs(self):
  22. """
  23. Get all the refs in the repository
  24. :return: list of tuple(name, sha)
  25. """
  26. raise NotImplementedError
  27. def has_revision(self, sha):
  28. """
  29. Is a given sha in this repository?
  30. :return: True or False
  31. """
  32. raise NotImplementedError
  33. def apply_pack(self, refs, read):
  34. """ Import a set of changes into a repository and update the refs
  35. :param refs: list of tuple(name, sha)
  36. :param read: callback to read from the incoming pack
  37. """
  38. raise NotImplementedError
  39. def generate_pack(self, want, have, write, progress):
  40. """
  41. Generate a pack containing all commits a client is missing
  42. :param want: is a list of sha's the client desires
  43. :param have: is a list of sha's the client has (allowing us to send the minimal pack)
  44. :param write: is a callback to write pack data to the client
  45. :param progress: is a callback to send progress messages to the client
  46. """
  47. raise NotImplementedError
  48. from dulwich.repo import Repo
  49. from dulwich.pack import PackData, Pack
  50. import sha, tempfile, os
  51. from dulwich.pack import write_pack_data
  52. class GitBackend(Backend):
  53. def __init__(self, gitdir=None):
  54. self.gitdir = gitdir
  55. if not self.gitdir:
  56. self.gitdir = tempfile.mkdtemp()
  57. Repo.create(self.gitdir)
  58. self.repo = Repo(self.gitdir)
  59. def get_refs(self):
  60. refs = []
  61. if self.repo.head():
  62. refs.append(('HEAD', self.repo.head()))
  63. for ref, sha in self.repo.heads().items():
  64. refs.append(('refs/heads/'+ref,sha))
  65. return refs
  66. def has_revision(self, sha):
  67. return self.repo.get_object(sha) != None
  68. def apply_pack(self, refs, read):
  69. # store the incoming pack in the repository
  70. fd, name = tempfile.mkstemp(suffix='.pack', prefix='', dir=self.repo.pack_dir())
  71. os.write(fd, read())
  72. os.close(fd)
  73. # strip '.pack' off our filename
  74. basename = name[:-5]
  75. # generate an index for it
  76. pd = PackData(name)
  77. pd.create_index_v2(basename+".idx")
  78. for oldsha, sha, ref in refs:
  79. if ref == "0" * 40:
  80. self.repo.remove_ref(ref)
  81. else:
  82. self.repo.set_ref(ref, sha)
  83. print "pack applied"
  84. def generate_pack(self, want, have, write, progress):
  85. progress("dul-daemon says what\n")
  86. sha_queue = []
  87. commits_to_send = want[:]
  88. for sha in commits_to_send:
  89. if sha in sha_queue:
  90. continue
  91. sha_queue.append((1,sha))
  92. c = self.repo.commit(sha)
  93. for p in c.parents():
  94. if not p in commits_to_send:
  95. commits_to_send.append(p)
  96. def parse_tree(tree, sha_queue):
  97. for mode, name, x in tree.entries():
  98. if not x in sha_queue:
  99. try:
  100. t = self.repo.get_tree(x)
  101. sha_queue.append((2, x))
  102. parse_tree(t, sha_queue)
  103. except:
  104. sha_queue.append((3, x))
  105. treesha = c.tree()
  106. if treesha not in sha_queue:
  107. sha_queue.append((2, treesha))
  108. t = self.repo.get_tree(treesha)
  109. parse_tree(t, sha_queue)
  110. progress("counting objects: %d\r" % len(sha_queue))
  111. progress("counting objects: %d, done.\n" % len(sha_queue))
  112. write_pack_data(write, (self.repo.get_object(sha).as_raw_string() for sha in sha_queue))
  113. progress("how was that, then?\n")
  114. class Handler(object):
  115. def __init__(self, backend, read, write):
  116. self.backend = backend
  117. self.proto = Protocol(read, write)
  118. def capabilities(self):
  119. return " ".join(self.default_capabilities())
  120. def handshake(self, blob):
  121. """
  122. Compare remote capabilites with our own and alter protocol accordingly
  123. :param blob: space seperated list of capabilities (i.e. wire format)
  124. """
  125. if not "\x00" in blob:
  126. return blob
  127. blob, caps = blob.split("\x00")
  128. # FIXME: Do something with this..
  129. caps = caps.split()
  130. return blob
  131. class UploadPackHandler(Handler):
  132. def default_capabilities(self):
  133. return ("multi_ack", "side-band-64k", "thin-pack", "ofs-delta")
  134. def handle(self):
  135. refs = self.backend.get_refs()
  136. if refs:
  137. self.proto.write_pkt_line("%s %s\x00%s\n" % (refs[0][1], refs[0][0], self.capabilities()))
  138. for i in range(1, len(refs)):
  139. ref = refs[i]
  140. self.proto.write_pkt_line("%s %s\n" % (ref[1], ref[0]))
  141. # i'm done..
  142. self.proto.write("0000")
  143. # Now client will either send "0000", meaning that it doesnt want to pull.
  144. # or it will start sending want want want commands
  145. want = self.proto.read_pkt_line()
  146. if want == None:
  147. return
  148. want = self.handshake(want)
  149. # Keep reading the list of demands until we hit another "0000"
  150. want_revs = []
  151. while want and want[:4] == 'want':
  152. want_rev = want[5:45]
  153. # FIXME: This check probably isnt needed?
  154. if self.backend.has_revision(want_rev):
  155. want_revs.append(want_rev)
  156. want = self.proto.read_pkt_line()
  157. # Client will now tell us which commits it already has - if we have them we ACK them
  158. # this allows client to stop looking at that commits parents (main reason why git pull is fast)
  159. last_sha = None
  160. have_revs = []
  161. have = self.proto.read_pkt_line()
  162. while have and have[:4] == 'have':
  163. have_ref = have[6:46]
  164. if self.backend.has_revision(have_ref):
  165. self.proto.write_pkt_line("ACK %s continue\n" % have_ref)
  166. last_sha = have_ref
  167. have_revs.append(have_ref)
  168. have = self.proto.read_pkt_line()
  169. # At some point client will stop sending commits and will tell us it is done
  170. assert(have[:4] == "done")
  171. # Oddness: Git seems to resend the last ACK, without the "continue" statement
  172. if last_sha:
  173. self.proto.write_pkt_line("ACK %s\n" % last_sha)
  174. # The exchange finishes with a NAK
  175. self.proto.write_pkt_line("NAK\n")
  176. self.backend.generate_pack(want_revs, have_revs, lambda x: self.proto.write_sideband(1, x), lambda x: self.proto.write_sideband(2, x))
  177. # we are done
  178. self.proto.write("0000")
  179. class ReceivePackHandler(Handler):
  180. def default_capabilities(self):
  181. return ("report-status", "delete-refs")
  182. def handle(self):
  183. refs = self.backend.get_refs()
  184. if refs:
  185. self.proto.write_pkt_line("%s %s\x00%s\n" % (refs[0][1], refs[0][0], self.capabilities()))
  186. for i in range(1, len(refs)):
  187. ref = refs[i]
  188. self.proto.write_pkt_line("%s %s\n" % (ref[1], ref[0]))
  189. else:
  190. self.proto.write_pkt_line("0000000000000000000000000000000000000000 capabilities^{} %s" % self.capabilities())
  191. self.proto.write("0000")
  192. client_refs = []
  193. ref = self.proto.read_pkt_line()
  194. # if ref is none then client doesnt want to send us anything..
  195. if ref is None:
  196. return
  197. ref = self.handshake(ref)
  198. # client will now send us a list of (oldsha, newsha, ref)
  199. while ref:
  200. client_refs.append(ref.split())
  201. ref = self.proto.read_pkt_line()
  202. # backend can now deal with this refs and read a pack using self.read
  203. self.backend.apply_pack(client_refs, self.proto.read)
  204. # when we have read all the pack from the client, it assumes everything worked OK
  205. # there is NO ack from the server before it reports victory.
  206. class TCPGitRequestHandler(SocketServer.StreamRequestHandler, Handler):
  207. def __init__(self, request, client_address, server):
  208. SocketServer.StreamRequestHandler.__init__(self, request, client_address, server)
  209. def handle(self):
  210. #FIXME: StreamRequestHandler seems to be the thing that calls handle(),
  211. #so we can't call this in a sane place??
  212. Handler.__init__(self, self.server.backend, self.rfile.read, self.wfile.write)
  213. request = self.proto.read_pkt_line()
  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. # params are null seperated
  218. 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
  224. else:
  225. return
  226. h = cls(self.backend, self.proto.read, self.proto.write)
  227. h.handle()
  228. class TCPGitServer(SocketServer.TCPServer):
  229. allow_reuse_address = True
  230. serve = SocketServer.TCPServer.serve_forever
  231. def __init__(self, backend, addr):
  232. self.backend = backend
  233. SocketServer.TCPServer.__init__(self, addr, TCPGitRequestHandler)