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