##// END OF EJS Templates
hidden: add support for --remote-hidden to HTTP peer...
Manuel Jacob -
r51313:315f5376 default
parent child Browse files
Show More
@@ -1,659 +1,663 b''
1 1 # httppeer.py - HTTP repository proxy classes for mercurial
2 2 #
3 3 # Copyright 2005, 2006 Olivia Mackall <olivia@selenic.com>
4 4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9
10 10 import errno
11 11 import io
12 12 import os
13 13 import socket
14 14 import struct
15 15
16 16 from concurrent import futures
17 17 from .i18n import _
18 18 from .pycompat import getattr
19 19 from . import (
20 20 bundle2,
21 21 error,
22 22 httpconnection,
23 23 pycompat,
24 24 statichttprepo,
25 25 url as urlmod,
26 26 util,
27 27 wireprotov1peer,
28 28 )
29 29 from .utils import urlutil
30 30
31 31 httplib = util.httplib
32 32 urlerr = util.urlerr
33 33 urlreq = util.urlreq
34 34
35 35
36 36 def encodevalueinheaders(value, header, limit):
37 37 """Encode a string value into multiple HTTP headers.
38 38
39 39 ``value`` will be encoded into 1 or more HTTP headers with the names
40 40 ``header-<N>`` where ``<N>`` is an integer starting at 1. Each header
41 41 name + value will be at most ``limit`` bytes long.
42 42
43 43 Returns an iterable of 2-tuples consisting of header names and
44 44 values as native strings.
45 45 """
46 46 # HTTP Headers are ASCII. Python 3 requires them to be unicodes,
47 47 # not bytes. This function always takes bytes in as arguments.
48 48 fmt = pycompat.strurl(header) + r'-%s'
49 49 # Note: it is *NOT* a bug that the last bit here is a bytestring
50 50 # and not a unicode: we're just getting the encoded length anyway,
51 51 # and using an r-string to make it portable between Python 2 and 3
52 52 # doesn't work because then the \r is a literal backslash-r
53 53 # instead of a carriage return.
54 54 valuelen = limit - len(fmt % '000') - len(b': \r\n')
55 55 result = []
56 56
57 57 n = 0
58 58 for i in range(0, len(value), valuelen):
59 59 n += 1
60 60 result.append((fmt % str(n), pycompat.strurl(value[i : i + valuelen])))
61 61
62 62 return result
63 63
64 64
65 65 class _multifile:
66 66 def __init__(self, *fileobjs):
67 67 for f in fileobjs:
68 68 if not util.safehasattr(f, b'length'):
69 69 raise ValueError(
70 70 b'_multifile only supports file objects that '
71 71 b'have a length but this one does not:',
72 72 type(f),
73 73 f,
74 74 )
75 75 self._fileobjs = fileobjs
76 76 self._index = 0
77 77
78 78 @property
79 79 def length(self):
80 80 return sum(f.length for f in self._fileobjs)
81 81
82 82 def read(self, amt=None):
83 83 if amt <= 0:
84 84 return b''.join(f.read() for f in self._fileobjs)
85 85 parts = []
86 86 while amt and self._index < len(self._fileobjs):
87 87 parts.append(self._fileobjs[self._index].read(amt))
88 88 got = len(parts[-1])
89 89 if got < amt:
90 90 self._index += 1
91 91 amt -= got
92 92 return b''.join(parts)
93 93
94 94 def seek(self, offset, whence=os.SEEK_SET):
95 95 if whence != os.SEEK_SET:
96 96 raise NotImplementedError(
97 97 b'_multifile does not support anything other'
98 98 b' than os.SEEK_SET for whence on seek()'
99 99 )
100 100 if offset != 0:
101 101 raise NotImplementedError(
102 102 b'_multifile only supports seeking to start, but that '
103 103 b'could be fixed if you need it'
104 104 )
105 105 for f in self._fileobjs:
106 106 f.seek(0)
107 107 self._index = 0
108 108
109 109
110 110 def makev1commandrequest(
111 ui, requestbuilder, caps, capablefn, repobaseurl, cmd, args
111 ui,
112 requestbuilder,
113 caps,
114 capablefn,
115 repobaseurl,
116 cmd,
117 args,
118 remotehidden=False,
112 119 ):
113 120 """Make an HTTP request to run a command for a version 1 client.
114 121
115 122 ``caps`` is a set of known server capabilities. The value may be
116 123 None if capabilities are not yet known.
117 124
118 125 ``capablefn`` is a function to evaluate a capability.
119 126
120 127 ``cmd``, ``args``, and ``data`` define the command, its arguments, and
121 128 raw data to pass to it.
122 129 """
123 130 if cmd == b'pushkey':
124 131 args[b'data'] = b''
125 132 data = args.pop(b'data', None)
126 133 headers = args.pop(b'headers', {})
127 134
128 135 ui.debug(b"sending %s command\n" % cmd)
129 136 q = [(b'cmd', cmd)]
137 if remotehidden:
138 q.append(('access-hidden', '1'))
130 139 headersize = 0
131 140 # Important: don't use self.capable() here or else you end up
132 141 # with infinite recursion when trying to look up capabilities
133 142 # for the first time.
134 143 postargsok = caps is not None and b'httppostargs' in caps
135 144
136 145 # Send arguments via POST.
137 146 if postargsok and args:
138 147 strargs = urlreq.urlencode(sorted(args.items()))
139 148 if not data:
140 149 data = strargs
141 150 else:
142 151 if isinstance(data, bytes):
143 152 i = io.BytesIO(data)
144 153 i.length = len(data)
145 154 data = i
146 155 argsio = io.BytesIO(strargs)
147 156 argsio.length = len(strargs)
148 157 data = _multifile(argsio, data)
149 158 headers['X-HgArgs-Post'] = len(strargs)
150 159 elif args:
151 160 # Calling self.capable() can infinite loop if we are calling
152 161 # "capabilities". But that command should never accept wire
153 162 # protocol arguments. So this should never happen.
154 163 assert cmd != b'capabilities'
155 164 httpheader = capablefn(b'httpheader')
156 165 if httpheader:
157 166 headersize = int(httpheader.split(b',', 1)[0])
158 167
159 168 # Send arguments via HTTP headers.
160 169 if headersize > 0:
161 170 # The headers can typically carry more data than the URL.
162 171 encoded_args = urlreq.urlencode(sorted(args.items()))
163 172 for header, value in encodevalueinheaders(
164 173 encoded_args, b'X-HgArg', headersize
165 174 ):
166 175 headers[header] = value
167 176 # Send arguments via query string (Mercurial <1.9).
168 177 else:
169 178 q += sorted(args.items())
170 179
171 180 qs = b'?%s' % urlreq.urlencode(q)
172 181 cu = b"%s%s" % (repobaseurl, qs)
173 182 size = 0
174 183 if util.safehasattr(data, b'length'):
175 184 size = data.length
176 185 elif data is not None:
177 186 size = len(data)
178 187 if data is not None and 'Content-Type' not in headers:
179 188 headers['Content-Type'] = 'application/mercurial-0.1'
180 189
181 190 # Tell the server we accept application/mercurial-0.2 and multiple
182 191 # compression formats if the server is capable of emitting those
183 192 # payloads.
184 193 # Note: Keep this set empty by default, as client advertisement of
185 194 # protocol parameters should only occur after the handshake.
186 195 protoparams = set()
187 196
188 197 mediatypes = set()
189 198 if caps is not None:
190 199 mt = capablefn(b'httpmediatype')
191 200 if mt:
192 201 protoparams.add(b'0.1')
193 202 mediatypes = set(mt.split(b','))
194 203
195 204 protoparams.add(b'partial-pull')
196 205
197 206 if b'0.2tx' in mediatypes:
198 207 protoparams.add(b'0.2')
199 208
200 209 if b'0.2tx' in mediatypes and capablefn(b'compression'):
201 210 # We /could/ compare supported compression formats and prune
202 211 # non-mutually supported or error if nothing is mutually supported.
203 212 # For now, send the full list to the server and have it error.
204 213 comps = [
205 214 e.wireprotosupport().name
206 215 for e in util.compengines.supportedwireengines(util.CLIENTROLE)
207 216 ]
208 217 protoparams.add(b'comp=%s' % b','.join(comps))
209 218
210 219 if protoparams:
211 220 protoheaders = encodevalueinheaders(
212 221 b' '.join(sorted(protoparams)), b'X-HgProto', headersize or 1024
213 222 )
214 223 for header, value in protoheaders:
215 224 headers[header] = value
216 225
217 226 varyheaders = []
218 227 for header in headers:
219 228 if header.lower().startswith('x-hg'):
220 229 varyheaders.append(header)
221 230
222 231 if varyheaders:
223 232 headers['Vary'] = ','.join(sorted(varyheaders))
224 233
225 234 req = requestbuilder(pycompat.strurl(cu), data, headers)
226 235
227 236 if data is not None:
228 237 ui.debug(b"sending %d bytes\n" % size)
229 238 req.add_unredirected_header('Content-Length', '%d' % size)
230 239
231 240 return req, cu, qs
232 241
233 242
234 243 def sendrequest(ui, opener, req):
235 244 """Send a prepared HTTP request.
236 245
237 246 Returns the response object.
238 247 """
239 248 dbg = ui.debug
240 249 if ui.debugflag and ui.configbool(b'devel', b'debug.peer-request'):
241 250 line = b'devel-peer-request: %s\n'
242 251 dbg(
243 252 line
244 253 % b'%s %s'
245 254 % (
246 255 pycompat.bytesurl(req.get_method()),
247 256 pycompat.bytesurl(req.get_full_url()),
248 257 )
249 258 )
250 259 hgargssize = None
251 260
252 261 for header, value in sorted(req.header_items()):
253 262 header = pycompat.bytesurl(header)
254 263 value = pycompat.bytesurl(value)
255 264 if header.startswith(b'X-hgarg-'):
256 265 if hgargssize is None:
257 266 hgargssize = 0
258 267 hgargssize += len(value)
259 268 else:
260 269 dbg(line % b' %s %s' % (header, value))
261 270
262 271 if hgargssize is not None:
263 272 dbg(
264 273 line
265 274 % b' %d bytes of commands arguments in headers'
266 275 % hgargssize
267 276 )
268 277 data = req.data
269 278 if data is not None:
270 279 length = getattr(data, 'length', None)
271 280 if length is None:
272 281 length = len(data)
273 282 dbg(line % b' %d bytes of data' % length)
274 283
275 284 start = util.timer()
276 285
277 286 res = None
278 287 try:
279 288 res = opener.open(req)
280 289 except urlerr.httperror as inst:
281 290 if inst.code == 401:
282 291 raise error.Abort(_(b'authorization failed'))
283 292 raise
284 293 except httplib.HTTPException as inst:
285 294 ui.debug(
286 295 b'http error requesting %s\n'
287 296 % urlutil.hidepassword(req.get_full_url())
288 297 )
289 298 ui.traceback()
290 299 raise IOError(None, inst)
291 300 finally:
292 301 if ui.debugflag and ui.configbool(b'devel', b'debug.peer-request'):
293 302 code = res.code if res else -1
294 303 dbg(
295 304 line
296 305 % b' finished in %.4f seconds (%d)'
297 306 % (util.timer() - start, code)
298 307 )
299 308
300 309 # Insert error handlers for common I/O failures.
301 310 urlmod.wrapresponse(res)
302 311
303 312 return res
304 313
305 314
306 315 class RedirectedRepoError(error.RepoError):
307 316 def __init__(self, msg, respurl):
308 317 super(RedirectedRepoError, self).__init__(msg)
309 318 self.respurl = respurl
310 319
311 320
312 321 def parsev1commandresponse(ui, baseurl, requrl, qs, resp, compressible):
313 322 # record the url we got redirected to
314 323 redirected = False
315 324 respurl = pycompat.bytesurl(resp.geturl())
316 325 if respurl.endswith(qs):
317 326 respurl = respurl[: -len(qs)]
318 327 qsdropped = False
319 328 else:
320 329 qsdropped = True
321 330
322 331 if baseurl.rstrip(b'/') != respurl.rstrip(b'/'):
323 332 redirected = True
324 333 if not ui.quiet:
325 334 ui.warn(_(b'real URL is %s\n') % respurl)
326 335
327 336 try:
328 337 proto = pycompat.bytesurl(resp.getheader('content-type', ''))
329 338 except AttributeError:
330 339 proto = pycompat.bytesurl(resp.headers.get('content-type', ''))
331 340
332 341 safeurl = urlutil.hidepassword(baseurl)
333 342 if proto.startswith(b'application/hg-error'):
334 343 raise error.OutOfBandError(resp.read())
335 344
336 345 # Pre 1.0 versions of Mercurial used text/plain and
337 346 # application/hg-changegroup. We don't support such old servers.
338 347 if not proto.startswith(b'application/mercurial-'):
339 348 ui.debug(b"requested URL: '%s'\n" % urlutil.hidepassword(requrl))
340 349 msg = _(
341 350 b"'%s' does not appear to be an hg repository:\n"
342 351 b"---%%<--- (%s)\n%s\n---%%<---\n"
343 352 ) % (safeurl, proto or b'no content-type', resp.read(1024))
344 353
345 354 # Some servers may strip the query string from the redirect. We
346 355 # raise a special error type so callers can react to this specially.
347 356 if redirected and qsdropped:
348 357 raise RedirectedRepoError(msg, respurl)
349 358 else:
350 359 raise error.RepoError(msg)
351 360
352 361 try:
353 362 subtype = proto.split(b'-', 1)[1]
354 363
355 364 version_info = tuple([int(n) for n in subtype.split(b'.')])
356 365 except ValueError:
357 366 raise error.RepoError(
358 367 _(b"'%s' sent a broken Content-Type header (%s)") % (safeurl, proto)
359 368 )
360 369
361 370 # TODO consider switching to a decompression reader that uses
362 371 # generators.
363 372 if version_info == (0, 1):
364 373 if compressible:
365 374 resp = util.compengines[b'zlib'].decompressorreader(resp)
366 375
367 376 elif version_info == (0, 2):
368 377 # application/mercurial-0.2 always identifies the compression
369 378 # engine in the payload header.
370 379 elen = struct.unpack(b'B', util.readexactly(resp, 1))[0]
371 380 ename = util.readexactly(resp, elen)
372 381 engine = util.compengines.forwiretype(ename)
373 382
374 383 resp = engine.decompressorreader(resp)
375 384 else:
376 385 raise error.RepoError(
377 386 _(b"'%s' uses newer protocol %s") % (safeurl, subtype)
378 387 )
379 388
380 389 return respurl, proto, resp
381 390
382 391
383 392 class httppeer(wireprotov1peer.wirepeer):
384 393 def __init__(
385 394 self, ui, path, url, opener, requestbuilder, caps, remotehidden=False
386 395 ):
387 396 super().__init__(ui, path=path, remotehidden=remotehidden)
388 if remotehidden:
389 msg = _(
390 b"ignoring `--remote-hidden` request\n"
391 b"(access to hidden changeset for http peers not "
392 b"supported yet)\n"
393 )
394 ui.warn(msg)
395 397 self._url = url
396 398 self._caps = caps
397 399 self.limitedarguments = caps is not None and b'httppostargs' not in caps
398 400 self._urlopener = opener
399 401 self._requestbuilder = requestbuilder
402 self._remotehidden = remotehidden
400 403
401 404 def __del__(self):
402 405 for h in self._urlopener.handlers:
403 406 h.close()
404 407 getattr(h, "close_all", lambda: None)()
405 408
406 409 # Begin of ipeerconnection interface.
407 410
408 411 def url(self):
409 412 return self.path.loc
410 413
411 414 def local(self):
412 415 return None
413 416
414 417 def canpush(self):
415 418 return True
416 419
417 420 def close(self):
418 421 try:
419 422 reqs, sent, recv = (
420 423 self._urlopener.requestscount,
421 424 self._urlopener.sentbytescount,
422 425 self._urlopener.receivedbytescount,
423 426 )
424 427 except AttributeError:
425 428 return
426 429 self.ui.note(
427 430 _(
428 431 b'(sent %d HTTP requests and %d bytes; '
429 432 b'received %d bytes in responses)\n'
430 433 )
431 434 % (reqs, sent, recv)
432 435 )
433 436
434 437 # End of ipeerconnection interface.
435 438
436 439 # Begin of ipeercommands interface.
437 440
438 441 def capabilities(self):
439 442 return self._caps
440 443
441 444 # End of ipeercommands interface.
442 445
443 446 def _callstream(self, cmd, _compressible=False, **args):
444 447 args = pycompat.byteskwargs(args)
445 448
446 449 req, cu, qs = makev1commandrequest(
447 450 self.ui,
448 451 self._requestbuilder,
449 452 self._caps,
450 453 self.capable,
451 454 self._url,
452 455 cmd,
453 456 args,
457 self._remotehidden,
454 458 )
455 459
456 460 resp = sendrequest(self.ui, self._urlopener, req)
457 461
458 462 self._url, ct, resp = parsev1commandresponse(
459 463 self.ui, self._url, cu, qs, resp, _compressible
460 464 )
461 465
462 466 return resp
463 467
464 468 def _call(self, cmd, **args):
465 469 fp = self._callstream(cmd, **args)
466 470 try:
467 471 return fp.read()
468 472 finally:
469 473 # if using keepalive, allow connection to be reused
470 474 fp.close()
471 475
472 476 def _callpush(self, cmd, cg, **args):
473 477 # have to stream bundle to a temp file because we do not have
474 478 # http 1.1 chunked transfer.
475 479
476 480 types = self.capable(b'unbundle')
477 481 try:
478 482 types = types.split(b',')
479 483 except AttributeError:
480 484 # servers older than d1b16a746db6 will send 'unbundle' as a
481 485 # boolean capability. They only support headerless/uncompressed
482 486 # bundles.
483 487 types = [b""]
484 488 for x in types:
485 489 if x in bundle2.bundletypes:
486 490 type = x
487 491 break
488 492
489 493 tempname = bundle2.writebundle(self.ui, cg, None, type)
490 494 fp = httpconnection.httpsendfile(self.ui, tempname, b"rb")
491 495 headers = {'Content-Type': 'application/mercurial-0.1'}
492 496
493 497 try:
494 498 r = self._call(cmd, data=fp, headers=headers, **args)
495 499 vals = r.split(b'\n', 1)
496 500 if len(vals) < 2:
497 501 raise error.ResponseError(_(b"unexpected response:"), r)
498 502 return vals
499 503 except urlerr.httperror:
500 504 # Catch and re-raise these so we don't try and treat them
501 505 # like generic socket errors. They lack any values in
502 506 # .args on Python 3 which breaks our socket.error block.
503 507 raise
504 508 except socket.error as err:
505 509 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
506 510 raise error.Abort(_(b'push failed: %s') % err.args[1])
507 511 raise error.Abort(err.args[1])
508 512 finally:
509 513 fp.close()
510 514 os.unlink(tempname)
511 515
512 516 def _calltwowaystream(self, cmd, fp, **args):
513 517 filename = None
514 518 try:
515 519 # dump bundle to disk
516 520 fd, filename = pycompat.mkstemp(prefix=b"hg-bundle-", suffix=b".hg")
517 521 with os.fdopen(fd, "wb") as fh:
518 522 d = fp.read(4096)
519 523 while d:
520 524 fh.write(d)
521 525 d = fp.read(4096)
522 526 # start http push
523 527 with httpconnection.httpsendfile(self.ui, filename, b"rb") as fp_:
524 528 headers = {'Content-Type': 'application/mercurial-0.1'}
525 529 return self._callstream(cmd, data=fp_, headers=headers, **args)
526 530 finally:
527 531 if filename is not None:
528 532 os.unlink(filename)
529 533
530 534 def _callcompressable(self, cmd, **args):
531 535 return self._callstream(cmd, _compressible=True, **args)
532 536
533 537 def _abort(self, exception):
534 538 raise exception
535 539
536 540
537 541 class queuedcommandfuture(futures.Future):
538 542 """Wraps result() on command futures to trigger submission on call."""
539 543
540 544 def result(self, timeout=None):
541 545 if self.done():
542 546 return futures.Future.result(self, timeout)
543 547
544 548 self._peerexecutor.sendcommands()
545 549
546 550 # sendcommands() will restore the original __class__ and self.result
547 551 # will resolve to Future.result.
548 552 return self.result(timeout)
549 553
550 554
551 555 def performhandshake(ui, url, opener, requestbuilder):
552 556 # The handshake is a request to the capabilities command.
553 557
554 558 caps = None
555 559
556 560 def capable(x):
557 561 raise error.ProgrammingError(b'should not be called')
558 562
559 563 args = {}
560 564
561 565 req, requrl, qs = makev1commandrequest(
562 566 ui, requestbuilder, caps, capable, url, b'capabilities', args
563 567 )
564 568 resp = sendrequest(ui, opener, req)
565 569
566 570 # The server may redirect us to the repo root, stripping the
567 571 # ?cmd=capabilities query string from the URL. The server would likely
568 572 # return HTML in this case and ``parsev1commandresponse()`` would raise.
569 573 # We catch this special case and re-issue the capabilities request against
570 574 # the new URL.
571 575 #
572 576 # We should ideally not do this, as a redirect that drops the query
573 577 # string from the URL is arguably a server bug. (Garbage in, garbage out).
574 578 # However, Mercurial clients for several years appeared to handle this
575 579 # issue without behavior degradation. And according to issue 5860, it may
576 580 # be a longstanding bug in some server implementations. So we allow a
577 581 # redirect that drops the query string to "just work."
578 582 try:
579 583 respurl, ct, resp = parsev1commandresponse(
580 584 ui, url, requrl, qs, resp, compressible=False
581 585 )
582 586 except RedirectedRepoError as e:
583 587 req, requrl, qs = makev1commandrequest(
584 588 ui, requestbuilder, caps, capable, e.respurl, b'capabilities', args
585 589 )
586 590 resp = sendrequest(ui, opener, req)
587 591 respurl, ct, resp = parsev1commandresponse(
588 592 ui, url, requrl, qs, resp, compressible=False
589 593 )
590 594
591 595 try:
592 596 rawdata = resp.read()
593 597 finally:
594 598 resp.close()
595 599
596 600 if not ct.startswith(b'application/mercurial-'):
597 601 raise error.ProgrammingError(b'unexpected content-type: %s' % ct)
598 602
599 603 info = {b'v1capabilities': set(rawdata.split())}
600 604
601 605 return respurl, info
602 606
603 607
604 608 def _make_peer(
605 609 ui, path, opener=None, requestbuilder=urlreq.request, remotehidden=False
606 610 ):
607 611 """Construct an appropriate HTTP peer instance.
608 612
609 613 ``opener`` is an ``url.opener`` that should be used to establish
610 614 connections, perform HTTP requests.
611 615
612 616 ``requestbuilder`` is the type used for constructing HTTP requests.
613 617 It exists as an argument so extensions can override the default.
614 618 """
615 619 if path.url.query or path.url.fragment:
616 620 msg = _(b'unsupported URL component: "%s"')
617 621 msg %= path.url.query or path.url.fragment
618 622 raise error.Abort(msg)
619 623
620 624 # urllib cannot handle URLs with embedded user or passwd.
621 625 url, authinfo = path.url.authinfo()
622 626 ui.debug(b'using %s\n' % url)
623 627
624 628 opener = opener or urlmod.opener(ui, authinfo)
625 629
626 630 respurl, info = performhandshake(ui, url, opener, requestbuilder)
627 631
628 632 return httppeer(
629 633 ui,
630 634 path,
631 635 respurl,
632 636 opener,
633 637 requestbuilder,
634 638 info[b'v1capabilities'],
635 639 remotehidden=remotehidden,
636 640 )
637 641
638 642
639 643 def make_peer(
640 644 ui, path, create, intents=None, createopts=None, remotehidden=False
641 645 ):
642 646 if create:
643 647 raise error.Abort(_(b'cannot create new http repository'))
644 648 try:
645 649 if path.url.scheme == b'https' and not urlmod.has_https:
646 650 raise error.Abort(
647 651 _(b'Python support for SSL and HTTPS is not installed')
648 652 )
649 653
650 654 inst = _make_peer(ui, path, remotehidden=remotehidden)
651 655
652 656 return inst
653 657 except error.RepoError as httpexception:
654 658 try:
655 659 r = statichttprepo.make_peer(ui, b"static-" + path.loc, create)
656 660 ui.note(_(b'(falling back to static-http)\n'))
657 661 return r
658 662 except error.RepoError:
659 663 raise httpexception # use the original http RepoError instead
@@ -1,218 +1,312 b''
1 1 ========================================================
2 2 Test the ability to access a hidden revision on a server
3 3 ========================================================
4 4
5 5 #require serve
6 6
7 7 $ . $TESTDIR/testlib/obsmarker-common.sh
8 8 $ cat >> $HGRCPATH << EOF
9 9 > [phases]
10 10 > # public changeset are not obsolete
11 11 > publish=false
12 12 > [experimental]
13 13 > evolution=all
14 14 > [ui]
15 15 > logtemplate='{rev}:{node|short} {desc} [{phase}]\n'
16 16 > EOF
17 17
18 18 Setup a simple repository with some hidden revisions
19 19 ----------------------------------------------------
20 20
21 21 Testing the `served.hidden` view
22 22
23 23 $ hg init repo-with-hidden
24 24 $ cd repo-with-hidden
25 25
26 26 $ echo 0 > a
27 27 $ hg ci -qAm "c_Public"
28 28 $ hg phase --public
29 29 $ echo 1 > a
30 30 $ hg ci -m "c_Amend_Old"
31 31 $ echo 2 > a
32 32 $ hg ci -m "c_Amend_New" --amend
33 33 $ hg up ".^"
34 34 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
35 35 $ echo 3 > a
36 36 $ hg ci -m "c_Pruned"
37 37 created new head
38 38 $ hg debugobsolete --record-parents `getid 'desc("c_Pruned")'` -d '0 0'
39 39 1 new obsolescence markers
40 40 obsoleted 1 changesets
41 41 $ hg up ".^"
42 42 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
43 43 $ echo 4 > a
44 44 $ hg ci -m "c_Secret" --secret
45 45 created new head
46 46 $ echo 5 > a
47 47 $ hg ci -m "c_Secret_Pruned" --secret
48 48 $ hg debugobsolete --record-parents `getid 'desc("c_Secret_Pruned")'` -d '0 0'
49 49 1 new obsolescence markers
50 50 obsoleted 1 changesets
51 51 $ hg up null
52 52 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
53 53
54 54 $ hg log -G -T '{rev}:{node|short} {desc} [{phase}]\n' --hidden
55 55 x 5:8d28cbe335f3 c_Secret_Pruned [secret]
56 56 |
57 57 o 4:1c6afd79eb66 c_Secret [secret]
58 58 |
59 59 | x 3:5d1575e42c25 c_Pruned [draft]
60 60 |/
61 61 | o 2:c33affeb3f6b c_Amend_New [draft]
62 62 |/
63 63 | x 1:be215fbb8c50 c_Amend_Old [draft]
64 64 |/
65 65 o 0:5f354f46e585 c_Public [public]
66 66
67 67 $ hg debugobsolete
68 68 be215fbb8c5090028b00154c1fe877ad1b376c61 c33affeb3f6b4e9621d1839d6175ddc07708807c 0 (Thu Jan 01 00:00:00 1970 +0000) {'ef1': '9', 'operation': 'amend', 'user': 'test'}
69 69 5d1575e42c25b7f2db75cd4e0b881b1c35158fae 0 {5f354f46e5853535841ec7a128423e991ca4d59b} (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
70 70 8d28cbe335f311bc89332d7bbe8a07889b6914a0 0 {1c6afd79eb6663275bbe30097e162b1c24ced0f0} (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
71 71
72 72 $ cd ..
73 73
74 74 Test the feature
75 75 ================
76 76
77 77 Check cache pre-warm
78 78 --------------------
79 79
80 80 $ ls -1 repo-with-hidden/.hg/cache
81 81 branch2
82 82 branch2-base
83 83 branch2-served
84 84 branch2-served.hidden
85 85 branch2-visible
86 86 rbc-names-v1
87 87 rbc-revs-v1
88 88 tags2
89 89 tags2-visible
90 90
91 91 Check that the `served.hidden` repoview
92 92 ---------------------------------------
93 93
94 94 $ hg -R repo-with-hidden serve -p $HGPORT -d --pid-file hg.pid --config web.view=served.hidden
95 95 $ cat hg.pid >> $DAEMON_PIDS
96 96
97 97 changesets in secret and higher phases are not visible through hgweb
98 98
99 99 $ hg -R repo-with-hidden log --template "revision: {rev}\\n" --rev "reverse(not secret())"
100 100 revision: 2
101 101 revision: 0
102 102 $ hg -R repo-with-hidden log --template "revision: {rev}\\n" --rev "reverse(not secret())" --hidden
103 103 revision: 3
104 104 revision: 2
105 105 revision: 1
106 106 revision: 0
107 107 $ get-with-headers.py localhost:$HGPORT 'log?style=raw' | grep revision:
108 108 revision: 3
109 109 revision: 2
110 110 revision: 1
111 111 revision: 0
112 112
113 113 $ killdaemons.py
114 114
115 115 Test --remote-hidden for local peer
116 116 -----------------------------------
117 117
118 118 $ hg clone --pull repo-with-hidden client
119 119 requesting all changes
120 120 adding changesets
121 121 adding manifests
122 122 adding file changes
123 123 added 2 changesets with 2 changes to 1 files
124 124 2 new obsolescence markers
125 125 new changesets 5f354f46e585:c33affeb3f6b (1 drafts)
126 126 updating to branch default
127 127 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
128 128 $ hg -R client log -G --hidden -v
129 129 @ 1:c33affeb3f6b c_Amend_New [draft]
130 130 |
131 131 o 0:5f354f46e585 c_Public [public]
132 132
133 133
134 134 pulling an hidden changeset should fail:
135 135
136 136 $ hg -R client pull -r be215fbb8c50
137 137 pulling from $TESTTMP/repo-with-hidden
138 138 abort: filtered revision 'be215fbb8c50' (not in 'served' subset)
139 139 [10]
140 140
141 141 pulling an hidden changeset with --remote-hidden should succeed:
142 142
143 143 $ hg -R client pull --remote-hidden --traceback -r be215fbb8c50
144 144 pulling from $TESTTMP/repo-with-hidden
145 145 searching for changes
146 146 adding changesets
147 147 adding manifests
148 148 adding file changes
149 149 added 1 changesets with 1 changes to 1 files (+1 heads)
150 150 (1 other changesets obsolete on arrival)
151 151 (run 'hg heads' to see heads)
152 152 $ hg -R client log -G --hidden -v
153 153 x 2:be215fbb8c50 c_Amend_Old [draft]
154 154 |
155 155 | @ 1:c33affeb3f6b c_Amend_New [draft]
156 156 |/
157 157 o 0:5f354f46e585 c_Public [public]
158 158
159 159
160 160 Pulling a secret changeset is still forbidden:
161 161
162 162 secret visible:
163 163
164 164 $ hg -R client pull --remote-hidden -r 8d28cbe335f3
165 165 pulling from $TESTTMP/repo-with-hidden
166 166 abort: filtered revision '8d28cbe335f3' (not in 'served.hidden' subset)
167 167 [10]
168 168
169 169 secret hidden:
170 170
171 171 $ hg -R client pull --remote-hidden -r 1c6afd79eb66
172 172 pulling from $TESTTMP/repo-with-hidden
173 173 abort: filtered revision '1c6afd79eb66' (not in 'served.hidden' subset)
174 174 [10]
175 175
176 176 Test accessing hidden changeset through hgweb
177 177 ---------------------------------------------
178 178
179 179 $ hg -R repo-with-hidden serve -p $HGPORT -d --pid-file hg.pid --config "experimental.server.allow-hidden-access=*" -E error.log --accesslog access.log
180 180 $ cat hg.pid >> $DAEMON_PIDS
181 181
182 182 Hidden changeset are hidden by default:
183 183
184 184 $ get-with-headers.py localhost:$HGPORT 'log?style=raw' | grep revision:
185 185 revision: 2
186 186 revision: 0
187 187
188 188 Hidden changeset are visible when requested:
189 189
190 190 $ get-with-headers.py localhost:$HGPORT 'log?style=raw&access-hidden=1' | grep revision:
191 191 revision: 3
192 192 revision: 2
193 193 revision: 1
194 194 revision: 0
195 195
196 196 Same check on a server that do not allow hidden access:
197 197 ```````````````````````````````````````````````````````
198 198
199 199 $ hg -R repo-with-hidden serve -p $HGPORT1 -d --pid-file hg2.pid --config "experimental.server.allow-hidden-access=" -E error.log --accesslog access.log
200 200 $ cat hg2.pid >> $DAEMON_PIDS
201 201
202 202 Hidden changeset are hidden by default:
203 203
204 204 $ get-with-headers.py localhost:$HGPORT1 'log?style=raw' | grep revision:
205 205 revision: 2
206 206 revision: 0
207 207
208 208 Hidden changeset are still hidden despite being the hidden access request:
209 209
210 210 $ get-with-headers.py localhost:$HGPORT1 'log?style=raw&access-hidden=1' | grep revision:
211 211 revision: 2
212 212 revision: 0
213 213
214 Test --remote-hidden for http peer
215 ----------------------------------
216
217 $ hg clone --pull http://localhost:$HGPORT client-http
218 requesting all changes
219 adding changesets
220 adding manifests
221 adding file changes
222 added 2 changesets with 2 changes to 1 files
223 2 new obsolescence markers
224 new changesets 5f354f46e585:c33affeb3f6b (1 drafts)
225 updating to branch default
226 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
227 $ hg -R client-http log -G --hidden -v
228 @ 1:c33affeb3f6b c_Amend_New [draft]
229 |
230 o 0:5f354f46e585 c_Public [public]
231
232
233 pulling an hidden changeset should fail:
234
235 $ hg -R client-http pull -r be215fbb8c50
236 pulling from http://localhost:$HGPORT/
237 abort: filtered revision 'be215fbb8c50' (not in 'served' subset)
238 [255]
239
240 pulling an hidden changeset with --remote-hidden should succeed:
241
242 $ hg -R client-http pull --remote-hidden -r be215fbb8c50
243 pulling from http://localhost:$HGPORT/
244 searching for changes
245 adding changesets
246 adding manifests
247 adding file changes
248 added 1 changesets with 1 changes to 1 files (+1 heads)
249 (1 other changesets obsolete on arrival)
250 (run 'hg heads' to see heads)
251 $ hg -R client-http log -G --hidden -v
252 x 2:be215fbb8c50 c_Amend_Old [draft]
253 |
254 | @ 1:c33affeb3f6b c_Amend_New [draft]
255 |/
256 o 0:5f354f46e585 c_Public [public]
257
258
259 Pulling a secret changeset is still forbidden:
260
261 secret visible:
262
263 $ hg -R client-http pull --remote-hidden -r 8d28cbe335f3
264 pulling from http://localhost:$HGPORT/
265 abort: filtered revision '8d28cbe335f3' (not in 'served.hidden' subset)
266 [255]
267
268 secret hidden:
269
270 $ hg -R client-http pull --remote-hidden -r 1c6afd79eb66
271 pulling from http://localhost:$HGPORT/
272 abort: filtered revision '1c6afd79eb66' (not in 'served.hidden' subset)
273 [255]
274
275 Same check on a server that do not allow hidden access:
276 ```````````````````````````````````````````````````````
277
278 $ hg clone --pull http://localhost:$HGPORT1 client-http2
279 requesting all changes
280 adding changesets
281 adding manifests
282 adding file changes
283 added 2 changesets with 2 changes to 1 files
284 2 new obsolescence markers
285 new changesets 5f354f46e585:c33affeb3f6b (1 drafts)
286 updating to branch default
287 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
288 $ hg -R client-http2 log -G --hidden -v
289 @ 1:c33affeb3f6b c_Amend_New [draft]
290 |
291 o 0:5f354f46e585 c_Public [public]
292
293
294 pulling an hidden changeset should fail:
295
296 $ hg -R client-http2 pull -r be215fbb8c50
297 pulling from http://localhost:$HGPORT1/
298 abort: filtered revision 'be215fbb8c50' (not in 'served' subset)
299 [255]
300
301 pulling an hidden changeset with --remote-hidden should fail too:
302
303 $ hg -R client-http2 pull --remote-hidden -r be215fbb8c50
304 pulling from http://localhost:$HGPORT1/
305 abort: filtered revision 'be215fbb8c50' (not in 'served' subset)
306 [255]
307
214 308 =============
215 309 Final cleanup
216 310 =============
217 311
218 312 $ killdaemons.py
General Comments 0
You need to be logged in to leave comments. Login now