##// END OF EJS Templates
wireprotov2: support response caching...
Gregory Szorc -
r40057:c537144f default
parent child Browse files
Show More
This diff has been collapsed as it changes many lines, (645 lines changed) Show them Hide them
@@ -0,0 +1,645 b''
1 $ . $TESTDIR/wireprotohelpers.sh
2 $ cat >> $HGRCPATH << EOF
3 > [extensions]
4 > blackbox =
5 > [blackbox]
6 > track = simplecache
7 > EOF
8 $ hg init server
9 $ enablehttpv2 server
10 $ cd server
11 $ cat >> .hg/hgrc << EOF
12 > [extensions]
13 > simplecache = $TESTDIR/wireprotosimplecache.py
14 > EOF
15
16 $ echo a0 > a
17 $ echo b0 > b
18 $ hg -q commit -A -m 'commit 0'
19 $ echo a1 > a
20 $ hg commit -m 'commit 1'
21 $ echo b1 > b
22 $ hg commit -m 'commit 2'
23 $ echo a2 > a
24 $ echo b2 > b
25 $ hg commit -m 'commit 3'
26
27 $ hg log -G -T '{rev}:{node} {desc}'
28 @ 3:50590a86f3ff5d1e9a1624a7a6957884565cc8e8 commit 3
29 |
30 o 2:4d01eda50c6ac5f7e89cbe1880143a32f559c302 commit 2
31 |
32 o 1:4432d83626e8a98655f062ec1f2a43b07f7fbbb0 commit 1
33 |
34 o 0:3390ef850073fbc2f0dfff2244342c8e9229013a commit 0
35
36
37 $ hg --debug debugindex -m
38 rev linkrev nodeid p1 p2
39 0 0 992f4779029a3df8d0666d00bb924f69634e2641 0000000000000000000000000000000000000000 0000000000000000000000000000000000000000
40 1 1 a988fb43583e871d1ed5750ee074c6d840bbbfc8 992f4779029a3df8d0666d00bb924f69634e2641 0000000000000000000000000000000000000000
41 2 2 a8853dafacfca6fc807055a660d8b835141a3bb4 a988fb43583e871d1ed5750ee074c6d840bbbfc8 0000000000000000000000000000000000000000
42 3 3 3fe11dfbb13645782b0addafbe75a87c210ffddc a8853dafacfca6fc807055a660d8b835141a3bb4 0000000000000000000000000000000000000000
43
44 $ hg serve -p $HGPORT -d --pid-file hg.pid -E error.log
45 $ cat hg.pid > $DAEMON_PIDS
46
47 Performing the same request should result in same result, with 2nd response
48 coming from cache.
49
50 $ sendhttpv2peer << EOF
51 > command manifestdata
52 > nodes eval:[b'\x99\x2f\x47\x79\x02\x9a\x3d\xf8\xd0\x66\x6d\x00\xbb\x92\x4f\x69\x63\x4e\x26\x41']
53 > tree eval:b''
54 > fields eval:[b'parents']
55 > EOF
56 creating http peer for wire protocol version 2
57 sending manifestdata command
58 s> POST /api/exp-http-v2-0002/ro/manifestdata HTTP/1.1\r\n
59 s> Accept-Encoding: identity\r\n
60 s> accept: application/mercurial-exp-framing-0005\r\n
61 s> content-type: application/mercurial-exp-framing-0005\r\n
62 s> content-length: 83\r\n
63 s> host: $LOCALIP:$HGPORT\r\n (glob)
64 s> user-agent: Mercurial debugwireproto\r\n
65 s> \r\n
66 s> K\x00\x00\x01\x00\x01\x01\x11\xa2Dargs\xa3Ffields\x81GparentsEnodes\x81T\x99/Gy\x02\x9a=\xf8\xd0fm\x00\xbb\x92OicN&ADtree@DnameLmanifestdata
67 s> makefile('rb', None)
68 s> HTTP/1.1 200 OK\r\n
69 s> Server: testing stub value\r\n
70 s> Date: $HTTP_DATE$\r\n
71 s> Content-Type: application/mercurial-exp-framing-0005\r\n
72 s> Transfer-Encoding: chunked\r\n
73 s> \r\n
74 s> 13\r\n
75 s> \x0b\x00\x00\x01\x00\x02\x011
76 s> \xa1FstatusBok
77 s> \r\n
78 received frame(size=11; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=continuation)
79 s> 63\r\n
80 s> [\x00\x00\x01\x00\x02\x001
81 s> \xa1Jtotalitems\x01\xa2DnodeT\x99/Gy\x02\x9a=\xf8\xd0fm\x00\xbb\x92OicN&AGparents\x82T\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00T\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
82 s> \r\n
83 received frame(size=91; request=1; stream=2; streamflags=; type=command-response; flags=continuation)
84 s> 8\r\n
85 s> \x00\x00\x00\x01\x00\x02\x002
86 s> \r\n
87 s> 0\r\n
88 s> \r\n
89 received frame(size=0; request=1; stream=2; streamflags=; type=command-response; flags=eos)
90 response: gen[
91 {
92 b'totalitems': 1
93 },
94 {
95 b'node': b'\x99/Gy\x02\x9a=\xf8\xd0fm\x00\xbb\x92OicN&A',
96 b'parents': [
97 b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
98 b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
99 ]
100 }
101 ]
102
103 $ sendhttpv2peer << EOF
104 > command manifestdata
105 > nodes eval:[b'\x99\x2f\x47\x79\x02\x9a\x3d\xf8\xd0\x66\x6d\x00\xbb\x92\x4f\x69\x63\x4e\x26\x41']
106 > tree eval:b''
107 > fields eval:[b'parents']
108 > EOF
109 creating http peer for wire protocol version 2
110 sending manifestdata command
111 s> POST /api/exp-http-v2-0002/ro/manifestdata HTTP/1.1\r\n
112 s> Accept-Encoding: identity\r\n
113 s> accept: application/mercurial-exp-framing-0005\r\n
114 s> content-type: application/mercurial-exp-framing-0005\r\n
115 s> content-length: 83\r\n
116 s> host: $LOCALIP:$HGPORT\r\n (glob)
117 s> user-agent: Mercurial debugwireproto\r\n
118 s> \r\n
119 s> K\x00\x00\x01\x00\x01\x01\x11\xa2Dargs\xa3Ffields\x81GparentsEnodes\x81T\x99/Gy\x02\x9a=\xf8\xd0fm\x00\xbb\x92OicN&ADtree@DnameLmanifestdata
120 s> makefile('rb', None)
121 s> HTTP/1.1 200 OK\r\n
122 s> Server: testing stub value\r\n
123 s> Date: $HTTP_DATE$\r\n
124 s> Content-Type: application/mercurial-exp-framing-0005\r\n
125 s> Transfer-Encoding: chunked\r\n
126 s> \r\n
127 s> 13\r\n
128 s> \x0b\x00\x00\x01\x00\x02\x011
129 s> \xa1FstatusBok
130 s> \r\n
131 received frame(size=11; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=continuation)
132 s> 63\r\n
133 s> [\x00\x00\x01\x00\x02\x001
134 s> \xa1Jtotalitems\x01\xa2DnodeT\x99/Gy\x02\x9a=\xf8\xd0fm\x00\xbb\x92OicN&AGparents\x82T\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00T\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
135 s> \r\n
136 received frame(size=91; request=1; stream=2; streamflags=; type=command-response; flags=continuation)
137 s> 8\r\n
138 s> \x00\x00\x00\x01\x00\x02\x002
139 s> \r\n
140 s> 0\r\n
141 s> \r\n
142 received frame(size=0; request=1; stream=2; streamflags=; type=command-response; flags=eos)
143 response: gen[
144 {
145 b'totalitems': 1
146 },
147 {
148 b'node': b'\x99/Gy\x02\x9a=\xf8\xd0fm\x00\xbb\x92OicN&A',
149 b'parents': [
150 b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
151 b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
152 ]
153 }
154 ]
155
156 Sending different request doesn't yield cache hit.
157
158 $ sendhttpv2peer << EOF
159 > command manifestdata
160 > nodes eval:[b'\x99\x2f\x47\x79\x02\x9a\x3d\xf8\xd0\x66\x6d\x00\xbb\x92\x4f\x69\x63\x4e\x26\x41', b'\xa9\x88\xfb\x43\x58\x3e\x87\x1d\x1e\xd5\x75\x0e\xe0\x74\xc6\xd8\x40\xbb\xbf\xc8']
161 > tree eval:b''
162 > fields eval:[b'parents']
163 > EOF
164 creating http peer for wire protocol version 2
165 sending manifestdata command
166 s> POST /api/exp-http-v2-0002/ro/manifestdata HTTP/1.1\r\n
167 s> Accept-Encoding: identity\r\n
168 s> accept: application/mercurial-exp-framing-0005\r\n
169 s> content-type: application/mercurial-exp-framing-0005\r\n
170 s> content-length: 104\r\n
171 s> host: $LOCALIP:$HGPORT\r\n (glob)
172 s> user-agent: Mercurial debugwireproto\r\n
173 s> \r\n
174 s> `\x00\x00\x01\x00\x01\x01\x11\xa2Dargs\xa3Ffields\x81GparentsEnodes\x82T\x99/Gy\x02\x9a=\xf8\xd0fm\x00\xbb\x92OicN&AT\xa9\x88\xfbCX>\x87\x1d\x1e\xd5u\x0e\xe0t\xc6\xd8@\xbb\xbf\xc8Dtree@DnameLmanifestdata
175 s> makefile('rb', None)
176 s> HTTP/1.1 200 OK\r\n
177 s> Server: testing stub value\r\n
178 s> Date: $HTTP_DATE$\r\n
179 s> Content-Type: application/mercurial-exp-framing-0005\r\n
180 s> Transfer-Encoding: chunked\r\n
181 s> \r\n
182 s> 13\r\n
183 s> \x0b\x00\x00\x01\x00\x02\x011
184 s> \xa1FstatusBok
185 s> \r\n
186 received frame(size=11; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=continuation)
187 s> b1\r\n
188 s> \xa9\x00\x00\x01\x00\x02\x001
189 s> \xa1Jtotalitems\x02\xa2DnodeT\x99/Gy\x02\x9a=\xf8\xd0fm\x00\xbb\x92OicN&AGparents\x82T\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00T\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa2DnodeT\xa9\x88\xfbCX>\x87\x1d\x1e\xd5u\x0e\xe0t\xc6\xd8@\xbb\xbf\xc8Gparents\x82T\x99/Gy\x02\x9a=\xf8\xd0fm\x00\xbb\x92OicN&AT\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
190 s> \r\n
191 received frame(size=169; request=1; stream=2; streamflags=; type=command-response; flags=continuation)
192 s> 8\r\n
193 s> \x00\x00\x00\x01\x00\x02\x002
194 s> \r\n
195 s> 0\r\n
196 s> \r\n
197 received frame(size=0; request=1; stream=2; streamflags=; type=command-response; flags=eos)
198 response: gen[
199 {
200 b'totalitems': 2
201 },
202 {
203 b'node': b'\x99/Gy\x02\x9a=\xf8\xd0fm\x00\xbb\x92OicN&A',
204 b'parents': [
205 b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
206 b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
207 ]
208 },
209 {
210 b'node': b'\xa9\x88\xfbCX>\x87\x1d\x1e\xd5u\x0e\xe0t\xc6\xd8@\xbb\xbf\xc8',
211 b'parents': [
212 b'\x99/Gy\x02\x9a=\xf8\xd0fm\x00\xbb\x92OicN&A',
213 b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
214 ]
215 }
216 ]
217
218 $ cat .hg/blackbox.log
219 *> cacher constructed for manifestdata (glob)
220 *> cache miss for c045a581599d58608efd3d93d8129841f2af04a0 (glob)
221 *> storing cache entry for c045a581599d58608efd3d93d8129841f2af04a0 (glob)
222 *> cacher constructed for manifestdata (glob)
223 *> cache hit for c045a581599d58608efd3d93d8129841f2af04a0 (glob)
224 *> cacher constructed for manifestdata (glob)
225 *> cache miss for 6ed2f740a1cdd12c9e99c4f27695543143c26a11 (glob)
226 *> storing cache entry for 6ed2f740a1cdd12c9e99c4f27695543143c26a11 (glob)
227
228 $ cat error.log
229
230 $ killdaemons.py
231 $ rm .hg/blackbox.log
232
233 Try with object caching mode
234
235 $ cat >> .hg/hgrc << EOF
236 > [simplecache]
237 > cacheobjects = true
238 > EOF
239
240 $ hg serve -p $HGPORT -d --pid-file hg.pid -E error.log
241 $ cat hg.pid > $DAEMON_PIDS
242
243 $ sendhttpv2peer << EOF
244 > command manifestdata
245 > nodes eval:[b'\x99\x2f\x47\x79\x02\x9a\x3d\xf8\xd0\x66\x6d\x00\xbb\x92\x4f\x69\x63\x4e\x26\x41']
246 > tree eval:b''
247 > fields eval:[b'parents']
248 > EOF
249 creating http peer for wire protocol version 2
250 sending manifestdata command
251 s> POST /api/exp-http-v2-0002/ro/manifestdata HTTP/1.1\r\n
252 s> Accept-Encoding: identity\r\n
253 s> accept: application/mercurial-exp-framing-0005\r\n
254 s> content-type: application/mercurial-exp-framing-0005\r\n
255 s> content-length: 83\r\n
256 s> host: $LOCALIP:$HGPORT\r\n (glob)
257 s> user-agent: Mercurial debugwireproto\r\n
258 s> \r\n
259 s> K\x00\x00\x01\x00\x01\x01\x11\xa2Dargs\xa3Ffields\x81GparentsEnodes\x81T\x99/Gy\x02\x9a=\xf8\xd0fm\x00\xbb\x92OicN&ADtree@DnameLmanifestdata
260 s> makefile('rb', None)
261 s> HTTP/1.1 200 OK\r\n
262 s> Server: testing stub value\r\n
263 s> Date: $HTTP_DATE$\r\n
264 s> Content-Type: application/mercurial-exp-framing-0005\r\n
265 s> Transfer-Encoding: chunked\r\n
266 s> \r\n
267 s> 13\r\n
268 s> \x0b\x00\x00\x01\x00\x02\x011
269 s> \xa1FstatusBok
270 s> \r\n
271 received frame(size=11; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=continuation)
272 s> 63\r\n
273 s> [\x00\x00\x01\x00\x02\x001
274 s> \xa1Jtotalitems\x01\xa2DnodeT\x99/Gy\x02\x9a=\xf8\xd0fm\x00\xbb\x92OicN&AGparents\x82T\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00T\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
275 s> \r\n
276 received frame(size=91; request=1; stream=2; streamflags=; type=command-response; flags=continuation)
277 s> 8\r\n
278 s> \x00\x00\x00\x01\x00\x02\x002
279 s> \r\n
280 s> 0\r\n
281 s> \r\n
282 received frame(size=0; request=1; stream=2; streamflags=; type=command-response; flags=eos)
283 response: gen[
284 {
285 b'totalitems': 1
286 },
287 {
288 b'node': b'\x99/Gy\x02\x9a=\xf8\xd0fm\x00\xbb\x92OicN&A',
289 b'parents': [
290 b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
291 b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
292 ]
293 }
294 ]
295
296 $ sendhttpv2peer << EOF
297 > command manifestdata
298 > nodes eval:[b'\x99\x2f\x47\x79\x02\x9a\x3d\xf8\xd0\x66\x6d\x00\xbb\x92\x4f\x69\x63\x4e\x26\x41']
299 > tree eval:b''
300 > fields eval:[b'parents']
301 > EOF
302 creating http peer for wire protocol version 2
303 sending manifestdata command
304 s> POST /api/exp-http-v2-0002/ro/manifestdata HTTP/1.1\r\n
305 s> Accept-Encoding: identity\r\n
306 s> accept: application/mercurial-exp-framing-0005\r\n
307 s> content-type: application/mercurial-exp-framing-0005\r\n
308 s> content-length: 83\r\n
309 s> host: $LOCALIP:$HGPORT\r\n (glob)
310 s> user-agent: Mercurial debugwireproto\r\n
311 s> \r\n
312 s> K\x00\x00\x01\x00\x01\x01\x11\xa2Dargs\xa3Ffields\x81GparentsEnodes\x81T\x99/Gy\x02\x9a=\xf8\xd0fm\x00\xbb\x92OicN&ADtree@DnameLmanifestdata
313 s> makefile('rb', None)
314 s> HTTP/1.1 200 OK\r\n
315 s> Server: testing stub value\r\n
316 s> Date: $HTTP_DATE$\r\n
317 s> Content-Type: application/mercurial-exp-framing-0005\r\n
318 s> Transfer-Encoding: chunked\r\n
319 s> \r\n
320 s> 13\r\n
321 s> \x0b\x00\x00\x01\x00\x02\x011
322 s> \xa1FstatusBok
323 s> \r\n
324 received frame(size=11; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=continuation)
325 s> 63\r\n
326 s> [\x00\x00\x01\x00\x02\x001
327 s> \xa1Jtotalitems\x01\xa2DnodeT\x99/Gy\x02\x9a=\xf8\xd0fm\x00\xbb\x92OicN&AGparents\x82T\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00T\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
328 s> \r\n
329 received frame(size=91; request=1; stream=2; streamflags=; type=command-response; flags=continuation)
330 s> 8\r\n
331 s> \x00\x00\x00\x01\x00\x02\x002
332 s> \r\n
333 s> 0\r\n
334 s> \r\n
335 received frame(size=0; request=1; stream=2; streamflags=; type=command-response; flags=eos)
336 response: gen[
337 {
338 b'totalitems': 1
339 },
340 {
341 b'node': b'\x99/Gy\x02\x9a=\xf8\xd0fm\x00\xbb\x92OicN&A',
342 b'parents': [
343 b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
344 b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
345 ]
346 }
347 ]
348
349 $ cat .hg/blackbox.log
350 *> cacher constructed for manifestdata (glob)
351 *> cache miss for c045a581599d58608efd3d93d8129841f2af04a0 (glob)
352 *> storing cache entry for c045a581599d58608efd3d93d8129841f2af04a0 (glob)
353 *> cacher constructed for manifestdata (glob)
354 *> cache hit for c045a581599d58608efd3d93d8129841f2af04a0 (glob)
355
356 $ cat error.log
357
358 $ killdaemons.py
359 $ rm .hg/blackbox.log
360
361 A non-cacheable command does not instantiate cacher
362
363 $ hg serve -p $HGPORT -d --pid-file hg.pid -E error.log
364 $ cat hg.pid > $DAEMON_PIDS
365 $ sendhttpv2peer << EOF
366 > command capabilities
367 > EOF
368 creating http peer for wire protocol version 2
369 sending capabilities command
370 s> POST /api/exp-http-v2-0002/ro/capabilities HTTP/1.1\r\n
371 s> Accept-Encoding: identity\r\n
372 s> accept: application/mercurial-exp-framing-0005\r\n
373 s> content-type: application/mercurial-exp-framing-0005\r\n
374 s> content-length: 27\r\n
375 s> host: $LOCALIP:$HGPORT\r\n (glob)
376 s> user-agent: Mercurial debugwireproto\r\n
377 s> \r\n
378 s> \x13\x00\x00\x01\x00\x01\x01\x11\xa1DnameLcapabilities
379 s> makefile('rb', None)
380 s> HTTP/1.1 200 OK\r\n
381 s> Server: testing stub value\r\n
382 s> Date: $HTTP_DATE$\r\n
383 s> Content-Type: application/mercurial-exp-framing-0005\r\n
384 s> Transfer-Encoding: chunked\r\n
385 s> \r\n
386 s> 13\r\n
387 s> \x0b\x00\x00\x01\x00\x02\x011
388 s> \xa1FstatusBok
389 s> \r\n
390 received frame(size=11; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=continuation)
391 s> 52b\r\n
392 s> #\x05\x00\x01\x00\x02\x001
393 s> \xa5Hcommands\xaaIbranchmap\xa2Dargs\xa0Kpermissions\x81DpullLcapabilities\xa2Dargs\xa0Kpermissions\x81DpullMchangesetdata\xa2Dargs\xa4Ffields\xa4Gdefault\xd9\x01\x02\x80Hrequired\xf4DtypeCsetKvalidvalues\xd9\x01\x02\x84IbookmarksGparentsEphaseHrevisionInoderange\xa3Gdefault\xf6Hrequired\xf4DtypeDlistEnodes\xa3Gdefault\xf6Hrequired\xf4DtypeDlistJnodesdepth\xa3Gdefault\xf6Hrequired\xf4DtypeCintKpermissions\x81DpullHfiledata\xa2Dargs\xa4Ffields\xa4Gdefault\xd9\x01\x02\x80Hrequired\xf4DtypeCsetKvalidvalues\xd9\x01\x02\x82GparentsHrevisionKhaveparents\xa3Gdefault\xf4Hrequired\xf4DtypeDboolEnodes\xa2Hrequired\xf5DtypeDlistDpath\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullEheads\xa2Dargs\xa1Jpubliconly\xa3Gdefault\xf4Hrequired\xf4DtypeDboolKpermissions\x81DpullEknown\xa2Dargs\xa1Enodes\xa3Gdefault\x80Hrequired\xf4DtypeDlistKpermissions\x81DpullHlistkeys\xa2Dargs\xa1Inamespace\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullFlookup\xa2Dargs\xa1Ckey\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullLmanifestdata\xa2Dargs\xa4Ffields\xa4Gdefault\xd9\x01\x02\x80Hrequired\xf4DtypeCsetKvalidvalues\xd9\x01\x02\x82GparentsHrevisionKhaveparents\xa3Gdefault\xf4Hrequired\xf4DtypeDboolEnodes\xa2Hrequired\xf5DtypeDlistDtree\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullGpushkey\xa2Dargs\xa4Ckey\xa2Hrequired\xf5DtypeEbytesInamespace\xa2Hrequired\xf5DtypeEbytesCnew\xa2Hrequired\xf5DtypeEbytesCold\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpushKcompression\x82\xa1DnameDzstd\xa1DnameDzlibQframingmediatypes\x81X&application/mercurial-exp-framing-0005Rpathfilterprefixes\xd9\x01\x02\x82Epath:Lrootfilesin:Nrawrepoformats\x82LgeneraldeltaHrevlogv1
394 s> \r\n
395 received frame(size=1315; request=1; stream=2; streamflags=; type=command-response; flags=continuation)
396 s> 8\r\n
397 s> \x00\x00\x00\x01\x00\x02\x002
398 s> \r\n
399 s> 0\r\n
400 s> \r\n
401 received frame(size=0; request=1; stream=2; streamflags=; type=command-response; flags=eos)
402 response: gen[
403 {
404 b'commands': {
405 b'branchmap': {
406 b'args': {},
407 b'permissions': [
408 b'pull'
409 ]
410 },
411 b'capabilities': {
412 b'args': {},
413 b'permissions': [
414 b'pull'
415 ]
416 },
417 b'changesetdata': {
418 b'args': {
419 b'fields': {
420 b'default': set([]),
421 b'required': False,
422 b'type': b'set',
423 b'validvalues': set([
424 b'bookmarks',
425 b'parents',
426 b'phase',
427 b'revision'
428 ])
429 },
430 b'noderange': {
431 b'default': None,
432 b'required': False,
433 b'type': b'list'
434 },
435 b'nodes': {
436 b'default': None,
437 b'required': False,
438 b'type': b'list'
439 },
440 b'nodesdepth': {
441 b'default': None,
442 b'required': False,
443 b'type': b'int'
444 }
445 },
446 b'permissions': [
447 b'pull'
448 ]
449 },
450 b'filedata': {
451 b'args': {
452 b'fields': {
453 b'default': set([]),
454 b'required': False,
455 b'type': b'set',
456 b'validvalues': set([
457 b'parents',
458 b'revision'
459 ])
460 },
461 b'haveparents': {
462 b'default': False,
463 b'required': False,
464 b'type': b'bool'
465 },
466 b'nodes': {
467 b'required': True,
468 b'type': b'list'
469 },
470 b'path': {
471 b'required': True,
472 b'type': b'bytes'
473 }
474 },
475 b'permissions': [
476 b'pull'
477 ]
478 },
479 b'heads': {
480 b'args': {
481 b'publiconly': {
482 b'default': False,
483 b'required': False,
484 b'type': b'bool'
485 }
486 },
487 b'permissions': [
488 b'pull'
489 ]
490 },
491 b'known': {
492 b'args': {
493 b'nodes': {
494 b'default': [],
495 b'required': False,
496 b'type': b'list'
497 }
498 },
499 b'permissions': [
500 b'pull'
501 ]
502 },
503 b'listkeys': {
504 b'args': {
505 b'namespace': {
506 b'required': True,
507 b'type': b'bytes'
508 }
509 },
510 b'permissions': [
511 b'pull'
512 ]
513 },
514 b'lookup': {
515 b'args': {
516 b'key': {
517 b'required': True,
518 b'type': b'bytes'
519 }
520 },
521 b'permissions': [
522 b'pull'
523 ]
524 },
525 b'manifestdata': {
526 b'args': {
527 b'fields': {
528 b'default': set([]),
529 b'required': False,
530 b'type': b'set',
531 b'validvalues': set([
532 b'parents',
533 b'revision'
534 ])
535 },
536 b'haveparents': {
537 b'default': False,
538 b'required': False,
539 b'type': b'bool'
540 },
541 b'nodes': {
542 b'required': True,
543 b'type': b'list'
544 },
545 b'tree': {
546 b'required': True,
547 b'type': b'bytes'
548 }
549 },
550 b'permissions': [
551 b'pull'
552 ]
553 },
554 b'pushkey': {
555 b'args': {
556 b'key': {
557 b'required': True,
558 b'type': b'bytes'
559 },
560 b'namespace': {
561 b'required': True,
562 b'type': b'bytes'
563 },
564 b'new': {
565 b'required': True,
566 b'type': b'bytes'
567 },
568 b'old': {
569 b'required': True,
570 b'type': b'bytes'
571 }
572 },
573 b'permissions': [
574 b'push'
575 ]
576 }
577 },
578 b'compression': [
579 {
580 b'name': b'zstd'
581 },
582 {
583 b'name': b'zlib'
584 }
585 ],
586 b'framingmediatypes': [
587 b'application/mercurial-exp-framing-0005'
588 ],
589 b'pathfilterprefixes': set([
590 b'path:',
591 b'rootfilesin:'
592 ]),
593 b'rawrepoformats': [
594 b'generaldelta',
595 b'revlogv1'
596 ]
597 }
598 ]
599
600 $ test -f .hg/blackbox.log
601 [1]
602
603 An error is not cached
604
605 $ sendhttpv2peer << EOF
606 > command manifestdata
607 > nodes eval:[b'\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa']
608 > tree eval:b''
609 > fields eval:[b'parents']
610 > EOF
611 creating http peer for wire protocol version 2
612 sending manifestdata command
613 s> POST /api/exp-http-v2-0002/ro/manifestdata HTTP/1.1\r\n
614 s> Accept-Encoding: identity\r\n
615 s> accept: application/mercurial-exp-framing-0005\r\n
616 s> content-type: application/mercurial-exp-framing-0005\r\n
617 s> content-length: 83\r\n
618 s> host: $LOCALIP:$HGPORT\r\n (glob)
619 s> user-agent: Mercurial debugwireproto\r\n
620 s> \r\n
621 s> K\x00\x00\x01\x00\x01\x01\x11\xa2Dargs\xa3Ffields\x81GparentsEnodes\x81T\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaaDtree@DnameLmanifestdata
622 s> makefile('rb', None)
623 s> HTTP/1.1 200 OK\r\n
624 s> Server: testing stub value\r\n
625 s> Date: $HTTP_DATE$\r\n
626 s> Content-Type: application/mercurial-exp-framing-0005\r\n
627 s> Transfer-Encoding: chunked\r\n
628 s> \r\n
629 s> 51\r\n
630 s> I\x00\x00\x01\x00\x02\x012
631 s> \xa2Eerror\xa2Dargs\x81T\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaaGmessagePunknown node: %sFstatusEerror
632 s> \r\n
633 received frame(size=73; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=eos)
634 s> 0\r\n
635 s> \r\n
636 abort: unknown node: \xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa! (esc)
637 [255]
638
639 $ cat .hg/blackbox.log
640 *> cacher constructed for manifestdata (glob)
641 *> cache miss for 9d1bb421d99e913d45f2d099aa49728514292dd2 (glob)
642 *> cacher exiting due to error (glob)
643
644 $ killdaemons.py
645 $ rm .hg/blackbox.log
@@ -0,0 +1,100 b''
1 # wireprotosimplecache.py - Extension providing in-memory wire protocol cache
2 #
3 # Copyright 2018 Gregory Szorc <gregory.szorc@gmail.com>
4 #
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
7
8 from __future__ import absolute_import
9
10 from mercurial import (
11 extensions,
12 registrar,
13 repository,
14 util,
15 wireprototypes,
16 wireprotov2server,
17 )
18 from mercurial.utils import (
19 interfaceutil,
20 )
21
22 CACHE = None
23
24 configtable = {}
25 configitem = registrar.configitem(configtable)
26
27 configitem('simplecache', 'cacheobjects',
28 default=False)
29
30 @interfaceutil.implementer(repository.iwireprotocolcommandcacher)
31 class memorycacher(object):
32 def __init__(self, ui, command, encodefn):
33 self.ui = ui
34 self.encodefn = encodefn
35 self.key = None
36 self.cacheobjects = ui.configbool('simplecache', 'cacheobjects')
37 self.buffered = []
38
39 ui.log('simplecache', 'cacher constructed for %s\n', command)
40
41 def __enter__(self):
42 return self
43
44 def __exit__(self, exctype, excvalue, exctb):
45 if exctype:
46 self.ui.log('simplecache', 'cacher exiting due to error\n')
47
48 def adjustcachekeystate(self, state):
49 # Needed in order to make tests deterministic. Don't copy this
50 # pattern for production caches!
51 del state[b'repo']
52
53 def setcachekey(self, key):
54 self.key = key
55 return True
56
57 def lookup(self):
58 if self.key not in CACHE:
59 self.ui.log('simplecache', 'cache miss for %s\n', self.key)
60 return None
61
62 entry = CACHE[self.key]
63 self.ui.log('simplecache', 'cache hit for %s\n', self.key)
64
65 if self.cacheobjects:
66 return {
67 'objs': entry,
68 }
69 else:
70 return {
71 'objs': [wireprototypes.encodedresponse(entry)],
72 }
73
74 def onobject(self, obj):
75 if self.cacheobjects:
76 self.buffered.append(obj)
77 else:
78 self.buffered.extend(self.encodefn(obj))
79
80 yield obj
81
82 def onfinished(self):
83 self.ui.log('simplecache', 'storing cache entry for %s\n', self.key)
84 if self.cacheobjects:
85 CACHE[self.key] = self.buffered
86 else:
87 CACHE[self.key] = b''.join(self.buffered)
88
89 return []
90
91 def makeresponsecacher(orig, repo, proto, command, args, objencoderfn):
92 return memorycacher(repo.ui, command, objencoderfn)
93
94 def extsetup(ui):
95 global CACHE
96
97 CACHE = util.lrucachedict(10000)
98
99 extensions.wrapfunction(wireprotov2server, 'makeresponsecacher',
100 makeresponsecacher)
@@ -1,1659 +1,1816 b''
1 1 # repository.py - Interfaces and base classes for repositories and peers.
2 2 #
3 3 # Copyright 2017 Gregory Szorc <gregory.szorc@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 from .i18n import _
11 11 from . import (
12 12 error,
13 13 )
14 14 from .utils import (
15 15 interfaceutil,
16 16 )
17 17
18 18 # When narrowing is finalized and no longer subject to format changes,
19 19 # we should move this to just "narrow" or similar.
20 20 NARROW_REQUIREMENT = 'narrowhg-experimental'
21 21
22 22 # Local repository feature string.
23 23
24 24 # Revlogs are being used for file storage.
25 25 REPO_FEATURE_REVLOG_FILE_STORAGE = b'revlogfilestorage'
26 26 # The storage part of the repository is shared from an external source.
27 27 REPO_FEATURE_SHARED_STORAGE = b'sharedstore'
28 28 # LFS supported for backing file storage.
29 29 REPO_FEATURE_LFS = b'lfs'
30 30
31 31 class ipeerconnection(interfaceutil.Interface):
32 32 """Represents a "connection" to a repository.
33 33
34 34 This is the base interface for representing a connection to a repository.
35 35 It holds basic properties and methods applicable to all peer types.
36 36
37 37 This is not a complete interface definition and should not be used
38 38 outside of this module.
39 39 """
40 40 ui = interfaceutil.Attribute("""ui.ui instance""")
41 41
42 42 def url():
43 43 """Returns a URL string representing this peer.
44 44
45 45 Currently, implementations expose the raw URL used to construct the
46 46 instance. It may contain credentials as part of the URL. The
47 47 expectations of the value aren't well-defined and this could lead to
48 48 data leakage.
49 49
50 50 TODO audit/clean consumers and more clearly define the contents of this
51 51 value.
52 52 """
53 53
54 54 def local():
55 55 """Returns a local repository instance.
56 56
57 57 If the peer represents a local repository, returns an object that
58 58 can be used to interface with it. Otherwise returns ``None``.
59 59 """
60 60
61 61 def peer():
62 62 """Returns an object conforming to this interface.
63 63
64 64 Most implementations will ``return self``.
65 65 """
66 66
67 67 def canpush():
68 68 """Returns a boolean indicating if this peer can be pushed to."""
69 69
70 70 def close():
71 71 """Close the connection to this peer.
72 72
73 73 This is called when the peer will no longer be used. Resources
74 74 associated with the peer should be cleaned up.
75 75 """
76 76
77 77 class ipeercapabilities(interfaceutil.Interface):
78 78 """Peer sub-interface related to capabilities."""
79 79
80 80 def capable(name):
81 81 """Determine support for a named capability.
82 82
83 83 Returns ``False`` if capability not supported.
84 84
85 85 Returns ``True`` if boolean capability is supported. Returns a string
86 86 if capability support is non-boolean.
87 87
88 88 Capability strings may or may not map to wire protocol capabilities.
89 89 """
90 90
91 91 def requirecap(name, purpose):
92 92 """Require a capability to be present.
93 93
94 94 Raises a ``CapabilityError`` if the capability isn't present.
95 95 """
96 96
97 97 class ipeercommands(interfaceutil.Interface):
98 98 """Client-side interface for communicating over the wire protocol.
99 99
100 100 This interface is used as a gateway to the Mercurial wire protocol.
101 101 methods commonly call wire protocol commands of the same name.
102 102 """
103 103
104 104 def branchmap():
105 105 """Obtain heads in named branches.
106 106
107 107 Returns a dict mapping branch name to an iterable of nodes that are
108 108 heads on that branch.
109 109 """
110 110
111 111 def capabilities():
112 112 """Obtain capabilities of the peer.
113 113
114 114 Returns a set of string capabilities.
115 115 """
116 116
117 117 def clonebundles():
118 118 """Obtains the clone bundles manifest for the repo.
119 119
120 120 Returns the manifest as unparsed bytes.
121 121 """
122 122
123 123 def debugwireargs(one, two, three=None, four=None, five=None):
124 124 """Used to facilitate debugging of arguments passed over the wire."""
125 125
126 126 def getbundle(source, **kwargs):
127 127 """Obtain remote repository data as a bundle.
128 128
129 129 This command is how the bulk of repository data is transferred from
130 130 the peer to the local repository
131 131
132 132 Returns a generator of bundle data.
133 133 """
134 134
135 135 def heads():
136 136 """Determine all known head revisions in the peer.
137 137
138 138 Returns an iterable of binary nodes.
139 139 """
140 140
141 141 def known(nodes):
142 142 """Determine whether multiple nodes are known.
143 143
144 144 Accepts an iterable of nodes whose presence to check for.
145 145
146 146 Returns an iterable of booleans indicating of the corresponding node
147 147 at that index is known to the peer.
148 148 """
149 149
150 150 def listkeys(namespace):
151 151 """Obtain all keys in a pushkey namespace.
152 152
153 153 Returns an iterable of key names.
154 154 """
155 155
156 156 def lookup(key):
157 157 """Resolve a value to a known revision.
158 158
159 159 Returns a binary node of the resolved revision on success.
160 160 """
161 161
162 162 def pushkey(namespace, key, old, new):
163 163 """Set a value using the ``pushkey`` protocol.
164 164
165 165 Arguments correspond to the pushkey namespace and key to operate on and
166 166 the old and new values for that key.
167 167
168 168 Returns a string with the peer result. The value inside varies by the
169 169 namespace.
170 170 """
171 171
172 172 def stream_out():
173 173 """Obtain streaming clone data.
174 174
175 175 Successful result should be a generator of data chunks.
176 176 """
177 177
178 178 def unbundle(bundle, heads, url):
179 179 """Transfer repository data to the peer.
180 180
181 181 This is how the bulk of data during a push is transferred.
182 182
183 183 Returns the integer number of heads added to the peer.
184 184 """
185 185
186 186 class ipeerlegacycommands(interfaceutil.Interface):
187 187 """Interface for implementing support for legacy wire protocol commands.
188 188
189 189 Wire protocol commands transition to legacy status when they are no longer
190 190 used by modern clients. To facilitate identifying which commands are
191 191 legacy, the interfaces are split.
192 192 """
193 193
194 194 def between(pairs):
195 195 """Obtain nodes between pairs of nodes.
196 196
197 197 ``pairs`` is an iterable of node pairs.
198 198
199 199 Returns an iterable of iterables of nodes corresponding to each
200 200 requested pair.
201 201 """
202 202
203 203 def branches(nodes):
204 204 """Obtain ancestor changesets of specific nodes back to a branch point.
205 205
206 206 For each requested node, the peer finds the first ancestor node that is
207 207 a DAG root or is a merge.
208 208
209 209 Returns an iterable of iterables with the resolved values for each node.
210 210 """
211 211
212 212 def changegroup(nodes, source):
213 213 """Obtain a changegroup with data for descendants of specified nodes."""
214 214
215 215 def changegroupsubset(bases, heads, source):
216 216 pass
217 217
218 218 class ipeercommandexecutor(interfaceutil.Interface):
219 219 """Represents a mechanism to execute remote commands.
220 220
221 221 This is the primary interface for requesting that wire protocol commands
222 222 be executed. Instances of this interface are active in a context manager
223 223 and have a well-defined lifetime. When the context manager exits, all
224 224 outstanding requests are waited on.
225 225 """
226 226
227 227 def callcommand(name, args):
228 228 """Request that a named command be executed.
229 229
230 230 Receives the command name and a dictionary of command arguments.
231 231
232 232 Returns a ``concurrent.futures.Future`` that will resolve to the
233 233 result of that command request. That exact value is left up to
234 234 the implementation and possibly varies by command.
235 235
236 236 Not all commands can coexist with other commands in an executor
237 237 instance: it depends on the underlying wire protocol transport being
238 238 used and the command itself.
239 239
240 240 Implementations MAY call ``sendcommands()`` automatically if the
241 241 requested command can not coexist with other commands in this executor.
242 242
243 243 Implementations MAY call ``sendcommands()`` automatically when the
244 244 future's ``result()`` is called. So, consumers using multiple
245 245 commands with an executor MUST ensure that ``result()`` is not called
246 246 until all command requests have been issued.
247 247 """
248 248
249 249 def sendcommands():
250 250 """Trigger submission of queued command requests.
251 251
252 252 Not all transports submit commands as soon as they are requested to
253 253 run. When called, this method forces queued command requests to be
254 254 issued. It will no-op if all commands have already been sent.
255 255
256 256 When called, no more new commands may be issued with this executor.
257 257 """
258 258
259 259 def close():
260 260 """Signal that this command request is finished.
261 261
262 262 When called, no more new commands may be issued. All outstanding
263 263 commands that have previously been issued are waited on before
264 264 returning. This not only includes waiting for the futures to resolve,
265 265 but also waiting for all response data to arrive. In other words,
266 266 calling this waits for all on-wire state for issued command requests
267 267 to finish.
268 268
269 269 When used as a context manager, this method is called when exiting the
270 270 context manager.
271 271
272 272 This method may call ``sendcommands()`` if there are buffered commands.
273 273 """
274 274
275 275 class ipeerrequests(interfaceutil.Interface):
276 276 """Interface for executing commands on a peer."""
277 277
278 278 def commandexecutor():
279 279 """A context manager that resolves to an ipeercommandexecutor.
280 280
281 281 The object this resolves to can be used to issue command requests
282 282 to the peer.
283 283
284 284 Callers should call its ``callcommand`` method to issue command
285 285 requests.
286 286
287 287 A new executor should be obtained for each distinct set of commands
288 288 (possibly just a single command) that the consumer wants to execute
289 289 as part of a single operation or round trip. This is because some
290 290 peers are half-duplex and/or don't support persistent connections.
291 291 e.g. in the case of HTTP peers, commands sent to an executor represent
292 292 a single HTTP request. While some peers may support multiple command
293 293 sends over the wire per executor, consumers need to code to the least
294 294 capable peer. So it should be assumed that command executors buffer
295 295 called commands until they are told to send them and that each
296 296 command executor could result in a new connection or wire-level request
297 297 being issued.
298 298 """
299 299
300 300 class ipeerbase(ipeerconnection, ipeercapabilities, ipeerrequests):
301 301 """Unified interface for peer repositories.
302 302
303 303 All peer instances must conform to this interface.
304 304 """
305 305
306 306 @interfaceutil.implementer(ipeerbase)
307 307 class peer(object):
308 308 """Base class for peer repositories."""
309 309
310 310 def capable(self, name):
311 311 caps = self.capabilities()
312 312 if name in caps:
313 313 return True
314 314
315 315 name = '%s=' % name
316 316 for cap in caps:
317 317 if cap.startswith(name):
318 318 return cap[len(name):]
319 319
320 320 return False
321 321
322 322 def requirecap(self, name, purpose):
323 323 if self.capable(name):
324 324 return
325 325
326 326 raise error.CapabilityError(
327 327 _('cannot %s; remote repository does not support the %r '
328 328 'capability') % (purpose, name))
329 329
330 330 class iverifyproblem(interfaceutil.Interface):
331 331 """Represents a problem with the integrity of the repository.
332 332
333 333 Instances of this interface are emitted to describe an integrity issue
334 334 with a repository (e.g. corrupt storage, missing data, etc).
335 335
336 336 Instances are essentially messages associated with severity.
337 337 """
338 338 warning = interfaceutil.Attribute(
339 339 """Message indicating a non-fatal problem.""")
340 340
341 341 error = interfaceutil.Attribute(
342 342 """Message indicating a fatal problem.""")
343 343
344 344 node = interfaceutil.Attribute(
345 345 """Revision encountering the problem.
346 346
347 347 ``None`` means the problem doesn't apply to a single revision.
348 348 """)
349 349
350 350 class irevisiondelta(interfaceutil.Interface):
351 351 """Represents a delta between one revision and another.
352 352
353 353 Instances convey enough information to allow a revision to be exchanged
354 354 with another repository.
355 355
356 356 Instances represent the fulltext revision data or a delta against
357 357 another revision. Therefore the ``revision`` and ``delta`` attributes
358 358 are mutually exclusive.
359 359
360 360 Typically used for changegroup generation.
361 361 """
362 362
363 363 node = interfaceutil.Attribute(
364 364 """20 byte node of this revision.""")
365 365
366 366 p1node = interfaceutil.Attribute(
367 367 """20 byte node of 1st parent of this revision.""")
368 368
369 369 p2node = interfaceutil.Attribute(
370 370 """20 byte node of 2nd parent of this revision.""")
371 371
372 372 linknode = interfaceutil.Attribute(
373 373 """20 byte node of the changelog revision this node is linked to.""")
374 374
375 375 flags = interfaceutil.Attribute(
376 376 """2 bytes of integer flags that apply to this revision.""")
377 377
378 378 basenode = interfaceutil.Attribute(
379 379 """20 byte node of the revision this data is a delta against.
380 380
381 381 ``nullid`` indicates that the revision is a full revision and not
382 382 a delta.
383 383 """)
384 384
385 385 baserevisionsize = interfaceutil.Attribute(
386 386 """Size of base revision this delta is against.
387 387
388 388 May be ``None`` if ``basenode`` is ``nullid``.
389 389 """)
390 390
391 391 revision = interfaceutil.Attribute(
392 392 """Raw fulltext of revision data for this node.""")
393 393
394 394 delta = interfaceutil.Attribute(
395 395 """Delta between ``basenode`` and ``node``.
396 396
397 397 Stored in the bdiff delta format.
398 398 """)
399 399
400 400 class ifilerevisionssequence(interfaceutil.Interface):
401 401 """Contains index data for all revisions of a file.
402 402
403 403 Types implementing this behave like lists of tuples. The index
404 404 in the list corresponds to the revision number. The values contain
405 405 index metadata.
406 406
407 407 The *null* revision (revision number -1) is always the last item
408 408 in the index.
409 409 """
410 410
411 411 def __len__():
412 412 """The total number of revisions."""
413 413
414 414 def __getitem__(rev):
415 415 """Returns the object having a specific revision number.
416 416
417 417 Returns an 8-tuple with the following fields:
418 418
419 419 offset+flags
420 420 Contains the offset and flags for the revision. 64-bit unsigned
421 421 integer where first 6 bytes are the offset and the next 2 bytes
422 422 are flags. The offset can be 0 if it is not used by the store.
423 423 compressed size
424 424 Size of the revision data in the store. It can be 0 if it isn't
425 425 needed by the store.
426 426 uncompressed size
427 427 Fulltext size. It can be 0 if it isn't needed by the store.
428 428 base revision
429 429 Revision number of revision the delta for storage is encoded
430 430 against. -1 indicates not encoded against a base revision.
431 431 link revision
432 432 Revision number of changelog revision this entry is related to.
433 433 p1 revision
434 434 Revision number of 1st parent. -1 if no 1st parent.
435 435 p2 revision
436 436 Revision number of 2nd parent. -1 if no 1st parent.
437 437 node
438 438 Binary node value for this revision number.
439 439
440 440 Negative values should index off the end of the sequence. ``-1``
441 441 should return the null revision. ``-2`` should return the most
442 442 recent revision.
443 443 """
444 444
445 445 def __contains__(rev):
446 446 """Whether a revision number exists."""
447 447
448 448 def insert(self, i, entry):
449 449 """Add an item to the index at specific revision."""
450 450
451 451 class ifileindex(interfaceutil.Interface):
452 452 """Storage interface for index data of a single file.
453 453
454 454 File storage data is divided into index metadata and data storage.
455 455 This interface defines the index portion of the interface.
456 456
457 457 The index logically consists of:
458 458
459 459 * A mapping between revision numbers and nodes.
460 460 * DAG data (storing and querying the relationship between nodes).
461 461 * Metadata to facilitate storage.
462 462 """
463 463 def __len__():
464 464 """Obtain the number of revisions stored for this file."""
465 465
466 466 def __iter__():
467 467 """Iterate over revision numbers for this file."""
468 468
469 469 def revs(start=0, stop=None):
470 470 """Iterate over revision numbers for this file, with control."""
471 471
472 472 def parents(node):
473 473 """Returns a 2-tuple of parent nodes for a revision.
474 474
475 475 Values will be ``nullid`` if the parent is empty.
476 476 """
477 477
478 478 def parentrevs(rev):
479 479 """Like parents() but operates on revision numbers."""
480 480
481 481 def rev(node):
482 482 """Obtain the revision number given a node.
483 483
484 484 Raises ``error.LookupError`` if the node is not known.
485 485 """
486 486
487 487 def node(rev):
488 488 """Obtain the node value given a revision number.
489 489
490 490 Raises ``IndexError`` if the node is not known.
491 491 """
492 492
493 493 def lookup(node):
494 494 """Attempt to resolve a value to a node.
495 495
496 496 Value can be a binary node, hex node, revision number, or a string
497 497 that can be converted to an integer.
498 498
499 499 Raises ``error.LookupError`` if a node could not be resolved.
500 500 """
501 501
502 502 def linkrev(rev):
503 503 """Obtain the changeset revision number a revision is linked to."""
504 504
505 505 def iscensored(rev):
506 506 """Return whether a revision's content has been censored."""
507 507
508 508 def commonancestorsheads(node1, node2):
509 509 """Obtain an iterable of nodes containing heads of common ancestors.
510 510
511 511 See ``ancestor.commonancestorsheads()``.
512 512 """
513 513
514 514 def descendants(revs):
515 515 """Obtain descendant revision numbers for a set of revision numbers.
516 516
517 517 If ``nullrev`` is in the set, this is equivalent to ``revs()``.
518 518 """
519 519
520 520 def heads(start=None, stop=None):
521 521 """Obtain a list of nodes that are DAG heads, with control.
522 522
523 523 The set of revisions examined can be limited by specifying
524 524 ``start`` and ``stop``. ``start`` is a node. ``stop`` is an
525 525 iterable of nodes. DAG traversal starts at earlier revision
526 526 ``start`` and iterates forward until any node in ``stop`` is
527 527 encountered.
528 528 """
529 529
530 530 def children(node):
531 531 """Obtain nodes that are children of a node.
532 532
533 533 Returns a list of nodes.
534 534 """
535 535
536 536 class ifiledata(interfaceutil.Interface):
537 537 """Storage interface for data storage of a specific file.
538 538
539 539 This complements ``ifileindex`` and provides an interface for accessing
540 540 data for a tracked file.
541 541 """
542 542 def size(rev):
543 543 """Obtain the fulltext size of file data.
544 544
545 545 Any metadata is excluded from size measurements.
546 546 """
547 547
548 548 def revision(node, raw=False):
549 549 """"Obtain fulltext data for a node.
550 550
551 551 By default, any storage transformations are applied before the data
552 552 is returned. If ``raw`` is True, non-raw storage transformations
553 553 are not applied.
554 554
555 555 The fulltext data may contain a header containing metadata. Most
556 556 consumers should use ``read()`` to obtain the actual file data.
557 557 """
558 558
559 559 def read(node):
560 560 """Resolve file fulltext data.
561 561
562 562 This is similar to ``revision()`` except any metadata in the data
563 563 headers is stripped.
564 564 """
565 565
566 566 def renamed(node):
567 567 """Obtain copy metadata for a node.
568 568
569 569 Returns ``False`` if no copy metadata is stored or a 2-tuple of
570 570 (path, node) from which this revision was copied.
571 571 """
572 572
573 573 def cmp(node, fulltext):
574 574 """Compare fulltext to another revision.
575 575
576 576 Returns True if the fulltext is different from what is stored.
577 577
578 578 This takes copy metadata into account.
579 579
580 580 TODO better document the copy metadata and censoring logic.
581 581 """
582 582
583 583 def emitrevisions(nodes,
584 584 nodesorder=None,
585 585 revisiondata=False,
586 586 assumehaveparentrevisions=False,
587 587 deltaprevious=False):
588 588 """Produce ``irevisiondelta`` for revisions.
589 589
590 590 Given an iterable of nodes, emits objects conforming to the
591 591 ``irevisiondelta`` interface that describe revisions in storage.
592 592
593 593 This method is a generator.
594 594
595 595 The input nodes may be unordered. Implementations must ensure that a
596 596 node's parents are emitted before the node itself. Transitively, this
597 597 means that a node may only be emitted once all its ancestors in
598 598 ``nodes`` have also been emitted.
599 599
600 600 By default, emits "index" data (the ``node``, ``p1node``, and
601 601 ``p2node`` attributes). If ``revisiondata`` is set, revision data
602 602 will also be present on the emitted objects.
603 603
604 604 With default argument values, implementations can choose to emit
605 605 either fulltext revision data or a delta. When emitting deltas,
606 606 implementations must consider whether the delta's base revision
607 607 fulltext is available to the receiver.
608 608
609 609 The base revision fulltext is guaranteed to be available if any of
610 610 the following are met:
611 611
612 612 * Its fulltext revision was emitted by this method call.
613 613 * A delta for that revision was emitted by this method call.
614 614 * ``assumehaveparentrevisions`` is True and the base revision is a
615 615 parent of the node.
616 616
617 617 ``nodesorder`` can be used to control the order that revisions are
618 618 emitted. By default, revisions can be reordered as long as they are
619 619 in DAG topological order (see above). If the value is ``nodes``,
620 620 the iteration order from ``nodes`` should be used. If the value is
621 621 ``storage``, then the native order from the backing storage layer
622 622 is used. (Not all storage layers will have strong ordering and behavior
623 623 of this mode is storage-dependent.) ``nodes`` ordering can force
624 624 revisions to be emitted before their ancestors, so consumers should
625 625 use it with care.
626 626
627 627 The ``linknode`` attribute on the returned ``irevisiondelta`` may not
628 628 be set and it is the caller's responsibility to resolve it, if needed.
629 629
630 630 If ``deltaprevious`` is True and revision data is requested, all
631 631 revision data should be emitted as deltas against the revision
632 632 emitted just prior. The initial revision should be a delta against
633 633 its 1st parent.
634 634 """
635 635
636 636 class ifilemutation(interfaceutil.Interface):
637 637 """Storage interface for mutation events of a tracked file."""
638 638
639 639 def add(filedata, meta, transaction, linkrev, p1, p2):
640 640 """Add a new revision to the store.
641 641
642 642 Takes file data, dictionary of metadata, a transaction, linkrev,
643 643 and parent nodes.
644 644
645 645 Returns the node that was added.
646 646
647 647 May no-op if a revision matching the supplied data is already stored.
648 648 """
649 649
650 650 def addrevision(revisiondata, transaction, linkrev, p1, p2, node=None,
651 651 flags=0, cachedelta=None):
652 652 """Add a new revision to the store.
653 653
654 654 This is similar to ``add()`` except it operates at a lower level.
655 655
656 656 The data passed in already contains a metadata header, if any.
657 657
658 658 ``node`` and ``flags`` can be used to define the expected node and
659 659 the flags to use with storage.
660 660
661 661 ``add()`` is usually called when adding files from e.g. the working
662 662 directory. ``addrevision()`` is often called by ``add()`` and for
663 663 scenarios where revision data has already been computed, such as when
664 664 applying raw data from a peer repo.
665 665 """
666 666
667 667 def addgroup(deltas, linkmapper, transaction, addrevisioncb=None):
668 668 """Process a series of deltas for storage.
669 669
670 670 ``deltas`` is an iterable of 7-tuples of
671 671 (node, p1, p2, linknode, deltabase, delta, flags) defining revisions
672 672 to add.
673 673
674 674 The ``delta`` field contains ``mpatch`` data to apply to a base
675 675 revision, identified by ``deltabase``. The base node can be
676 676 ``nullid``, in which case the header from the delta can be ignored
677 677 and the delta used as the fulltext.
678 678
679 679 ``addrevisioncb`` should be called for each node as it is committed.
680 680
681 681 Returns a list of nodes that were processed. A node will be in the list
682 682 even if it existed in the store previously.
683 683 """
684 684
685 685 def censorrevision(tr, node, tombstone=b''):
686 686 """Remove the content of a single revision.
687 687
688 688 The specified ``node`` will have its content purged from storage.
689 689 Future attempts to access the revision data for this node will
690 690 result in failure.
691 691
692 692 A ``tombstone`` message can optionally be stored. This message may be
693 693 displayed to users when they attempt to access the missing revision
694 694 data.
695 695
696 696 Storage backends may have stored deltas against the previous content
697 697 in this revision. As part of censoring a revision, these storage
698 698 backends are expected to rewrite any internally stored deltas such
699 699 that they no longer reference the deleted content.
700 700 """
701 701
702 702 def getstrippoint(minlink):
703 703 """Find the minimum revision that must be stripped to strip a linkrev.
704 704
705 705 Returns a 2-tuple containing the minimum revision number and a set
706 706 of all revisions numbers that would be broken by this strip.
707 707
708 708 TODO this is highly revlog centric and should be abstracted into
709 709 a higher-level deletion API. ``repair.strip()`` relies on this.
710 710 """
711 711
712 712 def strip(minlink, transaction):
713 713 """Remove storage of items starting at a linkrev.
714 714
715 715 This uses ``getstrippoint()`` to determine the first node to remove.
716 716 Then it effectively truncates storage for all revisions after that.
717 717
718 718 TODO this is highly revlog centric and should be abstracted into a
719 719 higher-level deletion API.
720 720 """
721 721
722 722 class ifilestorage(ifileindex, ifiledata, ifilemutation):
723 723 """Complete storage interface for a single tracked file."""
724 724
725 725 def files():
726 726 """Obtain paths that are backing storage for this file.
727 727
728 728 TODO this is used heavily by verify code and there should probably
729 729 be a better API for that.
730 730 """
731 731
732 732 def storageinfo(exclusivefiles=False, sharedfiles=False,
733 733 revisionscount=False, trackedsize=False,
734 734 storedsize=False):
735 735 """Obtain information about storage for this file's data.
736 736
737 737 Returns a dict describing storage for this tracked path. The keys
738 738 in the dict map to arguments of the same. The arguments are bools
739 739 indicating whether to calculate and obtain that data.
740 740
741 741 exclusivefiles
742 742 Iterable of (vfs, path) describing files that are exclusively
743 743 used to back storage for this tracked path.
744 744
745 745 sharedfiles
746 746 Iterable of (vfs, path) describing files that are used to back
747 747 storage for this tracked path. Those files may also provide storage
748 748 for other stored entities.
749 749
750 750 revisionscount
751 751 Number of revisions available for retrieval.
752 752
753 753 trackedsize
754 754 Total size in bytes of all tracked revisions. This is a sum of the
755 755 length of the fulltext of all revisions.
756 756
757 757 storedsize
758 758 Total size in bytes used to store data for all tracked revisions.
759 759 This is commonly less than ``trackedsize`` due to internal usage
760 760 of deltas rather than fulltext revisions.
761 761
762 762 Not all storage backends may support all queries are have a reasonable
763 763 value to use. In that case, the value should be set to ``None`` and
764 764 callers are expected to handle this special value.
765 765 """
766 766
767 767 def verifyintegrity(state):
768 768 """Verifies the integrity of file storage.
769 769
770 770 ``state`` is a dict holding state of the verifier process. It can be
771 771 used to communicate data between invocations of multiple storage
772 772 primitives.
773 773
774 774 If individual revisions cannot have their revision content resolved,
775 775 the method is expected to set the ``skipread`` key to a set of nodes
776 776 that encountered problems.
777 777
778 778 The method yields objects conforming to the ``iverifyproblem``
779 779 interface.
780 780 """
781 781
782 782 class idirs(interfaceutil.Interface):
783 783 """Interface representing a collection of directories from paths.
784 784
785 785 This interface is essentially a derived data structure representing
786 786 directories from a collection of paths.
787 787 """
788 788
789 789 def addpath(path):
790 790 """Add a path to the collection.
791 791
792 792 All directories in the path will be added to the collection.
793 793 """
794 794
795 795 def delpath(path):
796 796 """Remove a path from the collection.
797 797
798 798 If the removal was the last path in a particular directory, the
799 799 directory is removed from the collection.
800 800 """
801 801
802 802 def __iter__():
803 803 """Iterate over the directories in this collection of paths."""
804 804
805 805 def __contains__(path):
806 806 """Whether a specific directory is in this collection."""
807 807
808 808 class imanifestdict(interfaceutil.Interface):
809 809 """Interface representing a manifest data structure.
810 810
811 811 A manifest is effectively a dict mapping paths to entries. Each entry
812 812 consists of a binary node and extra flags affecting that entry.
813 813 """
814 814
815 815 def __getitem__(path):
816 816 """Returns the binary node value for a path in the manifest.
817 817
818 818 Raises ``KeyError`` if the path does not exist in the manifest.
819 819
820 820 Equivalent to ``self.find(path)[0]``.
821 821 """
822 822
823 823 def find(path):
824 824 """Returns the entry for a path in the manifest.
825 825
826 826 Returns a 2-tuple of (node, flags).
827 827
828 828 Raises ``KeyError`` if the path does not exist in the manifest.
829 829 """
830 830
831 831 def __len__():
832 832 """Return the number of entries in the manifest."""
833 833
834 834 def __nonzero__():
835 835 """Returns True if the manifest has entries, False otherwise."""
836 836
837 837 __bool__ = __nonzero__
838 838
839 839 def __setitem__(path, node):
840 840 """Define the node value for a path in the manifest.
841 841
842 842 If the path is already in the manifest, its flags will be copied to
843 843 the new entry.
844 844 """
845 845
846 846 def __contains__(path):
847 847 """Whether a path exists in the manifest."""
848 848
849 849 def __delitem__(path):
850 850 """Remove a path from the manifest.
851 851
852 852 Raises ``KeyError`` if the path is not in the manifest.
853 853 """
854 854
855 855 def __iter__():
856 856 """Iterate over paths in the manifest."""
857 857
858 858 def iterkeys():
859 859 """Iterate over paths in the manifest."""
860 860
861 861 def keys():
862 862 """Obtain a list of paths in the manifest."""
863 863
864 864 def filesnotin(other, match=None):
865 865 """Obtain the set of paths in this manifest but not in another.
866 866
867 867 ``match`` is an optional matcher function to be applied to both
868 868 manifests.
869 869
870 870 Returns a set of paths.
871 871 """
872 872
873 873 def dirs():
874 874 """Returns an object implementing the ``idirs`` interface."""
875 875
876 876 def hasdir(dir):
877 877 """Returns a bool indicating if a directory is in this manifest."""
878 878
879 879 def matches(match):
880 880 """Generate a new manifest filtered through a matcher.
881 881
882 882 Returns an object conforming to the ``imanifestdict`` interface.
883 883 """
884 884
885 885 def walk(match):
886 886 """Generator of paths in manifest satisfying a matcher.
887 887
888 888 This is equivalent to ``self.matches(match).iterkeys()`` except a new
889 889 manifest object is not created.
890 890
891 891 If the matcher has explicit files listed and they don't exist in
892 892 the manifest, ``match.bad()`` is called for each missing file.
893 893 """
894 894
895 895 def diff(other, match=None, clean=False):
896 896 """Find differences between this manifest and another.
897 897
898 898 This manifest is compared to ``other``.
899 899
900 900 If ``match`` is provided, the two manifests are filtered against this
901 901 matcher and only entries satisfying the matcher are compared.
902 902
903 903 If ``clean`` is True, unchanged files are included in the returned
904 904 object.
905 905
906 906 Returns a dict with paths as keys and values of 2-tuples of 2-tuples of
907 907 the form ``((node1, flag1), (node2, flag2))`` where ``(node1, flag1)``
908 908 represents the node and flags for this manifest and ``(node2, flag2)``
909 909 are the same for the other manifest.
910 910 """
911 911
912 912 def setflag(path, flag):
913 913 """Set the flag value for a given path.
914 914
915 915 Raises ``KeyError`` if the path is not already in the manifest.
916 916 """
917 917
918 918 def get(path, default=None):
919 919 """Obtain the node value for a path or a default value if missing."""
920 920
921 921 def flags(path, default=''):
922 922 """Return the flags value for a path or a default value if missing."""
923 923
924 924 def copy():
925 925 """Return a copy of this manifest."""
926 926
927 927 def items():
928 928 """Returns an iterable of (path, node) for items in this manifest."""
929 929
930 930 def iteritems():
931 931 """Identical to items()."""
932 932
933 933 def iterentries():
934 934 """Returns an iterable of (path, node, flags) for this manifest.
935 935
936 936 Similar to ``iteritems()`` except items are a 3-tuple and include
937 937 flags.
938 938 """
939 939
940 940 def text():
941 941 """Obtain the raw data representation for this manifest.
942 942
943 943 Result is used to create a manifest revision.
944 944 """
945 945
946 946 def fastdelta(base, changes):
947 947 """Obtain a delta between this manifest and another given changes.
948 948
949 949 ``base`` in the raw data representation for another manifest.
950 950
951 951 ``changes`` is an iterable of ``(path, to_delete)``.
952 952
953 953 Returns a 2-tuple containing ``bytearray(self.text())`` and the
954 954 delta between ``base`` and this manifest.
955 955 """
956 956
957 957 class imanifestrevisionbase(interfaceutil.Interface):
958 958 """Base interface representing a single revision of a manifest.
959 959
960 960 Should not be used as a primary interface: should always be inherited
961 961 as part of a larger interface.
962 962 """
963 963
964 964 def new():
965 965 """Obtain a new manifest instance.
966 966
967 967 Returns an object conforming to the ``imanifestrevisionwritable``
968 968 interface. The instance will be associated with the same
969 969 ``imanifestlog`` collection as this instance.
970 970 """
971 971
972 972 def copy():
973 973 """Obtain a copy of this manifest instance.
974 974
975 975 Returns an object conforming to the ``imanifestrevisionwritable``
976 976 interface. The instance will be associated with the same
977 977 ``imanifestlog`` collection as this instance.
978 978 """
979 979
980 980 def read():
981 981 """Obtain the parsed manifest data structure.
982 982
983 983 The returned object conforms to the ``imanifestdict`` interface.
984 984 """
985 985
986 986 class imanifestrevisionstored(imanifestrevisionbase):
987 987 """Interface representing a manifest revision committed to storage."""
988 988
989 989 def node():
990 990 """The binary node for this manifest."""
991 991
992 992 parents = interfaceutil.Attribute(
993 993 """List of binary nodes that are parents for this manifest revision."""
994 994 )
995 995
996 996 def readdelta(shallow=False):
997 997 """Obtain the manifest data structure representing changes from parent.
998 998
999 999 This manifest is compared to its 1st parent. A new manifest representing
1000 1000 those differences is constructed.
1001 1001
1002 1002 The returned object conforms to the ``imanifestdict`` interface.
1003 1003 """
1004 1004
1005 1005 def readfast(shallow=False):
1006 1006 """Calls either ``read()`` or ``readdelta()``.
1007 1007
1008 1008 The faster of the two options is called.
1009 1009 """
1010 1010
1011 1011 def find(key):
1012 1012 """Calls self.read().find(key)``.
1013 1013
1014 1014 Returns a 2-tuple of ``(node, flags)`` or raises ``KeyError``.
1015 1015 """
1016 1016
1017 1017 class imanifestrevisionwritable(imanifestrevisionbase):
1018 1018 """Interface representing a manifest revision that can be committed."""
1019 1019
1020 1020 def write(transaction, linkrev, p1node, p2node, added, removed, match=None):
1021 1021 """Add this revision to storage.
1022 1022
1023 1023 Takes a transaction object, the changeset revision number it will
1024 1024 be associated with, its parent nodes, and lists of added and
1025 1025 removed paths.
1026 1026
1027 1027 If match is provided, storage can choose not to inspect or write out
1028 1028 items that do not match. Storage is still required to be able to provide
1029 1029 the full manifest in the future for any directories written (these
1030 1030 manifests should not be "narrowed on disk").
1031 1031
1032 1032 Returns the binary node of the created revision.
1033 1033 """
1034 1034
1035 1035 class imanifeststorage(interfaceutil.Interface):
1036 1036 """Storage interface for manifest data."""
1037 1037
1038 1038 tree = interfaceutil.Attribute(
1039 1039 """The path to the directory this manifest tracks.
1040 1040
1041 1041 The empty bytestring represents the root manifest.
1042 1042 """)
1043 1043
1044 1044 index = interfaceutil.Attribute(
1045 1045 """An ``ifilerevisionssequence`` instance.""")
1046 1046
1047 1047 indexfile = interfaceutil.Attribute(
1048 1048 """Path of revlog index file.
1049 1049
1050 1050 TODO this is revlog specific and should not be exposed.
1051 1051 """)
1052 1052
1053 1053 opener = interfaceutil.Attribute(
1054 1054 """VFS opener to use to access underlying files used for storage.
1055 1055
1056 1056 TODO this is revlog specific and should not be exposed.
1057 1057 """)
1058 1058
1059 1059 version = interfaceutil.Attribute(
1060 1060 """Revlog version number.
1061 1061
1062 1062 TODO this is revlog specific and should not be exposed.
1063 1063 """)
1064 1064
1065 1065 _generaldelta = interfaceutil.Attribute(
1066 1066 """Whether generaldelta storage is being used.
1067 1067
1068 1068 TODO this is revlog specific and should not be exposed.
1069 1069 """)
1070 1070
1071 1071 fulltextcache = interfaceutil.Attribute(
1072 1072 """Dict with cache of fulltexts.
1073 1073
1074 1074 TODO this doesn't feel appropriate for the storage interface.
1075 1075 """)
1076 1076
1077 1077 def __len__():
1078 1078 """Obtain the number of revisions stored for this manifest."""
1079 1079
1080 1080 def __iter__():
1081 1081 """Iterate over revision numbers for this manifest."""
1082 1082
1083 1083 def rev(node):
1084 1084 """Obtain the revision number given a binary node.
1085 1085
1086 1086 Raises ``error.LookupError`` if the node is not known.
1087 1087 """
1088 1088
1089 1089 def node(rev):
1090 1090 """Obtain the node value given a revision number.
1091 1091
1092 1092 Raises ``error.LookupError`` if the revision is not known.
1093 1093 """
1094 1094
1095 1095 def lookup(value):
1096 1096 """Attempt to resolve a value to a node.
1097 1097
1098 1098 Value can be a binary node, hex node, revision number, or a bytes
1099 1099 that can be converted to an integer.
1100 1100
1101 1101 Raises ``error.LookupError`` if a ndoe could not be resolved.
1102 1102 """
1103 1103
1104 1104 def parents(node):
1105 1105 """Returns a 2-tuple of parent nodes for a node.
1106 1106
1107 1107 Values will be ``nullid`` if the parent is empty.
1108 1108 """
1109 1109
1110 1110 def parentrevs(rev):
1111 1111 """Like parents() but operates on revision numbers."""
1112 1112
1113 1113 def linkrev(rev):
1114 1114 """Obtain the changeset revision number a revision is linked to."""
1115 1115
1116 1116 def revision(node, _df=None, raw=False):
1117 1117 """Obtain fulltext data for a node."""
1118 1118
1119 1119 def revdiff(rev1, rev2):
1120 1120 """Obtain a delta between two revision numbers.
1121 1121
1122 1122 The returned data is the result of ``bdiff.bdiff()`` on the raw
1123 1123 revision data.
1124 1124 """
1125 1125
1126 1126 def cmp(node, fulltext):
1127 1127 """Compare fulltext to another revision.
1128 1128
1129 1129 Returns True if the fulltext is different from what is stored.
1130 1130 """
1131 1131
1132 1132 def emitrevisions(nodes,
1133 1133 nodesorder=None,
1134 1134 revisiondata=False,
1135 1135 assumehaveparentrevisions=False):
1136 1136 """Produce ``irevisiondelta`` describing revisions.
1137 1137
1138 1138 See the documentation for ``ifiledata`` for more.
1139 1139 """
1140 1140
1141 1141 def addgroup(deltas, linkmapper, transaction, addrevisioncb=None):
1142 1142 """Process a series of deltas for storage.
1143 1143
1144 1144 See the documentation in ``ifilemutation`` for more.
1145 1145 """
1146 1146
1147 1147 def rawsize(rev):
1148 1148 """Obtain the size of tracked data.
1149 1149
1150 1150 Is equivalent to ``len(m.revision(node, raw=True))``.
1151 1151
1152 1152 TODO this method is only used by upgrade code and may be removed.
1153 1153 """
1154 1154
1155 1155 def getstrippoint(minlink):
1156 1156 """Find minimum revision that must be stripped to strip a linkrev.
1157 1157
1158 1158 See the documentation in ``ifilemutation`` for more.
1159 1159 """
1160 1160
1161 1161 def strip(minlink, transaction):
1162 1162 """Remove storage of items starting at a linkrev.
1163 1163
1164 1164 See the documentation in ``ifilemutation`` for more.
1165 1165 """
1166 1166
1167 1167 def checksize():
1168 1168 """Obtain the expected sizes of backing files.
1169 1169
1170 1170 TODO this is used by verify and it should not be part of the interface.
1171 1171 """
1172 1172
1173 1173 def files():
1174 1174 """Obtain paths that are backing storage for this manifest.
1175 1175
1176 1176 TODO this is used by verify and there should probably be a better API
1177 1177 for this functionality.
1178 1178 """
1179 1179
1180 1180 def deltaparent(rev):
1181 1181 """Obtain the revision that a revision is delta'd against.
1182 1182
1183 1183 TODO delta encoding is an implementation detail of storage and should
1184 1184 not be exposed to the storage interface.
1185 1185 """
1186 1186
1187 1187 def clone(tr, dest, **kwargs):
1188 1188 """Clone this instance to another."""
1189 1189
1190 1190 def clearcaches(clear_persisted_data=False):
1191 1191 """Clear any caches associated with this instance."""
1192 1192
1193 1193 def dirlog(d):
1194 1194 """Obtain a manifest storage instance for a tree."""
1195 1195
1196 1196 def add(m, transaction, link, p1, p2, added, removed, readtree=None,
1197 1197 match=None):
1198 1198 """Add a revision to storage.
1199 1199
1200 1200 ``m`` is an object conforming to ``imanifestdict``.
1201 1201
1202 1202 ``link`` is the linkrev revision number.
1203 1203
1204 1204 ``p1`` and ``p2`` are the parent revision numbers.
1205 1205
1206 1206 ``added`` and ``removed`` are iterables of added and removed paths,
1207 1207 respectively.
1208 1208
1209 1209 ``readtree`` is a function that can be used to read the child tree(s)
1210 1210 when recursively writing the full tree structure when using
1211 1211 treemanifets.
1212 1212
1213 1213 ``match`` is a matcher that can be used to hint to storage that not all
1214 1214 paths must be inspected; this is an optimization and can be safely
1215 1215 ignored. Note that the storage must still be able to reproduce a full
1216 1216 manifest including files that did not match.
1217 1217 """
1218 1218
1219 1219 def storageinfo(exclusivefiles=False, sharedfiles=False,
1220 1220 revisionscount=False, trackedsize=False,
1221 1221 storedsize=False):
1222 1222 """Obtain information about storage for this manifest's data.
1223 1223
1224 1224 See ``ifilestorage.storageinfo()`` for a description of this method.
1225 1225 This one behaves the same way, except for manifest data.
1226 1226 """
1227 1227
1228 1228 class imanifestlog(interfaceutil.Interface):
1229 1229 """Interface representing a collection of manifest snapshots.
1230 1230
1231 1231 Represents the root manifest in a repository.
1232 1232
1233 1233 Also serves as a means to access nested tree manifests and to cache
1234 1234 tree manifests.
1235 1235 """
1236 1236
1237 1237 def __getitem__(node):
1238 1238 """Obtain a manifest instance for a given binary node.
1239 1239
1240 1240 Equivalent to calling ``self.get('', node)``.
1241 1241
1242 1242 The returned object conforms to the ``imanifestrevisionstored``
1243 1243 interface.
1244 1244 """
1245 1245
1246 1246 def get(tree, node, verify=True):
1247 1247 """Retrieve the manifest instance for a given directory and binary node.
1248 1248
1249 1249 ``node`` always refers to the node of the root manifest (which will be
1250 1250 the only manifest if flat manifests are being used).
1251 1251
1252 1252 If ``tree`` is the empty string, the root manifest is returned.
1253 1253 Otherwise the manifest for the specified directory will be returned
1254 1254 (requires tree manifests).
1255 1255
1256 1256 If ``verify`` is True, ``LookupError`` is raised if the node is not
1257 1257 known.
1258 1258
1259 1259 The returned object conforms to the ``imanifestrevisionstored``
1260 1260 interface.
1261 1261 """
1262 1262
1263 1263 def getstorage(tree):
1264 1264 """Retrieve an interface to storage for a particular tree.
1265 1265
1266 1266 If ``tree`` is the empty bytestring, storage for the root manifest will
1267 1267 be returned. Otherwise storage for a tree manifest is returned.
1268 1268
1269 1269 TODO formalize interface for returned object.
1270 1270 """
1271 1271
1272 1272 def clearcaches():
1273 1273 """Clear caches associated with this collection."""
1274 1274
1275 1275 def rev(node):
1276 1276 """Obtain the revision number for a binary node.
1277 1277
1278 1278 Raises ``error.LookupError`` if the node is not known.
1279 1279 """
1280 1280
1281 1281 class ilocalrepositoryfilestorage(interfaceutil.Interface):
1282 1282 """Local repository sub-interface providing access to tracked file storage.
1283 1283
1284 1284 This interface defines how a repository accesses storage for a single
1285 1285 tracked file path.
1286 1286 """
1287 1287
1288 1288 def file(f):
1289 1289 """Obtain a filelog for a tracked path.
1290 1290
1291 1291 The returned type conforms to the ``ifilestorage`` interface.
1292 1292 """
1293 1293
1294 1294 class ilocalrepositorymain(interfaceutil.Interface):
1295 1295 """Main interface for local repositories.
1296 1296
1297 1297 This currently captures the reality of things - not how things should be.
1298 1298 """
1299 1299
1300 1300 supportedformats = interfaceutil.Attribute(
1301 1301 """Set of requirements that apply to stream clone.
1302 1302
1303 1303 This is actually a class attribute and is shared among all instances.
1304 1304 """)
1305 1305
1306 1306 supported = interfaceutil.Attribute(
1307 1307 """Set of requirements that this repo is capable of opening.""")
1308 1308
1309 1309 requirements = interfaceutil.Attribute(
1310 1310 """Set of requirements this repo uses.""")
1311 1311
1312 1312 features = interfaceutil.Attribute(
1313 1313 """Set of "features" this repository supports.
1314 1314
1315 1315 A "feature" is a loosely-defined term. It can refer to a feature
1316 1316 in the classical sense or can describe an implementation detail
1317 1317 of the repository. For example, a ``readonly`` feature may denote
1318 1318 the repository as read-only. Or a ``revlogfilestore`` feature may
1319 1319 denote that the repository is using revlogs for file storage.
1320 1320
1321 1321 The intent of features is to provide a machine-queryable mechanism
1322 1322 for repo consumers to test for various repository characteristics.
1323 1323
1324 1324 Features are similar to ``requirements``. The main difference is that
1325 1325 requirements are stored on-disk and represent requirements to open the
1326 1326 repository. Features are more run-time capabilities of the repository
1327 1327 and more granular capabilities (which may be derived from requirements).
1328 1328 """)
1329 1329
1330 1330 filtername = interfaceutil.Attribute(
1331 1331 """Name of the repoview that is active on this repo.""")
1332 1332
1333 1333 wvfs = interfaceutil.Attribute(
1334 1334 """VFS used to access the working directory.""")
1335 1335
1336 1336 vfs = interfaceutil.Attribute(
1337 1337 """VFS rooted at the .hg directory.
1338 1338
1339 1339 Used to access repository data not in the store.
1340 1340 """)
1341 1341
1342 1342 svfs = interfaceutil.Attribute(
1343 1343 """VFS rooted at the store.
1344 1344
1345 1345 Used to access repository data in the store. Typically .hg/store.
1346 1346 But can point elsewhere if the store is shared.
1347 1347 """)
1348 1348
1349 1349 root = interfaceutil.Attribute(
1350 1350 """Path to the root of the working directory.""")
1351 1351
1352 1352 path = interfaceutil.Attribute(
1353 1353 """Path to the .hg directory.""")
1354 1354
1355 1355 origroot = interfaceutil.Attribute(
1356 1356 """The filesystem path that was used to construct the repo.""")
1357 1357
1358 1358 auditor = interfaceutil.Attribute(
1359 1359 """A pathauditor for the working directory.
1360 1360
1361 1361 This checks if a path refers to a nested repository.
1362 1362
1363 1363 Operates on the filesystem.
1364 1364 """)
1365 1365
1366 1366 nofsauditor = interfaceutil.Attribute(
1367 1367 """A pathauditor for the working directory.
1368 1368
1369 1369 This is like ``auditor`` except it doesn't do filesystem checks.
1370 1370 """)
1371 1371
1372 1372 baseui = interfaceutil.Attribute(
1373 1373 """Original ui instance passed into constructor.""")
1374 1374
1375 1375 ui = interfaceutil.Attribute(
1376 1376 """Main ui instance for this instance.""")
1377 1377
1378 1378 sharedpath = interfaceutil.Attribute(
1379 1379 """Path to the .hg directory of the repo this repo was shared from.""")
1380 1380
1381 1381 store = interfaceutil.Attribute(
1382 1382 """A store instance.""")
1383 1383
1384 1384 spath = interfaceutil.Attribute(
1385 1385 """Path to the store.""")
1386 1386
1387 1387 sjoin = interfaceutil.Attribute(
1388 1388 """Alias to self.store.join.""")
1389 1389
1390 1390 cachevfs = interfaceutil.Attribute(
1391 1391 """A VFS used to access the cache directory.
1392 1392
1393 1393 Typically .hg/cache.
1394 1394 """)
1395 1395
1396 1396 filteredrevcache = interfaceutil.Attribute(
1397 1397 """Holds sets of revisions to be filtered.""")
1398 1398
1399 1399 names = interfaceutil.Attribute(
1400 1400 """A ``namespaces`` instance.""")
1401 1401
1402 1402 def close():
1403 1403 """Close the handle on this repository."""
1404 1404
1405 1405 def peer():
1406 1406 """Obtain an object conforming to the ``peer`` interface."""
1407 1407
1408 1408 def unfiltered():
1409 1409 """Obtain an unfiltered/raw view of this repo."""
1410 1410
1411 1411 def filtered(name, visibilityexceptions=None):
1412 1412 """Obtain a named view of this repository."""
1413 1413
1414 1414 obsstore = interfaceutil.Attribute(
1415 1415 """A store of obsolescence data.""")
1416 1416
1417 1417 changelog = interfaceutil.Attribute(
1418 1418 """A handle on the changelog revlog.""")
1419 1419
1420 1420 manifestlog = interfaceutil.Attribute(
1421 1421 """An instance conforming to the ``imanifestlog`` interface.
1422 1422
1423 1423 Provides access to manifests for the repository.
1424 1424 """)
1425 1425
1426 1426 dirstate = interfaceutil.Attribute(
1427 1427 """Working directory state.""")
1428 1428
1429 1429 narrowpats = interfaceutil.Attribute(
1430 1430 """Matcher patterns for this repository's narrowspec.""")
1431 1431
1432 1432 def narrowmatch():
1433 1433 """Obtain a matcher for the narrowspec."""
1434 1434
1435 1435 def setnarrowpats(newincludes, newexcludes):
1436 1436 """Define the narrowspec for this repository."""
1437 1437
1438 1438 def __getitem__(changeid):
1439 1439 """Try to resolve a changectx."""
1440 1440
1441 1441 def __contains__(changeid):
1442 1442 """Whether a changeset exists."""
1443 1443
1444 1444 def __nonzero__():
1445 1445 """Always returns True."""
1446 1446 return True
1447 1447
1448 1448 __bool__ = __nonzero__
1449 1449
1450 1450 def __len__():
1451 1451 """Returns the number of changesets in the repo."""
1452 1452
1453 1453 def __iter__():
1454 1454 """Iterate over revisions in the changelog."""
1455 1455
1456 1456 def revs(expr, *args):
1457 1457 """Evaluate a revset.
1458 1458
1459 1459 Emits revisions.
1460 1460 """
1461 1461
1462 1462 def set(expr, *args):
1463 1463 """Evaluate a revset.
1464 1464
1465 1465 Emits changectx instances.
1466 1466 """
1467 1467
1468 1468 def anyrevs(specs, user=False, localalias=None):
1469 1469 """Find revisions matching one of the given revsets."""
1470 1470
1471 1471 def url():
1472 1472 """Returns a string representing the location of this repo."""
1473 1473
1474 1474 def hook(name, throw=False, **args):
1475 1475 """Call a hook."""
1476 1476
1477 1477 def tags():
1478 1478 """Return a mapping of tag to node."""
1479 1479
1480 1480 def tagtype(tagname):
1481 1481 """Return the type of a given tag."""
1482 1482
1483 1483 def tagslist():
1484 1484 """Return a list of tags ordered by revision."""
1485 1485
1486 1486 def nodetags(node):
1487 1487 """Return the tags associated with a node."""
1488 1488
1489 1489 def nodebookmarks(node):
1490 1490 """Return the list of bookmarks pointing to the specified node."""
1491 1491
1492 1492 def branchmap():
1493 1493 """Return a mapping of branch to heads in that branch."""
1494 1494
1495 1495 def revbranchcache():
1496 1496 pass
1497 1497
1498 1498 def branchtip(branchtip, ignoremissing=False):
1499 1499 """Return the tip node for a given branch."""
1500 1500
1501 1501 def lookup(key):
1502 1502 """Resolve the node for a revision."""
1503 1503
1504 1504 def lookupbranch(key):
1505 1505 """Look up the branch name of the given revision or branch name."""
1506 1506
1507 1507 def known(nodes):
1508 1508 """Determine whether a series of nodes is known.
1509 1509
1510 1510 Returns a list of bools.
1511 1511 """
1512 1512
1513 1513 def local():
1514 1514 """Whether the repository is local."""
1515 1515 return True
1516 1516
1517 1517 def publishing():
1518 1518 """Whether the repository is a publishing repository."""
1519 1519
1520 1520 def cancopy():
1521 1521 pass
1522 1522
1523 1523 def shared():
1524 1524 """The type of shared repository or None."""
1525 1525
1526 1526 def wjoin(f, *insidef):
1527 1527 """Calls self.vfs.reljoin(self.root, f, *insidef)"""
1528 1528
1529 1529 def setparents(p1, p2):
1530 1530 """Set the parent nodes of the working directory."""
1531 1531
1532 1532 def filectx(path, changeid=None, fileid=None):
1533 1533 """Obtain a filectx for the given file revision."""
1534 1534
1535 1535 def getcwd():
1536 1536 """Obtain the current working directory from the dirstate."""
1537 1537
1538 1538 def pathto(f, cwd=None):
1539 1539 """Obtain the relative path to a file."""
1540 1540
1541 1541 def adddatafilter(name, fltr):
1542 1542 pass
1543 1543
1544 1544 def wread(filename):
1545 1545 """Read a file from wvfs, using data filters."""
1546 1546
1547 1547 def wwrite(filename, data, flags, backgroundclose=False, **kwargs):
1548 1548 """Write data to a file in the wvfs, using data filters."""
1549 1549
1550 1550 def wwritedata(filename, data):
1551 1551 """Resolve data for writing to the wvfs, using data filters."""
1552 1552
1553 1553 def currenttransaction():
1554 1554 """Obtain the current transaction instance or None."""
1555 1555
1556 1556 def transaction(desc, report=None):
1557 1557 """Open a new transaction to write to the repository."""
1558 1558
1559 1559 def undofiles():
1560 1560 """Returns a list of (vfs, path) for files to undo transactions."""
1561 1561
1562 1562 def recover():
1563 1563 """Roll back an interrupted transaction."""
1564 1564
1565 1565 def rollback(dryrun=False, force=False):
1566 1566 """Undo the last transaction.
1567 1567
1568 1568 DANGEROUS.
1569 1569 """
1570 1570
1571 1571 def updatecaches(tr=None, full=False):
1572 1572 """Warm repo caches."""
1573 1573
1574 1574 def invalidatecaches():
1575 1575 """Invalidate cached data due to the repository mutating."""
1576 1576
1577 1577 def invalidatevolatilesets():
1578 1578 pass
1579 1579
1580 1580 def invalidatedirstate():
1581 1581 """Invalidate the dirstate."""
1582 1582
1583 1583 def invalidate(clearfilecache=False):
1584 1584 pass
1585 1585
1586 1586 def invalidateall():
1587 1587 pass
1588 1588
1589 1589 def lock(wait=True):
1590 1590 """Lock the repository store and return a lock instance."""
1591 1591
1592 1592 def wlock(wait=True):
1593 1593 """Lock the non-store parts of the repository."""
1594 1594
1595 1595 def currentwlock():
1596 1596 """Return the wlock if it's held or None."""
1597 1597
1598 1598 def checkcommitpatterns(wctx, vdirs, match, status, fail):
1599 1599 pass
1600 1600
1601 1601 def commit(text='', user=None, date=None, match=None, force=False,
1602 1602 editor=False, extra=None):
1603 1603 """Add a new revision to the repository."""
1604 1604
1605 1605 def commitctx(ctx, error=False):
1606 1606 """Commit a commitctx instance to the repository."""
1607 1607
1608 1608 def destroying():
1609 1609 """Inform the repository that nodes are about to be destroyed."""
1610 1610
1611 1611 def destroyed():
1612 1612 """Inform the repository that nodes have been destroyed."""
1613 1613
1614 1614 def status(node1='.', node2=None, match=None, ignored=False,
1615 1615 clean=False, unknown=False, listsubrepos=False):
1616 1616 """Convenience method to call repo[x].status()."""
1617 1617
1618 1618 def addpostdsstatus(ps):
1619 1619 pass
1620 1620
1621 1621 def postdsstatus():
1622 1622 pass
1623 1623
1624 1624 def clearpostdsstatus():
1625 1625 pass
1626 1626
1627 1627 def heads(start=None):
1628 1628 """Obtain list of nodes that are DAG heads."""
1629 1629
1630 1630 def branchheads(branch=None, start=None, closed=False):
1631 1631 pass
1632 1632
1633 1633 def branches(nodes):
1634 1634 pass
1635 1635
1636 1636 def between(pairs):
1637 1637 pass
1638 1638
1639 1639 def checkpush(pushop):
1640 1640 pass
1641 1641
1642 1642 prepushoutgoinghooks = interfaceutil.Attribute(
1643 1643 """util.hooks instance.""")
1644 1644
1645 1645 def pushkey(namespace, key, old, new):
1646 1646 pass
1647 1647
1648 1648 def listkeys(namespace):
1649 1649 pass
1650 1650
1651 1651 def debugwireargs(one, two, three=None, four=None, five=None):
1652 1652 pass
1653 1653
1654 1654 def savecommitmessage(text):
1655 1655 pass
1656 1656
1657 1657 class completelocalrepository(ilocalrepositorymain,
1658 1658 ilocalrepositoryfilestorage):
1659 1659 """Complete interface for a local repository."""
1660
1661 class iwireprotocolcommandcacher(interfaceutil.Interface):
1662 """Represents a caching backend for wire protocol commands.
1663
1664 Wire protocol version 2 supports transparent caching of many commands.
1665 To leverage this caching, servers can activate objects that cache
1666 command responses. Objects handle both cache writing and reading.
1667 This interface defines how that response caching mechanism works.
1668
1669 Wire protocol version 2 commands emit a series of objects that are
1670 serialized and sent to the client. The caching layer exists between
1671 the invocation of the command function and the sending of its output
1672 objects to an output layer.
1673
1674 Instances of this interface represent a binding to a cache that
1675 can serve a response (in place of calling a command function) and/or
1676 write responses to a cache for subsequent use.
1677
1678 When a command request arrives, the following happens with regards
1679 to this interface:
1680
1681 1. The server determines whether the command request is cacheable.
1682 2. If it is, an instance of this interface is spawned.
1683 3. The cacher is activated in a context manager (``__enter__`` is called).
1684 4. A cache *key* for that request is derived. This will call the
1685 instance's ``adjustcachekeystate()`` method so the derivation
1686 can be influenced.
1687 5. The cacher is informed of the derived cache key via a call to
1688 ``setcachekey()``.
1689 6. The cacher's ``lookup()`` method is called to test for presence of
1690 the derived key in the cache.
1691 7. If ``lookup()`` returns a hit, that cached result is used in place
1692 of invoking the command function. ``__exit__`` is called and the instance
1693 is discarded.
1694 8. The command function is invoked.
1695 9. ``onobject()`` is called for each object emitted by the command
1696 function.
1697 10. After the final object is seen, ``onoutputfinished()`` is called.
1698 11. ``__exit__`` is called to signal the end of use of the instance.
1699
1700 Cache *key* derivation can be influenced by the instance.
1701
1702 Cache keys are initially derived by a deterministic representation of
1703 the command request. This includes the command name, arguments, protocol
1704 version, etc. This initial key derivation is performed by CBOR-encoding a
1705 data structure and feeding that output into a hasher.
1706
1707 Instances of this interface can influence this initial key derivation
1708 via ``adjustcachekeystate()``.
1709
1710 The instance is informed of the derived cache key via a call to
1711 ``setcachekey()``. The instance must store the key locally so it can
1712 be consulted on subsequent operations that may require it.
1713
1714 When constructed, the instance has access to a callable that can be used
1715 for encoding response objects. This callable receives as its single
1716 argument an object emitted by a command function. It returns an iterable
1717 of bytes chunks representing the encoded object. Unless the cacher is
1718 caching native Python objects in memory or has a way of reconstructing
1719 the original Python objects, implementations typically call this function
1720 to produce bytes from the output objects and then store those bytes in
1721 the cache. When it comes time to re-emit those bytes, they are wrapped
1722 in a ``wireprototypes.encodedresponse`` instance to tell the output
1723 layer that they are pre-encoded.
1724
1725 When receiving the objects emitted by the command function, instances
1726 can choose what to do with those objects. The simplest thing to do is
1727 re-emit the original objects. They will be forwarded to the output
1728 layer and will be processed as if the cacher did not exist.
1729
1730 Implementations could also choose to not emit objects - instead locally
1731 buffering objects or their encoded representation. They could then emit
1732 a single "coalesced" object when ``onoutputfinished()`` is called. In
1733 this way, the implementation would function as a filtering layer of
1734 sorts.
1735
1736 When caching objects, typically the encoded form of the object will
1737 be stored. Keep in mind that if the original object is forwarded to
1738 the output layer, it will need to be encoded there as well. For large
1739 output, this redundant encoding could add overhead. Implementations
1740 could wrap the encoded object data in ``wireprototypes.encodedresponse``
1741 instances to avoid this overhead.
1742 """
1743 def __enter__():
1744 """Marks the instance as active.
1745
1746 Should return self.
1747 """
1748
1749 def __exit__(exctype, excvalue, exctb):
1750 """Called when cacher is no longer used.
1751
1752 This can be used by implementations to perform cleanup actions (e.g.
1753 disconnecting network sockets, aborting a partially cached response.
1754 """
1755
1756 def adjustcachekeystate(state):
1757 """Influences cache key derivation by adjusting state to derive key.
1758
1759 A dict defining the state used to derive the cache key is passed.
1760
1761 Implementations can modify this dict to record additional state that
1762 is wanted to influence key derivation.
1763
1764 Implementations are *highly* encouraged to not modify or delete
1765 existing keys.
1766 """
1767
1768 def setcachekey(key):
1769 """Record the derived cache key for this request.
1770
1771 Instances may mutate the key for internal usage, as desired. e.g.
1772 instances may wish to prepend the repo name, introduce path
1773 components for filesystem or URL addressing, etc. Behavior is up to
1774 the cache.
1775
1776 Returns a bool indicating if the request is cacheable by this
1777 instance.
1778 """
1779
1780 def lookup():
1781 """Attempt to resolve an entry in the cache.
1782
1783 The instance is instructed to look for the cache key that it was
1784 informed about via the call to ``setcachekey()``.
1785
1786 If there's no cache hit or the cacher doesn't wish to use the cached
1787 entry, ``None`` should be returned.
1788
1789 Else, a dict defining the cached result should be returned. The
1790 dict may have the following keys:
1791
1792 objs
1793 An iterable of objects that should be sent to the client. That
1794 iterable of objects is expected to be what the command function
1795 would return if invoked or an equivalent representation thereof.
1796 """
1797
1798 def onobject(obj):
1799 """Called when a new object is emitted from the command function.
1800
1801 Receives as its argument the object that was emitted from the
1802 command function.
1803
1804 This method returns an iterator of objects to forward to the output
1805 layer. The easiest implementation is a generator that just
1806 ``yield obj``.
1807 """
1808
1809 def onfinished():
1810 """Called after all objects have been emitted from the command function.
1811
1812 Implementations should return an iterator of objects to forward to
1813 the output layer.
1814
1815 This method can be a generator.
1816 """
@@ -1,369 +1,370 b''
1 1 # Copyright 2018 Gregory Szorc <gregory.szorc@gmail.com>
2 2 #
3 3 # This software may be used and distributed according to the terms of the
4 4 # GNU General Public License version 2 or any later version.
5 5
6 6 from __future__ import absolute_import
7 7
8 8 from .node import (
9 9 bin,
10 10 hex,
11 11 )
12 12 from .i18n import _
13 13 from .thirdparty import (
14 14 attr,
15 15 )
16 16 from . import (
17 17 error,
18 18 util,
19 19 )
20 20 from .utils import (
21 21 interfaceutil,
22 22 )
23 23
24 24 # Names of the SSH protocol implementations.
25 25 SSHV1 = 'ssh-v1'
26 26 # These are advertised over the wire. Increment the counters at the end
27 27 # to reflect BC breakages.
28 28 SSHV2 = 'exp-ssh-v2-0002'
29 29 HTTP_WIREPROTO_V2 = 'exp-http-v2-0002'
30 30
31 31 # All available wire protocol transports.
32 32 TRANSPORTS = {
33 33 SSHV1: {
34 34 'transport': 'ssh',
35 35 'version': 1,
36 36 },
37 37 SSHV2: {
38 38 'transport': 'ssh',
39 39 # TODO mark as version 2 once all commands are implemented.
40 40 'version': 1,
41 41 },
42 42 'http-v1': {
43 43 'transport': 'http',
44 44 'version': 1,
45 45 },
46 46 HTTP_WIREPROTO_V2: {
47 47 'transport': 'http',
48 48 'version': 2,
49 49 }
50 50 }
51 51
52 52 class bytesresponse(object):
53 53 """A wire protocol response consisting of raw bytes."""
54 54 def __init__(self, data):
55 55 self.data = data
56 56
57 57 class ooberror(object):
58 58 """wireproto reply: failure of a batch of operation
59 59
60 60 Something failed during a batch call. The error message is stored in
61 61 `self.message`.
62 62 """
63 63 def __init__(self, message):
64 64 self.message = message
65 65
66 66 class pushres(object):
67 67 """wireproto reply: success with simple integer return
68 68
69 69 The call was successful and returned an integer contained in `self.res`.
70 70 """
71 71 def __init__(self, res, output):
72 72 self.res = res
73 73 self.output = output
74 74
75 75 class pusherr(object):
76 76 """wireproto reply: failure
77 77
78 78 The call failed. The `self.res` attribute contains the error message.
79 79 """
80 80 def __init__(self, res, output):
81 81 self.res = res
82 82 self.output = output
83 83
84 84 class streamres(object):
85 85 """wireproto reply: binary stream
86 86
87 87 The call was successful and the result is a stream.
88 88
89 89 Accepts a generator containing chunks of data to be sent to the client.
90 90
91 91 ``prefer_uncompressed`` indicates that the data is expected to be
92 92 uncompressable and that the stream should therefore use the ``none``
93 93 engine.
94 94 """
95 95 def __init__(self, gen=None, prefer_uncompressed=False):
96 96 self.gen = gen
97 97 self.prefer_uncompressed = prefer_uncompressed
98 98
99 99 class streamreslegacy(object):
100 100 """wireproto reply: uncompressed binary stream
101 101
102 102 The call was successful and the result is a stream.
103 103
104 104 Accepts a generator containing chunks of data to be sent to the client.
105 105
106 106 Like ``streamres``, but sends an uncompressed data for "version 1" clients
107 107 using the application/mercurial-0.1 media type.
108 108 """
109 109 def __init__(self, gen=None):
110 110 self.gen = gen
111 111
112 112 # list of nodes encoding / decoding
113 113 def decodelist(l, sep=' '):
114 114 if l:
115 115 return [bin(v) for v in l.split(sep)]
116 116 return []
117 117
118 118 def encodelist(l, sep=' '):
119 119 try:
120 120 return sep.join(map(hex, l))
121 121 except TypeError:
122 122 raise
123 123
124 124 # batched call argument encoding
125 125
126 126 def escapebatcharg(plain):
127 127 return (plain
128 128 .replace(':', ':c')
129 129 .replace(',', ':o')
130 130 .replace(';', ':s')
131 131 .replace('=', ':e'))
132 132
133 133 def unescapebatcharg(escaped):
134 134 return (escaped
135 135 .replace(':e', '=')
136 136 .replace(':s', ';')
137 137 .replace(':o', ',')
138 138 .replace(':c', ':'))
139 139
140 140 # mapping of options accepted by getbundle and their types
141 141 #
142 142 # Meant to be extended by extensions. It is extensions responsibility to ensure
143 143 # such options are properly processed in exchange.getbundle.
144 144 #
145 145 # supported types are:
146 146 #
147 147 # :nodes: list of binary nodes
148 148 # :csv: list of comma-separated values
149 149 # :scsv: list of comma-separated values return as set
150 150 # :plain: string with no transformation needed.
151 151 GETBUNDLE_ARGUMENTS = {
152 152 'heads': 'nodes',
153 153 'bookmarks': 'boolean',
154 154 'common': 'nodes',
155 155 'obsmarkers': 'boolean',
156 156 'phases': 'boolean',
157 157 'bundlecaps': 'scsv',
158 158 'listkeys': 'csv',
159 159 'cg': 'boolean',
160 160 'cbattempted': 'boolean',
161 161 'stream': 'boolean',
162 162 }
163 163
164 164 class baseprotocolhandler(interfaceutil.Interface):
165 165 """Abstract base class for wire protocol handlers.
166 166
167 167 A wire protocol handler serves as an interface between protocol command
168 168 handlers and the wire protocol transport layer. Protocol handlers provide
169 169 methods to read command arguments, redirect stdio for the duration of
170 170 the request, handle response types, etc.
171 171 """
172 172
173 173 name = interfaceutil.Attribute(
174 174 """The name of the protocol implementation.
175 175
176 176 Used for uniquely identifying the transport type.
177 177 """)
178 178
179 179 def getargs(args):
180 180 """return the value for arguments in <args>
181 181
182 182 For version 1 transports, returns a list of values in the same
183 183 order they appear in ``args``. For version 2 transports, returns
184 184 a dict mapping argument name to value.
185 185 """
186 186
187 187 def getprotocaps():
188 188 """Returns the list of protocol-level capabilities of client
189 189
190 190 Returns a list of capabilities as declared by the client for
191 191 the current request (or connection for stateful protocol handlers)."""
192 192
193 193 def getpayload():
194 194 """Provide a generator for the raw payload.
195 195
196 196 The caller is responsible for ensuring that the full payload is
197 197 processed.
198 198 """
199 199
200 200 def mayberedirectstdio():
201 201 """Context manager to possibly redirect stdio.
202 202
203 203 The context manager yields a file-object like object that receives
204 204 stdout and stderr output when the context manager is active. Or it
205 205 yields ``None`` if no I/O redirection occurs.
206 206
207 207 The intent of this context manager is to capture stdio output
208 208 so it may be sent in the response. Some transports support streaming
209 209 stdio to the client in real time. For these transports, stdio output
210 210 won't be captured.
211 211 """
212 212
213 213 def client():
214 214 """Returns a string representation of this client (as bytes)."""
215 215
216 216 def addcapabilities(repo, caps):
217 217 """Adds advertised capabilities specific to this protocol.
218 218
219 219 Receives the list of capabilities collected so far.
220 220
221 221 Returns a list of capabilities. The passed in argument can be returned.
222 222 """
223 223
224 224 def checkperm(perm):
225 225 """Validate that the client has permissions to perform a request.
226 226
227 227 The argument is the permission required to proceed. If the client
228 228 doesn't have that permission, the exception should raise or abort
229 229 in a protocol specific manner.
230 230 """
231 231
232 232 class commandentry(object):
233 233 """Represents a declared wire protocol command."""
234 234 def __init__(self, func, args='', transports=None,
235 permission='push'):
235 permission='push', cachekeyfn=None):
236 236 self.func = func
237 237 self.args = args
238 238 self.transports = transports or set()
239 239 self.permission = permission
240 self.cachekeyfn = cachekeyfn
240 241
241 242 def _merge(self, func, args):
242 243 """Merge this instance with an incoming 2-tuple.
243 244
244 245 This is called when a caller using the old 2-tuple API attempts
245 246 to replace an instance. The incoming values are merged with
246 247 data not captured by the 2-tuple and a new instance containing
247 248 the union of the two objects is returned.
248 249 """
249 250 return commandentry(func, args=args, transports=set(self.transports),
250 251 permission=self.permission)
251 252
252 253 # Old code treats instances as 2-tuples. So expose that interface.
253 254 def __iter__(self):
254 255 yield self.func
255 256 yield self.args
256 257
257 258 def __getitem__(self, i):
258 259 if i == 0:
259 260 return self.func
260 261 elif i == 1:
261 262 return self.args
262 263 else:
263 264 raise IndexError('can only access elements 0 and 1')
264 265
265 266 class commanddict(dict):
266 267 """Container for registered wire protocol commands.
267 268
268 269 It behaves like a dict. But __setitem__ is overwritten to allow silent
269 270 coercion of values from 2-tuples for API compatibility.
270 271 """
271 272 def __setitem__(self, k, v):
272 273 if isinstance(v, commandentry):
273 274 pass
274 275 # Cast 2-tuples to commandentry instances.
275 276 elif isinstance(v, tuple):
276 277 if len(v) != 2:
277 278 raise ValueError('command tuples must have exactly 2 elements')
278 279
279 280 # It is common for extensions to wrap wire protocol commands via
280 281 # e.g. ``wireproto.commands[x] = (newfn, args)``. Because callers
281 282 # doing this aren't aware of the new API that uses objects to store
282 283 # command entries, we automatically merge old state with new.
283 284 if k in self:
284 285 v = self[k]._merge(v[0], v[1])
285 286 else:
286 287 # Use default values from @wireprotocommand.
287 288 v = commandentry(v[0], args=v[1],
288 289 transports=set(TRANSPORTS),
289 290 permission='push')
290 291 else:
291 292 raise ValueError('command entries must be commandentry instances '
292 293 'or 2-tuples')
293 294
294 295 return super(commanddict, self).__setitem__(k, v)
295 296
296 297 def commandavailable(self, command, proto):
297 298 """Determine if a command is available for the requested protocol."""
298 299 assert proto.name in TRANSPORTS
299 300
300 301 entry = self.get(command)
301 302
302 303 if not entry:
303 304 return False
304 305
305 306 if proto.name not in entry.transports:
306 307 return False
307 308
308 309 return True
309 310
310 311 def supportedcompengines(ui, role):
311 312 """Obtain the list of supported compression engines for a request."""
312 313 assert role in (util.CLIENTROLE, util.SERVERROLE)
313 314
314 315 compengines = util.compengines.supportedwireengines(role)
315 316
316 317 # Allow config to override default list and ordering.
317 318 if role == util.SERVERROLE:
318 319 configengines = ui.configlist('server', 'compressionengines')
319 320 config = 'server.compressionengines'
320 321 else:
321 322 # This is currently implemented mainly to facilitate testing. In most
322 323 # cases, the server should be in charge of choosing a compression engine
323 324 # because a server has the most to lose from a sub-optimal choice. (e.g.
324 325 # CPU DoS due to an expensive engine or a network DoS due to poor
325 326 # compression ratio).
326 327 configengines = ui.configlist('experimental',
327 328 'clientcompressionengines')
328 329 config = 'experimental.clientcompressionengines'
329 330
330 331 # No explicit config. Filter out the ones that aren't supposed to be
331 332 # advertised and return default ordering.
332 333 if not configengines:
333 334 attr = 'serverpriority' if role == util.SERVERROLE else 'clientpriority'
334 335 return [e for e in compengines
335 336 if getattr(e.wireprotosupport(), attr) > 0]
336 337
337 338 # If compression engines are listed in the config, assume there is a good
338 339 # reason for it (like server operators wanting to achieve specific
339 340 # performance characteristics). So fail fast if the config references
340 341 # unusable compression engines.
341 342 validnames = set(e.name() for e in compengines)
342 343 invalidnames = set(e for e in configengines if e not in validnames)
343 344 if invalidnames:
344 345 raise error.Abort(_('invalid compression engine defined in %s: %s') %
345 346 (config, ', '.join(sorted(invalidnames))))
346 347
347 348 compengines = [e for e in compengines if e.name() in configengines]
348 349 compengines = sorted(compengines,
349 350 key=lambda e: configengines.index(e.name()))
350 351
351 352 if not compengines:
352 353 raise error.Abort(_('%s config option does not specify any known '
353 354 'compression engines') % config,
354 355 hint=_('usable compression engines: %s') %
355 356 ', '.sorted(validnames))
356 357
357 358 return compengines
358 359
359 360 @attr.s
360 361 class encodedresponse(object):
361 362 """Represents response data that is already content encoded.
362 363
363 364 Wire protocol version 2 only.
364 365
365 366 Commands typically emit Python objects that are encoded and sent over the
366 367 wire. If commands emit an object of this type, the encoding step is bypassed
367 368 and the content from this object is used instead.
368 369 """
369 370 data = attr.ib()
@@ -1,970 +1,1129 b''
1 1 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
2 2 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 3 #
4 4 # This software may be used and distributed according to the terms of the
5 5 # GNU General Public License version 2 or any later version.
6 6
7 7 from __future__ import absolute_import
8 8
9 9 import contextlib
10 import hashlib
10 11
11 12 from .i18n import _
12 13 from .node import (
13 14 hex,
14 15 nullid,
15 16 )
16 17 from . import (
17 18 discovery,
18 19 encoding,
19 20 error,
20 21 narrowspec,
21 22 pycompat,
22 23 streamclone,
23 24 util,
24 25 wireprotoframing,
25 26 wireprototypes,
26 27 )
27 28 from .utils import (
29 cborutil,
28 30 interfaceutil,
29 31 stringutil,
30 32 )
31 33
32 34 FRAMINGTYPE = b'application/mercurial-exp-framing-0005'
33 35
34 36 HTTP_WIREPROTO_V2 = wireprototypes.HTTP_WIREPROTO_V2
35 37
36 38 COMMANDS = wireprototypes.commanddict()
37 39
40 # Value inserted into cache key computation function. Change the value to
41 # force new cache keys for every command request. This should be done when
42 # there is a change to how caching works, etc.
43 GLOBAL_CACHE_VERSION = 1
44
38 45 def handlehttpv2request(rctx, req, res, checkperm, urlparts):
39 46 from .hgweb import common as hgwebcommon
40 47
41 48 # URL space looks like: <permissions>/<command>, where <permission> can
42 49 # be ``ro`` or ``rw`` to signal read-only or read-write, respectively.
43 50
44 51 # Root URL does nothing meaningful... yet.
45 52 if not urlparts:
46 53 res.status = b'200 OK'
47 54 res.headers[b'Content-Type'] = b'text/plain'
48 55 res.setbodybytes(_('HTTP version 2 API handler'))
49 56 return
50 57
51 58 if len(urlparts) == 1:
52 59 res.status = b'404 Not Found'
53 60 res.headers[b'Content-Type'] = b'text/plain'
54 61 res.setbodybytes(_('do not know how to process %s\n') %
55 62 req.dispatchpath)
56 63 return
57 64
58 65 permission, command = urlparts[0:2]
59 66
60 67 if permission not in (b'ro', b'rw'):
61 68 res.status = b'404 Not Found'
62 69 res.headers[b'Content-Type'] = b'text/plain'
63 70 res.setbodybytes(_('unknown permission: %s') % permission)
64 71 return
65 72
66 73 if req.method != 'POST':
67 74 res.status = b'405 Method Not Allowed'
68 75 res.headers[b'Allow'] = b'POST'
69 76 res.setbodybytes(_('commands require POST requests'))
70 77 return
71 78
72 79 # At some point we'll want to use our own API instead of recycling the
73 80 # behavior of version 1 of the wire protocol...
74 81 # TODO return reasonable responses - not responses that overload the
75 82 # HTTP status line message for error reporting.
76 83 try:
77 84 checkperm(rctx, req, 'pull' if permission == b'ro' else 'push')
78 85 except hgwebcommon.ErrorResponse as e:
79 86 res.status = hgwebcommon.statusmessage(e.code, pycompat.bytestr(e))
80 87 for k, v in e.headers:
81 88 res.headers[k] = v
82 89 res.setbodybytes('permission denied')
83 90 return
84 91
85 92 # We have a special endpoint to reflect the request back at the client.
86 93 if command == b'debugreflect':
87 94 _processhttpv2reflectrequest(rctx.repo.ui, rctx.repo, req, res)
88 95 return
89 96
90 97 # Extra commands that we handle that aren't really wire protocol
91 98 # commands. Think extra hard before making this hackery available to
92 99 # extension.
93 100 extracommands = {'multirequest'}
94 101
95 102 if command not in COMMANDS and command not in extracommands:
96 103 res.status = b'404 Not Found'
97 104 res.headers[b'Content-Type'] = b'text/plain'
98 105 res.setbodybytes(_('unknown wire protocol command: %s\n') % command)
99 106 return
100 107
101 108 repo = rctx.repo
102 109 ui = repo.ui
103 110
104 111 proto = httpv2protocolhandler(req, ui)
105 112
106 113 if (not COMMANDS.commandavailable(command, proto)
107 114 and command not in extracommands):
108 115 res.status = b'404 Not Found'
109 116 res.headers[b'Content-Type'] = b'text/plain'
110 117 res.setbodybytes(_('invalid wire protocol command: %s') % command)
111 118 return
112 119
113 120 # TODO consider cases where proxies may add additional Accept headers.
114 121 if req.headers.get(b'Accept') != FRAMINGTYPE:
115 122 res.status = b'406 Not Acceptable'
116 123 res.headers[b'Content-Type'] = b'text/plain'
117 124 res.setbodybytes(_('client MUST specify Accept header with value: %s\n')
118 125 % FRAMINGTYPE)
119 126 return
120 127
121 128 if req.headers.get(b'Content-Type') != FRAMINGTYPE:
122 129 res.status = b'415 Unsupported Media Type'
123 130 # TODO we should send a response with appropriate media type,
124 131 # since client does Accept it.
125 132 res.headers[b'Content-Type'] = b'text/plain'
126 133 res.setbodybytes(_('client MUST send Content-Type header with '
127 134 'value: %s\n') % FRAMINGTYPE)
128 135 return
129 136
130 137 _processhttpv2request(ui, repo, req, res, permission, command, proto)
131 138
132 139 def _processhttpv2reflectrequest(ui, repo, req, res):
133 140 """Reads unified frame protocol request and dumps out state to client.
134 141
135 142 This special endpoint can be used to help debug the wire protocol.
136 143
137 144 Instead of routing the request through the normal dispatch mechanism,
138 145 we instead read all frames, decode them, and feed them into our state
139 146 tracker. We then dump the log of all that activity back out to the
140 147 client.
141 148 """
142 149 import json
143 150
144 151 # Reflection APIs have a history of being abused, accidentally disclosing
145 152 # sensitive data, etc. So we have a config knob.
146 153 if not ui.configbool('experimental', 'web.api.debugreflect'):
147 154 res.status = b'404 Not Found'
148 155 res.headers[b'Content-Type'] = b'text/plain'
149 156 res.setbodybytes(_('debugreflect service not available'))
150 157 return
151 158
152 159 # We assume we have a unified framing protocol request body.
153 160
154 161 reactor = wireprotoframing.serverreactor()
155 162 states = []
156 163
157 164 while True:
158 165 frame = wireprotoframing.readframe(req.bodyfh)
159 166
160 167 if not frame:
161 168 states.append(b'received: <no frame>')
162 169 break
163 170
164 171 states.append(b'received: %d %d %d %s' % (frame.typeid, frame.flags,
165 172 frame.requestid,
166 173 frame.payload))
167 174
168 175 action, meta = reactor.onframerecv(frame)
169 176 states.append(json.dumps((action, meta), sort_keys=True,
170 177 separators=(', ', ': ')))
171 178
172 179 action, meta = reactor.oninputeof()
173 180 meta['action'] = action
174 181 states.append(json.dumps(meta, sort_keys=True, separators=(', ',': ')))
175 182
176 183 res.status = b'200 OK'
177 184 res.headers[b'Content-Type'] = b'text/plain'
178 185 res.setbodybytes(b'\n'.join(states))
179 186
180 187 def _processhttpv2request(ui, repo, req, res, authedperm, reqcommand, proto):
181 188 """Post-validation handler for HTTPv2 requests.
182 189
183 190 Called when the HTTP request contains unified frame-based protocol
184 191 frames for evaluation.
185 192 """
186 193 # TODO Some HTTP clients are full duplex and can receive data before
187 194 # the entire request is transmitted. Figure out a way to indicate support
188 195 # for that so we can opt into full duplex mode.
189 196 reactor = wireprotoframing.serverreactor(deferoutput=True)
190 197 seencommand = False
191 198
192 199 outstream = reactor.makeoutputstream()
193 200
194 201 while True:
195 202 frame = wireprotoframing.readframe(req.bodyfh)
196 203 if not frame:
197 204 break
198 205
199 206 action, meta = reactor.onframerecv(frame)
200 207
201 208 if action == 'wantframe':
202 209 # Need more data before we can do anything.
203 210 continue
204 211 elif action == 'runcommand':
205 212 sentoutput = _httpv2runcommand(ui, repo, req, res, authedperm,
206 213 reqcommand, reactor, outstream,
207 214 meta, issubsequent=seencommand)
208 215
209 216 if sentoutput:
210 217 return
211 218
212 219 seencommand = True
213 220
214 221 elif action == 'error':
215 222 # TODO define proper error mechanism.
216 223 res.status = b'200 OK'
217 224 res.headers[b'Content-Type'] = b'text/plain'
218 225 res.setbodybytes(meta['message'] + b'\n')
219 226 return
220 227 else:
221 228 raise error.ProgrammingError(
222 229 'unhandled action from frame processor: %s' % action)
223 230
224 231 action, meta = reactor.oninputeof()
225 232 if action == 'sendframes':
226 233 # We assume we haven't started sending the response yet. If we're
227 234 # wrong, the response type will raise an exception.
228 235 res.status = b'200 OK'
229 236 res.headers[b'Content-Type'] = FRAMINGTYPE
230 237 res.setbodygen(meta['framegen'])
231 238 elif action == 'noop':
232 239 pass
233 240 else:
234 241 raise error.ProgrammingError('unhandled action from frame processor: %s'
235 242 % action)
236 243
237 244 def _httpv2runcommand(ui, repo, req, res, authedperm, reqcommand, reactor,
238 245 outstream, command, issubsequent):
239 246 """Dispatch a wire protocol command made from HTTPv2 requests.
240 247
241 248 The authenticated permission (``authedperm``) along with the original
242 249 command from the URL (``reqcommand``) are passed in.
243 250 """
244 251 # We already validated that the session has permissions to perform the
245 252 # actions in ``authedperm``. In the unified frame protocol, the canonical
246 253 # command to run is expressed in a frame. However, the URL also requested
247 254 # to run a specific command. We need to be careful that the command we
248 255 # run doesn't have permissions requirements greater than what was granted
249 256 # by ``authedperm``.
250 257 #
251 258 # Our rule for this is we only allow one command per HTTP request and
252 259 # that command must match the command in the URL. However, we make
253 260 # an exception for the ``multirequest`` URL. This URL is allowed to
254 261 # execute multiple commands. We double check permissions of each command
255 262 # as it is invoked to ensure there is no privilege escalation.
256 263 # TODO consider allowing multiple commands to regular command URLs
257 264 # iff each command is the same.
258 265
259 266 proto = httpv2protocolhandler(req, ui, args=command['args'])
260 267
261 268 if reqcommand == b'multirequest':
262 269 if not COMMANDS.commandavailable(command['command'], proto):
263 270 # TODO proper error mechanism
264 271 res.status = b'200 OK'
265 272 res.headers[b'Content-Type'] = b'text/plain'
266 273 res.setbodybytes(_('wire protocol command not available: %s') %
267 274 command['command'])
268 275 return True
269 276
270 277 # TODO don't use assert here, since it may be elided by -O.
271 278 assert authedperm in (b'ro', b'rw')
272 279 wirecommand = COMMANDS[command['command']]
273 280 assert wirecommand.permission in ('push', 'pull')
274 281
275 282 if authedperm == b'ro' and wirecommand.permission != 'pull':
276 283 # TODO proper error mechanism
277 284 res.status = b'403 Forbidden'
278 285 res.headers[b'Content-Type'] = b'text/plain'
279 286 res.setbodybytes(_('insufficient permissions to execute '
280 287 'command: %s') % command['command'])
281 288 return True
282 289
283 290 # TODO should we also call checkperm() here? Maybe not if we're going
284 291 # to overhaul that API. The granted scope from the URL check should
285 292 # be good enough.
286 293
287 294 else:
288 295 # Don't allow multiple commands outside of ``multirequest`` URL.
289 296 if issubsequent:
290 297 # TODO proper error mechanism
291 298 res.status = b'200 OK'
292 299 res.headers[b'Content-Type'] = b'text/plain'
293 300 res.setbodybytes(_('multiple commands cannot be issued to this '
294 301 'URL'))
295 302 return True
296 303
297 304 if reqcommand != command['command']:
298 305 # TODO define proper error mechanism
299 306 res.status = b'200 OK'
300 307 res.headers[b'Content-Type'] = b'text/plain'
301 308 res.setbodybytes(_('command in frame must match command in URL'))
302 309 return True
303 310
304 311 res.status = b'200 OK'
305 312 res.headers[b'Content-Type'] = FRAMINGTYPE
306 313
307 314 try:
308 315 objs = dispatch(repo, proto, command['command'])
309 316
310 317 action, meta = reactor.oncommandresponsereadyobjects(
311 318 outstream, command['requestid'], objs)
312 319
313 320 except error.WireprotoCommandError as e:
314 321 action, meta = reactor.oncommanderror(
315 322 outstream, command['requestid'], e.message, e.messageargs)
316 323
317 324 except Exception as e:
318 325 action, meta = reactor.onservererror(
319 326 outstream, command['requestid'],
320 327 _('exception when invoking command: %s') %
321 328 stringutil.forcebytestr(e))
322 329
323 330 if action == 'sendframes':
324 331 res.setbodygen(meta['framegen'])
325 332 return True
326 333 elif action == 'noop':
327 334 return False
328 335 else:
329 336 raise error.ProgrammingError('unhandled event from reactor: %s' %
330 337 action)
331 338
332 339 def getdispatchrepo(repo, proto, command):
333 340 return repo.filtered('served')
334 341
335 342 def dispatch(repo, proto, command):
343 """Run a wire protocol command.
344
345 Returns an iterable of objects that will be sent to the client.
346 """
336 347 repo = getdispatchrepo(repo, proto, command)
337 348
338 func, spec = COMMANDS[command]
349 entry = COMMANDS[command]
350 func = entry.func
351 spec = entry.args
352
339 353 args = proto.getargs(spec)
340 354
341 return func(repo, proto, **pycompat.strkwargs(args))
355 # There is some duplicate boilerplate code here for calling the command and
356 # emitting objects. It is either that or a lot of indented code that looks
357 # like a pyramid (since there are a lot of code paths that result in not
358 # using the cacher).
359 callcommand = lambda: func(repo, proto, **pycompat.strkwargs(args))
360
361 # Request is not cacheable. Don't bother instantiating a cacher.
362 if not entry.cachekeyfn:
363 for o in callcommand():
364 yield o
365 return
366
367 cacher = makeresponsecacher(repo, proto, command, args,
368 cborutil.streamencode)
369
370 # But we have no cacher. Do default handling.
371 if not cacher:
372 for o in callcommand():
373 yield o
374 return
375
376 with cacher:
377 cachekey = entry.cachekeyfn(repo, proto, cacher, **args)
378
379 # No cache key or the cacher doesn't like it. Do default handling.
380 if cachekey is None or not cacher.setcachekey(cachekey):
381 for o in callcommand():
382 yield o
383 return
384
385 # Serve it from the cache, if possible.
386 cached = cacher.lookup()
387
388 if cached:
389 for o in cached['objs']:
390 yield o
391 return
392
393 # Else call the command and feed its output into the cacher, allowing
394 # the cacher to buffer/mutate objects as it desires.
395 for o in callcommand():
396 for o in cacher.onobject(o):
397 yield o
398
399 for o in cacher.onfinished():
400 yield o
342 401
343 402 @interfaceutil.implementer(wireprototypes.baseprotocolhandler)
344 403 class httpv2protocolhandler(object):
345 404 def __init__(self, req, ui, args=None):
346 405 self._req = req
347 406 self._ui = ui
348 407 self._args = args
349 408
350 409 @property
351 410 def name(self):
352 411 return HTTP_WIREPROTO_V2
353 412
354 413 def getargs(self, args):
355 414 # First look for args that were passed but aren't registered on this
356 415 # command.
357 416 extra = set(self._args) - set(args)
358 417 if extra:
359 418 raise error.WireprotoCommandError(
360 419 'unsupported argument to command: %s' %
361 420 ', '.join(sorted(extra)))
362 421
363 422 # And look for required arguments that are missing.
364 423 missing = {a for a in args if args[a]['required']} - set(self._args)
365 424
366 425 if missing:
367 426 raise error.WireprotoCommandError(
368 427 'missing required arguments: %s' % ', '.join(sorted(missing)))
369 428
370 429 # Now derive the arguments to pass to the command, taking into
371 430 # account the arguments specified by the client.
372 431 data = {}
373 432 for k, meta in sorted(args.items()):
374 433 # This argument wasn't passed by the client.
375 434 if k not in self._args:
376 435 data[k] = meta['default']()
377 436 continue
378 437
379 438 v = self._args[k]
380 439
381 440 # Sets may be expressed as lists. Silently normalize.
382 441 if meta['type'] == 'set' and isinstance(v, list):
383 442 v = set(v)
384 443
385 444 # TODO consider more/stronger type validation.
386 445
387 446 data[k] = v
388 447
389 448 return data
390 449
391 450 def getprotocaps(self):
392 451 # Protocol capabilities are currently not implemented for HTTP V2.
393 452 return set()
394 453
395 454 def getpayload(self):
396 455 raise NotImplementedError
397 456
398 457 @contextlib.contextmanager
399 458 def mayberedirectstdio(self):
400 459 raise NotImplementedError
401 460
402 461 def client(self):
403 462 raise NotImplementedError
404 463
405 464 def addcapabilities(self, repo, caps):
406 465 return caps
407 466
408 467 def checkperm(self, perm):
409 468 raise NotImplementedError
410 469
411 470 def httpv2apidescriptor(req, repo):
412 471 proto = httpv2protocolhandler(req, repo.ui)
413 472
414 473 return _capabilitiesv2(repo, proto)
415 474
416 475 def _capabilitiesv2(repo, proto):
417 476 """Obtain the set of capabilities for version 2 transports.
418 477
419 478 These capabilities are distinct from the capabilities for version 1
420 479 transports.
421 480 """
422 481 compression = []
423 482 for engine in wireprototypes.supportedcompengines(repo.ui, util.SERVERROLE):
424 483 compression.append({
425 484 b'name': engine.wireprotosupport().name,
426 485 })
427 486
428 487 caps = {
429 488 'commands': {},
430 489 'compression': compression,
431 490 'framingmediatypes': [FRAMINGTYPE],
432 491 'pathfilterprefixes': set(narrowspec.VALID_PREFIXES),
433 492 }
434 493
435 494 for command, entry in COMMANDS.items():
436 495 args = {}
437 496
438 497 for arg, meta in entry.args.items():
439 498 args[arg] = {
440 499 # TODO should this be a normalized type using CBOR's
441 500 # terminology?
442 501 b'type': meta['type'],
443 502 b'required': meta['required'],
444 503 }
445 504
446 505 if not meta['required']:
447 506 args[arg][b'default'] = meta['default']()
448 507
449 508 if meta['validvalues']:
450 509 args[arg][b'validvalues'] = meta['validvalues']
451 510
452 511 caps['commands'][command] = {
453 512 'args': args,
454 513 'permissions': [entry.permission],
455 514 }
456 515
457 516 if streamclone.allowservergeneration(repo):
458 517 caps['rawrepoformats'] = sorted(repo.requirements &
459 518 repo.supportedformats)
460 519
461 520 return proto.addcapabilities(repo, caps)
462 521
463 def wireprotocommand(name, args=None, permission='push'):
522 def wireprotocommand(name, args=None, permission='push', cachekeyfn=None):
464 523 """Decorator to declare a wire protocol command.
465 524
466 525 ``name`` is the name of the wire protocol command being provided.
467 526
468 527 ``args`` is a dict defining arguments accepted by the command. Keys are
469 528 the argument name. Values are dicts with the following keys:
470 529
471 530 ``type``
472 531 The argument data type. Must be one of the following string
473 532 literals: ``bytes``, ``int``, ``list``, ``dict``, ``set``,
474 533 or ``bool``.
475 534
476 535 ``default``
477 536 A callable returning the default value for this argument. If not
478 537 specified, ``None`` will be the default value.
479 538
480 539 ``example``
481 540 An example value for this argument.
482 541
483 542 ``validvalues``
484 543 Set of recognized values for this argument.
485 544
486 545 ``permission`` defines the permission type needed to run this command.
487 546 Can be ``push`` or ``pull``. These roughly map to read-write and read-only,
488 547 respectively. Default is to assume command requires ``push`` permissions
489 548 because otherwise commands not declaring their permissions could modify
490 549 a repository that is supposed to be read-only.
491 550
551 ``cachekeyfn`` defines an optional callable that can derive the
552 cache key for this request.
553
492 554 Wire protocol commands are generators of objects to be serialized and
493 555 sent to the client.
494 556
495 557 If a command raises an uncaught exception, this will be translated into
496 558 a command error.
559
560 All commands can opt in to being cacheable by defining a function
561 (``cachekeyfn``) that is called to derive a cache key. This function
562 receives the same arguments as the command itself plus a ``cacher``
563 argument containing the active cacher for the request and returns a bytes
564 containing the key in a cache the response to this command may be cached
565 under.
497 566 """
498 567 transports = {k for k, v in wireprototypes.TRANSPORTS.items()
499 568 if v['version'] == 2}
500 569
501 570 if permission not in ('push', 'pull'):
502 571 raise error.ProgrammingError('invalid wire protocol permission; '
503 572 'got %s; expected "push" or "pull"' %
504 573 permission)
505 574
506 575 if args is None:
507 576 args = {}
508 577
509 578 if not isinstance(args, dict):
510 579 raise error.ProgrammingError('arguments for version 2 commands '
511 580 'must be declared as dicts')
512 581
513 582 for arg, meta in args.items():
514 583 if arg == '*':
515 584 raise error.ProgrammingError('* argument name not allowed on '
516 585 'version 2 commands')
517 586
518 587 if not isinstance(meta, dict):
519 588 raise error.ProgrammingError('arguments for version 2 commands '
520 589 'must declare metadata as a dict')
521 590
522 591 if 'type' not in meta:
523 592 raise error.ProgrammingError('%s argument for command %s does not '
524 593 'declare type field' % (arg, name))
525 594
526 595 if meta['type'] not in ('bytes', 'int', 'list', 'dict', 'set', 'bool'):
527 596 raise error.ProgrammingError('%s argument for command %s has '
528 597 'illegal type: %s' % (arg, name,
529 598 meta['type']))
530 599
531 600 if 'example' not in meta:
532 601 raise error.ProgrammingError('%s argument for command %s does not '
533 602 'declare example field' % (arg, name))
534 603
535 604 meta['required'] = 'default' not in meta
536 605
537 606 meta.setdefault('default', lambda: None)
538 607 meta.setdefault('validvalues', None)
539 608
540 609 def register(func):
541 610 if name in COMMANDS:
542 611 raise error.ProgrammingError('%s command already registered '
543 612 'for version 2' % name)
544 613
545 614 COMMANDS[name] = wireprototypes.commandentry(
546 func, args=args, transports=transports, permission=permission)
615 func, args=args, transports=transports, permission=permission,
616 cachekeyfn=cachekeyfn)
547 617
548 618 return func
549 619
550 620 return register
551 621
622 def makecommandcachekeyfn(command, localversion=None, allargs=False):
623 """Construct a cache key derivation function with common features.
624
625 By default, the cache key is a hash of:
626
627 * The command name.
628 * A global cache version number.
629 * A local cache version number (passed via ``localversion``).
630 * All the arguments passed to the command.
631 * The media type used.
632 * Wire protocol version string.
633 * The repository path.
634 """
635 if not allargs:
636 raise error.ProgrammingError('only allargs=True is currently supported')
637
638 if localversion is None:
639 raise error.ProgrammingError('must set localversion argument value')
640
641 def cachekeyfn(repo, proto, cacher, **args):
642 spec = COMMANDS[command]
643
644 # Commands that mutate the repo can not be cached.
645 if spec.permission == 'push':
646 return None
647
648 # TODO config option to disable caching.
649
650 # Our key derivation strategy is to construct a data structure
651 # holding everything that could influence cacheability and to hash
652 # the CBOR representation of that. Using CBOR seems like it might
653 # be overkill. However, simpler hashing mechanisms are prone to
654 # duplicate input issues. e.g. if you just concatenate two values,
655 # "foo"+"bar" is identical to "fo"+"obar". Using CBOR provides
656 # "padding" between values and prevents these problems.
657
658 # Seed the hash with various data.
659 state = {
660 # To invalidate all cache keys.
661 b'globalversion': GLOBAL_CACHE_VERSION,
662 # More granular cache key invalidation.
663 b'localversion': localversion,
664 # Cache keys are segmented by command.
665 b'command': pycompat.sysbytes(command),
666 # Throw in the media type and API version strings so changes
667 # to exchange semantics invalid cache.
668 b'mediatype': FRAMINGTYPE,
669 b'version': HTTP_WIREPROTO_V2,
670 # So same requests for different repos don't share cache keys.
671 b'repo': repo.root,
672 }
673
674 # The arguments passed to us will have already been normalized.
675 # Default values will be set, etc. This is important because it
676 # means that it doesn't matter if clients send an explicit argument
677 # or rely on the default value: it will all normalize to the same
678 # set of arguments on the server and therefore the same cache key.
679 #
680 # Arguments by their very nature must support being encoded to CBOR.
681 # And the CBOR encoder is deterministic. So we hash the arguments
682 # by feeding the CBOR of their representation into the hasher.
683 if allargs:
684 state[b'args'] = pycompat.byteskwargs(args)
685
686 cacher.adjustcachekeystate(state)
687
688 hasher = hashlib.sha1()
689 for chunk in cborutil.streamencode(state):
690 hasher.update(chunk)
691
692 return pycompat.sysbytes(hasher.hexdigest())
693
694 return cachekeyfn
695
696 def makeresponsecacher(repo, proto, command, args, objencoderfn):
697 """Construct a cacher for a cacheable command.
698
699 Returns an ``iwireprotocolcommandcacher`` instance.
700
701 Extensions can monkeypatch this function to provide custom caching
702 backends.
703 """
704 return None
705
552 706 @wireprotocommand('branchmap', permission='pull')
553 707 def branchmapv2(repo, proto):
554 708 yield {encoding.fromlocal(k): v
555 709 for k, v in repo.branchmap().iteritems()}
556 710
557 711 @wireprotocommand('capabilities', permission='pull')
558 712 def capabilitiesv2(repo, proto):
559 713 yield _capabilitiesv2(repo, proto)
560 714
561 715 @wireprotocommand(
562 716 'changesetdata',
563 717 args={
564 718 'noderange': {
565 719 'type': 'list',
566 720 'default': lambda: None,
567 721 'example': [[b'0123456...'], [b'abcdef...']],
568 722 },
569 723 'nodes': {
570 724 'type': 'list',
571 725 'default': lambda: None,
572 726 'example': [b'0123456...'],
573 727 },
574 728 'nodesdepth': {
575 729 'type': 'int',
576 730 'default': lambda: None,
577 731 'example': 10,
578 732 },
579 733 'fields': {
580 734 'type': 'set',
581 735 'default': set,
582 736 'example': {b'parents', b'revision'},
583 737 'validvalues': {b'bookmarks', b'parents', b'phase', b'revision'},
584 738 },
585 739 },
586 740 permission='pull')
587 741 def changesetdata(repo, proto, noderange, nodes, nodesdepth, fields):
588 742 # TODO look for unknown fields and abort when they can't be serviced.
589 743 # This could probably be validated by dispatcher using validvalues.
590 744
591 745 if noderange is None and nodes is None:
592 746 raise error.WireprotoCommandError(
593 747 'noderange or nodes must be defined')
594 748
595 749 if nodesdepth is not None and nodes is None:
596 750 raise error.WireprotoCommandError(
597 751 'nodesdepth requires the nodes argument')
598 752
599 753 if noderange is not None:
600 754 if len(noderange) != 2:
601 755 raise error.WireprotoCommandError(
602 756 'noderange must consist of 2 elements')
603 757
604 758 if not noderange[1]:
605 759 raise error.WireprotoCommandError(
606 760 'heads in noderange request cannot be empty')
607 761
608 762 cl = repo.changelog
609 763 hasnode = cl.hasnode
610 764
611 765 seen = set()
612 766 outgoing = []
613 767
614 768 if nodes is not None:
615 769 outgoing = [n for n in nodes if hasnode(n)]
616 770
617 771 if nodesdepth:
618 772 outgoing = [cl.node(r) for r in
619 773 repo.revs(b'ancestors(%ln, %d)', outgoing,
620 774 nodesdepth - 1)]
621 775
622 776 seen |= set(outgoing)
623 777
624 778 if noderange is not None:
625 779 if noderange[0]:
626 780 common = [n for n in noderange[0] if hasnode(n)]
627 781 else:
628 782 common = [nullid]
629 783
630 784 for n in discovery.outgoing(repo, common, noderange[1]).missing:
631 785 if n not in seen:
632 786 outgoing.append(n)
633 787 # Don't need to add to seen here because this is the final
634 788 # source of nodes and there should be no duplicates in this
635 789 # list.
636 790
637 791 seen.clear()
638 792 publishing = repo.publishing()
639 793
640 794 if outgoing:
641 795 repo.hook('preoutgoing', throw=True, source='serve')
642 796
643 797 yield {
644 798 b'totalitems': len(outgoing),
645 799 }
646 800
647 801 # The phases of nodes already transferred to the client may have changed
648 802 # since the client last requested data. We send phase-only records
649 803 # for these revisions, if requested.
650 804 if b'phase' in fields and noderange is not None:
651 805 # TODO skip nodes whose phase will be reflected by a node in the
652 806 # outgoing set. This is purely an optimization to reduce data
653 807 # size.
654 808 for node in noderange[0]:
655 809 yield {
656 810 b'node': node,
657 811 b'phase': b'public' if publishing else repo[node].phasestr()
658 812 }
659 813
660 814 nodebookmarks = {}
661 815 for mark, node in repo._bookmarks.items():
662 816 nodebookmarks.setdefault(node, set()).add(mark)
663 817
664 818 # It is already topologically sorted by revision number.
665 819 for node in outgoing:
666 820 d = {
667 821 b'node': node,
668 822 }
669 823
670 824 if b'parents' in fields:
671 825 d[b'parents'] = cl.parents(node)
672 826
673 827 if b'phase' in fields:
674 828 if publishing:
675 829 d[b'phase'] = b'public'
676 830 else:
677 831 ctx = repo[node]
678 832 d[b'phase'] = ctx.phasestr()
679 833
680 834 if b'bookmarks' in fields and node in nodebookmarks:
681 835 d[b'bookmarks'] = sorted(nodebookmarks[node])
682 836 del nodebookmarks[node]
683 837
684 838 followingmeta = []
685 839 followingdata = []
686 840
687 841 if b'revision' in fields:
688 842 revisiondata = cl.revision(node, raw=True)
689 843 followingmeta.append((b'revision', len(revisiondata)))
690 844 followingdata.append(revisiondata)
691 845
692 846 # TODO make it possible for extensions to wrap a function or register
693 847 # a handler to service custom fields.
694 848
695 849 if followingmeta:
696 850 d[b'fieldsfollowing'] = followingmeta
697 851
698 852 yield d
699 853
700 854 for extra in followingdata:
701 855 yield extra
702 856
703 857 # If requested, send bookmarks from nodes that didn't have revision
704 858 # data sent so receiver is aware of any bookmark updates.
705 859 if b'bookmarks' in fields:
706 860 for node, marks in sorted(nodebookmarks.iteritems()):
707 861 yield {
708 862 b'node': node,
709 863 b'bookmarks': sorted(marks),
710 864 }
711 865
712 866 class FileAccessError(Exception):
713 867 """Represents an error accessing a specific file."""
714 868
715 869 def __init__(self, path, msg, args):
716 870 self.path = path
717 871 self.msg = msg
718 872 self.args = args
719 873
720 874 def getfilestore(repo, proto, path):
721 875 """Obtain a file storage object for use with wire protocol.
722 876
723 877 Exists as a standalone function so extensions can monkeypatch to add
724 878 access control.
725 879 """
726 880 # This seems to work even if the file doesn't exist. So catch
727 881 # "empty" files and return an error.
728 882 fl = repo.file(path)
729 883
730 884 if not len(fl):
731 885 raise FileAccessError(path, 'unknown file: %s', (path,))
732 886
733 887 return fl
734 888
735 889 @wireprotocommand(
736 890 'filedata',
737 891 args={
738 892 'haveparents': {
739 893 'type': 'bool',
740 894 'default': lambda: False,
741 895 'example': True,
742 896 },
743 897 'nodes': {
744 898 'type': 'list',
745 899 'example': [b'0123456...'],
746 900 },
747 901 'fields': {
748 902 'type': 'set',
749 903 'default': set,
750 904 'example': {b'parents', b'revision'},
751 905 'validvalues': {b'parents', b'revision'},
752 906 },
753 907 'path': {
754 908 'type': 'bytes',
755 909 'example': b'foo.txt',
756 910 }
757 911 },
758 permission='pull')
912 permission='pull',
913 # TODO censoring a file revision won't invalidate the cache.
914 # Figure out a way to take censoring into account when deriving
915 # the cache key.
916 cachekeyfn=makecommandcachekeyfn('filedata', 1, allargs=True))
759 917 def filedata(repo, proto, haveparents, nodes, fields, path):
760 918 try:
761 919 # Extensions may wish to access the protocol handler.
762 920 store = getfilestore(repo, proto, path)
763 921 except FileAccessError as e:
764 922 raise error.WireprotoCommandError(e.msg, e.args)
765 923
766 924 # Validate requested nodes.
767 925 for node in nodes:
768 926 try:
769 927 store.rev(node)
770 928 except error.LookupError:
771 929 raise error.WireprotoCommandError('unknown file node: %s',
772 930 (hex(node),))
773 931
774 932 revisions = store.emitrevisions(nodes,
775 933 revisiondata=b'revision' in fields,
776 934 assumehaveparentrevisions=haveparents)
777 935
778 936 yield {
779 937 b'totalitems': len(nodes),
780 938 }
781 939
782 940 for revision in revisions:
783 941 d = {
784 942 b'node': revision.node,
785 943 }
786 944
787 945 if b'parents' in fields:
788 946 d[b'parents'] = [revision.p1node, revision.p2node]
789 947
790 948 followingmeta = []
791 949 followingdata = []
792 950
793 951 if b'revision' in fields:
794 952 if revision.revision is not None:
795 953 followingmeta.append((b'revision', len(revision.revision)))
796 954 followingdata.append(revision.revision)
797 955 else:
798 956 d[b'deltabasenode'] = revision.basenode
799 957 followingmeta.append((b'delta', len(revision.delta)))
800 958 followingdata.append(revision.delta)
801 959
802 960 if followingmeta:
803 961 d[b'fieldsfollowing'] = followingmeta
804 962
805 963 yield d
806 964
807 965 for extra in followingdata:
808 966 yield extra
809 967
810 968 @wireprotocommand(
811 969 'heads',
812 970 args={
813 971 'publiconly': {
814 972 'type': 'bool',
815 973 'default': lambda: False,
816 974 'example': False,
817 975 },
818 976 },
819 977 permission='pull')
820 978 def headsv2(repo, proto, publiconly):
821 979 if publiconly:
822 980 repo = repo.filtered('immutable')
823 981
824 982 yield repo.heads()
825 983
826 984 @wireprotocommand(
827 985 'known',
828 986 args={
829 987 'nodes': {
830 988 'type': 'list',
831 989 'default': list,
832 990 'example': [b'deadbeef'],
833 991 },
834 992 },
835 993 permission='pull')
836 994 def knownv2(repo, proto, nodes):
837 995 result = b''.join(b'1' if n else b'0' for n in repo.known(nodes))
838 996 yield result
839 997
840 998 @wireprotocommand(
841 999 'listkeys',
842 1000 args={
843 1001 'namespace': {
844 1002 'type': 'bytes',
845 1003 'example': b'ns',
846 1004 },
847 1005 },
848 1006 permission='pull')
849 1007 def listkeysv2(repo, proto, namespace):
850 1008 keys = repo.listkeys(encoding.tolocal(namespace))
851 1009 keys = {encoding.fromlocal(k): encoding.fromlocal(v)
852 1010 for k, v in keys.iteritems()}
853 1011
854 1012 yield keys
855 1013
856 1014 @wireprotocommand(
857 1015 'lookup',
858 1016 args={
859 1017 'key': {
860 1018 'type': 'bytes',
861 1019 'example': b'foo',
862 1020 },
863 1021 },
864 1022 permission='pull')
865 1023 def lookupv2(repo, proto, key):
866 1024 key = encoding.tolocal(key)
867 1025
868 1026 # TODO handle exception.
869 1027 node = repo.lookup(key)
870 1028
871 1029 yield node
872 1030
873 1031 @wireprotocommand(
874 1032 'manifestdata',
875 1033 args={
876 1034 'nodes': {
877 1035 'type': 'list',
878 1036 'example': [b'0123456...'],
879 1037 },
880 1038 'haveparents': {
881 1039 'type': 'bool',
882 1040 'default': lambda: False,
883 1041 'example': True,
884 1042 },
885 1043 'fields': {
886 1044 'type': 'set',
887 1045 'default': set,
888 1046 'example': {b'parents', b'revision'},
889 1047 'validvalues': {b'parents', b'revision'},
890 1048 },
891 1049 'tree': {
892 1050 'type': 'bytes',
893 1051 'example': b'',
894 1052 },
895 1053 },
896 permission='pull')
1054 permission='pull',
1055 cachekeyfn=makecommandcachekeyfn('manifestdata', 1, allargs=True))
897 1056 def manifestdata(repo, proto, haveparents, nodes, fields, tree):
898 1057 store = repo.manifestlog.getstorage(tree)
899 1058
900 1059 # Validate the node is known and abort on unknown revisions.
901 1060 for node in nodes:
902 1061 try:
903 1062 store.rev(node)
904 1063 except error.LookupError:
905 1064 raise error.WireprotoCommandError(
906 1065 'unknown node: %s', (node,))
907 1066
908 1067 revisions = store.emitrevisions(nodes,
909 1068 revisiondata=b'revision' in fields,
910 1069 assumehaveparentrevisions=haveparents)
911 1070
912 1071 yield {
913 1072 b'totalitems': len(nodes),
914 1073 }
915 1074
916 1075 for revision in revisions:
917 1076 d = {
918 1077 b'node': revision.node,
919 1078 }
920 1079
921 1080 if b'parents' in fields:
922 1081 d[b'parents'] = [revision.p1node, revision.p2node]
923 1082
924 1083 followingmeta = []
925 1084 followingdata = []
926 1085
927 1086 if b'revision' in fields:
928 1087 if revision.revision is not None:
929 1088 followingmeta.append((b'revision', len(revision.revision)))
930 1089 followingdata.append(revision.revision)
931 1090 else:
932 1091 d[b'deltabasenode'] = revision.basenode
933 1092 followingmeta.append((b'delta', len(revision.delta)))
934 1093 followingdata.append(revision.delta)
935 1094
936 1095 if followingmeta:
937 1096 d[b'fieldsfollowing'] = followingmeta
938 1097
939 1098 yield d
940 1099
941 1100 for extra in followingdata:
942 1101 yield extra
943 1102
944 1103 @wireprotocommand(
945 1104 'pushkey',
946 1105 args={
947 1106 'namespace': {
948 1107 'type': 'bytes',
949 1108 'example': b'ns',
950 1109 },
951 1110 'key': {
952 1111 'type': 'bytes',
953 1112 'example': b'key',
954 1113 },
955 1114 'old': {
956 1115 'type': 'bytes',
957 1116 'example': b'old',
958 1117 },
959 1118 'new': {
960 1119 'type': 'bytes',
961 1120 'example': 'new',
962 1121 },
963 1122 },
964 1123 permission='push')
965 1124 def pushkeyv2(repo, proto, namespace, key, old, new):
966 1125 # TODO handle ui output redirection
967 1126 yield repo.pushkey(encoding.tolocal(namespace),
968 1127 encoding.tolocal(key),
969 1128 encoding.tolocal(old),
970 1129 encoding.tolocal(new))
General Comments 0
You need to be logged in to leave comments. Login now