Fix formatting issues.
[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 # or (at your option) any later version 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 tempfile
21
22 from dulwich.protocol import (
23     Protocol,
24     ProtocolFile,
25     TCP_GIT_PORT,
26     extract_capabilities,
27     )
28 from dulwich.repo import (
29     Repo,
30     )
31 from dulwich.pack import (
32     write_pack_data,
33     )
34
35 class Backend(object):
36
37     def get_refs(self):
38         """
39         Get all the refs in the repository
40
41         :return: dict of name -> sha
42         """
43         raise NotImplementedError
44
45     def apply_pack(self, refs, read):
46         """ Import a set of changes into a repository and update the refs
47
48         :param refs: list of tuple(name, sha)
49         :param read: callback to read from the incoming pack
50         """
51         raise NotImplementedError
52
53     def fetch_objects(self, determine_wants, graph_walker, progress):
54         """
55         Yield the objects required for a list of commits.
56
57         :param progress: is a callback to send progress messages to the client
58         """
59         raise NotImplementedError
60
61
62 class GitBackend(Backend):
63
64     def __init__(self, gitdir=None):
65         self.gitdir = gitdir
66
67         if not self.gitdir:
68             self.gitdir = tempfile.mkdtemp()
69             Repo.create(self.gitdir)
70
71         self.repo = Repo(self.gitdir)
72         self.fetch_objects = self.repo.fetch_objects
73         self.get_refs = self.repo.get_refs
74
75     def apply_pack(self, refs, read):
76         fd, commit = self.repo.object_store.add_thin_pack()
77         fd.write(read())
78         fd.close()
79         commit()
80
81         for oldsha, sha, ref in refs:
82             if ref == "0" * 40:
83                 self.repo.remove_ref(ref)
84             else:
85                 self.repo.set_ref(ref, sha)
86
87         print "pack applied"
88
89
90 class Handler(object):
91
92     def __init__(self, backend, read, write):
93         self.backend = backend
94         self.proto = Protocol(read, write)
95
96     def capabilities(self):
97         return " ".join(self.default_capabilities())
98
99
100 class UploadPackHandler(Handler):
101
102     def default_capabilities(self):
103         return ("multi_ack", "side-band-64k", "thin-pack", "ofs-delta")
104
105     def handle(self):
106         def determine_wants(heads):
107             keys = heads.keys()
108             if keys:
109                 self.proto.write_pkt_line("%s %s\x00%s\n" % ( heads[keys[0]], keys[0], self.capabilities()))
110                 for k in keys[1:]:
111                     self.proto.write_pkt_line("%s %s\n" % (heads[k], k))
112
113             # i'm done..
114             self.proto.write("0000")
115
116             # Now client will either send "0000", meaning that it doesnt want to pull.
117             # or it will start sending want want want commands
118             want = self.proto.read_pkt_line()
119             if want == None:
120                 return []
121
122             want, self.client_capabilities = extract_capabilities(want)
123
124             want_revs = []
125             while want and want[:4] == 'want':
126                 want_revs.append(want[5:45])
127                 want = self.proto.read_pkt_line()
128             return want_revs
129
130         progress = lambda x: self.proto.write_sideband(2, x)
131         write = lambda x: self.proto.write_sideband(1, x)
132
133         class ProtocolGraphWalker(object):
134
135             def __init__(self, proto):
136                 self.proto = proto
137                 self._last_sha = None
138
139             def ack(self, have_ref):
140                 self.proto.write_pkt_line("ACK %s continue\n" % have_ref)
141
142             def next(self):
143                 have = self.proto.read_pkt_line()
144                 if have[:4] == 'have':
145                     return have[5:45]
146
147                 #if have[:4] == 'done':
148                 #    return None
149
150                 if self._last_sha:
151                     # Oddness: Git seems to resend the last ACK, without the "continue" statement
152                     self.proto.write_pkt_line("ACK %s\n" % self._last_sha)
153
154                 # The exchange finishes with a NAK
155                 self.proto.write_pkt_line("NAK\n")
156
157         graph_walker = ProtocolGraphWalker(self.proto)
158         num_objects, objects_iter = self.backend.fetch_objects(determine_wants, graph_walker, progress)
159
160         # Do they want any objects?
161         if num_objects == 0:
162             return
163
164         progress("dul-daemon says what\n")
165         progress("counting objects: %d, done.\n" % num_objects)
166         write_pack_data(ProtocolFile(None, write), objects_iter, num_objects)
167         progress("how was that, then?\n")
168         # we are done
169         self.proto.write("0000")
170
171
172 class ReceivePackHandler(Handler):
173
174     def default_capabilities(self):
175         return ("report-status", "delete-refs")
176
177     def handle(self):
178         refs = self.backend.get_refs().items()
179
180         if refs:
181             self.proto.write_pkt_line("%s %s\x00%s\n" % (refs[0][1], refs[0][0], self.capabilities()))
182             for i in range(1, len(refs)):
183                 ref = refs[i]
184                 self.proto.write_pkt_line("%s %s\n" % (ref[1], ref[0]))
185         else:
186             self.proto.write_pkt_line("0000000000000000000000000000000000000000 capabilities^{} %s" % self.capabilities())
187
188         self.proto.write("0000")
189
190         client_refs = []
191         ref = self.proto.read_pkt_line()
192
193         # if ref is none then client doesnt want to send us anything..
194         if ref is None:
195             return
196
197         ref, client_capabilities = extract_capabilities(ref)
198
199         # client will now send us a list of (oldsha, newsha, ref)
200         while ref:
201             client_refs.append(ref.split())
202             ref = self.proto.read_pkt_line()
203
204         # backend can now deal with this refs and read a pack using self.read
205         self.backend.apply_pack(client_refs, self.proto.read)
206
207         # when we have read all the pack from the client, it assumes everything worked OK
208         # there is NO ack from the server before it reports victory.
209
210
211 class TCPGitRequestHandler(SocketServer.StreamRequestHandler):
212
213     def handle(self):
214         proto = Protocol(self.rfile.read, self.wfile.write)
215         command, args = proto.read_cmd()
216
217         # switch case to handle the specific git command
218         if command == 'git-upload-pack':
219             cls = UploadPackHandler
220         elif command == 'git-receive-pack':
221             cls = ReceivePackHandler
222         else:
223             return
224
225         h = cls(self.server.backend, self.rfile.read, self.wfile.write)
226         h.handle()
227
228
229 class TCPGitServer(SocketServer.TCPServer):
230
231     allow_reuse_address = True
232     serve = SocketServer.TCPServer.serve_forever
233
234     def __init__(self, backend, listen_addr, port=TCP_GIT_PORT):
235         self.backend = backend
236         SocketServer.TCPServer.__init__(self, (listen_addr, port), TCPGitRequestHandler)
237
238