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