server.py 11 KB

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