server.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  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. # import dulwich as git
  20. class Backend(object):
  21. def __init__(self):
  22. pass
  23. def get_refs(self):
  24. raise NotImplementedError
  25. def has_revision(self):
  26. raise NotImplementedError
  27. def apply_pack(self, refs, read):
  28. raise NotImplementedError
  29. def generate_pack(self, want, have, write, progress):
  30. raise NotImplementedError
  31. class Handler(object):
  32. def __init__(self, backend, read, write):
  33. self.backend = backend
  34. self.read = read
  35. self.write = write
  36. def read_pkt_line(self):
  37. """
  38. reads a 'git line' of info from the stream
  39. """
  40. size = int(self.read(4), 16)
  41. if size == 0:
  42. return None
  43. return self.read(size-4)
  44. def write_pkt_line(self, line):
  45. self.write("%04x%s" % (len(line)+4, line))
  46. def write_sideband(self, channel, blob):
  47. # a pktline can be a max of 65535. a sideband line can therefore be
  48. # 65535-5 = 65530
  49. # WTF: Why have the len in ASCII, but the channel in binary.
  50. while blob:
  51. self.write_pkt_line("%s%s" % (chr(channel), blob[:65530]))
  52. blob = blob[65530:]
  53. def handle(self):
  54. raise NotImplementedError
  55. class UploadPackHandler(Handler):
  56. def handle(self):
  57. refs = self.backend.get_refs()
  58. self.write_pkt_line("%s %s\x00multi_ack side-band-64k thin-pack ofs-delta\0x0a" % (refs[0][1], refs[0][0]))
  59. for i in range(1, len(refs)-1):
  60. ref = refs[i]
  61. self.write_pkt_line("%s %s\0x0a" % (ref[1], ref[0]))
  62. # i'm done...
  63. self.write("0000")
  64. # Now client will either send "0000", meaning that it doesnt want to pull.
  65. # or it will start sending want want want commands
  66. want = self.read_pkt_line()
  67. if want == None:
  68. return
  69. # Keep reading the list of demands until we hit another "0000"
  70. want_revs = []
  71. while want and want[:4] == 'want':
  72. want_rev = want[5:]
  73. # FIXME: This check probably isnt needed?
  74. if self.backend.has_revision(want_rev):
  75. want_revs.append(want_rev)
  76. want = self.read_pkt_line()
  77. # Client will now tell us which commits it already has - if we have them we ACK them
  78. # this allows client to stop looking at that commits parents (main reason why git pull is fast)
  79. last_sha = None
  80. have_revs = []
  81. have = self.read_pkt_line()
  82. while have and have[:4] == 'have':
  83. have_ref = have[6:40]
  84. if self.backend.has_revision(hav_rev):
  85. self.write_pkt_line("ACK %s continue\n" % sha)
  86. last_sha = sha
  87. have_revs.append(rev_id)
  88. have = self.read_pkt_line()
  89. # At some point client will stop sending commits and will tell us it is done
  90. assert(have[:4] == "done")
  91. # Oddness: Git seems to resend the last ACK, without the "continue" statement
  92. if last_sha:
  93. self.write_pkt_line("ACK %s\n" % last_sha)
  94. # The exchange finishes with a NAK
  95. self.write_pkt_line("NAK\n")
  96. #if True: # False: #self.no_progress == False:
  97. # self.write_sideband(2, "Bazaar is preparing your pack, plz hold.\n")
  98. # for x in range(1,200):
  99. # self.write_sideband(2, "Counting objects: %d\x0d" % x*2)
  100. # self.write_sideband(2, "Counting objects: 200, done.\n")
  101. # for x in range(1,100):
  102. # self.write_sideband(2, "Compressiong objects: %d (%d/%d)\x0d" % (x, x*2, 200))
  103. # self.write_sideband(2, "Compressing objects: 100% (200/200), done.\n")
  104. self.backend.generate_pack(want_revs, have_revs, self.write, None)
  105. class ReceivePackHandler(Handler):
  106. def handle(self):
  107. refs = self.backend.get_refs()
  108. self.write_pkt_line("%s %s\x00multi_ack side-band-64k thin-pack ofs-delta\0x0a" % (refs[0][1], refs[0][0]))
  109. for i in range(1, len(refs)-1):
  110. ref = refs[i]
  111. self.write_pkt_line("%s %s\0x0a" % (ref[1], ref[0]))
  112. self.write("0000")
  113. client_refs = []
  114. ref = self.read_pkt_line()
  115. while ref:
  116. client_refs.append(ref.split())
  117. ref = self.read_pkt_line()
  118. self.backend.apply_pack(client_refs, self.read)
  119. class TCPGitRequestHandler(SocketServer.StreamRequestHandler, Handler):
  120. def __init__(self, request, client_address, server):
  121. SocketServer.StreamRequestHandler.__init__(self, request, client_address, server)
  122. def handle(self):
  123. #FIXME: StreamRequestHandler seems to be the thing that calls handle(),
  124. #so we can't call this in a sane place??
  125. Handler.__init__(self, self.server.backend, self.rfile.read, self.wfile.write)
  126. request = self.read_pkt_line()
  127. # up until the space is the command to run, everything after is parameters
  128. splice_point = request.find(' ')
  129. command, params = request[:splice_point], request[splice_point+1:]
  130. # params are null seperated
  131. params = params.split(chr(0))
  132. # switch case to handle the specific git command
  133. if command == 'git-upload-pack':
  134. cls = UploadPackHandler
  135. elif command == 'git-receive-pack':
  136. cls = ReceivePackHandler
  137. else:
  138. return
  139. h = cls(self.backend, self.read, self.write)
  140. h.handle()
  141. class TCPGitServer(SocketServer.TCPServer):
  142. allow_reuse_address = True
  143. serve = SocketServer.TCPServer.serve_forever
  144. def __init__(self, backend, addr):
  145. self.backend = backend
  146. SocketServer.TCPServer.__init__(self, addr, TCPGitRequestHandler)