##// END OF EJS Templates
peer: have a common constructor and use it...
marmoute -
r50646:a6e2a668 default
parent child Browse files
Show More
@@ -1,642 +1,642 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 111 ui, requestbuilder, caps, capablefn, repobaseurl, cmd, args
112 112 ):
113 113 """Make an HTTP request to run a command for a version 1 client.
114 114
115 115 ``caps`` is a set of known server capabilities. The value may be
116 116 None if capabilities are not yet known.
117 117
118 118 ``capablefn`` is a function to evaluate a capability.
119 119
120 120 ``cmd``, ``args``, and ``data`` define the command, its arguments, and
121 121 raw data to pass to it.
122 122 """
123 123 if cmd == b'pushkey':
124 124 args[b'data'] = b''
125 125 data = args.pop(b'data', None)
126 126 headers = args.pop(b'headers', {})
127 127
128 128 ui.debug(b"sending %s command\n" % cmd)
129 129 q = [(b'cmd', cmd)]
130 130 headersize = 0
131 131 # Important: don't use self.capable() here or else you end up
132 132 # with infinite recursion when trying to look up capabilities
133 133 # for the first time.
134 134 postargsok = caps is not None and b'httppostargs' in caps
135 135
136 136 # Send arguments via POST.
137 137 if postargsok and args:
138 138 strargs = urlreq.urlencode(sorted(args.items()))
139 139 if not data:
140 140 data = strargs
141 141 else:
142 142 if isinstance(data, bytes):
143 143 i = io.BytesIO(data)
144 144 i.length = len(data)
145 145 data = i
146 146 argsio = io.BytesIO(strargs)
147 147 argsio.length = len(strargs)
148 148 data = _multifile(argsio, data)
149 149 headers['X-HgArgs-Post'] = len(strargs)
150 150 elif args:
151 151 # Calling self.capable() can infinite loop if we are calling
152 152 # "capabilities". But that command should never accept wire
153 153 # protocol arguments. So this should never happen.
154 154 assert cmd != b'capabilities'
155 155 httpheader = capablefn(b'httpheader')
156 156 if httpheader:
157 157 headersize = int(httpheader.split(b',', 1)[0])
158 158
159 159 # Send arguments via HTTP headers.
160 160 if headersize > 0:
161 161 # The headers can typically carry more data than the URL.
162 162 encoded_args = urlreq.urlencode(sorted(args.items()))
163 163 for header, value in encodevalueinheaders(
164 164 encoded_args, b'X-HgArg', headersize
165 165 ):
166 166 headers[header] = value
167 167 # Send arguments via query string (Mercurial <1.9).
168 168 else:
169 169 q += sorted(args.items())
170 170
171 171 qs = b'?%s' % urlreq.urlencode(q)
172 172 cu = b"%s%s" % (repobaseurl, qs)
173 173 size = 0
174 174 if util.safehasattr(data, b'length'):
175 175 size = data.length
176 176 elif data is not None:
177 177 size = len(data)
178 178 if data is not None and 'Content-Type' not in headers:
179 179 headers['Content-Type'] = 'application/mercurial-0.1'
180 180
181 181 # Tell the server we accept application/mercurial-0.2 and multiple
182 182 # compression formats if the server is capable of emitting those
183 183 # payloads.
184 184 # Note: Keep this set empty by default, as client advertisement of
185 185 # protocol parameters should only occur after the handshake.
186 186 protoparams = set()
187 187
188 188 mediatypes = set()
189 189 if caps is not None:
190 190 mt = capablefn(b'httpmediatype')
191 191 if mt:
192 192 protoparams.add(b'0.1')
193 193 mediatypes = set(mt.split(b','))
194 194
195 195 protoparams.add(b'partial-pull')
196 196
197 197 if b'0.2tx' in mediatypes:
198 198 protoparams.add(b'0.2')
199 199
200 200 if b'0.2tx' in mediatypes and capablefn(b'compression'):
201 201 # We /could/ compare supported compression formats and prune
202 202 # non-mutually supported or error if nothing is mutually supported.
203 203 # For now, send the full list to the server and have it error.
204 204 comps = [
205 205 e.wireprotosupport().name
206 206 for e in util.compengines.supportedwireengines(util.CLIENTROLE)
207 207 ]
208 208 protoparams.add(b'comp=%s' % b','.join(comps))
209 209
210 210 if protoparams:
211 211 protoheaders = encodevalueinheaders(
212 212 b' '.join(sorted(protoparams)), b'X-HgProto', headersize or 1024
213 213 )
214 214 for header, value in protoheaders:
215 215 headers[header] = value
216 216
217 217 varyheaders = []
218 218 for header in headers:
219 219 if header.lower().startswith('x-hg'):
220 220 varyheaders.append(header)
221 221
222 222 if varyheaders:
223 223 headers['Vary'] = ','.join(sorted(varyheaders))
224 224
225 225 req = requestbuilder(pycompat.strurl(cu), data, headers)
226 226
227 227 if data is not None:
228 228 ui.debug(b"sending %d bytes\n" % size)
229 229 req.add_unredirected_header('Content-Length', '%d' % size)
230 230
231 231 return req, cu, qs
232 232
233 233
234 234 def sendrequest(ui, opener, req):
235 235 """Send a prepared HTTP request.
236 236
237 237 Returns the response object.
238 238 """
239 239 dbg = ui.debug
240 240 if ui.debugflag and ui.configbool(b'devel', b'debug.peer-request'):
241 241 line = b'devel-peer-request: %s\n'
242 242 dbg(
243 243 line
244 244 % b'%s %s'
245 245 % (
246 246 pycompat.bytesurl(req.get_method()),
247 247 pycompat.bytesurl(req.get_full_url()),
248 248 )
249 249 )
250 250 hgargssize = None
251 251
252 252 for header, value in sorted(req.header_items()):
253 253 header = pycompat.bytesurl(header)
254 254 value = pycompat.bytesurl(value)
255 255 if header.startswith(b'X-hgarg-'):
256 256 if hgargssize is None:
257 257 hgargssize = 0
258 258 hgargssize += len(value)
259 259 else:
260 260 dbg(line % b' %s %s' % (header, value))
261 261
262 262 if hgargssize is not None:
263 263 dbg(
264 264 line
265 265 % b' %d bytes of commands arguments in headers'
266 266 % hgargssize
267 267 )
268 268 data = req.data
269 269 if data is not None:
270 270 length = getattr(data, 'length', None)
271 271 if length is None:
272 272 length = len(data)
273 273 dbg(line % b' %d bytes of data' % length)
274 274
275 275 start = util.timer()
276 276
277 277 res = None
278 278 try:
279 279 res = opener.open(req)
280 280 except urlerr.httperror as inst:
281 281 if inst.code == 401:
282 282 raise error.Abort(_(b'authorization failed'))
283 283 raise
284 284 except httplib.HTTPException as inst:
285 285 ui.debug(
286 286 b'http error requesting %s\n'
287 287 % urlutil.hidepassword(req.get_full_url())
288 288 )
289 289 ui.traceback()
290 290 raise IOError(None, inst)
291 291 finally:
292 292 if ui.debugflag and ui.configbool(b'devel', b'debug.peer-request'):
293 293 code = res.code if res else -1
294 294 dbg(
295 295 line
296 296 % b' finished in %.4f seconds (%d)'
297 297 % (util.timer() - start, code)
298 298 )
299 299
300 300 # Insert error handlers for common I/O failures.
301 301 urlmod.wrapresponse(res)
302 302
303 303 return res
304 304
305 305
306 306 class RedirectedRepoError(error.RepoError):
307 307 def __init__(self, msg, respurl):
308 308 super(RedirectedRepoError, self).__init__(msg)
309 309 self.respurl = respurl
310 310
311 311
312 312 def parsev1commandresponse(ui, baseurl, requrl, qs, resp, compressible):
313 313 # record the url we got redirected to
314 314 redirected = False
315 315 respurl = pycompat.bytesurl(resp.geturl())
316 316 if respurl.endswith(qs):
317 317 respurl = respurl[: -len(qs)]
318 318 qsdropped = False
319 319 else:
320 320 qsdropped = True
321 321
322 322 if baseurl.rstrip(b'/') != respurl.rstrip(b'/'):
323 323 redirected = True
324 324 if not ui.quiet:
325 325 ui.warn(_(b'real URL is %s\n') % respurl)
326 326
327 327 try:
328 328 proto = pycompat.bytesurl(resp.getheader('content-type', ''))
329 329 except AttributeError:
330 330 proto = pycompat.bytesurl(resp.headers.get('content-type', ''))
331 331
332 332 safeurl = urlutil.hidepassword(baseurl)
333 333 if proto.startswith(b'application/hg-error'):
334 334 raise error.OutOfBandError(resp.read())
335 335
336 336 # Pre 1.0 versions of Mercurial used text/plain and
337 337 # application/hg-changegroup. We don't support such old servers.
338 338 if not proto.startswith(b'application/mercurial-'):
339 339 ui.debug(b"requested URL: '%s'\n" % urlutil.hidepassword(requrl))
340 340 msg = _(
341 341 b"'%s' does not appear to be an hg repository:\n"
342 342 b"---%%<--- (%s)\n%s\n---%%<---\n"
343 343 ) % (safeurl, proto or b'no content-type', resp.read(1024))
344 344
345 345 # Some servers may strip the query string from the redirect. We
346 346 # raise a special error type so callers can react to this specially.
347 347 if redirected and qsdropped:
348 348 raise RedirectedRepoError(msg, respurl)
349 349 else:
350 350 raise error.RepoError(msg)
351 351
352 352 try:
353 353 subtype = proto.split(b'-', 1)[1]
354 354
355 355 version_info = tuple([int(n) for n in subtype.split(b'.')])
356 356 except ValueError:
357 357 raise error.RepoError(
358 358 _(b"'%s' sent a broken Content-Type header (%s)") % (safeurl, proto)
359 359 )
360 360
361 361 # TODO consider switching to a decompression reader that uses
362 362 # generators.
363 363 if version_info == (0, 1):
364 364 if compressible:
365 365 resp = util.compengines[b'zlib'].decompressorreader(resp)
366 366
367 367 elif version_info == (0, 2):
368 368 # application/mercurial-0.2 always identifies the compression
369 369 # engine in the payload header.
370 370 elen = struct.unpack(b'B', util.readexactly(resp, 1))[0]
371 371 ename = util.readexactly(resp, elen)
372 372 engine = util.compengines.forwiretype(ename)
373 373
374 374 resp = engine.decompressorreader(resp)
375 375 else:
376 376 raise error.RepoError(
377 377 _(b"'%s' uses newer protocol %s") % (safeurl, subtype)
378 378 )
379 379
380 380 return respurl, proto, resp
381 381
382 382
383 383 class httppeer(wireprotov1peer.wirepeer):
384 384 def __init__(self, ui, path, url, opener, requestbuilder, caps):
385 self.ui = ui
385 super().__init__(ui)
386 386 self._path = path
387 387 self._url = url
388 388 self._caps = caps
389 389 self.limitedarguments = caps is not None and b'httppostargs' not in caps
390 390 self._urlopener = opener
391 391 self._requestbuilder = requestbuilder
392 392
393 393 def __del__(self):
394 394 for h in self._urlopener.handlers:
395 395 h.close()
396 396 getattr(h, "close_all", lambda: None)()
397 397
398 398 # Begin of ipeerconnection interface.
399 399
400 400 def url(self):
401 401 return self._path
402 402
403 403 def local(self):
404 404 return None
405 405
406 406 def canpush(self):
407 407 return True
408 408
409 409 def close(self):
410 410 try:
411 411 reqs, sent, recv = (
412 412 self._urlopener.requestscount,
413 413 self._urlopener.sentbytescount,
414 414 self._urlopener.receivedbytescount,
415 415 )
416 416 except AttributeError:
417 417 return
418 418 self.ui.note(
419 419 _(
420 420 b'(sent %d HTTP requests and %d bytes; '
421 421 b'received %d bytes in responses)\n'
422 422 )
423 423 % (reqs, sent, recv)
424 424 )
425 425
426 426 # End of ipeerconnection interface.
427 427
428 428 # Begin of ipeercommands interface.
429 429
430 430 def capabilities(self):
431 431 return self._caps
432 432
433 433 # End of ipeercommands interface.
434 434
435 435 def _callstream(self, cmd, _compressible=False, **args):
436 436 args = pycompat.byteskwargs(args)
437 437
438 438 req, cu, qs = makev1commandrequest(
439 439 self.ui,
440 440 self._requestbuilder,
441 441 self._caps,
442 442 self.capable,
443 443 self._url,
444 444 cmd,
445 445 args,
446 446 )
447 447
448 448 resp = sendrequest(self.ui, self._urlopener, req)
449 449
450 450 self._url, ct, resp = parsev1commandresponse(
451 451 self.ui, self._url, cu, qs, resp, _compressible
452 452 )
453 453
454 454 return resp
455 455
456 456 def _call(self, cmd, **args):
457 457 fp = self._callstream(cmd, **args)
458 458 try:
459 459 return fp.read()
460 460 finally:
461 461 # if using keepalive, allow connection to be reused
462 462 fp.close()
463 463
464 464 def _callpush(self, cmd, cg, **args):
465 465 # have to stream bundle to a temp file because we do not have
466 466 # http 1.1 chunked transfer.
467 467
468 468 types = self.capable(b'unbundle')
469 469 try:
470 470 types = types.split(b',')
471 471 except AttributeError:
472 472 # servers older than d1b16a746db6 will send 'unbundle' as a
473 473 # boolean capability. They only support headerless/uncompressed
474 474 # bundles.
475 475 types = [b""]
476 476 for x in types:
477 477 if x in bundle2.bundletypes:
478 478 type = x
479 479 break
480 480
481 481 tempname = bundle2.writebundle(self.ui, cg, None, type)
482 482 fp = httpconnection.httpsendfile(self.ui, tempname, b"rb")
483 483 headers = {'Content-Type': 'application/mercurial-0.1'}
484 484
485 485 try:
486 486 r = self._call(cmd, data=fp, headers=headers, **args)
487 487 vals = r.split(b'\n', 1)
488 488 if len(vals) < 2:
489 489 raise error.ResponseError(_(b"unexpected response:"), r)
490 490 return vals
491 491 except urlerr.httperror:
492 492 # Catch and re-raise these so we don't try and treat them
493 493 # like generic socket errors. They lack any values in
494 494 # .args on Python 3 which breaks our socket.error block.
495 495 raise
496 496 except socket.error as err:
497 497 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
498 498 raise error.Abort(_(b'push failed: %s') % err.args[1])
499 499 raise error.Abort(err.args[1])
500 500 finally:
501 501 fp.close()
502 502 os.unlink(tempname)
503 503
504 504 def _calltwowaystream(self, cmd, fp, **args):
505 505 filename = None
506 506 try:
507 507 # dump bundle to disk
508 508 fd, filename = pycompat.mkstemp(prefix=b"hg-bundle-", suffix=b".hg")
509 509 with os.fdopen(fd, "wb") as fh:
510 510 d = fp.read(4096)
511 511 while d:
512 512 fh.write(d)
513 513 d = fp.read(4096)
514 514 # start http push
515 515 with httpconnection.httpsendfile(self.ui, filename, b"rb") as fp_:
516 516 headers = {'Content-Type': 'application/mercurial-0.1'}
517 517 return self._callstream(cmd, data=fp_, headers=headers, **args)
518 518 finally:
519 519 if filename is not None:
520 520 os.unlink(filename)
521 521
522 522 def _callcompressable(self, cmd, **args):
523 523 return self._callstream(cmd, _compressible=True, **args)
524 524
525 525 def _abort(self, exception):
526 526 raise exception
527 527
528 528
529 529 class queuedcommandfuture(futures.Future):
530 530 """Wraps result() on command futures to trigger submission on call."""
531 531
532 532 def result(self, timeout=None):
533 533 if self.done():
534 534 return futures.Future.result(self, timeout)
535 535
536 536 self._peerexecutor.sendcommands()
537 537
538 538 # sendcommands() will restore the original __class__ and self.result
539 539 # will resolve to Future.result.
540 540 return self.result(timeout)
541 541
542 542
543 543 def performhandshake(ui, url, opener, requestbuilder):
544 544 # The handshake is a request to the capabilities command.
545 545
546 546 caps = None
547 547
548 548 def capable(x):
549 549 raise error.ProgrammingError(b'should not be called')
550 550
551 551 args = {}
552 552
553 553 req, requrl, qs = makev1commandrequest(
554 554 ui, requestbuilder, caps, capable, url, b'capabilities', args
555 555 )
556 556 resp = sendrequest(ui, opener, req)
557 557
558 558 # The server may redirect us to the repo root, stripping the
559 559 # ?cmd=capabilities query string from the URL. The server would likely
560 560 # return HTML in this case and ``parsev1commandresponse()`` would raise.
561 561 # We catch this special case and re-issue the capabilities request against
562 562 # the new URL.
563 563 #
564 564 # We should ideally not do this, as a redirect that drops the query
565 565 # string from the URL is arguably a server bug. (Garbage in, garbage out).
566 566 # However, Mercurial clients for several years appeared to handle this
567 567 # issue without behavior degradation. And according to issue 5860, it may
568 568 # be a longstanding bug in some server implementations. So we allow a
569 569 # redirect that drops the query string to "just work."
570 570 try:
571 571 respurl, ct, resp = parsev1commandresponse(
572 572 ui, url, requrl, qs, resp, compressible=False
573 573 )
574 574 except RedirectedRepoError as e:
575 575 req, requrl, qs = makev1commandrequest(
576 576 ui, requestbuilder, caps, capable, e.respurl, b'capabilities', args
577 577 )
578 578 resp = sendrequest(ui, opener, req)
579 579 respurl, ct, resp = parsev1commandresponse(
580 580 ui, url, requrl, qs, resp, compressible=False
581 581 )
582 582
583 583 try:
584 584 rawdata = resp.read()
585 585 finally:
586 586 resp.close()
587 587
588 588 if not ct.startswith(b'application/mercurial-'):
589 589 raise error.ProgrammingError(b'unexpected content-type: %s' % ct)
590 590
591 591 info = {b'v1capabilities': set(rawdata.split())}
592 592
593 593 return respurl, info
594 594
595 595
596 596 def makepeer(ui, path, opener=None, requestbuilder=urlreq.request):
597 597 """Construct an appropriate HTTP peer instance.
598 598
599 599 ``opener`` is an ``url.opener`` that should be used to establish
600 600 connections, perform HTTP requests.
601 601
602 602 ``requestbuilder`` is the type used for constructing HTTP requests.
603 603 It exists as an argument so extensions can override the default.
604 604 """
605 605 u = urlutil.url(path)
606 606 if u.query or u.fragment:
607 607 raise error.Abort(
608 608 _(b'unsupported URL component: "%s"') % (u.query or u.fragment)
609 609 )
610 610
611 611 # urllib cannot handle URLs with embedded user or passwd.
612 612 url, authinfo = u.authinfo()
613 613 ui.debug(b'using %s\n' % url)
614 614
615 615 opener = opener or urlmod.opener(ui, authinfo)
616 616
617 617 respurl, info = performhandshake(ui, url, opener, requestbuilder)
618 618
619 619 return httppeer(
620 620 ui, path, respurl, opener, requestbuilder, info[b'v1capabilities']
621 621 )
622 622
623 623
624 624 def make_peer(ui, path, create, intents=None, createopts=None):
625 625 if create:
626 626 raise error.Abort(_(b'cannot create new http repository'))
627 627 try:
628 628 if path.startswith(b'https:') and not urlmod.has_https:
629 629 raise error.Abort(
630 630 _(b'Python support for SSL and HTTPS is not installed')
631 631 )
632 632
633 633 inst = makepeer(ui, path)
634 634
635 635 return inst
636 636 except error.RepoError as httpexception:
637 637 try:
638 638 r = statichttprepo.make_peer(ui, b"static-" + path, create)
639 639 ui.note(_(b'(falling back to static-http)\n'))
640 640 return r
641 641 except error.RepoError:
642 642 raise httpexception # use the original http RepoError instead
@@ -1,2053 +1,2059 b''
1 1 # repository.py - Interfaces and base classes for repositories and peers.
2 2 # coding: utf-8
3 3 #
4 4 # Copyright 2017 Gregory Szorc <gregory.szorc@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 from ..i18n import _
11 11 from .. import error
12 12 from . import util as interfaceutil
13 13
14 14 # Local repository feature string.
15 15
16 16 # Revlogs are being used for file storage.
17 17 REPO_FEATURE_REVLOG_FILE_STORAGE = b'revlogfilestorage'
18 18 # The storage part of the repository is shared from an external source.
19 19 REPO_FEATURE_SHARED_STORAGE = b'sharedstore'
20 20 # LFS supported for backing file storage.
21 21 REPO_FEATURE_LFS = b'lfs'
22 22 # Repository supports being stream cloned.
23 23 REPO_FEATURE_STREAM_CLONE = b'streamclone'
24 24 # Repository supports (at least) some sidedata to be stored
25 25 REPO_FEATURE_SIDE_DATA = b'side-data'
26 26 # Files storage may lack data for all ancestors.
27 27 REPO_FEATURE_SHALLOW_FILE_STORAGE = b'shallowfilestorage'
28 28
29 29 REVISION_FLAG_CENSORED = 1 << 15
30 30 REVISION_FLAG_ELLIPSIS = 1 << 14
31 31 REVISION_FLAG_EXTSTORED = 1 << 13
32 32 REVISION_FLAG_HASCOPIESINFO = 1 << 12
33 33
34 34 REVISION_FLAGS_KNOWN = (
35 35 REVISION_FLAG_CENSORED
36 36 | REVISION_FLAG_ELLIPSIS
37 37 | REVISION_FLAG_EXTSTORED
38 38 | REVISION_FLAG_HASCOPIESINFO
39 39 )
40 40
41 41 CG_DELTAMODE_STD = b'default'
42 42 CG_DELTAMODE_PREV = b'previous'
43 43 CG_DELTAMODE_FULL = b'fulltext'
44 44 CG_DELTAMODE_P1 = b'p1'
45 45
46 46
47 47 ## Cache related constants:
48 48 #
49 49 # Used to control which cache should be warmed in a repo.updatecaches(…) call.
50 50
51 51 # Warm branchmaps of all known repoview's filter-level
52 52 CACHE_BRANCHMAP_ALL = b"branchmap-all"
53 53 # Warm branchmaps of repoview's filter-level used by server
54 54 CACHE_BRANCHMAP_SERVED = b"branchmap-served"
55 55 # Warm internal changelog cache (eg: persistent nodemap)
56 56 CACHE_CHANGELOG_CACHE = b"changelog-cache"
57 57 # Warm full manifest cache
58 58 CACHE_FULL_MANIFEST = b"full-manifest"
59 59 # Warm file-node-tags cache
60 60 CACHE_FILE_NODE_TAGS = b"file-node-tags"
61 61 # Warm internal manifestlog cache (eg: persistent nodemap)
62 62 CACHE_MANIFESTLOG_CACHE = b"manifestlog-cache"
63 63 # Warn rev branch cache
64 64 CACHE_REV_BRANCH = b"rev-branch-cache"
65 65 # Warm tags' cache for default repoview'
66 66 CACHE_TAGS_DEFAULT = b"tags-default"
67 67 # Warm tags' cache for repoview's filter-level used by server
68 68 CACHE_TAGS_SERVED = b"tags-served"
69 69
70 70 # the cache to warm by default after a simple transaction
71 71 # (this is a mutable set to let extension update it)
72 72 CACHES_DEFAULT = {
73 73 CACHE_BRANCHMAP_SERVED,
74 74 }
75 75
76 76 # the caches to warm when warming all of them
77 77 # (this is a mutable set to let extension update it)
78 78 CACHES_ALL = {
79 79 CACHE_BRANCHMAP_SERVED,
80 80 CACHE_BRANCHMAP_ALL,
81 81 CACHE_CHANGELOG_CACHE,
82 82 CACHE_FILE_NODE_TAGS,
83 83 CACHE_FULL_MANIFEST,
84 84 CACHE_MANIFESTLOG_CACHE,
85 85 CACHE_TAGS_DEFAULT,
86 86 CACHE_TAGS_SERVED,
87 87 }
88 88
89 89 # the cache to warm by default on simple call
90 90 # (this is a mutable set to let extension update it)
91 91 CACHES_POST_CLONE = CACHES_ALL.copy()
92 92 CACHES_POST_CLONE.discard(CACHE_FILE_NODE_TAGS)
93 93
94 94
95 95 class ipeerconnection(interfaceutil.Interface):
96 96 """Represents a "connection" to a repository.
97 97
98 98 This is the base interface for representing a connection to a repository.
99 99 It holds basic properties and methods applicable to all peer types.
100 100
101 101 This is not a complete interface definition and should not be used
102 102 outside of this module.
103 103 """
104 104
105 105 ui = interfaceutil.Attribute("""ui.ui instance""")
106 106
107 107 def url():
108 108 """Returns a URL string representing this peer.
109 109
110 110 Currently, implementations expose the raw URL used to construct the
111 111 instance. It may contain credentials as part of the URL. The
112 112 expectations of the value aren't well-defined and this could lead to
113 113 data leakage.
114 114
115 115 TODO audit/clean consumers and more clearly define the contents of this
116 116 value.
117 117 """
118 118
119 119 def local():
120 120 """Returns a local repository instance.
121 121
122 122 If the peer represents a local repository, returns an object that
123 123 can be used to interface with it. Otherwise returns ``None``.
124 124 """
125 125
126 126 def canpush():
127 127 """Returns a boolean indicating if this peer can be pushed to."""
128 128
129 129 def close():
130 130 """Close the connection to this peer.
131 131
132 132 This is called when the peer will no longer be used. Resources
133 133 associated with the peer should be cleaned up.
134 134 """
135 135
136 136
137 137 class ipeercapabilities(interfaceutil.Interface):
138 138 """Peer sub-interface related to capabilities."""
139 139
140 140 def capable(name):
141 141 """Determine support for a named capability.
142 142
143 143 Returns ``False`` if capability not supported.
144 144
145 145 Returns ``True`` if boolean capability is supported. Returns a string
146 146 if capability support is non-boolean.
147 147
148 148 Capability strings may or may not map to wire protocol capabilities.
149 149 """
150 150
151 151 def requirecap(name, purpose):
152 152 """Require a capability to be present.
153 153
154 154 Raises a ``CapabilityError`` if the capability isn't present.
155 155 """
156 156
157 157
158 158 class ipeercommands(interfaceutil.Interface):
159 159 """Client-side interface for communicating over the wire protocol.
160 160
161 161 This interface is used as a gateway to the Mercurial wire protocol.
162 162 methods commonly call wire protocol commands of the same name.
163 163 """
164 164
165 165 def branchmap():
166 166 """Obtain heads in named branches.
167 167
168 168 Returns a dict mapping branch name to an iterable of nodes that are
169 169 heads on that branch.
170 170 """
171 171
172 172 def capabilities():
173 173 """Obtain capabilities of the peer.
174 174
175 175 Returns a set of string capabilities.
176 176 """
177 177
178 178 def clonebundles():
179 179 """Obtains the clone bundles manifest for the repo.
180 180
181 181 Returns the manifest as unparsed bytes.
182 182 """
183 183
184 184 def debugwireargs(one, two, three=None, four=None, five=None):
185 185 """Used to facilitate debugging of arguments passed over the wire."""
186 186
187 187 def getbundle(source, **kwargs):
188 188 """Obtain remote repository data as a bundle.
189 189
190 190 This command is how the bulk of repository data is transferred from
191 191 the peer to the local repository
192 192
193 193 Returns a generator of bundle data.
194 194 """
195 195
196 196 def heads():
197 197 """Determine all known head revisions in the peer.
198 198
199 199 Returns an iterable of binary nodes.
200 200 """
201 201
202 202 def known(nodes):
203 203 """Determine whether multiple nodes are known.
204 204
205 205 Accepts an iterable of nodes whose presence to check for.
206 206
207 207 Returns an iterable of booleans indicating of the corresponding node
208 208 at that index is known to the peer.
209 209 """
210 210
211 211 def listkeys(namespace):
212 212 """Obtain all keys in a pushkey namespace.
213 213
214 214 Returns an iterable of key names.
215 215 """
216 216
217 217 def lookup(key):
218 218 """Resolve a value to a known revision.
219 219
220 220 Returns a binary node of the resolved revision on success.
221 221 """
222 222
223 223 def pushkey(namespace, key, old, new):
224 224 """Set a value using the ``pushkey`` protocol.
225 225
226 226 Arguments correspond to the pushkey namespace and key to operate on and
227 227 the old and new values for that key.
228 228
229 229 Returns a string with the peer result. The value inside varies by the
230 230 namespace.
231 231 """
232 232
233 233 def stream_out():
234 234 """Obtain streaming clone data.
235 235
236 236 Successful result should be a generator of data chunks.
237 237 """
238 238
239 239 def unbundle(bundle, heads, url):
240 240 """Transfer repository data to the peer.
241 241
242 242 This is how the bulk of data during a push is transferred.
243 243
244 244 Returns the integer number of heads added to the peer.
245 245 """
246 246
247 247
248 248 class ipeerlegacycommands(interfaceutil.Interface):
249 249 """Interface for implementing support for legacy wire protocol commands.
250 250
251 251 Wire protocol commands transition to legacy status when they are no longer
252 252 used by modern clients. To facilitate identifying which commands are
253 253 legacy, the interfaces are split.
254 254 """
255 255
256 256 def between(pairs):
257 257 """Obtain nodes between pairs of nodes.
258 258
259 259 ``pairs`` is an iterable of node pairs.
260 260
261 261 Returns an iterable of iterables of nodes corresponding to each
262 262 requested pair.
263 263 """
264 264
265 265 def branches(nodes):
266 266 """Obtain ancestor changesets of specific nodes back to a branch point.
267 267
268 268 For each requested node, the peer finds the first ancestor node that is
269 269 a DAG root or is a merge.
270 270
271 271 Returns an iterable of iterables with the resolved values for each node.
272 272 """
273 273
274 274 def changegroup(nodes, source):
275 275 """Obtain a changegroup with data for descendants of specified nodes."""
276 276
277 277 def changegroupsubset(bases, heads, source):
278 278 pass
279 279
280 280
281 281 class ipeercommandexecutor(interfaceutil.Interface):
282 282 """Represents a mechanism to execute remote commands.
283 283
284 284 This is the primary interface for requesting that wire protocol commands
285 285 be executed. Instances of this interface are active in a context manager
286 286 and have a well-defined lifetime. When the context manager exits, all
287 287 outstanding requests are waited on.
288 288 """
289 289
290 290 def callcommand(name, args):
291 291 """Request that a named command be executed.
292 292
293 293 Receives the command name and a dictionary of command arguments.
294 294
295 295 Returns a ``concurrent.futures.Future`` that will resolve to the
296 296 result of that command request. That exact value is left up to
297 297 the implementation and possibly varies by command.
298 298
299 299 Not all commands can coexist with other commands in an executor
300 300 instance: it depends on the underlying wire protocol transport being
301 301 used and the command itself.
302 302
303 303 Implementations MAY call ``sendcommands()`` automatically if the
304 304 requested command can not coexist with other commands in this executor.
305 305
306 306 Implementations MAY call ``sendcommands()`` automatically when the
307 307 future's ``result()`` is called. So, consumers using multiple
308 308 commands with an executor MUST ensure that ``result()`` is not called
309 309 until all command requests have been issued.
310 310 """
311 311
312 312 def sendcommands():
313 313 """Trigger submission of queued command requests.
314 314
315 315 Not all transports submit commands as soon as they are requested to
316 316 run. When called, this method forces queued command requests to be
317 317 issued. It will no-op if all commands have already been sent.
318 318
319 319 When called, no more new commands may be issued with this executor.
320 320 """
321 321
322 322 def close():
323 323 """Signal that this command request is finished.
324 324
325 325 When called, no more new commands may be issued. All outstanding
326 326 commands that have previously been issued are waited on before
327 327 returning. This not only includes waiting for the futures to resolve,
328 328 but also waiting for all response data to arrive. In other words,
329 329 calling this waits for all on-wire state for issued command requests
330 330 to finish.
331 331
332 332 When used as a context manager, this method is called when exiting the
333 333 context manager.
334 334
335 335 This method may call ``sendcommands()`` if there are buffered commands.
336 336 """
337 337
338 338
339 339 class ipeerrequests(interfaceutil.Interface):
340 340 """Interface for executing commands on a peer."""
341 341
342 342 limitedarguments = interfaceutil.Attribute(
343 343 """True if the peer cannot receive large argument value for commands."""
344 344 )
345 345
346 346 def commandexecutor():
347 347 """A context manager that resolves to an ipeercommandexecutor.
348 348
349 349 The object this resolves to can be used to issue command requests
350 350 to the peer.
351 351
352 352 Callers should call its ``callcommand`` method to issue command
353 353 requests.
354 354
355 355 A new executor should be obtained for each distinct set of commands
356 356 (possibly just a single command) that the consumer wants to execute
357 357 as part of a single operation or round trip. This is because some
358 358 peers are half-duplex and/or don't support persistent connections.
359 359 e.g. in the case of HTTP peers, commands sent to an executor represent
360 360 a single HTTP request. While some peers may support multiple command
361 361 sends over the wire per executor, consumers need to code to the least
362 362 capable peer. So it should be assumed that command executors buffer
363 363 called commands until they are told to send them and that each
364 364 command executor could result in a new connection or wire-level request
365 365 being issued.
366 366 """
367 367
368 368
369 369 class ipeerbase(ipeerconnection, ipeercapabilities, ipeerrequests):
370 370 """Unified interface for peer repositories.
371 371
372 372 All peer instances must conform to this interface.
373 373 """
374 374
375 375
376 376 class ipeerv2(ipeerconnection, ipeercapabilities, ipeerrequests):
377 377 """Unified peer interface for wire protocol version 2 peers."""
378 378
379 379 apidescriptor = interfaceutil.Attribute(
380 380 """Data structure holding description of server API."""
381 381 )
382 382
383 383
384 384 @interfaceutil.implementer(ipeerbase)
385 385 class peer:
386 386 """Base class for peer repositories."""
387 387
388 388 limitedarguments = False
389 389
390 def __init__(
391 self,
392 ui,
393 ):
394 self.ui = ui
395
390 396 def capable(self, name):
391 397 caps = self.capabilities()
392 398 if name in caps:
393 399 return True
394 400
395 401 name = b'%s=' % name
396 402 for cap in caps:
397 403 if cap.startswith(name):
398 404 return cap[len(name) :]
399 405
400 406 return False
401 407
402 408 def requirecap(self, name, purpose):
403 409 if self.capable(name):
404 410 return
405 411
406 412 raise error.CapabilityError(
407 413 _(
408 414 b'cannot %s; remote repository does not support the '
409 415 b'\'%s\' capability'
410 416 )
411 417 % (purpose, name)
412 418 )
413 419
414 420
415 421 class iverifyproblem(interfaceutil.Interface):
416 422 """Represents a problem with the integrity of the repository.
417 423
418 424 Instances of this interface are emitted to describe an integrity issue
419 425 with a repository (e.g. corrupt storage, missing data, etc).
420 426
421 427 Instances are essentially messages associated with severity.
422 428 """
423 429
424 430 warning = interfaceutil.Attribute(
425 431 """Message indicating a non-fatal problem."""
426 432 )
427 433
428 434 error = interfaceutil.Attribute("""Message indicating a fatal problem.""")
429 435
430 436 node = interfaceutil.Attribute(
431 437 """Revision encountering the problem.
432 438
433 439 ``None`` means the problem doesn't apply to a single revision.
434 440 """
435 441 )
436 442
437 443
438 444 class irevisiondelta(interfaceutil.Interface):
439 445 """Represents a delta between one revision and another.
440 446
441 447 Instances convey enough information to allow a revision to be exchanged
442 448 with another repository.
443 449
444 450 Instances represent the fulltext revision data or a delta against
445 451 another revision. Therefore the ``revision`` and ``delta`` attributes
446 452 are mutually exclusive.
447 453
448 454 Typically used for changegroup generation.
449 455 """
450 456
451 457 node = interfaceutil.Attribute("""20 byte node of this revision.""")
452 458
453 459 p1node = interfaceutil.Attribute(
454 460 """20 byte node of 1st parent of this revision."""
455 461 )
456 462
457 463 p2node = interfaceutil.Attribute(
458 464 """20 byte node of 2nd parent of this revision."""
459 465 )
460 466
461 467 linknode = interfaceutil.Attribute(
462 468 """20 byte node of the changelog revision this node is linked to."""
463 469 )
464 470
465 471 flags = interfaceutil.Attribute(
466 472 """2 bytes of integer flags that apply to this revision.
467 473
468 474 This is a bitwise composition of the ``REVISION_FLAG_*`` constants.
469 475 """
470 476 )
471 477
472 478 basenode = interfaceutil.Attribute(
473 479 """20 byte node of the revision this data is a delta against.
474 480
475 481 ``nullid`` indicates that the revision is a full revision and not
476 482 a delta.
477 483 """
478 484 )
479 485
480 486 baserevisionsize = interfaceutil.Attribute(
481 487 """Size of base revision this delta is against.
482 488
483 489 May be ``None`` if ``basenode`` is ``nullid``.
484 490 """
485 491 )
486 492
487 493 revision = interfaceutil.Attribute(
488 494 """Raw fulltext of revision data for this node."""
489 495 )
490 496
491 497 delta = interfaceutil.Attribute(
492 498 """Delta between ``basenode`` and ``node``.
493 499
494 500 Stored in the bdiff delta format.
495 501 """
496 502 )
497 503
498 504 sidedata = interfaceutil.Attribute(
499 505 """Raw sidedata bytes for the given revision."""
500 506 )
501 507
502 508 protocol_flags = interfaceutil.Attribute(
503 509 """Single byte of integer flags that can influence the protocol.
504 510
505 511 This is a bitwise composition of the ``storageutil.CG_FLAG*`` constants.
506 512 """
507 513 )
508 514
509 515
510 516 class ifilerevisionssequence(interfaceutil.Interface):
511 517 """Contains index data for all revisions of a file.
512 518
513 519 Types implementing this behave like lists of tuples. The index
514 520 in the list corresponds to the revision number. The values contain
515 521 index metadata.
516 522
517 523 The *null* revision (revision number -1) is always the last item
518 524 in the index.
519 525 """
520 526
521 527 def __len__():
522 528 """The total number of revisions."""
523 529
524 530 def __getitem__(rev):
525 531 """Returns the object having a specific revision number.
526 532
527 533 Returns an 8-tuple with the following fields:
528 534
529 535 offset+flags
530 536 Contains the offset and flags for the revision. 64-bit unsigned
531 537 integer where first 6 bytes are the offset and the next 2 bytes
532 538 are flags. The offset can be 0 if it is not used by the store.
533 539 compressed size
534 540 Size of the revision data in the store. It can be 0 if it isn't
535 541 needed by the store.
536 542 uncompressed size
537 543 Fulltext size. It can be 0 if it isn't needed by the store.
538 544 base revision
539 545 Revision number of revision the delta for storage is encoded
540 546 against. -1 indicates not encoded against a base revision.
541 547 link revision
542 548 Revision number of changelog revision this entry is related to.
543 549 p1 revision
544 550 Revision number of 1st parent. -1 if no 1st parent.
545 551 p2 revision
546 552 Revision number of 2nd parent. -1 if no 1st parent.
547 553 node
548 554 Binary node value for this revision number.
549 555
550 556 Negative values should index off the end of the sequence. ``-1``
551 557 should return the null revision. ``-2`` should return the most
552 558 recent revision.
553 559 """
554 560
555 561 def __contains__(rev):
556 562 """Whether a revision number exists."""
557 563
558 564 def insert(self, i, entry):
559 565 """Add an item to the index at specific revision."""
560 566
561 567
562 568 class ifileindex(interfaceutil.Interface):
563 569 """Storage interface for index data of a single file.
564 570
565 571 File storage data is divided into index metadata and data storage.
566 572 This interface defines the index portion of the interface.
567 573
568 574 The index logically consists of:
569 575
570 576 * A mapping between revision numbers and nodes.
571 577 * DAG data (storing and querying the relationship between nodes).
572 578 * Metadata to facilitate storage.
573 579 """
574 580
575 581 nullid = interfaceutil.Attribute(
576 582 """node for the null revision for use as delta base."""
577 583 )
578 584
579 585 def __len__():
580 586 """Obtain the number of revisions stored for this file."""
581 587
582 588 def __iter__():
583 589 """Iterate over revision numbers for this file."""
584 590
585 591 def hasnode(node):
586 592 """Returns a bool indicating if a node is known to this store.
587 593
588 594 Implementations must only return True for full, binary node values:
589 595 hex nodes, revision numbers, and partial node matches must be
590 596 rejected.
591 597
592 598 The null node is never present.
593 599 """
594 600
595 601 def revs(start=0, stop=None):
596 602 """Iterate over revision numbers for this file, with control."""
597 603
598 604 def parents(node):
599 605 """Returns a 2-tuple of parent nodes for a revision.
600 606
601 607 Values will be ``nullid`` if the parent is empty.
602 608 """
603 609
604 610 def parentrevs(rev):
605 611 """Like parents() but operates on revision numbers."""
606 612
607 613 def rev(node):
608 614 """Obtain the revision number given a node.
609 615
610 616 Raises ``error.LookupError`` if the node is not known.
611 617 """
612 618
613 619 def node(rev):
614 620 """Obtain the node value given a revision number.
615 621
616 622 Raises ``IndexError`` if the node is not known.
617 623 """
618 624
619 625 def lookup(node):
620 626 """Attempt to resolve a value to a node.
621 627
622 628 Value can be a binary node, hex node, revision number, or a string
623 629 that can be converted to an integer.
624 630
625 631 Raises ``error.LookupError`` if a node could not be resolved.
626 632 """
627 633
628 634 def linkrev(rev):
629 635 """Obtain the changeset revision number a revision is linked to."""
630 636
631 637 def iscensored(rev):
632 638 """Return whether a revision's content has been censored."""
633 639
634 640 def commonancestorsheads(node1, node2):
635 641 """Obtain an iterable of nodes containing heads of common ancestors.
636 642
637 643 See ``ancestor.commonancestorsheads()``.
638 644 """
639 645
640 646 def descendants(revs):
641 647 """Obtain descendant revision numbers for a set of revision numbers.
642 648
643 649 If ``nullrev`` is in the set, this is equivalent to ``revs()``.
644 650 """
645 651
646 652 def heads(start=None, stop=None):
647 653 """Obtain a list of nodes that are DAG heads, with control.
648 654
649 655 The set of revisions examined can be limited by specifying
650 656 ``start`` and ``stop``. ``start`` is a node. ``stop`` is an
651 657 iterable of nodes. DAG traversal starts at earlier revision
652 658 ``start`` and iterates forward until any node in ``stop`` is
653 659 encountered.
654 660 """
655 661
656 662 def children(node):
657 663 """Obtain nodes that are children of a node.
658 664
659 665 Returns a list of nodes.
660 666 """
661 667
662 668
663 669 class ifiledata(interfaceutil.Interface):
664 670 """Storage interface for data storage of a specific file.
665 671
666 672 This complements ``ifileindex`` and provides an interface for accessing
667 673 data for a tracked file.
668 674 """
669 675
670 676 def size(rev):
671 677 """Obtain the fulltext size of file data.
672 678
673 679 Any metadata is excluded from size measurements.
674 680 """
675 681
676 682 def revision(node, raw=False):
677 683 """Obtain fulltext data for a node.
678 684
679 685 By default, any storage transformations are applied before the data
680 686 is returned. If ``raw`` is True, non-raw storage transformations
681 687 are not applied.
682 688
683 689 The fulltext data may contain a header containing metadata. Most
684 690 consumers should use ``read()`` to obtain the actual file data.
685 691 """
686 692
687 693 def rawdata(node):
688 694 """Obtain raw data for a node."""
689 695
690 696 def read(node):
691 697 """Resolve file fulltext data.
692 698
693 699 This is similar to ``revision()`` except any metadata in the data
694 700 headers is stripped.
695 701 """
696 702
697 703 def renamed(node):
698 704 """Obtain copy metadata for a node.
699 705
700 706 Returns ``False`` if no copy metadata is stored or a 2-tuple of
701 707 (path, node) from which this revision was copied.
702 708 """
703 709
704 710 def cmp(node, fulltext):
705 711 """Compare fulltext to another revision.
706 712
707 713 Returns True if the fulltext is different from what is stored.
708 714
709 715 This takes copy metadata into account.
710 716
711 717 TODO better document the copy metadata and censoring logic.
712 718 """
713 719
714 720 def emitrevisions(
715 721 nodes,
716 722 nodesorder=None,
717 723 revisiondata=False,
718 724 assumehaveparentrevisions=False,
719 725 deltamode=CG_DELTAMODE_STD,
720 726 ):
721 727 """Produce ``irevisiondelta`` for revisions.
722 728
723 729 Given an iterable of nodes, emits objects conforming to the
724 730 ``irevisiondelta`` interface that describe revisions in storage.
725 731
726 732 This method is a generator.
727 733
728 734 The input nodes may be unordered. Implementations must ensure that a
729 735 node's parents are emitted before the node itself. Transitively, this
730 736 means that a node may only be emitted once all its ancestors in
731 737 ``nodes`` have also been emitted.
732 738
733 739 By default, emits "index" data (the ``node``, ``p1node``, and
734 740 ``p2node`` attributes). If ``revisiondata`` is set, revision data
735 741 will also be present on the emitted objects.
736 742
737 743 With default argument values, implementations can choose to emit
738 744 either fulltext revision data or a delta. When emitting deltas,
739 745 implementations must consider whether the delta's base revision
740 746 fulltext is available to the receiver.
741 747
742 748 The base revision fulltext is guaranteed to be available if any of
743 749 the following are met:
744 750
745 751 * Its fulltext revision was emitted by this method call.
746 752 * A delta for that revision was emitted by this method call.
747 753 * ``assumehaveparentrevisions`` is True and the base revision is a
748 754 parent of the node.
749 755
750 756 ``nodesorder`` can be used to control the order that revisions are
751 757 emitted. By default, revisions can be reordered as long as they are
752 758 in DAG topological order (see above). If the value is ``nodes``,
753 759 the iteration order from ``nodes`` should be used. If the value is
754 760 ``storage``, then the native order from the backing storage layer
755 761 is used. (Not all storage layers will have strong ordering and behavior
756 762 of this mode is storage-dependent.) ``nodes`` ordering can force
757 763 revisions to be emitted before their ancestors, so consumers should
758 764 use it with care.
759 765
760 766 The ``linknode`` attribute on the returned ``irevisiondelta`` may not
761 767 be set and it is the caller's responsibility to resolve it, if needed.
762 768
763 769 If ``deltamode`` is CG_DELTAMODE_PREV and revision data is requested,
764 770 all revision data should be emitted as deltas against the revision
765 771 emitted just prior. The initial revision should be a delta against its
766 772 1st parent.
767 773 """
768 774
769 775
770 776 class ifilemutation(interfaceutil.Interface):
771 777 """Storage interface for mutation events of a tracked file."""
772 778
773 779 def add(filedata, meta, transaction, linkrev, p1, p2):
774 780 """Add a new revision to the store.
775 781
776 782 Takes file data, dictionary of metadata, a transaction, linkrev,
777 783 and parent nodes.
778 784
779 785 Returns the node that was added.
780 786
781 787 May no-op if a revision matching the supplied data is already stored.
782 788 """
783 789
784 790 def addrevision(
785 791 revisiondata,
786 792 transaction,
787 793 linkrev,
788 794 p1,
789 795 p2,
790 796 node=None,
791 797 flags=0,
792 798 cachedelta=None,
793 799 ):
794 800 """Add a new revision to the store and return its number.
795 801
796 802 This is similar to ``add()`` except it operates at a lower level.
797 803
798 804 The data passed in already contains a metadata header, if any.
799 805
800 806 ``node`` and ``flags`` can be used to define the expected node and
801 807 the flags to use with storage. ``flags`` is a bitwise value composed
802 808 of the various ``REVISION_FLAG_*`` constants.
803 809
804 810 ``add()`` is usually called when adding files from e.g. the working
805 811 directory. ``addrevision()`` is often called by ``add()`` and for
806 812 scenarios where revision data has already been computed, such as when
807 813 applying raw data from a peer repo.
808 814 """
809 815
810 816 def addgroup(
811 817 deltas,
812 818 linkmapper,
813 819 transaction,
814 820 addrevisioncb=None,
815 821 duplicaterevisioncb=None,
816 822 maybemissingparents=False,
817 823 ):
818 824 """Process a series of deltas for storage.
819 825
820 826 ``deltas`` is an iterable of 7-tuples of
821 827 (node, p1, p2, linknode, deltabase, delta, flags) defining revisions
822 828 to add.
823 829
824 830 The ``delta`` field contains ``mpatch`` data to apply to a base
825 831 revision, identified by ``deltabase``. The base node can be
826 832 ``nullid``, in which case the header from the delta can be ignored
827 833 and the delta used as the fulltext.
828 834
829 835 ``alwayscache`` instructs the lower layers to cache the content of the
830 836 newly added revision, even if it needs to be explicitly computed.
831 837 This used to be the default when ``addrevisioncb`` was provided up to
832 838 Mercurial 5.8.
833 839
834 840 ``addrevisioncb`` should be called for each new rev as it is committed.
835 841 ``duplicaterevisioncb`` should be called for all revs with a
836 842 pre-existing node.
837 843
838 844 ``maybemissingparents`` is a bool indicating whether the incoming
839 845 data may reference parents/ancestor revisions that aren't present.
840 846 This flag is set when receiving data into a "shallow" store that
841 847 doesn't hold all history.
842 848
843 849 Returns a list of nodes that were processed. A node will be in the list
844 850 even if it existed in the store previously.
845 851 """
846 852
847 853 def censorrevision(tr, node, tombstone=b''):
848 854 """Remove the content of a single revision.
849 855
850 856 The specified ``node`` will have its content purged from storage.
851 857 Future attempts to access the revision data for this node will
852 858 result in failure.
853 859
854 860 A ``tombstone`` message can optionally be stored. This message may be
855 861 displayed to users when they attempt to access the missing revision
856 862 data.
857 863
858 864 Storage backends may have stored deltas against the previous content
859 865 in this revision. As part of censoring a revision, these storage
860 866 backends are expected to rewrite any internally stored deltas such
861 867 that they no longer reference the deleted content.
862 868 """
863 869
864 870 def getstrippoint(minlink):
865 871 """Find the minimum revision that must be stripped to strip a linkrev.
866 872
867 873 Returns a 2-tuple containing the minimum revision number and a set
868 874 of all revisions numbers that would be broken by this strip.
869 875
870 876 TODO this is highly revlog centric and should be abstracted into
871 877 a higher-level deletion API. ``repair.strip()`` relies on this.
872 878 """
873 879
874 880 def strip(minlink, transaction):
875 881 """Remove storage of items starting at a linkrev.
876 882
877 883 This uses ``getstrippoint()`` to determine the first node to remove.
878 884 Then it effectively truncates storage for all revisions after that.
879 885
880 886 TODO this is highly revlog centric and should be abstracted into a
881 887 higher-level deletion API.
882 888 """
883 889
884 890
885 891 class ifilestorage(ifileindex, ifiledata, ifilemutation):
886 892 """Complete storage interface for a single tracked file."""
887 893
888 894 def files():
889 895 """Obtain paths that are backing storage for this file.
890 896
891 897 TODO this is used heavily by verify code and there should probably
892 898 be a better API for that.
893 899 """
894 900
895 901 def storageinfo(
896 902 exclusivefiles=False,
897 903 sharedfiles=False,
898 904 revisionscount=False,
899 905 trackedsize=False,
900 906 storedsize=False,
901 907 ):
902 908 """Obtain information about storage for this file's data.
903 909
904 910 Returns a dict describing storage for this tracked path. The keys
905 911 in the dict map to arguments of the same. The arguments are bools
906 912 indicating whether to calculate and obtain that data.
907 913
908 914 exclusivefiles
909 915 Iterable of (vfs, path) describing files that are exclusively
910 916 used to back storage for this tracked path.
911 917
912 918 sharedfiles
913 919 Iterable of (vfs, path) describing files that are used to back
914 920 storage for this tracked path. Those files may also provide storage
915 921 for other stored entities.
916 922
917 923 revisionscount
918 924 Number of revisions available for retrieval.
919 925
920 926 trackedsize
921 927 Total size in bytes of all tracked revisions. This is a sum of the
922 928 length of the fulltext of all revisions.
923 929
924 930 storedsize
925 931 Total size in bytes used to store data for all tracked revisions.
926 932 This is commonly less than ``trackedsize`` due to internal usage
927 933 of deltas rather than fulltext revisions.
928 934
929 935 Not all storage backends may support all queries are have a reasonable
930 936 value to use. In that case, the value should be set to ``None`` and
931 937 callers are expected to handle this special value.
932 938 """
933 939
934 940 def verifyintegrity(state):
935 941 """Verifies the integrity of file storage.
936 942
937 943 ``state`` is a dict holding state of the verifier process. It can be
938 944 used to communicate data between invocations of multiple storage
939 945 primitives.
940 946
941 947 If individual revisions cannot have their revision content resolved,
942 948 the method is expected to set the ``skipread`` key to a set of nodes
943 949 that encountered problems. If set, the method can also add the node(s)
944 950 to ``safe_renamed`` in order to indicate nodes that may perform the
945 951 rename checks with currently accessible data.
946 952
947 953 The method yields objects conforming to the ``iverifyproblem``
948 954 interface.
949 955 """
950 956
951 957
952 958 class idirs(interfaceutil.Interface):
953 959 """Interface representing a collection of directories from paths.
954 960
955 961 This interface is essentially a derived data structure representing
956 962 directories from a collection of paths.
957 963 """
958 964
959 965 def addpath(path):
960 966 """Add a path to the collection.
961 967
962 968 All directories in the path will be added to the collection.
963 969 """
964 970
965 971 def delpath(path):
966 972 """Remove a path from the collection.
967 973
968 974 If the removal was the last path in a particular directory, the
969 975 directory is removed from the collection.
970 976 """
971 977
972 978 def __iter__():
973 979 """Iterate over the directories in this collection of paths."""
974 980
975 981 def __contains__(path):
976 982 """Whether a specific directory is in this collection."""
977 983
978 984
979 985 class imanifestdict(interfaceutil.Interface):
980 986 """Interface representing a manifest data structure.
981 987
982 988 A manifest is effectively a dict mapping paths to entries. Each entry
983 989 consists of a binary node and extra flags affecting that entry.
984 990 """
985 991
986 992 def __getitem__(path):
987 993 """Returns the binary node value for a path in the manifest.
988 994
989 995 Raises ``KeyError`` if the path does not exist in the manifest.
990 996
991 997 Equivalent to ``self.find(path)[0]``.
992 998 """
993 999
994 1000 def find(path):
995 1001 """Returns the entry for a path in the manifest.
996 1002
997 1003 Returns a 2-tuple of (node, flags).
998 1004
999 1005 Raises ``KeyError`` if the path does not exist in the manifest.
1000 1006 """
1001 1007
1002 1008 def __len__():
1003 1009 """Return the number of entries in the manifest."""
1004 1010
1005 1011 def __nonzero__():
1006 1012 """Returns True if the manifest has entries, False otherwise."""
1007 1013
1008 1014 __bool__ = __nonzero__
1009 1015
1010 1016 def __setitem__(path, node):
1011 1017 """Define the node value for a path in the manifest.
1012 1018
1013 1019 If the path is already in the manifest, its flags will be copied to
1014 1020 the new entry.
1015 1021 """
1016 1022
1017 1023 def __contains__(path):
1018 1024 """Whether a path exists in the manifest."""
1019 1025
1020 1026 def __delitem__(path):
1021 1027 """Remove a path from the manifest.
1022 1028
1023 1029 Raises ``KeyError`` if the path is not in the manifest.
1024 1030 """
1025 1031
1026 1032 def __iter__():
1027 1033 """Iterate over paths in the manifest."""
1028 1034
1029 1035 def iterkeys():
1030 1036 """Iterate over paths in the manifest."""
1031 1037
1032 1038 def keys():
1033 1039 """Obtain a list of paths in the manifest."""
1034 1040
1035 1041 def filesnotin(other, match=None):
1036 1042 """Obtain the set of paths in this manifest but not in another.
1037 1043
1038 1044 ``match`` is an optional matcher function to be applied to both
1039 1045 manifests.
1040 1046
1041 1047 Returns a set of paths.
1042 1048 """
1043 1049
1044 1050 def dirs():
1045 1051 """Returns an object implementing the ``idirs`` interface."""
1046 1052
1047 1053 def hasdir(dir):
1048 1054 """Returns a bool indicating if a directory is in this manifest."""
1049 1055
1050 1056 def walk(match):
1051 1057 """Generator of paths in manifest satisfying a matcher.
1052 1058
1053 1059 If the matcher has explicit files listed and they don't exist in
1054 1060 the manifest, ``match.bad()`` is called for each missing file.
1055 1061 """
1056 1062
1057 1063 def diff(other, match=None, clean=False):
1058 1064 """Find differences between this manifest and another.
1059 1065
1060 1066 This manifest is compared to ``other``.
1061 1067
1062 1068 If ``match`` is provided, the two manifests are filtered against this
1063 1069 matcher and only entries satisfying the matcher are compared.
1064 1070
1065 1071 If ``clean`` is True, unchanged files are included in the returned
1066 1072 object.
1067 1073
1068 1074 Returns a dict with paths as keys and values of 2-tuples of 2-tuples of
1069 1075 the form ``((node1, flag1), (node2, flag2))`` where ``(node1, flag1)``
1070 1076 represents the node and flags for this manifest and ``(node2, flag2)``
1071 1077 are the same for the other manifest.
1072 1078 """
1073 1079
1074 1080 def setflag(path, flag):
1075 1081 """Set the flag value for a given path.
1076 1082
1077 1083 Raises ``KeyError`` if the path is not already in the manifest.
1078 1084 """
1079 1085
1080 1086 def get(path, default=None):
1081 1087 """Obtain the node value for a path or a default value if missing."""
1082 1088
1083 1089 def flags(path):
1084 1090 """Return the flags value for a path (default: empty bytestring)."""
1085 1091
1086 1092 def copy():
1087 1093 """Return a copy of this manifest."""
1088 1094
1089 1095 def items():
1090 1096 """Returns an iterable of (path, node) for items in this manifest."""
1091 1097
1092 1098 def iteritems():
1093 1099 """Identical to items()."""
1094 1100
1095 1101 def iterentries():
1096 1102 """Returns an iterable of (path, node, flags) for this manifest.
1097 1103
1098 1104 Similar to ``iteritems()`` except items are a 3-tuple and include
1099 1105 flags.
1100 1106 """
1101 1107
1102 1108 def text():
1103 1109 """Obtain the raw data representation for this manifest.
1104 1110
1105 1111 Result is used to create a manifest revision.
1106 1112 """
1107 1113
1108 1114 def fastdelta(base, changes):
1109 1115 """Obtain a delta between this manifest and another given changes.
1110 1116
1111 1117 ``base`` in the raw data representation for another manifest.
1112 1118
1113 1119 ``changes`` is an iterable of ``(path, to_delete)``.
1114 1120
1115 1121 Returns a 2-tuple containing ``bytearray(self.text())`` and the
1116 1122 delta between ``base`` and this manifest.
1117 1123
1118 1124 If this manifest implementation can't support ``fastdelta()``,
1119 1125 raise ``mercurial.manifest.FastdeltaUnavailable``.
1120 1126 """
1121 1127
1122 1128
1123 1129 class imanifestrevisionbase(interfaceutil.Interface):
1124 1130 """Base interface representing a single revision of a manifest.
1125 1131
1126 1132 Should not be used as a primary interface: should always be inherited
1127 1133 as part of a larger interface.
1128 1134 """
1129 1135
1130 1136 def copy():
1131 1137 """Obtain a copy of this manifest instance.
1132 1138
1133 1139 Returns an object conforming to the ``imanifestrevisionwritable``
1134 1140 interface. The instance will be associated with the same
1135 1141 ``imanifestlog`` collection as this instance.
1136 1142 """
1137 1143
1138 1144 def read():
1139 1145 """Obtain the parsed manifest data structure.
1140 1146
1141 1147 The returned object conforms to the ``imanifestdict`` interface.
1142 1148 """
1143 1149
1144 1150
1145 1151 class imanifestrevisionstored(imanifestrevisionbase):
1146 1152 """Interface representing a manifest revision committed to storage."""
1147 1153
1148 1154 def node():
1149 1155 """The binary node for this manifest."""
1150 1156
1151 1157 parents = interfaceutil.Attribute(
1152 1158 """List of binary nodes that are parents for this manifest revision."""
1153 1159 )
1154 1160
1155 1161 def readdelta(shallow=False):
1156 1162 """Obtain the manifest data structure representing changes from parent.
1157 1163
1158 1164 This manifest is compared to its 1st parent. A new manifest representing
1159 1165 those differences is constructed.
1160 1166
1161 1167 The returned object conforms to the ``imanifestdict`` interface.
1162 1168 """
1163 1169
1164 1170 def readfast(shallow=False):
1165 1171 """Calls either ``read()`` or ``readdelta()``.
1166 1172
1167 1173 The faster of the two options is called.
1168 1174 """
1169 1175
1170 1176 def find(key):
1171 1177 """Calls self.read().find(key)``.
1172 1178
1173 1179 Returns a 2-tuple of ``(node, flags)`` or raises ``KeyError``.
1174 1180 """
1175 1181
1176 1182
1177 1183 class imanifestrevisionwritable(imanifestrevisionbase):
1178 1184 """Interface representing a manifest revision that can be committed."""
1179 1185
1180 1186 def write(transaction, linkrev, p1node, p2node, added, removed, match=None):
1181 1187 """Add this revision to storage.
1182 1188
1183 1189 Takes a transaction object, the changeset revision number it will
1184 1190 be associated with, its parent nodes, and lists of added and
1185 1191 removed paths.
1186 1192
1187 1193 If match is provided, storage can choose not to inspect or write out
1188 1194 items that do not match. Storage is still required to be able to provide
1189 1195 the full manifest in the future for any directories written (these
1190 1196 manifests should not be "narrowed on disk").
1191 1197
1192 1198 Returns the binary node of the created revision.
1193 1199 """
1194 1200
1195 1201
1196 1202 class imanifeststorage(interfaceutil.Interface):
1197 1203 """Storage interface for manifest data."""
1198 1204
1199 1205 nodeconstants = interfaceutil.Attribute(
1200 1206 """nodeconstants used by the current repository."""
1201 1207 )
1202 1208
1203 1209 tree = interfaceutil.Attribute(
1204 1210 """The path to the directory this manifest tracks.
1205 1211
1206 1212 The empty bytestring represents the root manifest.
1207 1213 """
1208 1214 )
1209 1215
1210 1216 index = interfaceutil.Attribute(
1211 1217 """An ``ifilerevisionssequence`` instance."""
1212 1218 )
1213 1219
1214 1220 opener = interfaceutil.Attribute(
1215 1221 """VFS opener to use to access underlying files used for storage.
1216 1222
1217 1223 TODO this is revlog specific and should not be exposed.
1218 1224 """
1219 1225 )
1220 1226
1221 1227 _generaldelta = interfaceutil.Attribute(
1222 1228 """Whether generaldelta storage is being used.
1223 1229
1224 1230 TODO this is revlog specific and should not be exposed.
1225 1231 """
1226 1232 )
1227 1233
1228 1234 fulltextcache = interfaceutil.Attribute(
1229 1235 """Dict with cache of fulltexts.
1230 1236
1231 1237 TODO this doesn't feel appropriate for the storage interface.
1232 1238 """
1233 1239 )
1234 1240
1235 1241 def __len__():
1236 1242 """Obtain the number of revisions stored for this manifest."""
1237 1243
1238 1244 def __iter__():
1239 1245 """Iterate over revision numbers for this manifest."""
1240 1246
1241 1247 def rev(node):
1242 1248 """Obtain the revision number given a binary node.
1243 1249
1244 1250 Raises ``error.LookupError`` if the node is not known.
1245 1251 """
1246 1252
1247 1253 def node(rev):
1248 1254 """Obtain the node value given a revision number.
1249 1255
1250 1256 Raises ``error.LookupError`` if the revision is not known.
1251 1257 """
1252 1258
1253 1259 def lookup(value):
1254 1260 """Attempt to resolve a value to a node.
1255 1261
1256 1262 Value can be a binary node, hex node, revision number, or a bytes
1257 1263 that can be converted to an integer.
1258 1264
1259 1265 Raises ``error.LookupError`` if a ndoe could not be resolved.
1260 1266 """
1261 1267
1262 1268 def parents(node):
1263 1269 """Returns a 2-tuple of parent nodes for a node.
1264 1270
1265 1271 Values will be ``nullid`` if the parent is empty.
1266 1272 """
1267 1273
1268 1274 def parentrevs(rev):
1269 1275 """Like parents() but operates on revision numbers."""
1270 1276
1271 1277 def linkrev(rev):
1272 1278 """Obtain the changeset revision number a revision is linked to."""
1273 1279
1274 1280 def revision(node, _df=None):
1275 1281 """Obtain fulltext data for a node."""
1276 1282
1277 1283 def rawdata(node, _df=None):
1278 1284 """Obtain raw data for a node."""
1279 1285
1280 1286 def revdiff(rev1, rev2):
1281 1287 """Obtain a delta between two revision numbers.
1282 1288
1283 1289 The returned data is the result of ``bdiff.bdiff()`` on the raw
1284 1290 revision data.
1285 1291 """
1286 1292
1287 1293 def cmp(node, fulltext):
1288 1294 """Compare fulltext to another revision.
1289 1295
1290 1296 Returns True if the fulltext is different from what is stored.
1291 1297 """
1292 1298
1293 1299 def emitrevisions(
1294 1300 nodes,
1295 1301 nodesorder=None,
1296 1302 revisiondata=False,
1297 1303 assumehaveparentrevisions=False,
1298 1304 ):
1299 1305 """Produce ``irevisiondelta`` describing revisions.
1300 1306
1301 1307 See the documentation for ``ifiledata`` for more.
1302 1308 """
1303 1309
1304 1310 def addgroup(
1305 1311 deltas,
1306 1312 linkmapper,
1307 1313 transaction,
1308 1314 addrevisioncb=None,
1309 1315 duplicaterevisioncb=None,
1310 1316 ):
1311 1317 """Process a series of deltas for storage.
1312 1318
1313 1319 See the documentation in ``ifilemutation`` for more.
1314 1320 """
1315 1321
1316 1322 def rawsize(rev):
1317 1323 """Obtain the size of tracked data.
1318 1324
1319 1325 Is equivalent to ``len(m.rawdata(node))``.
1320 1326
1321 1327 TODO this method is only used by upgrade code and may be removed.
1322 1328 """
1323 1329
1324 1330 def getstrippoint(minlink):
1325 1331 """Find minimum revision that must be stripped to strip a linkrev.
1326 1332
1327 1333 See the documentation in ``ifilemutation`` for more.
1328 1334 """
1329 1335
1330 1336 def strip(minlink, transaction):
1331 1337 """Remove storage of items starting at a linkrev.
1332 1338
1333 1339 See the documentation in ``ifilemutation`` for more.
1334 1340 """
1335 1341
1336 1342 def checksize():
1337 1343 """Obtain the expected sizes of backing files.
1338 1344
1339 1345 TODO this is used by verify and it should not be part of the interface.
1340 1346 """
1341 1347
1342 1348 def files():
1343 1349 """Obtain paths that are backing storage for this manifest.
1344 1350
1345 1351 TODO this is used by verify and there should probably be a better API
1346 1352 for this functionality.
1347 1353 """
1348 1354
1349 1355 def deltaparent(rev):
1350 1356 """Obtain the revision that a revision is delta'd against.
1351 1357
1352 1358 TODO delta encoding is an implementation detail of storage and should
1353 1359 not be exposed to the storage interface.
1354 1360 """
1355 1361
1356 1362 def clone(tr, dest, **kwargs):
1357 1363 """Clone this instance to another."""
1358 1364
1359 1365 def clearcaches(clear_persisted_data=False):
1360 1366 """Clear any caches associated with this instance."""
1361 1367
1362 1368 def dirlog(d):
1363 1369 """Obtain a manifest storage instance for a tree."""
1364 1370
1365 1371 def add(
1366 1372 m, transaction, link, p1, p2, added, removed, readtree=None, match=None
1367 1373 ):
1368 1374 """Add a revision to storage.
1369 1375
1370 1376 ``m`` is an object conforming to ``imanifestdict``.
1371 1377
1372 1378 ``link`` is the linkrev revision number.
1373 1379
1374 1380 ``p1`` and ``p2`` are the parent revision numbers.
1375 1381
1376 1382 ``added`` and ``removed`` are iterables of added and removed paths,
1377 1383 respectively.
1378 1384
1379 1385 ``readtree`` is a function that can be used to read the child tree(s)
1380 1386 when recursively writing the full tree structure when using
1381 1387 treemanifets.
1382 1388
1383 1389 ``match`` is a matcher that can be used to hint to storage that not all
1384 1390 paths must be inspected; this is an optimization and can be safely
1385 1391 ignored. Note that the storage must still be able to reproduce a full
1386 1392 manifest including files that did not match.
1387 1393 """
1388 1394
1389 1395 def storageinfo(
1390 1396 exclusivefiles=False,
1391 1397 sharedfiles=False,
1392 1398 revisionscount=False,
1393 1399 trackedsize=False,
1394 1400 storedsize=False,
1395 1401 ):
1396 1402 """Obtain information about storage for this manifest's data.
1397 1403
1398 1404 See ``ifilestorage.storageinfo()`` for a description of this method.
1399 1405 This one behaves the same way, except for manifest data.
1400 1406 """
1401 1407
1402 1408
1403 1409 class imanifestlog(interfaceutil.Interface):
1404 1410 """Interface representing a collection of manifest snapshots.
1405 1411
1406 1412 Represents the root manifest in a repository.
1407 1413
1408 1414 Also serves as a means to access nested tree manifests and to cache
1409 1415 tree manifests.
1410 1416 """
1411 1417
1412 1418 nodeconstants = interfaceutil.Attribute(
1413 1419 """nodeconstants used by the current repository."""
1414 1420 )
1415 1421
1416 1422 def __getitem__(node):
1417 1423 """Obtain a manifest instance for a given binary node.
1418 1424
1419 1425 Equivalent to calling ``self.get('', node)``.
1420 1426
1421 1427 The returned object conforms to the ``imanifestrevisionstored``
1422 1428 interface.
1423 1429 """
1424 1430
1425 1431 def get(tree, node, verify=True):
1426 1432 """Retrieve the manifest instance for a given directory and binary node.
1427 1433
1428 1434 ``node`` always refers to the node of the root manifest (which will be
1429 1435 the only manifest if flat manifests are being used).
1430 1436
1431 1437 If ``tree`` is the empty string, the root manifest is returned.
1432 1438 Otherwise the manifest for the specified directory will be returned
1433 1439 (requires tree manifests).
1434 1440
1435 1441 If ``verify`` is True, ``LookupError`` is raised if the node is not
1436 1442 known.
1437 1443
1438 1444 The returned object conforms to the ``imanifestrevisionstored``
1439 1445 interface.
1440 1446 """
1441 1447
1442 1448 def getstorage(tree):
1443 1449 """Retrieve an interface to storage for a particular tree.
1444 1450
1445 1451 If ``tree`` is the empty bytestring, storage for the root manifest will
1446 1452 be returned. Otherwise storage for a tree manifest is returned.
1447 1453
1448 1454 TODO formalize interface for returned object.
1449 1455 """
1450 1456
1451 1457 def clearcaches():
1452 1458 """Clear caches associated with this collection."""
1453 1459
1454 1460 def rev(node):
1455 1461 """Obtain the revision number for a binary node.
1456 1462
1457 1463 Raises ``error.LookupError`` if the node is not known.
1458 1464 """
1459 1465
1460 1466 def update_caches(transaction):
1461 1467 """update whatever cache are relevant for the used storage."""
1462 1468
1463 1469
1464 1470 class ilocalrepositoryfilestorage(interfaceutil.Interface):
1465 1471 """Local repository sub-interface providing access to tracked file storage.
1466 1472
1467 1473 This interface defines how a repository accesses storage for a single
1468 1474 tracked file path.
1469 1475 """
1470 1476
1471 1477 def file(f):
1472 1478 """Obtain a filelog for a tracked path.
1473 1479
1474 1480 The returned type conforms to the ``ifilestorage`` interface.
1475 1481 """
1476 1482
1477 1483
1478 1484 class ilocalrepositorymain(interfaceutil.Interface):
1479 1485 """Main interface for local repositories.
1480 1486
1481 1487 This currently captures the reality of things - not how things should be.
1482 1488 """
1483 1489
1484 1490 nodeconstants = interfaceutil.Attribute(
1485 1491 """Constant nodes matching the hash function used by the repository."""
1486 1492 )
1487 1493 nullid = interfaceutil.Attribute(
1488 1494 """null revision for the hash function used by the repository."""
1489 1495 )
1490 1496
1491 1497 supported = interfaceutil.Attribute(
1492 1498 """Set of requirements that this repo is capable of opening."""
1493 1499 )
1494 1500
1495 1501 requirements = interfaceutil.Attribute(
1496 1502 """Set of requirements this repo uses."""
1497 1503 )
1498 1504
1499 1505 features = interfaceutil.Attribute(
1500 1506 """Set of "features" this repository supports.
1501 1507
1502 1508 A "feature" is a loosely-defined term. It can refer to a feature
1503 1509 in the classical sense or can describe an implementation detail
1504 1510 of the repository. For example, a ``readonly`` feature may denote
1505 1511 the repository as read-only. Or a ``revlogfilestore`` feature may
1506 1512 denote that the repository is using revlogs for file storage.
1507 1513
1508 1514 The intent of features is to provide a machine-queryable mechanism
1509 1515 for repo consumers to test for various repository characteristics.
1510 1516
1511 1517 Features are similar to ``requirements``. The main difference is that
1512 1518 requirements are stored on-disk and represent requirements to open the
1513 1519 repository. Features are more run-time capabilities of the repository
1514 1520 and more granular capabilities (which may be derived from requirements).
1515 1521 """
1516 1522 )
1517 1523
1518 1524 filtername = interfaceutil.Attribute(
1519 1525 """Name of the repoview that is active on this repo."""
1520 1526 )
1521 1527
1522 1528 wvfs = interfaceutil.Attribute(
1523 1529 """VFS used to access the working directory."""
1524 1530 )
1525 1531
1526 1532 vfs = interfaceutil.Attribute(
1527 1533 """VFS rooted at the .hg directory.
1528 1534
1529 1535 Used to access repository data not in the store.
1530 1536 """
1531 1537 )
1532 1538
1533 1539 svfs = interfaceutil.Attribute(
1534 1540 """VFS rooted at the store.
1535 1541
1536 1542 Used to access repository data in the store. Typically .hg/store.
1537 1543 But can point elsewhere if the store is shared.
1538 1544 """
1539 1545 )
1540 1546
1541 1547 root = interfaceutil.Attribute(
1542 1548 """Path to the root of the working directory."""
1543 1549 )
1544 1550
1545 1551 path = interfaceutil.Attribute("""Path to the .hg directory.""")
1546 1552
1547 1553 origroot = interfaceutil.Attribute(
1548 1554 """The filesystem path that was used to construct the repo."""
1549 1555 )
1550 1556
1551 1557 auditor = interfaceutil.Attribute(
1552 1558 """A pathauditor for the working directory.
1553 1559
1554 1560 This checks if a path refers to a nested repository.
1555 1561
1556 1562 Operates on the filesystem.
1557 1563 """
1558 1564 )
1559 1565
1560 1566 nofsauditor = interfaceutil.Attribute(
1561 1567 """A pathauditor for the working directory.
1562 1568
1563 1569 This is like ``auditor`` except it doesn't do filesystem checks.
1564 1570 """
1565 1571 )
1566 1572
1567 1573 baseui = interfaceutil.Attribute(
1568 1574 """Original ui instance passed into constructor."""
1569 1575 )
1570 1576
1571 1577 ui = interfaceutil.Attribute("""Main ui instance for this instance.""")
1572 1578
1573 1579 sharedpath = interfaceutil.Attribute(
1574 1580 """Path to the .hg directory of the repo this repo was shared from."""
1575 1581 )
1576 1582
1577 1583 store = interfaceutil.Attribute("""A store instance.""")
1578 1584
1579 1585 spath = interfaceutil.Attribute("""Path to the store.""")
1580 1586
1581 1587 sjoin = interfaceutil.Attribute("""Alias to self.store.join.""")
1582 1588
1583 1589 cachevfs = interfaceutil.Attribute(
1584 1590 """A VFS used to access the cache directory.
1585 1591
1586 1592 Typically .hg/cache.
1587 1593 """
1588 1594 )
1589 1595
1590 1596 wcachevfs = interfaceutil.Attribute(
1591 1597 """A VFS used to access the cache directory dedicated to working copy
1592 1598
1593 1599 Typically .hg/wcache.
1594 1600 """
1595 1601 )
1596 1602
1597 1603 filteredrevcache = interfaceutil.Attribute(
1598 1604 """Holds sets of revisions to be filtered."""
1599 1605 )
1600 1606
1601 1607 names = interfaceutil.Attribute("""A ``namespaces`` instance.""")
1602 1608
1603 1609 filecopiesmode = interfaceutil.Attribute(
1604 1610 """The way files copies should be dealt with in this repo."""
1605 1611 )
1606 1612
1607 1613 def close():
1608 1614 """Close the handle on this repository."""
1609 1615
1610 1616 def peer():
1611 1617 """Obtain an object conforming to the ``peer`` interface."""
1612 1618
1613 1619 def unfiltered():
1614 1620 """Obtain an unfiltered/raw view of this repo."""
1615 1621
1616 1622 def filtered(name, visibilityexceptions=None):
1617 1623 """Obtain a named view of this repository."""
1618 1624
1619 1625 obsstore = interfaceutil.Attribute("""A store of obsolescence data.""")
1620 1626
1621 1627 changelog = interfaceutil.Attribute("""A handle on the changelog revlog.""")
1622 1628
1623 1629 manifestlog = interfaceutil.Attribute(
1624 1630 """An instance conforming to the ``imanifestlog`` interface.
1625 1631
1626 1632 Provides access to manifests for the repository.
1627 1633 """
1628 1634 )
1629 1635
1630 1636 dirstate = interfaceutil.Attribute("""Working directory state.""")
1631 1637
1632 1638 narrowpats = interfaceutil.Attribute(
1633 1639 """Matcher patterns for this repository's narrowspec."""
1634 1640 )
1635 1641
1636 1642 def narrowmatch(match=None, includeexact=False):
1637 1643 """Obtain a matcher for the narrowspec."""
1638 1644
1639 1645 def setnarrowpats(newincludes, newexcludes):
1640 1646 """Define the narrowspec for this repository."""
1641 1647
1642 1648 def __getitem__(changeid):
1643 1649 """Try to resolve a changectx."""
1644 1650
1645 1651 def __contains__(changeid):
1646 1652 """Whether a changeset exists."""
1647 1653
1648 1654 def __nonzero__():
1649 1655 """Always returns True."""
1650 1656 return True
1651 1657
1652 1658 __bool__ = __nonzero__
1653 1659
1654 1660 def __len__():
1655 1661 """Returns the number of changesets in the repo."""
1656 1662
1657 1663 def __iter__():
1658 1664 """Iterate over revisions in the changelog."""
1659 1665
1660 1666 def revs(expr, *args):
1661 1667 """Evaluate a revset.
1662 1668
1663 1669 Emits revisions.
1664 1670 """
1665 1671
1666 1672 def set(expr, *args):
1667 1673 """Evaluate a revset.
1668 1674
1669 1675 Emits changectx instances.
1670 1676 """
1671 1677
1672 1678 def anyrevs(specs, user=False, localalias=None):
1673 1679 """Find revisions matching one of the given revsets."""
1674 1680
1675 1681 def url():
1676 1682 """Returns a string representing the location of this repo."""
1677 1683
1678 1684 def hook(name, throw=False, **args):
1679 1685 """Call a hook."""
1680 1686
1681 1687 def tags():
1682 1688 """Return a mapping of tag to node."""
1683 1689
1684 1690 def tagtype(tagname):
1685 1691 """Return the type of a given tag."""
1686 1692
1687 1693 def tagslist():
1688 1694 """Return a list of tags ordered by revision."""
1689 1695
1690 1696 def nodetags(node):
1691 1697 """Return the tags associated with a node."""
1692 1698
1693 1699 def nodebookmarks(node):
1694 1700 """Return the list of bookmarks pointing to the specified node."""
1695 1701
1696 1702 def branchmap():
1697 1703 """Return a mapping of branch to heads in that branch."""
1698 1704
1699 1705 def revbranchcache():
1700 1706 pass
1701 1707
1702 1708 def register_changeset(rev, changelogrevision):
1703 1709 """Extension point for caches for new nodes.
1704 1710
1705 1711 Multiple consumers are expected to need parts of the changelogrevision,
1706 1712 so it is provided as optimization to avoid duplicate lookups. A simple
1707 1713 cache would be fragile when other revisions are accessed, too."""
1708 1714 pass
1709 1715
1710 1716 def branchtip(branchtip, ignoremissing=False):
1711 1717 """Return the tip node for a given branch."""
1712 1718
1713 1719 def lookup(key):
1714 1720 """Resolve the node for a revision."""
1715 1721
1716 1722 def lookupbranch(key):
1717 1723 """Look up the branch name of the given revision or branch name."""
1718 1724
1719 1725 def known(nodes):
1720 1726 """Determine whether a series of nodes is known.
1721 1727
1722 1728 Returns a list of bools.
1723 1729 """
1724 1730
1725 1731 def local():
1726 1732 """Whether the repository is local."""
1727 1733 return True
1728 1734
1729 1735 def publishing():
1730 1736 """Whether the repository is a publishing repository."""
1731 1737
1732 1738 def cancopy():
1733 1739 pass
1734 1740
1735 1741 def shared():
1736 1742 """The type of shared repository or None."""
1737 1743
1738 1744 def wjoin(f, *insidef):
1739 1745 """Calls self.vfs.reljoin(self.root, f, *insidef)"""
1740 1746
1741 1747 def setparents(p1, p2):
1742 1748 """Set the parent nodes of the working directory."""
1743 1749
1744 1750 def filectx(path, changeid=None, fileid=None):
1745 1751 """Obtain a filectx for the given file revision."""
1746 1752
1747 1753 def getcwd():
1748 1754 """Obtain the current working directory from the dirstate."""
1749 1755
1750 1756 def pathto(f, cwd=None):
1751 1757 """Obtain the relative path to a file."""
1752 1758
1753 1759 def adddatafilter(name, fltr):
1754 1760 pass
1755 1761
1756 1762 def wread(filename):
1757 1763 """Read a file from wvfs, using data filters."""
1758 1764
1759 1765 def wwrite(filename, data, flags, backgroundclose=False, **kwargs):
1760 1766 """Write data to a file in the wvfs, using data filters."""
1761 1767
1762 1768 def wwritedata(filename, data):
1763 1769 """Resolve data for writing to the wvfs, using data filters."""
1764 1770
1765 1771 def currenttransaction():
1766 1772 """Obtain the current transaction instance or None."""
1767 1773
1768 1774 def transaction(desc, report=None):
1769 1775 """Open a new transaction to write to the repository."""
1770 1776
1771 1777 def undofiles():
1772 1778 """Returns a list of (vfs, path) for files to undo transactions."""
1773 1779
1774 1780 def recover():
1775 1781 """Roll back an interrupted transaction."""
1776 1782
1777 1783 def rollback(dryrun=False, force=False):
1778 1784 """Undo the last transaction.
1779 1785
1780 1786 DANGEROUS.
1781 1787 """
1782 1788
1783 1789 def updatecaches(tr=None, full=False, caches=None):
1784 1790 """Warm repo caches."""
1785 1791
1786 1792 def invalidatecaches():
1787 1793 """Invalidate cached data due to the repository mutating."""
1788 1794
1789 1795 def invalidatevolatilesets():
1790 1796 pass
1791 1797
1792 1798 def invalidatedirstate():
1793 1799 """Invalidate the dirstate."""
1794 1800
1795 1801 def invalidate(clearfilecache=False):
1796 1802 pass
1797 1803
1798 1804 def invalidateall():
1799 1805 pass
1800 1806
1801 1807 def lock(wait=True):
1802 1808 """Lock the repository store and return a lock instance."""
1803 1809
1804 1810 def wlock(wait=True):
1805 1811 """Lock the non-store parts of the repository."""
1806 1812
1807 1813 def currentwlock():
1808 1814 """Return the wlock if it's held or None."""
1809 1815
1810 1816 def checkcommitpatterns(wctx, match, status, fail):
1811 1817 pass
1812 1818
1813 1819 def commit(
1814 1820 text=b'',
1815 1821 user=None,
1816 1822 date=None,
1817 1823 match=None,
1818 1824 force=False,
1819 1825 editor=False,
1820 1826 extra=None,
1821 1827 ):
1822 1828 """Add a new revision to the repository."""
1823 1829
1824 1830 def commitctx(ctx, error=False, origctx=None):
1825 1831 """Commit a commitctx instance to the repository."""
1826 1832
1827 1833 def destroying():
1828 1834 """Inform the repository that nodes are about to be destroyed."""
1829 1835
1830 1836 def destroyed():
1831 1837 """Inform the repository that nodes have been destroyed."""
1832 1838
1833 1839 def status(
1834 1840 node1=b'.',
1835 1841 node2=None,
1836 1842 match=None,
1837 1843 ignored=False,
1838 1844 clean=False,
1839 1845 unknown=False,
1840 1846 listsubrepos=False,
1841 1847 ):
1842 1848 """Convenience method to call repo[x].status()."""
1843 1849
1844 1850 def addpostdsstatus(ps):
1845 1851 pass
1846 1852
1847 1853 def postdsstatus():
1848 1854 pass
1849 1855
1850 1856 def clearpostdsstatus():
1851 1857 pass
1852 1858
1853 1859 def heads(start=None):
1854 1860 """Obtain list of nodes that are DAG heads."""
1855 1861
1856 1862 def branchheads(branch=None, start=None, closed=False):
1857 1863 pass
1858 1864
1859 1865 def branches(nodes):
1860 1866 pass
1861 1867
1862 1868 def between(pairs):
1863 1869 pass
1864 1870
1865 1871 def checkpush(pushop):
1866 1872 pass
1867 1873
1868 1874 prepushoutgoinghooks = interfaceutil.Attribute("""util.hooks instance.""")
1869 1875
1870 1876 def pushkey(namespace, key, old, new):
1871 1877 pass
1872 1878
1873 1879 def listkeys(namespace):
1874 1880 pass
1875 1881
1876 1882 def debugwireargs(one, two, three=None, four=None, five=None):
1877 1883 pass
1878 1884
1879 1885 def savecommitmessage(text):
1880 1886 pass
1881 1887
1882 1888 def register_sidedata_computer(
1883 1889 kind, category, keys, computer, flags, replace=False
1884 1890 ):
1885 1891 pass
1886 1892
1887 1893 def register_wanted_sidedata(category):
1888 1894 pass
1889 1895
1890 1896
1891 1897 class completelocalrepository(
1892 1898 ilocalrepositorymain, ilocalrepositoryfilestorage
1893 1899 ):
1894 1900 """Complete interface for a local repository."""
1895 1901
1896 1902
1897 1903 class iwireprotocolcommandcacher(interfaceutil.Interface):
1898 1904 """Represents a caching backend for wire protocol commands.
1899 1905
1900 1906 Wire protocol version 2 supports transparent caching of many commands.
1901 1907 To leverage this caching, servers can activate objects that cache
1902 1908 command responses. Objects handle both cache writing and reading.
1903 1909 This interface defines how that response caching mechanism works.
1904 1910
1905 1911 Wire protocol version 2 commands emit a series of objects that are
1906 1912 serialized and sent to the client. The caching layer exists between
1907 1913 the invocation of the command function and the sending of its output
1908 1914 objects to an output layer.
1909 1915
1910 1916 Instances of this interface represent a binding to a cache that
1911 1917 can serve a response (in place of calling a command function) and/or
1912 1918 write responses to a cache for subsequent use.
1913 1919
1914 1920 When a command request arrives, the following happens with regards
1915 1921 to this interface:
1916 1922
1917 1923 1. The server determines whether the command request is cacheable.
1918 1924 2. If it is, an instance of this interface is spawned.
1919 1925 3. The cacher is activated in a context manager (``__enter__`` is called).
1920 1926 4. A cache *key* for that request is derived. This will call the
1921 1927 instance's ``adjustcachekeystate()`` method so the derivation
1922 1928 can be influenced.
1923 1929 5. The cacher is informed of the derived cache key via a call to
1924 1930 ``setcachekey()``.
1925 1931 6. The cacher's ``lookup()`` method is called to test for presence of
1926 1932 the derived key in the cache.
1927 1933 7. If ``lookup()`` returns a hit, that cached result is used in place
1928 1934 of invoking the command function. ``__exit__`` is called and the instance
1929 1935 is discarded.
1930 1936 8. The command function is invoked.
1931 1937 9. ``onobject()`` is called for each object emitted by the command
1932 1938 function.
1933 1939 10. After the final object is seen, ``onfinished()`` is called.
1934 1940 11. ``__exit__`` is called to signal the end of use of the instance.
1935 1941
1936 1942 Cache *key* derivation can be influenced by the instance.
1937 1943
1938 1944 Cache keys are initially derived by a deterministic representation of
1939 1945 the command request. This includes the command name, arguments, protocol
1940 1946 version, etc. This initial key derivation is performed by CBOR-encoding a
1941 1947 data structure and feeding that output into a hasher.
1942 1948
1943 1949 Instances of this interface can influence this initial key derivation
1944 1950 via ``adjustcachekeystate()``.
1945 1951
1946 1952 The instance is informed of the derived cache key via a call to
1947 1953 ``setcachekey()``. The instance must store the key locally so it can
1948 1954 be consulted on subsequent operations that may require it.
1949 1955
1950 1956 When constructed, the instance has access to a callable that can be used
1951 1957 for encoding response objects. This callable receives as its single
1952 1958 argument an object emitted by a command function. It returns an iterable
1953 1959 of bytes chunks representing the encoded object. Unless the cacher is
1954 1960 caching native Python objects in memory or has a way of reconstructing
1955 1961 the original Python objects, implementations typically call this function
1956 1962 to produce bytes from the output objects and then store those bytes in
1957 1963 the cache. When it comes time to re-emit those bytes, they are wrapped
1958 1964 in a ``wireprototypes.encodedresponse`` instance to tell the output
1959 1965 layer that they are pre-encoded.
1960 1966
1961 1967 When receiving the objects emitted by the command function, instances
1962 1968 can choose what to do with those objects. The simplest thing to do is
1963 1969 re-emit the original objects. They will be forwarded to the output
1964 1970 layer and will be processed as if the cacher did not exist.
1965 1971
1966 1972 Implementations could also choose to not emit objects - instead locally
1967 1973 buffering objects or their encoded representation. They could then emit
1968 1974 a single "coalesced" object when ``onfinished()`` is called. In
1969 1975 this way, the implementation would function as a filtering layer of
1970 1976 sorts.
1971 1977
1972 1978 When caching objects, typically the encoded form of the object will
1973 1979 be stored. Keep in mind that if the original object is forwarded to
1974 1980 the output layer, it will need to be encoded there as well. For large
1975 1981 output, this redundant encoding could add overhead. Implementations
1976 1982 could wrap the encoded object data in ``wireprototypes.encodedresponse``
1977 1983 instances to avoid this overhead.
1978 1984 """
1979 1985
1980 1986 def __enter__():
1981 1987 """Marks the instance as active.
1982 1988
1983 1989 Should return self.
1984 1990 """
1985 1991
1986 1992 def __exit__(exctype, excvalue, exctb):
1987 1993 """Called when cacher is no longer used.
1988 1994
1989 1995 This can be used by implementations to perform cleanup actions (e.g.
1990 1996 disconnecting network sockets, aborting a partially cached response.
1991 1997 """
1992 1998
1993 1999 def adjustcachekeystate(state):
1994 2000 """Influences cache key derivation by adjusting state to derive key.
1995 2001
1996 2002 A dict defining the state used to derive the cache key is passed.
1997 2003
1998 2004 Implementations can modify this dict to record additional state that
1999 2005 is wanted to influence key derivation.
2000 2006
2001 2007 Implementations are *highly* encouraged to not modify or delete
2002 2008 existing keys.
2003 2009 """
2004 2010
2005 2011 def setcachekey(key):
2006 2012 """Record the derived cache key for this request.
2007 2013
2008 2014 Instances may mutate the key for internal usage, as desired. e.g.
2009 2015 instances may wish to prepend the repo name, introduce path
2010 2016 components for filesystem or URL addressing, etc. Behavior is up to
2011 2017 the cache.
2012 2018
2013 2019 Returns a bool indicating if the request is cacheable by this
2014 2020 instance.
2015 2021 """
2016 2022
2017 2023 def lookup():
2018 2024 """Attempt to resolve an entry in the cache.
2019 2025
2020 2026 The instance is instructed to look for the cache key that it was
2021 2027 informed about via the call to ``setcachekey()``.
2022 2028
2023 2029 If there's no cache hit or the cacher doesn't wish to use the cached
2024 2030 entry, ``None`` should be returned.
2025 2031
2026 2032 Else, a dict defining the cached result should be returned. The
2027 2033 dict may have the following keys:
2028 2034
2029 2035 objs
2030 2036 An iterable of objects that should be sent to the client. That
2031 2037 iterable of objects is expected to be what the command function
2032 2038 would return if invoked or an equivalent representation thereof.
2033 2039 """
2034 2040
2035 2041 def onobject(obj):
2036 2042 """Called when a new object is emitted from the command function.
2037 2043
2038 2044 Receives as its argument the object that was emitted from the
2039 2045 command function.
2040 2046
2041 2047 This method returns an iterator of objects to forward to the output
2042 2048 layer. The easiest implementation is a generator that just
2043 2049 ``yield obj``.
2044 2050 """
2045 2051
2046 2052 def onfinished():
2047 2053 """Called after all objects have been emitted from the command function.
2048 2054
2049 2055 Implementations should return an iterator of objects to forward to
2050 2056 the output layer.
2051 2057
2052 2058 This method can be a generator.
2053 2059 """
@@ -1,3977 +1,3976 b''
1 1 # localrepo.py - read/write repository class for mercurial
2 2 # coding: utf-8
3 3 #
4 4 # Copyright 2005-2007 Olivia Mackall <olivia@selenic.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 functools
11 11 import os
12 12 import random
13 13 import sys
14 14 import time
15 15 import weakref
16 16
17 17 from concurrent import futures
18 18 from typing import (
19 19 Optional,
20 20 )
21 21
22 22 from .i18n import _
23 23 from .node import (
24 24 bin,
25 25 hex,
26 26 nullrev,
27 27 sha1nodeconstants,
28 28 short,
29 29 )
30 30 from .pycompat import (
31 31 delattr,
32 32 getattr,
33 33 )
34 34 from . import (
35 35 bookmarks,
36 36 branchmap,
37 37 bundle2,
38 38 bundlecaches,
39 39 changegroup,
40 40 color,
41 41 commit,
42 42 context,
43 43 dirstate,
44 44 dirstateguard,
45 45 discovery,
46 46 encoding,
47 47 error,
48 48 exchange,
49 49 extensions,
50 50 filelog,
51 51 hook,
52 52 lock as lockmod,
53 53 match as matchmod,
54 54 mergestate as mergestatemod,
55 55 mergeutil,
56 56 namespaces,
57 57 narrowspec,
58 58 obsolete,
59 59 pathutil,
60 60 phases,
61 61 pushkey,
62 62 pycompat,
63 63 rcutil,
64 64 repoview,
65 65 requirements as requirementsmod,
66 66 revlog,
67 67 revset,
68 68 revsetlang,
69 69 scmutil,
70 70 sparse,
71 71 store as storemod,
72 72 subrepoutil,
73 73 tags as tagsmod,
74 74 transaction,
75 75 txnutil,
76 76 util,
77 77 vfs as vfsmod,
78 78 wireprototypes,
79 79 )
80 80
81 81 from .interfaces import (
82 82 repository,
83 83 util as interfaceutil,
84 84 )
85 85
86 86 from .utils import (
87 87 hashutil,
88 88 procutil,
89 89 stringutil,
90 90 urlutil,
91 91 )
92 92
93 93 from .revlogutils import (
94 94 concurrency_checker as revlogchecker,
95 95 constants as revlogconst,
96 96 sidedata as sidedatamod,
97 97 )
98 98
99 99 release = lockmod.release
100 100 urlerr = util.urlerr
101 101 urlreq = util.urlreq
102 102
103 103 # set of (path, vfs-location) tuples. vfs-location is:
104 104 # - 'plain for vfs relative paths
105 105 # - '' for svfs relative paths
106 106 _cachedfiles = set()
107 107
108 108
109 109 class _basefilecache(scmutil.filecache):
110 110 """All filecache usage on repo are done for logic that should be unfiltered"""
111 111
112 112 def __get__(self, repo, type=None):
113 113 if repo is None:
114 114 return self
115 115 # proxy to unfiltered __dict__ since filtered repo has no entry
116 116 unfi = repo.unfiltered()
117 117 try:
118 118 return unfi.__dict__[self.sname]
119 119 except KeyError:
120 120 pass
121 121 return super(_basefilecache, self).__get__(unfi, type)
122 122
123 123 def set(self, repo, value):
124 124 return super(_basefilecache, self).set(repo.unfiltered(), value)
125 125
126 126
127 127 class repofilecache(_basefilecache):
128 128 """filecache for files in .hg but outside of .hg/store"""
129 129
130 130 def __init__(self, *paths):
131 131 super(repofilecache, self).__init__(*paths)
132 132 for path in paths:
133 133 _cachedfiles.add((path, b'plain'))
134 134
135 135 def join(self, obj, fname):
136 136 return obj.vfs.join(fname)
137 137
138 138
139 139 class storecache(_basefilecache):
140 140 """filecache for files in the store"""
141 141
142 142 def __init__(self, *paths):
143 143 super(storecache, self).__init__(*paths)
144 144 for path in paths:
145 145 _cachedfiles.add((path, b''))
146 146
147 147 def join(self, obj, fname):
148 148 return obj.sjoin(fname)
149 149
150 150
151 151 class changelogcache(storecache):
152 152 """filecache for the changelog"""
153 153
154 154 def __init__(self):
155 155 super(changelogcache, self).__init__()
156 156 _cachedfiles.add((b'00changelog.i', b''))
157 157 _cachedfiles.add((b'00changelog.n', b''))
158 158
159 159 def tracked_paths(self, obj):
160 160 paths = [self.join(obj, b'00changelog.i')]
161 161 if obj.store.opener.options.get(b'persistent-nodemap', False):
162 162 paths.append(self.join(obj, b'00changelog.n'))
163 163 return paths
164 164
165 165
166 166 class manifestlogcache(storecache):
167 167 """filecache for the manifestlog"""
168 168
169 169 def __init__(self):
170 170 super(manifestlogcache, self).__init__()
171 171 _cachedfiles.add((b'00manifest.i', b''))
172 172 _cachedfiles.add((b'00manifest.n', b''))
173 173
174 174 def tracked_paths(self, obj):
175 175 paths = [self.join(obj, b'00manifest.i')]
176 176 if obj.store.opener.options.get(b'persistent-nodemap', False):
177 177 paths.append(self.join(obj, b'00manifest.n'))
178 178 return paths
179 179
180 180
181 181 class mixedrepostorecache(_basefilecache):
182 182 """filecache for a mix files in .hg/store and outside"""
183 183
184 184 def __init__(self, *pathsandlocations):
185 185 # scmutil.filecache only uses the path for passing back into our
186 186 # join(), so we can safely pass a list of paths and locations
187 187 super(mixedrepostorecache, self).__init__(*pathsandlocations)
188 188 _cachedfiles.update(pathsandlocations)
189 189
190 190 def join(self, obj, fnameandlocation):
191 191 fname, location = fnameandlocation
192 192 if location == b'plain':
193 193 return obj.vfs.join(fname)
194 194 else:
195 195 if location != b'':
196 196 raise error.ProgrammingError(
197 197 b'unexpected location: %s' % location
198 198 )
199 199 return obj.sjoin(fname)
200 200
201 201
202 202 def isfilecached(repo, name):
203 203 """check if a repo has already cached "name" filecache-ed property
204 204
205 205 This returns (cachedobj-or-None, iscached) tuple.
206 206 """
207 207 cacheentry = repo.unfiltered()._filecache.get(name, None)
208 208 if not cacheentry:
209 209 return None, False
210 210 return cacheentry.obj, True
211 211
212 212
213 213 class unfilteredpropertycache(util.propertycache):
214 214 """propertycache that apply to unfiltered repo only"""
215 215
216 216 def __get__(self, repo, type=None):
217 217 unfi = repo.unfiltered()
218 218 if unfi is repo:
219 219 return super(unfilteredpropertycache, self).__get__(unfi)
220 220 return getattr(unfi, self.name)
221 221
222 222
223 223 class filteredpropertycache(util.propertycache):
224 224 """propertycache that must take filtering in account"""
225 225
226 226 def cachevalue(self, obj, value):
227 227 object.__setattr__(obj, self.name, value)
228 228
229 229
230 230 def hasunfilteredcache(repo, name):
231 231 """check if a repo has an unfilteredpropertycache value for <name>"""
232 232 return name in vars(repo.unfiltered())
233 233
234 234
235 235 def unfilteredmethod(orig):
236 236 """decorate method that always need to be run on unfiltered version"""
237 237
238 238 @functools.wraps(orig)
239 239 def wrapper(repo, *args, **kwargs):
240 240 return orig(repo.unfiltered(), *args, **kwargs)
241 241
242 242 return wrapper
243 243
244 244
245 245 moderncaps = {
246 246 b'lookup',
247 247 b'branchmap',
248 248 b'pushkey',
249 249 b'known',
250 250 b'getbundle',
251 251 b'unbundle',
252 252 }
253 253 legacycaps = moderncaps.union({b'changegroupsubset'})
254 254
255 255
256 256 @interfaceutil.implementer(repository.ipeercommandexecutor)
257 257 class localcommandexecutor:
258 258 def __init__(self, peer):
259 259 self._peer = peer
260 260 self._sent = False
261 261 self._closed = False
262 262
263 263 def __enter__(self):
264 264 return self
265 265
266 266 def __exit__(self, exctype, excvalue, exctb):
267 267 self.close()
268 268
269 269 def callcommand(self, command, args):
270 270 if self._sent:
271 271 raise error.ProgrammingError(
272 272 b'callcommand() cannot be used after sendcommands()'
273 273 )
274 274
275 275 if self._closed:
276 276 raise error.ProgrammingError(
277 277 b'callcommand() cannot be used after close()'
278 278 )
279 279
280 280 # We don't need to support anything fancy. Just call the named
281 281 # method on the peer and return a resolved future.
282 282 fn = getattr(self._peer, pycompat.sysstr(command))
283 283
284 284 f = futures.Future()
285 285
286 286 try:
287 287 result = fn(**pycompat.strkwargs(args))
288 288 except Exception:
289 289 pycompat.future_set_exception_info(f, sys.exc_info()[1:])
290 290 else:
291 291 f.set_result(result)
292 292
293 293 return f
294 294
295 295 def sendcommands(self):
296 296 self._sent = True
297 297
298 298 def close(self):
299 299 self._closed = True
300 300
301 301
302 302 @interfaceutil.implementer(repository.ipeercommands)
303 303 class localpeer(repository.peer):
304 304 '''peer for a local repo; reflects only the most recent API'''
305 305
306 306 def __init__(self, repo, caps=None):
307 super(localpeer, self).__init__()
307 super(localpeer, self).__init__(repo.ui)
308 308
309 309 if caps is None:
310 310 caps = moderncaps.copy()
311 311 self._repo = repo.filtered(b'served')
312 self.ui = repo.ui
313 312
314 313 if repo._wanted_sidedata:
315 314 formatted = bundle2.format_remote_wanted_sidedata(repo)
316 315 caps.add(b'exp-wanted-sidedata=' + formatted)
317 316
318 317 self._caps = repo._restrictcapabilities(caps)
319 318
320 319 # Begin of _basepeer interface.
321 320
322 321 def url(self):
323 322 return self._repo.url()
324 323
325 324 def local(self):
326 325 return self._repo
327 326
328 327 def canpush(self):
329 328 return True
330 329
331 330 def close(self):
332 331 self._repo.close()
333 332
334 333 # End of _basepeer interface.
335 334
336 335 # Begin of _basewirecommands interface.
337 336
338 337 def branchmap(self):
339 338 return self._repo.branchmap()
340 339
341 340 def capabilities(self):
342 341 return self._caps
343 342
344 343 def clonebundles(self):
345 344 return self._repo.tryread(bundlecaches.CB_MANIFEST_FILE)
346 345
347 346 def debugwireargs(self, one, two, three=None, four=None, five=None):
348 347 """Used to test argument passing over the wire"""
349 348 return b"%s %s %s %s %s" % (
350 349 one,
351 350 two,
352 351 pycompat.bytestr(three),
353 352 pycompat.bytestr(four),
354 353 pycompat.bytestr(five),
355 354 )
356 355
357 356 def getbundle(
358 357 self,
359 358 source,
360 359 heads=None,
361 360 common=None,
362 361 bundlecaps=None,
363 362 remote_sidedata=None,
364 363 **kwargs
365 364 ):
366 365 chunks = exchange.getbundlechunks(
367 366 self._repo,
368 367 source,
369 368 heads=heads,
370 369 common=common,
371 370 bundlecaps=bundlecaps,
372 371 remote_sidedata=remote_sidedata,
373 372 **kwargs
374 373 )[1]
375 374 cb = util.chunkbuffer(chunks)
376 375
377 376 if exchange.bundle2requested(bundlecaps):
378 377 # When requesting a bundle2, getbundle returns a stream to make the
379 378 # wire level function happier. We need to build a proper object
380 379 # from it in local peer.
381 380 return bundle2.getunbundler(self.ui, cb)
382 381 else:
383 382 return changegroup.getunbundler(b'01', cb, None)
384 383
385 384 def heads(self):
386 385 return self._repo.heads()
387 386
388 387 def known(self, nodes):
389 388 return self._repo.known(nodes)
390 389
391 390 def listkeys(self, namespace):
392 391 return self._repo.listkeys(namespace)
393 392
394 393 def lookup(self, key):
395 394 return self._repo.lookup(key)
396 395
397 396 def pushkey(self, namespace, key, old, new):
398 397 return self._repo.pushkey(namespace, key, old, new)
399 398
400 399 def stream_out(self):
401 400 raise error.Abort(_(b'cannot perform stream clone against local peer'))
402 401
403 402 def unbundle(self, bundle, heads, url):
404 403 """apply a bundle on a repo
405 404
406 405 This function handles the repo locking itself."""
407 406 try:
408 407 try:
409 408 bundle = exchange.readbundle(self.ui, bundle, None)
410 409 ret = exchange.unbundle(self._repo, bundle, heads, b'push', url)
411 410 if util.safehasattr(ret, b'getchunks'):
412 411 # This is a bundle20 object, turn it into an unbundler.
413 412 # This little dance should be dropped eventually when the
414 413 # API is finally improved.
415 414 stream = util.chunkbuffer(ret.getchunks())
416 415 ret = bundle2.getunbundler(self.ui, stream)
417 416 return ret
418 417 except Exception as exc:
419 418 # If the exception contains output salvaged from a bundle2
420 419 # reply, we need to make sure it is printed before continuing
421 420 # to fail. So we build a bundle2 with such output and consume
422 421 # it directly.
423 422 #
424 423 # This is not very elegant but allows a "simple" solution for
425 424 # issue4594
426 425 output = getattr(exc, '_bundle2salvagedoutput', ())
427 426 if output:
428 427 bundler = bundle2.bundle20(self._repo.ui)
429 428 for out in output:
430 429 bundler.addpart(out)
431 430 stream = util.chunkbuffer(bundler.getchunks())
432 431 b = bundle2.getunbundler(self.ui, stream)
433 432 bundle2.processbundle(self._repo, b)
434 433 raise
435 434 except error.PushRaced as exc:
436 435 raise error.ResponseError(
437 436 _(b'push failed:'), stringutil.forcebytestr(exc)
438 437 )
439 438
440 439 # End of _basewirecommands interface.
441 440
442 441 # Begin of peer interface.
443 442
444 443 def commandexecutor(self):
445 444 return localcommandexecutor(self)
446 445
447 446 # End of peer interface.
448 447
449 448
450 449 @interfaceutil.implementer(repository.ipeerlegacycommands)
451 450 class locallegacypeer(localpeer):
452 451 """peer extension which implements legacy methods too; used for tests with
453 452 restricted capabilities"""
454 453
455 454 def __init__(self, repo):
456 455 super(locallegacypeer, self).__init__(repo, caps=legacycaps)
457 456
458 457 # Begin of baselegacywirecommands interface.
459 458
460 459 def between(self, pairs):
461 460 return self._repo.between(pairs)
462 461
463 462 def branches(self, nodes):
464 463 return self._repo.branches(nodes)
465 464
466 465 def changegroup(self, nodes, source):
467 466 outgoing = discovery.outgoing(
468 467 self._repo, missingroots=nodes, ancestorsof=self._repo.heads()
469 468 )
470 469 return changegroup.makechangegroup(self._repo, outgoing, b'01', source)
471 470
472 471 def changegroupsubset(self, bases, heads, source):
473 472 outgoing = discovery.outgoing(
474 473 self._repo, missingroots=bases, ancestorsof=heads
475 474 )
476 475 return changegroup.makechangegroup(self._repo, outgoing, b'01', source)
477 476
478 477 # End of baselegacywirecommands interface.
479 478
480 479
481 480 # Functions receiving (ui, features) that extensions can register to impact
482 481 # the ability to load repositories with custom requirements. Only
483 482 # functions defined in loaded extensions are called.
484 483 #
485 484 # The function receives a set of requirement strings that the repository
486 485 # is capable of opening. Functions will typically add elements to the
487 486 # set to reflect that the extension knows how to handle that requirements.
488 487 featuresetupfuncs = set()
489 488
490 489
491 490 def _getsharedvfs(hgvfs, requirements):
492 491 """returns the vfs object pointing to root of shared source
493 492 repo for a shared repository
494 493
495 494 hgvfs is vfs pointing at .hg/ of current repo (shared one)
496 495 requirements is a set of requirements of current repo (shared one)
497 496 """
498 497 # The ``shared`` or ``relshared`` requirements indicate the
499 498 # store lives in the path contained in the ``.hg/sharedpath`` file.
500 499 # This is an absolute path for ``shared`` and relative to
501 500 # ``.hg/`` for ``relshared``.
502 501 sharedpath = hgvfs.read(b'sharedpath').rstrip(b'\n')
503 502 if requirementsmod.RELATIVE_SHARED_REQUIREMENT in requirements:
504 503 sharedpath = util.normpath(hgvfs.join(sharedpath))
505 504
506 505 sharedvfs = vfsmod.vfs(sharedpath, realpath=True)
507 506
508 507 if not sharedvfs.exists():
509 508 raise error.RepoError(
510 509 _(b'.hg/sharedpath points to nonexistent directory %s')
511 510 % sharedvfs.base
512 511 )
513 512 return sharedvfs
514 513
515 514
516 515 def _readrequires(vfs, allowmissing):
517 516 """reads the require file present at root of this vfs
518 517 and return a set of requirements
519 518
520 519 If allowmissing is True, we suppress FileNotFoundError if raised"""
521 520 # requires file contains a newline-delimited list of
522 521 # features/capabilities the opener (us) must have in order to use
523 522 # the repository. This file was introduced in Mercurial 0.9.2,
524 523 # which means very old repositories may not have one. We assume
525 524 # a missing file translates to no requirements.
526 525 read = vfs.tryread if allowmissing else vfs.read
527 526 return set(read(b'requires').splitlines())
528 527
529 528
530 529 def makelocalrepository(baseui, path: bytes, intents=None):
531 530 """Create a local repository object.
532 531
533 532 Given arguments needed to construct a local repository, this function
534 533 performs various early repository loading functionality (such as
535 534 reading the ``.hg/requires`` and ``.hg/hgrc`` files), validates that
536 535 the repository can be opened, derives a type suitable for representing
537 536 that repository, and returns an instance of it.
538 537
539 538 The returned object conforms to the ``repository.completelocalrepository``
540 539 interface.
541 540
542 541 The repository type is derived by calling a series of factory functions
543 542 for each aspect/interface of the final repository. These are defined by
544 543 ``REPO_INTERFACES``.
545 544
546 545 Each factory function is called to produce a type implementing a specific
547 546 interface. The cumulative list of returned types will be combined into a
548 547 new type and that type will be instantiated to represent the local
549 548 repository.
550 549
551 550 The factory functions each receive various state that may be consulted
552 551 as part of deriving a type.
553 552
554 553 Extensions should wrap these factory functions to customize repository type
555 554 creation. Note that an extension's wrapped function may be called even if
556 555 that extension is not loaded for the repo being constructed. Extensions
557 556 should check if their ``__name__`` appears in the
558 557 ``extensionmodulenames`` set passed to the factory function and no-op if
559 558 not.
560 559 """
561 560 ui = baseui.copy()
562 561 # Prevent copying repo configuration.
563 562 ui.copy = baseui.copy
564 563
565 564 # Working directory VFS rooted at repository root.
566 565 wdirvfs = vfsmod.vfs(path, expandpath=True, realpath=True)
567 566
568 567 # Main VFS for .hg/ directory.
569 568 hgpath = wdirvfs.join(b'.hg')
570 569 hgvfs = vfsmod.vfs(hgpath, cacheaudited=True)
571 570 # Whether this repository is shared one or not
572 571 shared = False
573 572 # If this repository is shared, vfs pointing to shared repo
574 573 sharedvfs = None
575 574
576 575 # The .hg/ path should exist and should be a directory. All other
577 576 # cases are errors.
578 577 if not hgvfs.isdir():
579 578 try:
580 579 hgvfs.stat()
581 580 except FileNotFoundError:
582 581 pass
583 582 except ValueError as e:
584 583 # Can be raised on Python 3.8 when path is invalid.
585 584 raise error.Abort(
586 585 _(b'invalid path %s: %s') % (path, stringutil.forcebytestr(e))
587 586 )
588 587
589 588 raise error.RepoError(_(b'repository %s not found') % path)
590 589
591 590 requirements = _readrequires(hgvfs, True)
592 591 shared = (
593 592 requirementsmod.SHARED_REQUIREMENT in requirements
594 593 or requirementsmod.RELATIVE_SHARED_REQUIREMENT in requirements
595 594 )
596 595 storevfs = None
597 596 if shared:
598 597 # This is a shared repo
599 598 sharedvfs = _getsharedvfs(hgvfs, requirements)
600 599 storevfs = vfsmod.vfs(sharedvfs.join(b'store'))
601 600 else:
602 601 storevfs = vfsmod.vfs(hgvfs.join(b'store'))
603 602
604 603 # if .hg/requires contains the sharesafe requirement, it means
605 604 # there exists a `.hg/store/requires` too and we should read it
606 605 # NOTE: presence of SHARESAFE_REQUIREMENT imply that store requirement
607 606 # is present. We never write SHARESAFE_REQUIREMENT for a repo if store
608 607 # is not present, refer checkrequirementscompat() for that
609 608 #
610 609 # However, if SHARESAFE_REQUIREMENT is not present, it means that the
611 610 # repository was shared the old way. We check the share source .hg/requires
612 611 # for SHARESAFE_REQUIREMENT to detect whether the current repository needs
613 612 # to be reshared
614 613 hint = _(b"see `hg help config.format.use-share-safe` for more information")
615 614 if requirementsmod.SHARESAFE_REQUIREMENT in requirements:
616 615
617 616 if (
618 617 shared
619 618 and requirementsmod.SHARESAFE_REQUIREMENT
620 619 not in _readrequires(sharedvfs, True)
621 620 ):
622 621 mismatch_warn = ui.configbool(
623 622 b'share', b'safe-mismatch.source-not-safe.warn'
624 623 )
625 624 mismatch_config = ui.config(
626 625 b'share', b'safe-mismatch.source-not-safe'
627 626 )
628 627 mismatch_verbose_upgrade = ui.configbool(
629 628 b'share', b'safe-mismatch.source-not-safe:verbose-upgrade'
630 629 )
631 630 if mismatch_config in (
632 631 b'downgrade-allow',
633 632 b'allow',
634 633 b'downgrade-abort',
635 634 ):
636 635 # prevent cyclic import localrepo -> upgrade -> localrepo
637 636 from . import upgrade
638 637
639 638 upgrade.downgrade_share_to_non_safe(
640 639 ui,
641 640 hgvfs,
642 641 sharedvfs,
643 642 requirements,
644 643 mismatch_config,
645 644 mismatch_warn,
646 645 mismatch_verbose_upgrade,
647 646 )
648 647 elif mismatch_config == b'abort':
649 648 raise error.Abort(
650 649 _(b"share source does not support share-safe requirement"),
651 650 hint=hint,
652 651 )
653 652 else:
654 653 raise error.Abort(
655 654 _(
656 655 b"share-safe mismatch with source.\nUnrecognized"
657 656 b" value '%s' of `share.safe-mismatch.source-not-safe`"
658 657 b" set."
659 658 )
660 659 % mismatch_config,
661 660 hint=hint,
662 661 )
663 662 else:
664 663 requirements |= _readrequires(storevfs, False)
665 664 elif shared:
666 665 sourcerequires = _readrequires(sharedvfs, False)
667 666 if requirementsmod.SHARESAFE_REQUIREMENT in sourcerequires:
668 667 mismatch_config = ui.config(b'share', b'safe-mismatch.source-safe')
669 668 mismatch_warn = ui.configbool(
670 669 b'share', b'safe-mismatch.source-safe.warn'
671 670 )
672 671 mismatch_verbose_upgrade = ui.configbool(
673 672 b'share', b'safe-mismatch.source-safe:verbose-upgrade'
674 673 )
675 674 if mismatch_config in (
676 675 b'upgrade-allow',
677 676 b'allow',
678 677 b'upgrade-abort',
679 678 ):
680 679 # prevent cyclic import localrepo -> upgrade -> localrepo
681 680 from . import upgrade
682 681
683 682 upgrade.upgrade_share_to_safe(
684 683 ui,
685 684 hgvfs,
686 685 storevfs,
687 686 requirements,
688 687 mismatch_config,
689 688 mismatch_warn,
690 689 mismatch_verbose_upgrade,
691 690 )
692 691 elif mismatch_config == b'abort':
693 692 raise error.Abort(
694 693 _(
695 694 b'version mismatch: source uses share-safe'
696 695 b' functionality while the current share does not'
697 696 ),
698 697 hint=hint,
699 698 )
700 699 else:
701 700 raise error.Abort(
702 701 _(
703 702 b"share-safe mismatch with source.\nUnrecognized"
704 703 b" value '%s' of `share.safe-mismatch.source-safe` set."
705 704 )
706 705 % mismatch_config,
707 706 hint=hint,
708 707 )
709 708
710 709 # The .hg/hgrc file may load extensions or contain config options
711 710 # that influence repository construction. Attempt to load it and
712 711 # process any new extensions that it may have pulled in.
713 712 if loadhgrc(ui, wdirvfs, hgvfs, requirements, sharedvfs):
714 713 afterhgrcload(ui, wdirvfs, hgvfs, requirements)
715 714 extensions.loadall(ui)
716 715 extensions.populateui(ui)
717 716
718 717 # Set of module names of extensions loaded for this repository.
719 718 extensionmodulenames = {m.__name__ for n, m in extensions.extensions(ui)}
720 719
721 720 supportedrequirements = gathersupportedrequirements(ui)
722 721
723 722 # We first validate the requirements are known.
724 723 ensurerequirementsrecognized(requirements, supportedrequirements)
725 724
726 725 # Then we validate that the known set is reasonable to use together.
727 726 ensurerequirementscompatible(ui, requirements)
728 727
729 728 # TODO there are unhandled edge cases related to opening repositories with
730 729 # shared storage. If storage is shared, we should also test for requirements
731 730 # compatibility in the pointed-to repo. This entails loading the .hg/hgrc in
732 731 # that repo, as that repo may load extensions needed to open it. This is a
733 732 # bit complicated because we don't want the other hgrc to overwrite settings
734 733 # in this hgrc.
735 734 #
736 735 # This bug is somewhat mitigated by the fact that we copy the .hg/requires
737 736 # file when sharing repos. But if a requirement is added after the share is
738 737 # performed, thereby introducing a new requirement for the opener, we may
739 738 # will not see that and could encounter a run-time error interacting with
740 739 # that shared store since it has an unknown-to-us requirement.
741 740
742 741 # At this point, we know we should be capable of opening the repository.
743 742 # Now get on with doing that.
744 743
745 744 features = set()
746 745
747 746 # The "store" part of the repository holds versioned data. How it is
748 747 # accessed is determined by various requirements. If `shared` or
749 748 # `relshared` requirements are present, this indicates current repository
750 749 # is a share and store exists in path mentioned in `.hg/sharedpath`
751 750 if shared:
752 751 storebasepath = sharedvfs.base
753 752 cachepath = sharedvfs.join(b'cache')
754 753 features.add(repository.REPO_FEATURE_SHARED_STORAGE)
755 754 else:
756 755 storebasepath = hgvfs.base
757 756 cachepath = hgvfs.join(b'cache')
758 757 wcachepath = hgvfs.join(b'wcache')
759 758
760 759 # The store has changed over time and the exact layout is dictated by
761 760 # requirements. The store interface abstracts differences across all
762 761 # of them.
763 762 store = makestore(
764 763 requirements,
765 764 storebasepath,
766 765 lambda base: vfsmod.vfs(base, cacheaudited=True),
767 766 )
768 767 hgvfs.createmode = store.createmode
769 768
770 769 storevfs = store.vfs
771 770 storevfs.options = resolvestorevfsoptions(ui, requirements, features)
772 771
773 772 if (
774 773 requirementsmod.REVLOGV2_REQUIREMENT in requirements
775 774 or requirementsmod.CHANGELOGV2_REQUIREMENT in requirements
776 775 ):
777 776 features.add(repository.REPO_FEATURE_SIDE_DATA)
778 777 # the revlogv2 docket introduced race condition that we need to fix
779 778 features.discard(repository.REPO_FEATURE_STREAM_CLONE)
780 779
781 780 # The cache vfs is used to manage cache files.
782 781 cachevfs = vfsmod.vfs(cachepath, cacheaudited=True)
783 782 cachevfs.createmode = store.createmode
784 783 # The cache vfs is used to manage cache files related to the working copy
785 784 wcachevfs = vfsmod.vfs(wcachepath, cacheaudited=True)
786 785 wcachevfs.createmode = store.createmode
787 786
788 787 # Now resolve the type for the repository object. We do this by repeatedly
789 788 # calling a factory function to produces types for specific aspects of the
790 789 # repo's operation. The aggregate returned types are used as base classes
791 790 # for a dynamically-derived type, which will represent our new repository.
792 791
793 792 bases = []
794 793 extrastate = {}
795 794
796 795 for iface, fn in REPO_INTERFACES:
797 796 # We pass all potentially useful state to give extensions tons of
798 797 # flexibility.
799 798 typ = fn()(
800 799 ui=ui,
801 800 intents=intents,
802 801 requirements=requirements,
803 802 features=features,
804 803 wdirvfs=wdirvfs,
805 804 hgvfs=hgvfs,
806 805 store=store,
807 806 storevfs=storevfs,
808 807 storeoptions=storevfs.options,
809 808 cachevfs=cachevfs,
810 809 wcachevfs=wcachevfs,
811 810 extensionmodulenames=extensionmodulenames,
812 811 extrastate=extrastate,
813 812 baseclasses=bases,
814 813 )
815 814
816 815 if not isinstance(typ, type):
817 816 raise error.ProgrammingError(
818 817 b'unable to construct type for %s' % iface
819 818 )
820 819
821 820 bases.append(typ)
822 821
823 822 # type() allows you to use characters in type names that wouldn't be
824 823 # recognized as Python symbols in source code. We abuse that to add
825 824 # rich information about our constructed repo.
826 825 name = pycompat.sysstr(
827 826 b'derivedrepo:%s<%s>' % (wdirvfs.base, b','.join(sorted(requirements)))
828 827 )
829 828
830 829 cls = type(name, tuple(bases), {})
831 830
832 831 return cls(
833 832 baseui=baseui,
834 833 ui=ui,
835 834 origroot=path,
836 835 wdirvfs=wdirvfs,
837 836 hgvfs=hgvfs,
838 837 requirements=requirements,
839 838 supportedrequirements=supportedrequirements,
840 839 sharedpath=storebasepath,
841 840 store=store,
842 841 cachevfs=cachevfs,
843 842 wcachevfs=wcachevfs,
844 843 features=features,
845 844 intents=intents,
846 845 )
847 846
848 847
849 848 def loadhgrc(
850 849 ui,
851 850 wdirvfs: vfsmod.vfs,
852 851 hgvfs: vfsmod.vfs,
853 852 requirements,
854 853 sharedvfs: Optional[vfsmod.vfs] = None,
855 854 ):
856 855 """Load hgrc files/content into a ui instance.
857 856
858 857 This is called during repository opening to load any additional
859 858 config files or settings relevant to the current repository.
860 859
861 860 Returns a bool indicating whether any additional configs were loaded.
862 861
863 862 Extensions should monkeypatch this function to modify how per-repo
864 863 configs are loaded. For example, an extension may wish to pull in
865 864 configs from alternate files or sources.
866 865
867 866 sharedvfs is vfs object pointing to source repo if the current one is a
868 867 shared one
869 868 """
870 869 if not rcutil.use_repo_hgrc():
871 870 return False
872 871
873 872 ret = False
874 873 # first load config from shared source if we has to
875 874 if requirementsmod.SHARESAFE_REQUIREMENT in requirements and sharedvfs:
876 875 try:
877 876 ui.readconfig(sharedvfs.join(b'hgrc'), root=sharedvfs.base)
878 877 ret = True
879 878 except IOError:
880 879 pass
881 880
882 881 try:
883 882 ui.readconfig(hgvfs.join(b'hgrc'), root=wdirvfs.base)
884 883 ret = True
885 884 except IOError:
886 885 pass
887 886
888 887 try:
889 888 ui.readconfig(hgvfs.join(b'hgrc-not-shared'), root=wdirvfs.base)
890 889 ret = True
891 890 except IOError:
892 891 pass
893 892
894 893 return ret
895 894
896 895
897 896 def afterhgrcload(ui, wdirvfs, hgvfs, requirements):
898 897 """Perform additional actions after .hg/hgrc is loaded.
899 898
900 899 This function is called during repository loading immediately after
901 900 the .hg/hgrc file is loaded and before per-repo extensions are loaded.
902 901
903 902 The function can be used to validate configs, automatically add
904 903 options (including extensions) based on requirements, etc.
905 904 """
906 905
907 906 # Map of requirements to list of extensions to load automatically when
908 907 # requirement is present.
909 908 autoextensions = {
910 909 b'git': [b'git'],
911 910 b'largefiles': [b'largefiles'],
912 911 b'lfs': [b'lfs'],
913 912 }
914 913
915 914 for requirement, names in sorted(autoextensions.items()):
916 915 if requirement not in requirements:
917 916 continue
918 917
919 918 for name in names:
920 919 if not ui.hasconfig(b'extensions', name):
921 920 ui.setconfig(b'extensions', name, b'', source=b'autoload')
922 921
923 922
924 923 def gathersupportedrequirements(ui):
925 924 """Determine the complete set of recognized requirements."""
926 925 # Start with all requirements supported by this file.
927 926 supported = set(localrepository._basesupported)
928 927
929 928 # Execute ``featuresetupfuncs`` entries if they belong to an extension
930 929 # relevant to this ui instance.
931 930 modules = {m.__name__ for n, m in extensions.extensions(ui)}
932 931
933 932 for fn in featuresetupfuncs:
934 933 if fn.__module__ in modules:
935 934 fn(ui, supported)
936 935
937 936 # Add derived requirements from registered compression engines.
938 937 for name in util.compengines:
939 938 engine = util.compengines[name]
940 939 if engine.available() and engine.revlogheader():
941 940 supported.add(b'exp-compression-%s' % name)
942 941 if engine.name() == b'zstd':
943 942 supported.add(requirementsmod.REVLOG_COMPRESSION_ZSTD)
944 943
945 944 return supported
946 945
947 946
948 947 def ensurerequirementsrecognized(requirements, supported):
949 948 """Validate that a set of local requirements is recognized.
950 949
951 950 Receives a set of requirements. Raises an ``error.RepoError`` if there
952 951 exists any requirement in that set that currently loaded code doesn't
953 952 recognize.
954 953
955 954 Returns a set of supported requirements.
956 955 """
957 956 missing = set()
958 957
959 958 for requirement in requirements:
960 959 if requirement in supported:
961 960 continue
962 961
963 962 if not requirement or not requirement[0:1].isalnum():
964 963 raise error.RequirementError(_(b'.hg/requires file is corrupt'))
965 964
966 965 missing.add(requirement)
967 966
968 967 if missing:
969 968 raise error.RequirementError(
970 969 _(b'repository requires features unknown to this Mercurial: %s')
971 970 % b' '.join(sorted(missing)),
972 971 hint=_(
973 972 b'see https://mercurial-scm.org/wiki/MissingRequirement '
974 973 b'for more information'
975 974 ),
976 975 )
977 976
978 977
979 978 def ensurerequirementscompatible(ui, requirements):
980 979 """Validates that a set of recognized requirements is mutually compatible.
981 980
982 981 Some requirements may not be compatible with others or require
983 982 config options that aren't enabled. This function is called during
984 983 repository opening to ensure that the set of requirements needed
985 984 to open a repository is sane and compatible with config options.
986 985
987 986 Extensions can monkeypatch this function to perform additional
988 987 checking.
989 988
990 989 ``error.RepoError`` should be raised on failure.
991 990 """
992 991 if (
993 992 requirementsmod.SPARSE_REQUIREMENT in requirements
994 993 and not sparse.enabled
995 994 ):
996 995 raise error.RepoError(
997 996 _(
998 997 b'repository is using sparse feature but '
999 998 b'sparse is not enabled; enable the '
1000 999 b'"sparse" extensions to access'
1001 1000 )
1002 1001 )
1003 1002
1004 1003
1005 1004 def makestore(requirements, path, vfstype):
1006 1005 """Construct a storage object for a repository."""
1007 1006 if requirementsmod.STORE_REQUIREMENT in requirements:
1008 1007 if requirementsmod.FNCACHE_REQUIREMENT in requirements:
1009 1008 dotencode = requirementsmod.DOTENCODE_REQUIREMENT in requirements
1010 1009 return storemod.fncachestore(path, vfstype, dotencode)
1011 1010
1012 1011 return storemod.encodedstore(path, vfstype)
1013 1012
1014 1013 return storemod.basicstore(path, vfstype)
1015 1014
1016 1015
1017 1016 def resolvestorevfsoptions(ui, requirements, features):
1018 1017 """Resolve the options to pass to the store vfs opener.
1019 1018
1020 1019 The returned dict is used to influence behavior of the storage layer.
1021 1020 """
1022 1021 options = {}
1023 1022
1024 1023 if requirementsmod.TREEMANIFEST_REQUIREMENT in requirements:
1025 1024 options[b'treemanifest'] = True
1026 1025
1027 1026 # experimental config: format.manifestcachesize
1028 1027 manifestcachesize = ui.configint(b'format', b'manifestcachesize')
1029 1028 if manifestcachesize is not None:
1030 1029 options[b'manifestcachesize'] = manifestcachesize
1031 1030
1032 1031 # In the absence of another requirement superseding a revlog-related
1033 1032 # requirement, we have to assume the repo is using revlog version 0.
1034 1033 # This revlog format is super old and we don't bother trying to parse
1035 1034 # opener options for it because those options wouldn't do anything
1036 1035 # meaningful on such old repos.
1037 1036 if (
1038 1037 requirementsmod.REVLOGV1_REQUIREMENT in requirements
1039 1038 or requirementsmod.REVLOGV2_REQUIREMENT in requirements
1040 1039 ):
1041 1040 options.update(resolverevlogstorevfsoptions(ui, requirements, features))
1042 1041 else: # explicitly mark repo as using revlogv0
1043 1042 options[b'revlogv0'] = True
1044 1043
1045 1044 if requirementsmod.COPIESSDC_REQUIREMENT in requirements:
1046 1045 options[b'copies-storage'] = b'changeset-sidedata'
1047 1046 else:
1048 1047 writecopiesto = ui.config(b'experimental', b'copies.write-to')
1049 1048 copiesextramode = (b'changeset-only', b'compatibility')
1050 1049 if writecopiesto in copiesextramode:
1051 1050 options[b'copies-storage'] = b'extra'
1052 1051
1053 1052 return options
1054 1053
1055 1054
1056 1055 def resolverevlogstorevfsoptions(ui, requirements, features):
1057 1056 """Resolve opener options specific to revlogs."""
1058 1057
1059 1058 options = {}
1060 1059 options[b'flagprocessors'] = {}
1061 1060
1062 1061 if requirementsmod.REVLOGV1_REQUIREMENT in requirements:
1063 1062 options[b'revlogv1'] = True
1064 1063 if requirementsmod.REVLOGV2_REQUIREMENT in requirements:
1065 1064 options[b'revlogv2'] = True
1066 1065 if requirementsmod.CHANGELOGV2_REQUIREMENT in requirements:
1067 1066 options[b'changelogv2'] = True
1068 1067 cmp_rank = ui.configbool(b'experimental', b'changelog-v2.compute-rank')
1069 1068 options[b'changelogv2.compute-rank'] = cmp_rank
1070 1069
1071 1070 if requirementsmod.GENERALDELTA_REQUIREMENT in requirements:
1072 1071 options[b'generaldelta'] = True
1073 1072
1074 1073 # experimental config: format.chunkcachesize
1075 1074 chunkcachesize = ui.configint(b'format', b'chunkcachesize')
1076 1075 if chunkcachesize is not None:
1077 1076 options[b'chunkcachesize'] = chunkcachesize
1078 1077
1079 1078 deltabothparents = ui.configbool(
1080 1079 b'storage', b'revlog.optimize-delta-parent-choice'
1081 1080 )
1082 1081 options[b'deltabothparents'] = deltabothparents
1083 1082 dps_cgds = ui.configint(
1084 1083 b'storage',
1085 1084 b'revlog.delta-parent-search.candidate-group-chunk-size',
1086 1085 )
1087 1086 options[b'delta-parent-search.candidate-group-chunk-size'] = dps_cgds
1088 1087 options[b'debug-delta'] = ui.configbool(b'debug', b'revlog.debug-delta')
1089 1088
1090 1089 issue6528 = ui.configbool(b'storage', b'revlog.issue6528.fix-incoming')
1091 1090 options[b'issue6528.fix-incoming'] = issue6528
1092 1091
1093 1092 lazydelta = ui.configbool(b'storage', b'revlog.reuse-external-delta')
1094 1093 lazydeltabase = False
1095 1094 if lazydelta:
1096 1095 lazydeltabase = ui.configbool(
1097 1096 b'storage', b'revlog.reuse-external-delta-parent'
1098 1097 )
1099 1098 if lazydeltabase is None:
1100 1099 lazydeltabase = not scmutil.gddeltaconfig(ui)
1101 1100 options[b'lazydelta'] = lazydelta
1102 1101 options[b'lazydeltabase'] = lazydeltabase
1103 1102
1104 1103 chainspan = ui.configbytes(b'experimental', b'maxdeltachainspan')
1105 1104 if 0 <= chainspan:
1106 1105 options[b'maxdeltachainspan'] = chainspan
1107 1106
1108 1107 mmapindexthreshold = ui.configbytes(b'experimental', b'mmapindexthreshold')
1109 1108 if mmapindexthreshold is not None:
1110 1109 options[b'mmapindexthreshold'] = mmapindexthreshold
1111 1110
1112 1111 withsparseread = ui.configbool(b'experimental', b'sparse-read')
1113 1112 srdensitythres = float(
1114 1113 ui.config(b'experimental', b'sparse-read.density-threshold')
1115 1114 )
1116 1115 srmingapsize = ui.configbytes(b'experimental', b'sparse-read.min-gap-size')
1117 1116 options[b'with-sparse-read'] = withsparseread
1118 1117 options[b'sparse-read-density-threshold'] = srdensitythres
1119 1118 options[b'sparse-read-min-gap-size'] = srmingapsize
1120 1119
1121 1120 sparserevlog = requirementsmod.SPARSEREVLOG_REQUIREMENT in requirements
1122 1121 options[b'sparse-revlog'] = sparserevlog
1123 1122 if sparserevlog:
1124 1123 options[b'generaldelta'] = True
1125 1124
1126 1125 maxchainlen = None
1127 1126 if sparserevlog:
1128 1127 maxchainlen = revlogconst.SPARSE_REVLOG_MAX_CHAIN_LENGTH
1129 1128 # experimental config: format.maxchainlen
1130 1129 maxchainlen = ui.configint(b'format', b'maxchainlen', maxchainlen)
1131 1130 if maxchainlen is not None:
1132 1131 options[b'maxchainlen'] = maxchainlen
1133 1132
1134 1133 for r in requirements:
1135 1134 # we allow multiple compression engine requirement to co-exist because
1136 1135 # strickly speaking, revlog seems to support mixed compression style.
1137 1136 #
1138 1137 # The compression used for new entries will be "the last one"
1139 1138 prefix = r.startswith
1140 1139 if prefix(b'revlog-compression-') or prefix(b'exp-compression-'):
1141 1140 options[b'compengine'] = r.split(b'-', 2)[2]
1142 1141
1143 1142 options[b'zlib.level'] = ui.configint(b'storage', b'revlog.zlib.level')
1144 1143 if options[b'zlib.level'] is not None:
1145 1144 if not (0 <= options[b'zlib.level'] <= 9):
1146 1145 msg = _(b'invalid value for `storage.revlog.zlib.level` config: %d')
1147 1146 raise error.Abort(msg % options[b'zlib.level'])
1148 1147 options[b'zstd.level'] = ui.configint(b'storage', b'revlog.zstd.level')
1149 1148 if options[b'zstd.level'] is not None:
1150 1149 if not (0 <= options[b'zstd.level'] <= 22):
1151 1150 msg = _(b'invalid value for `storage.revlog.zstd.level` config: %d')
1152 1151 raise error.Abort(msg % options[b'zstd.level'])
1153 1152
1154 1153 if requirementsmod.NARROW_REQUIREMENT in requirements:
1155 1154 options[b'enableellipsis'] = True
1156 1155
1157 1156 if ui.configbool(b'experimental', b'rust.index'):
1158 1157 options[b'rust.index'] = True
1159 1158 if requirementsmod.NODEMAP_REQUIREMENT in requirements:
1160 1159 slow_path = ui.config(
1161 1160 b'storage', b'revlog.persistent-nodemap.slow-path'
1162 1161 )
1163 1162 if slow_path not in (b'allow', b'warn', b'abort'):
1164 1163 default = ui.config_default(
1165 1164 b'storage', b'revlog.persistent-nodemap.slow-path'
1166 1165 )
1167 1166 msg = _(
1168 1167 b'unknown value for config '
1169 1168 b'"storage.revlog.persistent-nodemap.slow-path": "%s"\n'
1170 1169 )
1171 1170 ui.warn(msg % slow_path)
1172 1171 if not ui.quiet:
1173 1172 ui.warn(_(b'falling back to default value: %s\n') % default)
1174 1173 slow_path = default
1175 1174
1176 1175 msg = _(
1177 1176 b"accessing `persistent-nodemap` repository without associated "
1178 1177 b"fast implementation."
1179 1178 )
1180 1179 hint = _(
1181 1180 b"check `hg help config.format.use-persistent-nodemap` "
1182 1181 b"for details"
1183 1182 )
1184 1183 if not revlog.HAS_FAST_PERSISTENT_NODEMAP:
1185 1184 if slow_path == b'warn':
1186 1185 msg = b"warning: " + msg + b'\n'
1187 1186 ui.warn(msg)
1188 1187 if not ui.quiet:
1189 1188 hint = b'(' + hint + b')\n'
1190 1189 ui.warn(hint)
1191 1190 if slow_path == b'abort':
1192 1191 raise error.Abort(msg, hint=hint)
1193 1192 options[b'persistent-nodemap'] = True
1194 1193 if requirementsmod.DIRSTATE_V2_REQUIREMENT in requirements:
1195 1194 slow_path = ui.config(b'storage', b'dirstate-v2.slow-path')
1196 1195 if slow_path not in (b'allow', b'warn', b'abort'):
1197 1196 default = ui.config_default(b'storage', b'dirstate-v2.slow-path')
1198 1197 msg = _(b'unknown value for config "dirstate-v2.slow-path": "%s"\n')
1199 1198 ui.warn(msg % slow_path)
1200 1199 if not ui.quiet:
1201 1200 ui.warn(_(b'falling back to default value: %s\n') % default)
1202 1201 slow_path = default
1203 1202
1204 1203 msg = _(
1205 1204 b"accessing `dirstate-v2` repository without associated "
1206 1205 b"fast implementation."
1207 1206 )
1208 1207 hint = _(
1209 1208 b"check `hg help config.format.use-dirstate-v2` " b"for details"
1210 1209 )
1211 1210 if not dirstate.HAS_FAST_DIRSTATE_V2:
1212 1211 if slow_path == b'warn':
1213 1212 msg = b"warning: " + msg + b'\n'
1214 1213 ui.warn(msg)
1215 1214 if not ui.quiet:
1216 1215 hint = b'(' + hint + b')\n'
1217 1216 ui.warn(hint)
1218 1217 if slow_path == b'abort':
1219 1218 raise error.Abort(msg, hint=hint)
1220 1219 if ui.configbool(b'storage', b'revlog.persistent-nodemap.mmap'):
1221 1220 options[b'persistent-nodemap.mmap'] = True
1222 1221 if ui.configbool(b'devel', b'persistent-nodemap'):
1223 1222 options[b'devel-force-nodemap'] = True
1224 1223
1225 1224 return options
1226 1225
1227 1226
1228 1227 def makemain(**kwargs):
1229 1228 """Produce a type conforming to ``ilocalrepositorymain``."""
1230 1229 return localrepository
1231 1230
1232 1231
1233 1232 @interfaceutil.implementer(repository.ilocalrepositoryfilestorage)
1234 1233 class revlogfilestorage:
1235 1234 """File storage when using revlogs."""
1236 1235
1237 1236 def file(self, path):
1238 1237 if path.startswith(b'/'):
1239 1238 path = path[1:]
1240 1239
1241 1240 return filelog.filelog(self.svfs, path)
1242 1241
1243 1242
1244 1243 @interfaceutil.implementer(repository.ilocalrepositoryfilestorage)
1245 1244 class revlognarrowfilestorage:
1246 1245 """File storage when using revlogs and narrow files."""
1247 1246
1248 1247 def file(self, path):
1249 1248 if path.startswith(b'/'):
1250 1249 path = path[1:]
1251 1250
1252 1251 return filelog.narrowfilelog(self.svfs, path, self._storenarrowmatch)
1253 1252
1254 1253
1255 1254 def makefilestorage(requirements, features, **kwargs):
1256 1255 """Produce a type conforming to ``ilocalrepositoryfilestorage``."""
1257 1256 features.add(repository.REPO_FEATURE_REVLOG_FILE_STORAGE)
1258 1257 features.add(repository.REPO_FEATURE_STREAM_CLONE)
1259 1258
1260 1259 if requirementsmod.NARROW_REQUIREMENT in requirements:
1261 1260 return revlognarrowfilestorage
1262 1261 else:
1263 1262 return revlogfilestorage
1264 1263
1265 1264
1266 1265 # List of repository interfaces and factory functions for them. Each
1267 1266 # will be called in order during ``makelocalrepository()`` to iteratively
1268 1267 # derive the final type for a local repository instance. We capture the
1269 1268 # function as a lambda so we don't hold a reference and the module-level
1270 1269 # functions can be wrapped.
1271 1270 REPO_INTERFACES = [
1272 1271 (repository.ilocalrepositorymain, lambda: makemain),
1273 1272 (repository.ilocalrepositoryfilestorage, lambda: makefilestorage),
1274 1273 ]
1275 1274
1276 1275
1277 1276 @interfaceutil.implementer(repository.ilocalrepositorymain)
1278 1277 class localrepository:
1279 1278 """Main class for representing local repositories.
1280 1279
1281 1280 All local repositories are instances of this class.
1282 1281
1283 1282 Constructed on its own, instances of this class are not usable as
1284 1283 repository objects. To obtain a usable repository object, call
1285 1284 ``hg.repository()``, ``localrepo.instance()``, or
1286 1285 ``localrepo.makelocalrepository()``. The latter is the lowest-level.
1287 1286 ``instance()`` adds support for creating new repositories.
1288 1287 ``hg.repository()`` adds more extension integration, including calling
1289 1288 ``reposetup()``. Generally speaking, ``hg.repository()`` should be
1290 1289 used.
1291 1290 """
1292 1291
1293 1292 _basesupported = {
1294 1293 requirementsmod.ARCHIVED_PHASE_REQUIREMENT,
1295 1294 requirementsmod.BOOKMARKS_IN_STORE_REQUIREMENT,
1296 1295 requirementsmod.CHANGELOGV2_REQUIREMENT,
1297 1296 requirementsmod.COPIESSDC_REQUIREMENT,
1298 1297 requirementsmod.DIRSTATE_TRACKED_HINT_V1,
1299 1298 requirementsmod.DIRSTATE_V2_REQUIREMENT,
1300 1299 requirementsmod.DOTENCODE_REQUIREMENT,
1301 1300 requirementsmod.FNCACHE_REQUIREMENT,
1302 1301 requirementsmod.GENERALDELTA_REQUIREMENT,
1303 1302 requirementsmod.INTERNAL_PHASE_REQUIREMENT,
1304 1303 requirementsmod.NODEMAP_REQUIREMENT,
1305 1304 requirementsmod.RELATIVE_SHARED_REQUIREMENT,
1306 1305 requirementsmod.REVLOGV1_REQUIREMENT,
1307 1306 requirementsmod.REVLOGV2_REQUIREMENT,
1308 1307 requirementsmod.SHARED_REQUIREMENT,
1309 1308 requirementsmod.SHARESAFE_REQUIREMENT,
1310 1309 requirementsmod.SPARSE_REQUIREMENT,
1311 1310 requirementsmod.SPARSEREVLOG_REQUIREMENT,
1312 1311 requirementsmod.STORE_REQUIREMENT,
1313 1312 requirementsmod.TREEMANIFEST_REQUIREMENT,
1314 1313 }
1315 1314
1316 1315 # list of prefix for file which can be written without 'wlock'
1317 1316 # Extensions should extend this list when needed
1318 1317 _wlockfreeprefix = {
1319 1318 # We migh consider requiring 'wlock' for the next
1320 1319 # two, but pretty much all the existing code assume
1321 1320 # wlock is not needed so we keep them excluded for
1322 1321 # now.
1323 1322 b'hgrc',
1324 1323 b'requires',
1325 1324 # XXX cache is a complicatged business someone
1326 1325 # should investigate this in depth at some point
1327 1326 b'cache/',
1328 1327 # XXX shouldn't be dirstate covered by the wlock?
1329 1328 b'dirstate',
1330 1329 # XXX bisect was still a bit too messy at the time
1331 1330 # this changeset was introduced. Someone should fix
1332 1331 # the remainig bit and drop this line
1333 1332 b'bisect.state',
1334 1333 }
1335 1334
1336 1335 def __init__(
1337 1336 self,
1338 1337 baseui,
1339 1338 ui,
1340 1339 origroot: bytes,
1341 1340 wdirvfs: vfsmod.vfs,
1342 1341 hgvfs: vfsmod.vfs,
1343 1342 requirements,
1344 1343 supportedrequirements,
1345 1344 sharedpath: bytes,
1346 1345 store,
1347 1346 cachevfs: vfsmod.vfs,
1348 1347 wcachevfs: vfsmod.vfs,
1349 1348 features,
1350 1349 intents=None,
1351 1350 ):
1352 1351 """Create a new local repository instance.
1353 1352
1354 1353 Most callers should use ``hg.repository()``, ``localrepo.instance()``,
1355 1354 or ``localrepo.makelocalrepository()`` for obtaining a new repository
1356 1355 object.
1357 1356
1358 1357 Arguments:
1359 1358
1360 1359 baseui
1361 1360 ``ui.ui`` instance that ``ui`` argument was based off of.
1362 1361
1363 1362 ui
1364 1363 ``ui.ui`` instance for use by the repository.
1365 1364
1366 1365 origroot
1367 1366 ``bytes`` path to working directory root of this repository.
1368 1367
1369 1368 wdirvfs
1370 1369 ``vfs.vfs`` rooted at the working directory.
1371 1370
1372 1371 hgvfs
1373 1372 ``vfs.vfs`` rooted at .hg/
1374 1373
1375 1374 requirements
1376 1375 ``set`` of bytestrings representing repository opening requirements.
1377 1376
1378 1377 supportedrequirements
1379 1378 ``set`` of bytestrings representing repository requirements that we
1380 1379 know how to open. May be a supetset of ``requirements``.
1381 1380
1382 1381 sharedpath
1383 1382 ``bytes`` Defining path to storage base directory. Points to a
1384 1383 ``.hg/`` directory somewhere.
1385 1384
1386 1385 store
1387 1386 ``store.basicstore`` (or derived) instance providing access to
1388 1387 versioned storage.
1389 1388
1390 1389 cachevfs
1391 1390 ``vfs.vfs`` used for cache files.
1392 1391
1393 1392 wcachevfs
1394 1393 ``vfs.vfs`` used for cache files related to the working copy.
1395 1394
1396 1395 features
1397 1396 ``set`` of bytestrings defining features/capabilities of this
1398 1397 instance.
1399 1398
1400 1399 intents
1401 1400 ``set`` of system strings indicating what this repo will be used
1402 1401 for.
1403 1402 """
1404 1403 self.baseui = baseui
1405 1404 self.ui = ui
1406 1405 self.origroot = origroot
1407 1406 # vfs rooted at working directory.
1408 1407 self.wvfs = wdirvfs
1409 1408 self.root = wdirvfs.base
1410 1409 # vfs rooted at .hg/. Used to access most non-store paths.
1411 1410 self.vfs = hgvfs
1412 1411 self.path = hgvfs.base
1413 1412 self.requirements = requirements
1414 1413 self.nodeconstants = sha1nodeconstants
1415 1414 self.nullid = self.nodeconstants.nullid
1416 1415 self.supported = supportedrequirements
1417 1416 self.sharedpath = sharedpath
1418 1417 self.store = store
1419 1418 self.cachevfs = cachevfs
1420 1419 self.wcachevfs = wcachevfs
1421 1420 self.features = features
1422 1421
1423 1422 self.filtername = None
1424 1423
1425 1424 if self.ui.configbool(b'devel', b'all-warnings') or self.ui.configbool(
1426 1425 b'devel', b'check-locks'
1427 1426 ):
1428 1427 self.vfs.audit = self._getvfsward(self.vfs.audit)
1429 1428 # A list of callback to shape the phase if no data were found.
1430 1429 # Callback are in the form: func(repo, roots) --> processed root.
1431 1430 # This list it to be filled by extension during repo setup
1432 1431 self._phasedefaults = []
1433 1432
1434 1433 color.setup(self.ui)
1435 1434
1436 1435 self.spath = self.store.path
1437 1436 self.svfs = self.store.vfs
1438 1437 self.sjoin = self.store.join
1439 1438 if self.ui.configbool(b'devel', b'all-warnings') or self.ui.configbool(
1440 1439 b'devel', b'check-locks'
1441 1440 ):
1442 1441 if util.safehasattr(self.svfs, b'vfs'): # this is filtervfs
1443 1442 self.svfs.vfs.audit = self._getsvfsward(self.svfs.vfs.audit)
1444 1443 else: # standard vfs
1445 1444 self.svfs.audit = self._getsvfsward(self.svfs.audit)
1446 1445
1447 1446 self._dirstatevalidatewarned = False
1448 1447
1449 1448 self._branchcaches = branchmap.BranchMapCache()
1450 1449 self._revbranchcache = None
1451 1450 self._filterpats = {}
1452 1451 self._datafilters = {}
1453 1452 self._transref = self._lockref = self._wlockref = None
1454 1453
1455 1454 # A cache for various files under .hg/ that tracks file changes,
1456 1455 # (used by the filecache decorator)
1457 1456 #
1458 1457 # Maps a property name to its util.filecacheentry
1459 1458 self._filecache = {}
1460 1459
1461 1460 # hold sets of revision to be filtered
1462 1461 # should be cleared when something might have changed the filter value:
1463 1462 # - new changesets,
1464 1463 # - phase change,
1465 1464 # - new obsolescence marker,
1466 1465 # - working directory parent change,
1467 1466 # - bookmark changes
1468 1467 self.filteredrevcache = {}
1469 1468
1470 1469 # post-dirstate-status hooks
1471 1470 self._postdsstatus = []
1472 1471
1473 1472 # generic mapping between names and nodes
1474 1473 self.names = namespaces.namespaces()
1475 1474
1476 1475 # Key to signature value.
1477 1476 self._sparsesignaturecache = {}
1478 1477 # Signature to cached matcher instance.
1479 1478 self._sparsematchercache = {}
1480 1479
1481 1480 self._extrafilterid = repoview.extrafilter(ui)
1482 1481
1483 1482 self.filecopiesmode = None
1484 1483 if requirementsmod.COPIESSDC_REQUIREMENT in self.requirements:
1485 1484 self.filecopiesmode = b'changeset-sidedata'
1486 1485
1487 1486 self._wanted_sidedata = set()
1488 1487 self._sidedata_computers = {}
1489 1488 sidedatamod.set_sidedata_spec_for_repo(self)
1490 1489
1491 1490 def _getvfsward(self, origfunc):
1492 1491 """build a ward for self.vfs"""
1493 1492 rref = weakref.ref(self)
1494 1493
1495 1494 def checkvfs(path, mode=None):
1496 1495 ret = origfunc(path, mode=mode)
1497 1496 repo = rref()
1498 1497 if (
1499 1498 repo is None
1500 1499 or not util.safehasattr(repo, b'_wlockref')
1501 1500 or not util.safehasattr(repo, b'_lockref')
1502 1501 ):
1503 1502 return
1504 1503 if mode in (None, b'r', b'rb'):
1505 1504 return
1506 1505 if path.startswith(repo.path):
1507 1506 # truncate name relative to the repository (.hg)
1508 1507 path = path[len(repo.path) + 1 :]
1509 1508 if path.startswith(b'cache/'):
1510 1509 msg = b'accessing cache with vfs instead of cachevfs: "%s"'
1511 1510 repo.ui.develwarn(msg % path, stacklevel=3, config=b"cache-vfs")
1512 1511 # path prefixes covered by 'lock'
1513 1512 vfs_path_prefixes = (
1514 1513 b'journal.',
1515 1514 b'undo.',
1516 1515 b'strip-backup/',
1517 1516 b'cache/',
1518 1517 )
1519 1518 if any(path.startswith(prefix) for prefix in vfs_path_prefixes):
1520 1519 if repo._currentlock(repo._lockref) is None:
1521 1520 repo.ui.develwarn(
1522 1521 b'write with no lock: "%s"' % path,
1523 1522 stacklevel=3,
1524 1523 config=b'check-locks',
1525 1524 )
1526 1525 elif repo._currentlock(repo._wlockref) is None:
1527 1526 # rest of vfs files are covered by 'wlock'
1528 1527 #
1529 1528 # exclude special files
1530 1529 for prefix in self._wlockfreeprefix:
1531 1530 if path.startswith(prefix):
1532 1531 return
1533 1532 repo.ui.develwarn(
1534 1533 b'write with no wlock: "%s"' % path,
1535 1534 stacklevel=3,
1536 1535 config=b'check-locks',
1537 1536 )
1538 1537 return ret
1539 1538
1540 1539 return checkvfs
1541 1540
1542 1541 def _getsvfsward(self, origfunc):
1543 1542 """build a ward for self.svfs"""
1544 1543 rref = weakref.ref(self)
1545 1544
1546 1545 def checksvfs(path, mode=None):
1547 1546 ret = origfunc(path, mode=mode)
1548 1547 repo = rref()
1549 1548 if repo is None or not util.safehasattr(repo, b'_lockref'):
1550 1549 return
1551 1550 if mode in (None, b'r', b'rb'):
1552 1551 return
1553 1552 if path.startswith(repo.sharedpath):
1554 1553 # truncate name relative to the repository (.hg)
1555 1554 path = path[len(repo.sharedpath) + 1 :]
1556 1555 if repo._currentlock(repo._lockref) is None:
1557 1556 repo.ui.develwarn(
1558 1557 b'write with no lock: "%s"' % path, stacklevel=4
1559 1558 )
1560 1559 return ret
1561 1560
1562 1561 return checksvfs
1563 1562
1564 1563 def close(self):
1565 1564 self._writecaches()
1566 1565
1567 1566 def _writecaches(self):
1568 1567 if self._revbranchcache:
1569 1568 self._revbranchcache.write()
1570 1569
1571 1570 def _restrictcapabilities(self, caps):
1572 1571 if self.ui.configbool(b'experimental', b'bundle2-advertise'):
1573 1572 caps = set(caps)
1574 1573 capsblob = bundle2.encodecaps(
1575 1574 bundle2.getrepocaps(self, role=b'client')
1576 1575 )
1577 1576 caps.add(b'bundle2=' + urlreq.quote(capsblob))
1578 1577 if self.ui.configbool(b'experimental', b'narrow'):
1579 1578 caps.add(wireprototypes.NARROWCAP)
1580 1579 return caps
1581 1580
1582 1581 # Don't cache auditor/nofsauditor, or you'll end up with reference cycle:
1583 1582 # self -> auditor -> self._checknested -> self
1584 1583
1585 1584 @property
1586 1585 def auditor(self):
1587 1586 # This is only used by context.workingctx.match in order to
1588 1587 # detect files in subrepos.
1589 1588 return pathutil.pathauditor(self.root, callback=self._checknested)
1590 1589
1591 1590 @property
1592 1591 def nofsauditor(self):
1593 1592 # This is only used by context.basectx.match in order to detect
1594 1593 # files in subrepos.
1595 1594 return pathutil.pathauditor(
1596 1595 self.root, callback=self._checknested, realfs=False, cached=True
1597 1596 )
1598 1597
1599 1598 def _checknested(self, path):
1600 1599 """Determine if path is a legal nested repository."""
1601 1600 if not path.startswith(self.root):
1602 1601 return False
1603 1602 subpath = path[len(self.root) + 1 :]
1604 1603 normsubpath = util.pconvert(subpath)
1605 1604
1606 1605 # XXX: Checking against the current working copy is wrong in
1607 1606 # the sense that it can reject things like
1608 1607 #
1609 1608 # $ hg cat -r 10 sub/x.txt
1610 1609 #
1611 1610 # if sub/ is no longer a subrepository in the working copy
1612 1611 # parent revision.
1613 1612 #
1614 1613 # However, it can of course also allow things that would have
1615 1614 # been rejected before, such as the above cat command if sub/
1616 1615 # is a subrepository now, but was a normal directory before.
1617 1616 # The old path auditor would have rejected by mistake since it
1618 1617 # panics when it sees sub/.hg/.
1619 1618 #
1620 1619 # All in all, checking against the working copy seems sensible
1621 1620 # since we want to prevent access to nested repositories on
1622 1621 # the filesystem *now*.
1623 1622 ctx = self[None]
1624 1623 parts = util.splitpath(subpath)
1625 1624 while parts:
1626 1625 prefix = b'/'.join(parts)
1627 1626 if prefix in ctx.substate:
1628 1627 if prefix == normsubpath:
1629 1628 return True
1630 1629 else:
1631 1630 sub = ctx.sub(prefix)
1632 1631 return sub.checknested(subpath[len(prefix) + 1 :])
1633 1632 else:
1634 1633 parts.pop()
1635 1634 return False
1636 1635
1637 1636 def peer(self):
1638 1637 return localpeer(self) # not cached to avoid reference cycle
1639 1638
1640 1639 def unfiltered(self):
1641 1640 """Return unfiltered version of the repository
1642 1641
1643 1642 Intended to be overwritten by filtered repo."""
1644 1643 return self
1645 1644
1646 1645 def filtered(self, name, visibilityexceptions=None):
1647 1646 """Return a filtered version of a repository
1648 1647
1649 1648 The `name` parameter is the identifier of the requested view. This
1650 1649 will return a repoview object set "exactly" to the specified view.
1651 1650
1652 1651 This function does not apply recursive filtering to a repository. For
1653 1652 example calling `repo.filtered("served")` will return a repoview using
1654 1653 the "served" view, regardless of the initial view used by `repo`.
1655 1654
1656 1655 In other word, there is always only one level of `repoview` "filtering".
1657 1656 """
1658 1657 if self._extrafilterid is not None and b'%' not in name:
1659 1658 name = name + b'%' + self._extrafilterid
1660 1659
1661 1660 cls = repoview.newtype(self.unfiltered().__class__)
1662 1661 return cls(self, name, visibilityexceptions)
1663 1662
1664 1663 @mixedrepostorecache(
1665 1664 (b'bookmarks', b'plain'),
1666 1665 (b'bookmarks.current', b'plain'),
1667 1666 (b'bookmarks', b''),
1668 1667 (b'00changelog.i', b''),
1669 1668 )
1670 1669 def _bookmarks(self):
1671 1670 # Since the multiple files involved in the transaction cannot be
1672 1671 # written atomically (with current repository format), there is a race
1673 1672 # condition here.
1674 1673 #
1675 1674 # 1) changelog content A is read
1676 1675 # 2) outside transaction update changelog to content B
1677 1676 # 3) outside transaction update bookmark file referring to content B
1678 1677 # 4) bookmarks file content is read and filtered against changelog-A
1679 1678 #
1680 1679 # When this happens, bookmarks against nodes missing from A are dropped.
1681 1680 #
1682 1681 # Having this happening during read is not great, but it become worse
1683 1682 # when this happen during write because the bookmarks to the "unknown"
1684 1683 # nodes will be dropped for good. However, writes happen within locks.
1685 1684 # This locking makes it possible to have a race free consistent read.
1686 1685 # For this purpose data read from disc before locking are
1687 1686 # "invalidated" right after the locks are taken. This invalidations are
1688 1687 # "light", the `filecache` mechanism keep the data in memory and will
1689 1688 # reuse them if the underlying files did not changed. Not parsing the
1690 1689 # same data multiple times helps performances.
1691 1690 #
1692 1691 # Unfortunately in the case describe above, the files tracked by the
1693 1692 # bookmarks file cache might not have changed, but the in-memory
1694 1693 # content is still "wrong" because we used an older changelog content
1695 1694 # to process the on-disk data. So after locking, the changelog would be
1696 1695 # refreshed but `_bookmarks` would be preserved.
1697 1696 # Adding `00changelog.i` to the list of tracked file is not
1698 1697 # enough, because at the time we build the content for `_bookmarks` in
1699 1698 # (4), the changelog file has already diverged from the content used
1700 1699 # for loading `changelog` in (1)
1701 1700 #
1702 1701 # To prevent the issue, we force the changelog to be explicitly
1703 1702 # reloaded while computing `_bookmarks`. The data race can still happen
1704 1703 # without the lock (with a narrower window), but it would no longer go
1705 1704 # undetected during the lock time refresh.
1706 1705 #
1707 1706 # The new schedule is as follow
1708 1707 #
1709 1708 # 1) filecache logic detect that `_bookmarks` needs to be computed
1710 1709 # 2) cachestat for `bookmarks` and `changelog` are captured (for book)
1711 1710 # 3) We force `changelog` filecache to be tested
1712 1711 # 4) cachestat for `changelog` are captured (for changelog)
1713 1712 # 5) `_bookmarks` is computed and cached
1714 1713 #
1715 1714 # The step in (3) ensure we have a changelog at least as recent as the
1716 1715 # cache stat computed in (1). As a result at locking time:
1717 1716 # * if the changelog did not changed since (1) -> we can reuse the data
1718 1717 # * otherwise -> the bookmarks get refreshed.
1719 1718 self._refreshchangelog()
1720 1719 return bookmarks.bmstore(self)
1721 1720
1722 1721 def _refreshchangelog(self):
1723 1722 """make sure the in memory changelog match the on-disk one"""
1724 1723 if 'changelog' in vars(self) and self.currenttransaction() is None:
1725 1724 del self.changelog
1726 1725
1727 1726 @property
1728 1727 def _activebookmark(self):
1729 1728 return self._bookmarks.active
1730 1729
1731 1730 # _phasesets depend on changelog. what we need is to call
1732 1731 # _phasecache.invalidate() if '00changelog.i' was changed, but it
1733 1732 # can't be easily expressed in filecache mechanism.
1734 1733 @storecache(b'phaseroots', b'00changelog.i')
1735 1734 def _phasecache(self):
1736 1735 return phases.phasecache(self, self._phasedefaults)
1737 1736
1738 1737 @storecache(b'obsstore')
1739 1738 def obsstore(self):
1740 1739 return obsolete.makestore(self.ui, self)
1741 1740
1742 1741 @changelogcache()
1743 1742 def changelog(repo):
1744 1743 # load dirstate before changelog to avoid race see issue6303
1745 1744 repo.dirstate.prefetch_parents()
1746 1745 return repo.store.changelog(
1747 1746 txnutil.mayhavepending(repo.root),
1748 1747 concurrencychecker=revlogchecker.get_checker(repo.ui, b'changelog'),
1749 1748 )
1750 1749
1751 1750 @manifestlogcache()
1752 1751 def manifestlog(self):
1753 1752 return self.store.manifestlog(self, self._storenarrowmatch)
1754 1753
1755 1754 @repofilecache(b'dirstate')
1756 1755 def dirstate(self):
1757 1756 return self._makedirstate()
1758 1757
1759 1758 def _makedirstate(self):
1760 1759 """Extension point for wrapping the dirstate per-repo."""
1761 1760 sparsematchfn = None
1762 1761 if sparse.use_sparse(self):
1763 1762 sparsematchfn = lambda: sparse.matcher(self)
1764 1763 v2_req = requirementsmod.DIRSTATE_V2_REQUIREMENT
1765 1764 th = requirementsmod.DIRSTATE_TRACKED_HINT_V1
1766 1765 use_dirstate_v2 = v2_req in self.requirements
1767 1766 use_tracked_hint = th in self.requirements
1768 1767
1769 1768 return dirstate.dirstate(
1770 1769 self.vfs,
1771 1770 self.ui,
1772 1771 self.root,
1773 1772 self._dirstatevalidate,
1774 1773 sparsematchfn,
1775 1774 self.nodeconstants,
1776 1775 use_dirstate_v2,
1777 1776 use_tracked_hint=use_tracked_hint,
1778 1777 )
1779 1778
1780 1779 def _dirstatevalidate(self, node):
1781 1780 try:
1782 1781 self.changelog.rev(node)
1783 1782 return node
1784 1783 except error.LookupError:
1785 1784 if not self._dirstatevalidatewarned:
1786 1785 self._dirstatevalidatewarned = True
1787 1786 self.ui.warn(
1788 1787 _(b"warning: ignoring unknown working parent %s!\n")
1789 1788 % short(node)
1790 1789 )
1791 1790 return self.nullid
1792 1791
1793 1792 @storecache(narrowspec.FILENAME)
1794 1793 def narrowpats(self):
1795 1794 """matcher patterns for this repository's narrowspec
1796 1795
1797 1796 A tuple of (includes, excludes).
1798 1797 """
1799 1798 return narrowspec.load(self)
1800 1799
1801 1800 @storecache(narrowspec.FILENAME)
1802 1801 def _storenarrowmatch(self):
1803 1802 if requirementsmod.NARROW_REQUIREMENT not in self.requirements:
1804 1803 return matchmod.always()
1805 1804 include, exclude = self.narrowpats
1806 1805 return narrowspec.match(self.root, include=include, exclude=exclude)
1807 1806
1808 1807 @storecache(narrowspec.FILENAME)
1809 1808 def _narrowmatch(self):
1810 1809 if requirementsmod.NARROW_REQUIREMENT not in self.requirements:
1811 1810 return matchmod.always()
1812 1811 narrowspec.checkworkingcopynarrowspec(self)
1813 1812 include, exclude = self.narrowpats
1814 1813 return narrowspec.match(self.root, include=include, exclude=exclude)
1815 1814
1816 1815 def narrowmatch(self, match=None, includeexact=False):
1817 1816 """matcher corresponding the the repo's narrowspec
1818 1817
1819 1818 If `match` is given, then that will be intersected with the narrow
1820 1819 matcher.
1821 1820
1822 1821 If `includeexact` is True, then any exact matches from `match` will
1823 1822 be included even if they're outside the narrowspec.
1824 1823 """
1825 1824 if match:
1826 1825 if includeexact and not self._narrowmatch.always():
1827 1826 # do not exclude explicitly-specified paths so that they can
1828 1827 # be warned later on
1829 1828 em = matchmod.exact(match.files())
1830 1829 nm = matchmod.unionmatcher([self._narrowmatch, em])
1831 1830 return matchmod.intersectmatchers(match, nm)
1832 1831 return matchmod.intersectmatchers(match, self._narrowmatch)
1833 1832 return self._narrowmatch
1834 1833
1835 1834 def setnarrowpats(self, newincludes, newexcludes):
1836 1835 narrowspec.save(self, newincludes, newexcludes)
1837 1836 self.invalidate(clearfilecache=True)
1838 1837
1839 1838 @unfilteredpropertycache
1840 1839 def _quick_access_changeid_null(self):
1841 1840 return {
1842 1841 b'null': (nullrev, self.nodeconstants.nullid),
1843 1842 nullrev: (nullrev, self.nodeconstants.nullid),
1844 1843 self.nullid: (nullrev, self.nullid),
1845 1844 }
1846 1845
1847 1846 @unfilteredpropertycache
1848 1847 def _quick_access_changeid_wc(self):
1849 1848 # also fast path access to the working copy parents
1850 1849 # however, only do it for filter that ensure wc is visible.
1851 1850 quick = self._quick_access_changeid_null.copy()
1852 1851 cl = self.unfiltered().changelog
1853 1852 for node in self.dirstate.parents():
1854 1853 if node == self.nullid:
1855 1854 continue
1856 1855 rev = cl.index.get_rev(node)
1857 1856 if rev is None:
1858 1857 # unknown working copy parent case:
1859 1858 #
1860 1859 # skip the fast path and let higher code deal with it
1861 1860 continue
1862 1861 pair = (rev, node)
1863 1862 quick[rev] = pair
1864 1863 quick[node] = pair
1865 1864 # also add the parents of the parents
1866 1865 for r in cl.parentrevs(rev):
1867 1866 if r == nullrev:
1868 1867 continue
1869 1868 n = cl.node(r)
1870 1869 pair = (r, n)
1871 1870 quick[r] = pair
1872 1871 quick[n] = pair
1873 1872 p1node = self.dirstate.p1()
1874 1873 if p1node != self.nullid:
1875 1874 quick[b'.'] = quick[p1node]
1876 1875 return quick
1877 1876
1878 1877 @unfilteredmethod
1879 1878 def _quick_access_changeid_invalidate(self):
1880 1879 if '_quick_access_changeid_wc' in vars(self):
1881 1880 del self.__dict__['_quick_access_changeid_wc']
1882 1881
1883 1882 @property
1884 1883 def _quick_access_changeid(self):
1885 1884 """an helper dictionnary for __getitem__ calls
1886 1885
1887 1886 This contains a list of symbol we can recognise right away without
1888 1887 further processing.
1889 1888 """
1890 1889 if self.filtername in repoview.filter_has_wc:
1891 1890 return self._quick_access_changeid_wc
1892 1891 return self._quick_access_changeid_null
1893 1892
1894 1893 def __getitem__(self, changeid):
1895 1894 # dealing with special cases
1896 1895 if changeid is None:
1897 1896 return context.workingctx(self)
1898 1897 if isinstance(changeid, context.basectx):
1899 1898 return changeid
1900 1899
1901 1900 # dealing with multiple revisions
1902 1901 if isinstance(changeid, slice):
1903 1902 # wdirrev isn't contiguous so the slice shouldn't include it
1904 1903 return [
1905 1904 self[i]
1906 1905 for i in range(*changeid.indices(len(self)))
1907 1906 if i not in self.changelog.filteredrevs
1908 1907 ]
1909 1908
1910 1909 # dealing with some special values
1911 1910 quick_access = self._quick_access_changeid.get(changeid)
1912 1911 if quick_access is not None:
1913 1912 rev, node = quick_access
1914 1913 return context.changectx(self, rev, node, maybe_filtered=False)
1915 1914 if changeid == b'tip':
1916 1915 node = self.changelog.tip()
1917 1916 rev = self.changelog.rev(node)
1918 1917 return context.changectx(self, rev, node)
1919 1918
1920 1919 # dealing with arbitrary values
1921 1920 try:
1922 1921 if isinstance(changeid, int):
1923 1922 node = self.changelog.node(changeid)
1924 1923 rev = changeid
1925 1924 elif changeid == b'.':
1926 1925 # this is a hack to delay/avoid loading obsmarkers
1927 1926 # when we know that '.' won't be hidden
1928 1927 node = self.dirstate.p1()
1929 1928 rev = self.unfiltered().changelog.rev(node)
1930 1929 elif len(changeid) == self.nodeconstants.nodelen:
1931 1930 try:
1932 1931 node = changeid
1933 1932 rev = self.changelog.rev(changeid)
1934 1933 except error.FilteredLookupError:
1935 1934 changeid = hex(changeid) # for the error message
1936 1935 raise
1937 1936 except LookupError:
1938 1937 # check if it might have come from damaged dirstate
1939 1938 #
1940 1939 # XXX we could avoid the unfiltered if we had a recognizable
1941 1940 # exception for filtered changeset access
1942 1941 if (
1943 1942 self.local()
1944 1943 and changeid in self.unfiltered().dirstate.parents()
1945 1944 ):
1946 1945 msg = _(b"working directory has unknown parent '%s'!")
1947 1946 raise error.Abort(msg % short(changeid))
1948 1947 changeid = hex(changeid) # for the error message
1949 1948 raise
1950 1949
1951 1950 elif len(changeid) == 2 * self.nodeconstants.nodelen:
1952 1951 node = bin(changeid)
1953 1952 rev = self.changelog.rev(node)
1954 1953 else:
1955 1954 raise error.ProgrammingError(
1956 1955 b"unsupported changeid '%s' of type %s"
1957 1956 % (changeid, pycompat.bytestr(type(changeid)))
1958 1957 )
1959 1958
1960 1959 return context.changectx(self, rev, node)
1961 1960
1962 1961 except (error.FilteredIndexError, error.FilteredLookupError):
1963 1962 raise error.FilteredRepoLookupError(
1964 1963 _(b"filtered revision '%s'") % pycompat.bytestr(changeid)
1965 1964 )
1966 1965 except (IndexError, LookupError):
1967 1966 raise error.RepoLookupError(
1968 1967 _(b"unknown revision '%s'") % pycompat.bytestr(changeid)
1969 1968 )
1970 1969 except error.WdirUnsupported:
1971 1970 return context.workingctx(self)
1972 1971
1973 1972 def __contains__(self, changeid):
1974 1973 """True if the given changeid exists"""
1975 1974 try:
1976 1975 self[changeid]
1977 1976 return True
1978 1977 except error.RepoLookupError:
1979 1978 return False
1980 1979
1981 1980 def __nonzero__(self):
1982 1981 return True
1983 1982
1984 1983 __bool__ = __nonzero__
1985 1984
1986 1985 def __len__(self):
1987 1986 # no need to pay the cost of repoview.changelog
1988 1987 unfi = self.unfiltered()
1989 1988 return len(unfi.changelog)
1990 1989
1991 1990 def __iter__(self):
1992 1991 return iter(self.changelog)
1993 1992
1994 1993 def revs(self, expr: bytes, *args):
1995 1994 """Find revisions matching a revset.
1996 1995
1997 1996 The revset is specified as a string ``expr`` that may contain
1998 1997 %-formatting to escape certain types. See ``revsetlang.formatspec``.
1999 1998
2000 1999 Revset aliases from the configuration are not expanded. To expand
2001 2000 user aliases, consider calling ``scmutil.revrange()`` or
2002 2001 ``repo.anyrevs([expr], user=True)``.
2003 2002
2004 2003 Returns a smartset.abstractsmartset, which is a list-like interface
2005 2004 that contains integer revisions.
2006 2005 """
2007 2006 tree = revsetlang.spectree(expr, *args)
2008 2007 return revset.makematcher(tree)(self)
2009 2008
2010 2009 def set(self, expr: bytes, *args):
2011 2010 """Find revisions matching a revset and emit changectx instances.
2012 2011
2013 2012 This is a convenience wrapper around ``revs()`` that iterates the
2014 2013 result and is a generator of changectx instances.
2015 2014
2016 2015 Revset aliases from the configuration are not expanded. To expand
2017 2016 user aliases, consider calling ``scmutil.revrange()``.
2018 2017 """
2019 2018 for r in self.revs(expr, *args):
2020 2019 yield self[r]
2021 2020
2022 2021 def anyrevs(self, specs: bytes, user=False, localalias=None):
2023 2022 """Find revisions matching one of the given revsets.
2024 2023
2025 2024 Revset aliases from the configuration are not expanded by default. To
2026 2025 expand user aliases, specify ``user=True``. To provide some local
2027 2026 definitions overriding user aliases, set ``localalias`` to
2028 2027 ``{name: definitionstring}``.
2029 2028 """
2030 2029 if specs == [b'null']:
2031 2030 return revset.baseset([nullrev])
2032 2031 if specs == [b'.']:
2033 2032 quick_data = self._quick_access_changeid.get(b'.')
2034 2033 if quick_data is not None:
2035 2034 return revset.baseset([quick_data[0]])
2036 2035 if user:
2037 2036 m = revset.matchany(
2038 2037 self.ui,
2039 2038 specs,
2040 2039 lookup=revset.lookupfn(self),
2041 2040 localalias=localalias,
2042 2041 )
2043 2042 else:
2044 2043 m = revset.matchany(None, specs, localalias=localalias)
2045 2044 return m(self)
2046 2045
2047 2046 def url(self) -> bytes:
2048 2047 return b'file:' + self.root
2049 2048
2050 2049 def hook(self, name, throw=False, **args):
2051 2050 """Call a hook, passing this repo instance.
2052 2051
2053 2052 This a convenience method to aid invoking hooks. Extensions likely
2054 2053 won't call this unless they have registered a custom hook or are
2055 2054 replacing code that is expected to call a hook.
2056 2055 """
2057 2056 return hook.hook(self.ui, self, name, throw, **args)
2058 2057
2059 2058 @filteredpropertycache
2060 2059 def _tagscache(self):
2061 2060 """Returns a tagscache object that contains various tags related
2062 2061 caches."""
2063 2062
2064 2063 # This simplifies its cache management by having one decorated
2065 2064 # function (this one) and the rest simply fetch things from it.
2066 2065 class tagscache:
2067 2066 def __init__(self):
2068 2067 # These two define the set of tags for this repository. tags
2069 2068 # maps tag name to node; tagtypes maps tag name to 'global' or
2070 2069 # 'local'. (Global tags are defined by .hgtags across all
2071 2070 # heads, and local tags are defined in .hg/localtags.)
2072 2071 # They constitute the in-memory cache of tags.
2073 2072 self.tags = self.tagtypes = None
2074 2073
2075 2074 self.nodetagscache = self.tagslist = None
2076 2075
2077 2076 cache = tagscache()
2078 2077 cache.tags, cache.tagtypes = self._findtags()
2079 2078
2080 2079 return cache
2081 2080
2082 2081 def tags(self):
2083 2082 '''return a mapping of tag to node'''
2084 2083 t = {}
2085 2084 if self.changelog.filteredrevs:
2086 2085 tags, tt = self._findtags()
2087 2086 else:
2088 2087 tags = self._tagscache.tags
2089 2088 rev = self.changelog.rev
2090 2089 for k, v in tags.items():
2091 2090 try:
2092 2091 # ignore tags to unknown nodes
2093 2092 rev(v)
2094 2093 t[k] = v
2095 2094 except (error.LookupError, ValueError):
2096 2095 pass
2097 2096 return t
2098 2097
2099 2098 def _findtags(self):
2100 2099 """Do the hard work of finding tags. Return a pair of dicts
2101 2100 (tags, tagtypes) where tags maps tag name to node, and tagtypes
2102 2101 maps tag name to a string like \'global\' or \'local\'.
2103 2102 Subclasses or extensions are free to add their own tags, but
2104 2103 should be aware that the returned dicts will be retained for the
2105 2104 duration of the localrepo object."""
2106 2105
2107 2106 # XXX what tagtype should subclasses/extensions use? Currently
2108 2107 # mq and bookmarks add tags, but do not set the tagtype at all.
2109 2108 # Should each extension invent its own tag type? Should there
2110 2109 # be one tagtype for all such "virtual" tags? Or is the status
2111 2110 # quo fine?
2112 2111
2113 2112 # map tag name to (node, hist)
2114 2113 alltags = tagsmod.findglobaltags(self.ui, self)
2115 2114 # map tag name to tag type
2116 2115 tagtypes = {tag: b'global' for tag in alltags}
2117 2116
2118 2117 tagsmod.readlocaltags(self.ui, self, alltags, tagtypes)
2119 2118
2120 2119 # Build the return dicts. Have to re-encode tag names because
2121 2120 # the tags module always uses UTF-8 (in order not to lose info
2122 2121 # writing to the cache), but the rest of Mercurial wants them in
2123 2122 # local encoding.
2124 2123 tags = {}
2125 2124 for (name, (node, hist)) in alltags.items():
2126 2125 if node != self.nullid:
2127 2126 tags[encoding.tolocal(name)] = node
2128 2127 tags[b'tip'] = self.changelog.tip()
2129 2128 tagtypes = {
2130 2129 encoding.tolocal(name): value for (name, value) in tagtypes.items()
2131 2130 }
2132 2131 return (tags, tagtypes)
2133 2132
2134 2133 def tagtype(self, tagname):
2135 2134 """
2136 2135 return the type of the given tag. result can be:
2137 2136
2138 2137 'local' : a local tag
2139 2138 'global' : a global tag
2140 2139 None : tag does not exist
2141 2140 """
2142 2141
2143 2142 return self._tagscache.tagtypes.get(tagname)
2144 2143
2145 2144 def tagslist(self):
2146 2145 '''return a list of tags ordered by revision'''
2147 2146 if not self._tagscache.tagslist:
2148 2147 l = []
2149 2148 for t, n in self.tags().items():
2150 2149 l.append((self.changelog.rev(n), t, n))
2151 2150 self._tagscache.tagslist = [(t, n) for r, t, n in sorted(l)]
2152 2151
2153 2152 return self._tagscache.tagslist
2154 2153
2155 2154 def nodetags(self, node):
2156 2155 '''return the tags associated with a node'''
2157 2156 if not self._tagscache.nodetagscache:
2158 2157 nodetagscache = {}
2159 2158 for t, n in self._tagscache.tags.items():
2160 2159 nodetagscache.setdefault(n, []).append(t)
2161 2160 for tags in nodetagscache.values():
2162 2161 tags.sort()
2163 2162 self._tagscache.nodetagscache = nodetagscache
2164 2163 return self._tagscache.nodetagscache.get(node, [])
2165 2164
2166 2165 def nodebookmarks(self, node):
2167 2166 """return the list of bookmarks pointing to the specified node"""
2168 2167 return self._bookmarks.names(node)
2169 2168
2170 2169 def branchmap(self):
2171 2170 """returns a dictionary {branch: [branchheads]} with branchheads
2172 2171 ordered by increasing revision number"""
2173 2172 return self._branchcaches[self]
2174 2173
2175 2174 @unfilteredmethod
2176 2175 def revbranchcache(self):
2177 2176 if not self._revbranchcache:
2178 2177 self._revbranchcache = branchmap.revbranchcache(self.unfiltered())
2179 2178 return self._revbranchcache
2180 2179
2181 2180 def register_changeset(self, rev, changelogrevision):
2182 2181 self.revbranchcache().setdata(rev, changelogrevision)
2183 2182
2184 2183 def branchtip(self, branch, ignoremissing=False):
2185 2184 """return the tip node for a given branch
2186 2185
2187 2186 If ignoremissing is True, then this method will not raise an error.
2188 2187 This is helpful for callers that only expect None for a missing branch
2189 2188 (e.g. namespace).
2190 2189
2191 2190 """
2192 2191 try:
2193 2192 return self.branchmap().branchtip(branch)
2194 2193 except KeyError:
2195 2194 if not ignoremissing:
2196 2195 raise error.RepoLookupError(_(b"unknown branch '%s'") % branch)
2197 2196 else:
2198 2197 pass
2199 2198
2200 2199 def lookup(self, key):
2201 2200 node = scmutil.revsymbol(self, key).node()
2202 2201 if node is None:
2203 2202 raise error.RepoLookupError(_(b"unknown revision '%s'") % key)
2204 2203 return node
2205 2204
2206 2205 def lookupbranch(self, key):
2207 2206 if self.branchmap().hasbranch(key):
2208 2207 return key
2209 2208
2210 2209 return scmutil.revsymbol(self, key).branch()
2211 2210
2212 2211 def known(self, nodes):
2213 2212 cl = self.changelog
2214 2213 get_rev = cl.index.get_rev
2215 2214 filtered = cl.filteredrevs
2216 2215 result = []
2217 2216 for n in nodes:
2218 2217 r = get_rev(n)
2219 2218 resp = not (r is None or r in filtered)
2220 2219 result.append(resp)
2221 2220 return result
2222 2221
2223 2222 def local(self):
2224 2223 return self
2225 2224
2226 2225 def publishing(self):
2227 2226 # it's safe (and desirable) to trust the publish flag unconditionally
2228 2227 # so that we don't finalize changes shared between users via ssh or nfs
2229 2228 return self.ui.configbool(b'phases', b'publish', untrusted=True)
2230 2229
2231 2230 def cancopy(self):
2232 2231 # so statichttprepo's override of local() works
2233 2232 if not self.local():
2234 2233 return False
2235 2234 if not self.publishing():
2236 2235 return True
2237 2236 # if publishing we can't copy if there is filtered content
2238 2237 return not self.filtered(b'visible').changelog.filteredrevs
2239 2238
2240 2239 def shared(self):
2241 2240 '''the type of shared repository (None if not shared)'''
2242 2241 if self.sharedpath != self.path:
2243 2242 return b'store'
2244 2243 return None
2245 2244
2246 2245 def wjoin(self, f: bytes, *insidef: bytes) -> bytes:
2247 2246 return self.vfs.reljoin(self.root, f, *insidef)
2248 2247
2249 2248 def setparents(self, p1, p2=None):
2250 2249 if p2 is None:
2251 2250 p2 = self.nullid
2252 2251 self[None].setparents(p1, p2)
2253 2252 self._quick_access_changeid_invalidate()
2254 2253
2255 2254 def filectx(self, path: bytes, changeid=None, fileid=None, changectx=None):
2256 2255 """changeid must be a changeset revision, if specified.
2257 2256 fileid can be a file revision or node."""
2258 2257 return context.filectx(
2259 2258 self, path, changeid, fileid, changectx=changectx
2260 2259 )
2261 2260
2262 2261 def getcwd(self) -> bytes:
2263 2262 return self.dirstate.getcwd()
2264 2263
2265 2264 def pathto(self, f: bytes, cwd: Optional[bytes] = None) -> bytes:
2266 2265 return self.dirstate.pathto(f, cwd)
2267 2266
2268 2267 def _loadfilter(self, filter):
2269 2268 if filter not in self._filterpats:
2270 2269 l = []
2271 2270 for pat, cmd in self.ui.configitems(filter):
2272 2271 if cmd == b'!':
2273 2272 continue
2274 2273 mf = matchmod.match(self.root, b'', [pat])
2275 2274 fn = None
2276 2275 params = cmd
2277 2276 for name, filterfn in self._datafilters.items():
2278 2277 if cmd.startswith(name):
2279 2278 fn = filterfn
2280 2279 params = cmd[len(name) :].lstrip()
2281 2280 break
2282 2281 if not fn:
2283 2282 fn = lambda s, c, **kwargs: procutil.filter(s, c)
2284 2283 fn.__name__ = 'commandfilter'
2285 2284 # Wrap old filters not supporting keyword arguments
2286 2285 if not pycompat.getargspec(fn)[2]:
2287 2286 oldfn = fn
2288 2287 fn = lambda s, c, oldfn=oldfn, **kwargs: oldfn(s, c)
2289 2288 fn.__name__ = 'compat-' + oldfn.__name__
2290 2289 l.append((mf, fn, params))
2291 2290 self._filterpats[filter] = l
2292 2291 return self._filterpats[filter]
2293 2292
2294 2293 def _filter(self, filterpats, filename, data):
2295 2294 for mf, fn, cmd in filterpats:
2296 2295 if mf(filename):
2297 2296 self.ui.debug(
2298 2297 b"filtering %s through %s\n"
2299 2298 % (filename, cmd or pycompat.sysbytes(fn.__name__))
2300 2299 )
2301 2300 data = fn(data, cmd, ui=self.ui, repo=self, filename=filename)
2302 2301 break
2303 2302
2304 2303 return data
2305 2304
2306 2305 @unfilteredpropertycache
2307 2306 def _encodefilterpats(self):
2308 2307 return self._loadfilter(b'encode')
2309 2308
2310 2309 @unfilteredpropertycache
2311 2310 def _decodefilterpats(self):
2312 2311 return self._loadfilter(b'decode')
2313 2312
2314 2313 def adddatafilter(self, name, filter):
2315 2314 self._datafilters[name] = filter
2316 2315
2317 2316 def wread(self, filename: bytes) -> bytes:
2318 2317 if self.wvfs.islink(filename):
2319 2318 data = self.wvfs.readlink(filename)
2320 2319 else:
2321 2320 data = self.wvfs.read(filename)
2322 2321 return self._filter(self._encodefilterpats, filename, data)
2323 2322
2324 2323 def wwrite(
2325 2324 self,
2326 2325 filename: bytes,
2327 2326 data: bytes,
2328 2327 flags: bytes,
2329 2328 backgroundclose=False,
2330 2329 **kwargs
2331 2330 ) -> int:
2332 2331 """write ``data`` into ``filename`` in the working directory
2333 2332
2334 2333 This returns length of written (maybe decoded) data.
2335 2334 """
2336 2335 data = self._filter(self._decodefilterpats, filename, data)
2337 2336 if b'l' in flags:
2338 2337 self.wvfs.symlink(data, filename)
2339 2338 else:
2340 2339 self.wvfs.write(
2341 2340 filename, data, backgroundclose=backgroundclose, **kwargs
2342 2341 )
2343 2342 if b'x' in flags:
2344 2343 self.wvfs.setflags(filename, False, True)
2345 2344 else:
2346 2345 self.wvfs.setflags(filename, False, False)
2347 2346 return len(data)
2348 2347
2349 2348 def wwritedata(self, filename: bytes, data: bytes) -> bytes:
2350 2349 return self._filter(self._decodefilterpats, filename, data)
2351 2350
2352 2351 def currenttransaction(self):
2353 2352 """return the current transaction or None if non exists"""
2354 2353 if self._transref:
2355 2354 tr = self._transref()
2356 2355 else:
2357 2356 tr = None
2358 2357
2359 2358 if tr and tr.running():
2360 2359 return tr
2361 2360 return None
2362 2361
2363 2362 def transaction(self, desc, report=None):
2364 2363 if self.ui.configbool(b'devel', b'all-warnings') or self.ui.configbool(
2365 2364 b'devel', b'check-locks'
2366 2365 ):
2367 2366 if self._currentlock(self._lockref) is None:
2368 2367 raise error.ProgrammingError(b'transaction requires locking')
2369 2368 tr = self.currenttransaction()
2370 2369 if tr is not None:
2371 2370 return tr.nest(name=desc)
2372 2371
2373 2372 # abort here if the journal already exists
2374 2373 if self.svfs.exists(b"journal"):
2375 2374 raise error.RepoError(
2376 2375 _(b"abandoned transaction found"),
2377 2376 hint=_(b"run 'hg recover' to clean up transaction"),
2378 2377 )
2379 2378
2380 2379 idbase = b"%.40f#%f" % (random.random(), time.time())
2381 2380 ha = hex(hashutil.sha1(idbase).digest())
2382 2381 txnid = b'TXN:' + ha
2383 2382 self.hook(b'pretxnopen', throw=True, txnname=desc, txnid=txnid)
2384 2383
2385 2384 self._writejournal(desc)
2386 2385 renames = [(vfs, x, undoname(x)) for vfs, x in self._journalfiles()]
2387 2386 if report:
2388 2387 rp = report
2389 2388 else:
2390 2389 rp = self.ui.warn
2391 2390 vfsmap = {b'plain': self.vfs, b'store': self.svfs} # root of .hg/
2392 2391 # we must avoid cyclic reference between repo and transaction.
2393 2392 reporef = weakref.ref(self)
2394 2393 # Code to track tag movement
2395 2394 #
2396 2395 # Since tags are all handled as file content, it is actually quite hard
2397 2396 # to track these movement from a code perspective. So we fallback to a
2398 2397 # tracking at the repository level. One could envision to track changes
2399 2398 # to the '.hgtags' file through changegroup apply but that fails to
2400 2399 # cope with case where transaction expose new heads without changegroup
2401 2400 # being involved (eg: phase movement).
2402 2401 #
2403 2402 # For now, We gate the feature behind a flag since this likely comes
2404 2403 # with performance impacts. The current code run more often than needed
2405 2404 # and do not use caches as much as it could. The current focus is on
2406 2405 # the behavior of the feature so we disable it by default. The flag
2407 2406 # will be removed when we are happy with the performance impact.
2408 2407 #
2409 2408 # Once this feature is no longer experimental move the following
2410 2409 # documentation to the appropriate help section:
2411 2410 #
2412 2411 # The ``HG_TAG_MOVED`` variable will be set if the transaction touched
2413 2412 # tags (new or changed or deleted tags). In addition the details of
2414 2413 # these changes are made available in a file at:
2415 2414 # ``REPOROOT/.hg/changes/tags.changes``.
2416 2415 # Make sure you check for HG_TAG_MOVED before reading that file as it
2417 2416 # might exist from a previous transaction even if no tag were touched
2418 2417 # in this one. Changes are recorded in a line base format::
2419 2418 #
2420 2419 # <action> <hex-node> <tag-name>\n
2421 2420 #
2422 2421 # Actions are defined as follow:
2423 2422 # "-R": tag is removed,
2424 2423 # "+A": tag is added,
2425 2424 # "-M": tag is moved (old value),
2426 2425 # "+M": tag is moved (new value),
2427 2426 tracktags = lambda x: None
2428 2427 # experimental config: experimental.hook-track-tags
2429 2428 shouldtracktags = self.ui.configbool(
2430 2429 b'experimental', b'hook-track-tags'
2431 2430 )
2432 2431 if desc != b'strip' and shouldtracktags:
2433 2432 oldheads = self.changelog.headrevs()
2434 2433
2435 2434 def tracktags(tr2):
2436 2435 repo = reporef()
2437 2436 assert repo is not None # help pytype
2438 2437 oldfnodes = tagsmod.fnoderevs(repo.ui, repo, oldheads)
2439 2438 newheads = repo.changelog.headrevs()
2440 2439 newfnodes = tagsmod.fnoderevs(repo.ui, repo, newheads)
2441 2440 # notes: we compare lists here.
2442 2441 # As we do it only once buiding set would not be cheaper
2443 2442 changes = tagsmod.difftags(repo.ui, repo, oldfnodes, newfnodes)
2444 2443 if changes:
2445 2444 tr2.hookargs[b'tag_moved'] = b'1'
2446 2445 with repo.vfs(
2447 2446 b'changes/tags.changes', b'w', atomictemp=True
2448 2447 ) as changesfile:
2449 2448 # note: we do not register the file to the transaction
2450 2449 # because we needs it to still exist on the transaction
2451 2450 # is close (for txnclose hooks)
2452 2451 tagsmod.writediff(changesfile, changes)
2453 2452
2454 2453 def validate(tr2):
2455 2454 """will run pre-closing hooks"""
2456 2455 # XXX the transaction API is a bit lacking here so we take a hacky
2457 2456 # path for now
2458 2457 #
2459 2458 # We cannot add this as a "pending" hooks since the 'tr.hookargs'
2460 2459 # dict is copied before these run. In addition we needs the data
2461 2460 # available to in memory hooks too.
2462 2461 #
2463 2462 # Moreover, we also need to make sure this runs before txnclose
2464 2463 # hooks and there is no "pending" mechanism that would execute
2465 2464 # logic only if hooks are about to run.
2466 2465 #
2467 2466 # Fixing this limitation of the transaction is also needed to track
2468 2467 # other families of changes (bookmarks, phases, obsolescence).
2469 2468 #
2470 2469 # This will have to be fixed before we remove the experimental
2471 2470 # gating.
2472 2471 tracktags(tr2)
2473 2472 repo = reporef()
2474 2473 assert repo is not None # help pytype
2475 2474
2476 2475 singleheadopt = (b'experimental', b'single-head-per-branch')
2477 2476 singlehead = repo.ui.configbool(*singleheadopt)
2478 2477 if singlehead:
2479 2478 singleheadsub = repo.ui.configsuboptions(*singleheadopt)[1]
2480 2479 accountclosed = singleheadsub.get(
2481 2480 b"account-closed-heads", False
2482 2481 )
2483 2482 if singleheadsub.get(b"public-changes-only", False):
2484 2483 filtername = b"immutable"
2485 2484 else:
2486 2485 filtername = b"visible"
2487 2486 scmutil.enforcesinglehead(
2488 2487 repo, tr2, desc, accountclosed, filtername
2489 2488 )
2490 2489 if hook.hashook(repo.ui, b'pretxnclose-bookmark'):
2491 2490 for name, (old, new) in sorted(
2492 2491 tr.changes[b'bookmarks'].items()
2493 2492 ):
2494 2493 args = tr.hookargs.copy()
2495 2494 args.update(bookmarks.preparehookargs(name, old, new))
2496 2495 repo.hook(
2497 2496 b'pretxnclose-bookmark',
2498 2497 throw=True,
2499 2498 **pycompat.strkwargs(args)
2500 2499 )
2501 2500 if hook.hashook(repo.ui, b'pretxnclose-phase'):
2502 2501 cl = repo.unfiltered().changelog
2503 2502 for revs, (old, new) in tr.changes[b'phases']:
2504 2503 for rev in revs:
2505 2504 args = tr.hookargs.copy()
2506 2505 node = hex(cl.node(rev))
2507 2506 args.update(phases.preparehookargs(node, old, new))
2508 2507 repo.hook(
2509 2508 b'pretxnclose-phase',
2510 2509 throw=True,
2511 2510 **pycompat.strkwargs(args)
2512 2511 )
2513 2512
2514 2513 repo.hook(
2515 2514 b'pretxnclose', throw=True, **pycompat.strkwargs(tr.hookargs)
2516 2515 )
2517 2516
2518 2517 def releasefn(tr, success):
2519 2518 repo = reporef()
2520 2519 if repo is None:
2521 2520 # If the repo has been GC'd (and this release function is being
2522 2521 # called from transaction.__del__), there's not much we can do,
2523 2522 # so just leave the unfinished transaction there and let the
2524 2523 # user run `hg recover`.
2525 2524 return
2526 2525 if success:
2527 2526 # this should be explicitly invoked here, because
2528 2527 # in-memory changes aren't written out at closing
2529 2528 # transaction, if tr.addfilegenerator (via
2530 2529 # dirstate.write or so) isn't invoked while
2531 2530 # transaction running
2532 2531 repo.dirstate.write(None)
2533 2532 else:
2534 2533 # discard all changes (including ones already written
2535 2534 # out) in this transaction
2536 2535 narrowspec.restorebackup(self, b'journal.narrowspec')
2537 2536 narrowspec.restorewcbackup(self, b'journal.narrowspec.dirstate')
2538 2537 repo.dirstate.restorebackup(None, b'journal.dirstate')
2539 2538
2540 2539 repo.invalidate(clearfilecache=True)
2541 2540
2542 2541 tr = transaction.transaction(
2543 2542 rp,
2544 2543 self.svfs,
2545 2544 vfsmap,
2546 2545 b"journal",
2547 2546 b"undo",
2548 2547 aftertrans(renames),
2549 2548 self.store.createmode,
2550 2549 validator=validate,
2551 2550 releasefn=releasefn,
2552 2551 checkambigfiles=_cachedfiles,
2553 2552 name=desc,
2554 2553 )
2555 2554 tr.changes[b'origrepolen'] = len(self)
2556 2555 tr.changes[b'obsmarkers'] = set()
2557 2556 tr.changes[b'phases'] = []
2558 2557 tr.changes[b'bookmarks'] = {}
2559 2558
2560 2559 tr.hookargs[b'txnid'] = txnid
2561 2560 tr.hookargs[b'txnname'] = desc
2562 2561 tr.hookargs[b'changes'] = tr.changes
2563 2562 # note: writing the fncache only during finalize mean that the file is
2564 2563 # outdated when running hooks. As fncache is used for streaming clone,
2565 2564 # this is not expected to break anything that happen during the hooks.
2566 2565 tr.addfinalize(b'flush-fncache', self.store.write)
2567 2566
2568 2567 def txnclosehook(tr2):
2569 2568 """To be run if transaction is successful, will schedule a hook run"""
2570 2569 # Don't reference tr2 in hook() so we don't hold a reference.
2571 2570 # This reduces memory consumption when there are multiple
2572 2571 # transactions per lock. This can likely go away if issue5045
2573 2572 # fixes the function accumulation.
2574 2573 hookargs = tr2.hookargs
2575 2574
2576 2575 def hookfunc(unused_success):
2577 2576 repo = reporef()
2578 2577 assert repo is not None # help pytype
2579 2578
2580 2579 if hook.hashook(repo.ui, b'txnclose-bookmark'):
2581 2580 bmchanges = sorted(tr.changes[b'bookmarks'].items())
2582 2581 for name, (old, new) in bmchanges:
2583 2582 args = tr.hookargs.copy()
2584 2583 args.update(bookmarks.preparehookargs(name, old, new))
2585 2584 repo.hook(
2586 2585 b'txnclose-bookmark',
2587 2586 throw=False,
2588 2587 **pycompat.strkwargs(args)
2589 2588 )
2590 2589
2591 2590 if hook.hashook(repo.ui, b'txnclose-phase'):
2592 2591 cl = repo.unfiltered().changelog
2593 2592 phasemv = sorted(
2594 2593 tr.changes[b'phases'], key=lambda r: r[0][0]
2595 2594 )
2596 2595 for revs, (old, new) in phasemv:
2597 2596 for rev in revs:
2598 2597 args = tr.hookargs.copy()
2599 2598 node = hex(cl.node(rev))
2600 2599 args.update(phases.preparehookargs(node, old, new))
2601 2600 repo.hook(
2602 2601 b'txnclose-phase',
2603 2602 throw=False,
2604 2603 **pycompat.strkwargs(args)
2605 2604 )
2606 2605
2607 2606 repo.hook(
2608 2607 b'txnclose', throw=False, **pycompat.strkwargs(hookargs)
2609 2608 )
2610 2609
2611 2610 repo = reporef()
2612 2611 assert repo is not None # help pytype
2613 2612 repo._afterlock(hookfunc)
2614 2613
2615 2614 tr.addfinalize(b'txnclose-hook', txnclosehook)
2616 2615 # Include a leading "-" to make it happen before the transaction summary
2617 2616 # reports registered via scmutil.registersummarycallback() whose names
2618 2617 # are 00-txnreport etc. That way, the caches will be warm when the
2619 2618 # callbacks run.
2620 2619 tr.addpostclose(b'-warm-cache', self._buildcacheupdater(tr))
2621 2620
2622 2621 def txnaborthook(tr2):
2623 2622 """To be run if transaction is aborted"""
2624 2623 repo = reporef()
2625 2624 assert repo is not None # help pytype
2626 2625 repo.hook(
2627 2626 b'txnabort', throw=False, **pycompat.strkwargs(tr2.hookargs)
2628 2627 )
2629 2628
2630 2629 tr.addabort(b'txnabort-hook', txnaborthook)
2631 2630 # avoid eager cache invalidation. in-memory data should be identical
2632 2631 # to stored data if transaction has no error.
2633 2632 tr.addpostclose(b'refresh-filecachestats', self._refreshfilecachestats)
2634 2633 self._transref = weakref.ref(tr)
2635 2634 scmutil.registersummarycallback(self, tr, desc)
2636 2635 return tr
2637 2636
2638 2637 def _journalfiles(self):
2639 2638 first = (
2640 2639 (self.svfs, b'journal'),
2641 2640 (self.svfs, b'journal.narrowspec'),
2642 2641 (self.vfs, b'journal.narrowspec.dirstate'),
2643 2642 (self.vfs, b'journal.dirstate'),
2644 2643 )
2645 2644 middle = []
2646 2645 dirstate_data = self.dirstate.data_backup_filename(b'journal.dirstate')
2647 2646 if dirstate_data is not None:
2648 2647 middle.append((self.vfs, dirstate_data))
2649 2648 end = (
2650 2649 (self.vfs, b'journal.branch'),
2651 2650 (self.vfs, b'journal.desc'),
2652 2651 (bookmarks.bookmarksvfs(self), b'journal.bookmarks'),
2653 2652 (self.svfs, b'journal.phaseroots'),
2654 2653 )
2655 2654 return first + tuple(middle) + end
2656 2655
2657 2656 def undofiles(self):
2658 2657 return [(vfs, undoname(x)) for vfs, x in self._journalfiles()]
2659 2658
2660 2659 @unfilteredmethod
2661 2660 def _writejournal(self, desc):
2662 2661 self.dirstate.savebackup(None, b'journal.dirstate')
2663 2662 narrowspec.savewcbackup(self, b'journal.narrowspec.dirstate')
2664 2663 narrowspec.savebackup(self, b'journal.narrowspec')
2665 2664 self.vfs.write(
2666 2665 b"journal.branch", encoding.fromlocal(self.dirstate.branch())
2667 2666 )
2668 2667 self.vfs.write(b"journal.desc", b"%d\n%s\n" % (len(self), desc))
2669 2668 bookmarksvfs = bookmarks.bookmarksvfs(self)
2670 2669 bookmarksvfs.write(
2671 2670 b"journal.bookmarks", bookmarksvfs.tryread(b"bookmarks")
2672 2671 )
2673 2672 self.svfs.write(b"journal.phaseroots", self.svfs.tryread(b"phaseroots"))
2674 2673
2675 2674 def recover(self):
2676 2675 with self.lock():
2677 2676 if self.svfs.exists(b"journal"):
2678 2677 self.ui.status(_(b"rolling back interrupted transaction\n"))
2679 2678 vfsmap = {
2680 2679 b'': self.svfs,
2681 2680 b'plain': self.vfs,
2682 2681 }
2683 2682 transaction.rollback(
2684 2683 self.svfs,
2685 2684 vfsmap,
2686 2685 b"journal",
2687 2686 self.ui.warn,
2688 2687 checkambigfiles=_cachedfiles,
2689 2688 )
2690 2689 self.invalidate()
2691 2690 return True
2692 2691 else:
2693 2692 self.ui.warn(_(b"no interrupted transaction available\n"))
2694 2693 return False
2695 2694
2696 2695 def rollback(self, dryrun=False, force=False):
2697 2696 wlock = lock = dsguard = None
2698 2697 try:
2699 2698 wlock = self.wlock()
2700 2699 lock = self.lock()
2701 2700 if self.svfs.exists(b"undo"):
2702 2701 dsguard = dirstateguard.dirstateguard(self, b'rollback')
2703 2702
2704 2703 return self._rollback(dryrun, force, dsguard)
2705 2704 else:
2706 2705 self.ui.warn(_(b"no rollback information available\n"))
2707 2706 return 1
2708 2707 finally:
2709 2708 release(dsguard, lock, wlock)
2710 2709
2711 2710 @unfilteredmethod # Until we get smarter cache management
2712 2711 def _rollback(self, dryrun, force, dsguard):
2713 2712 ui = self.ui
2714 2713 try:
2715 2714 args = self.vfs.read(b'undo.desc').splitlines()
2716 2715 (oldlen, desc, detail) = (int(args[0]), args[1], None)
2717 2716 if len(args) >= 3:
2718 2717 detail = args[2]
2719 2718 oldtip = oldlen - 1
2720 2719
2721 2720 if detail and ui.verbose:
2722 2721 msg = _(
2723 2722 b'repository tip rolled back to revision %d'
2724 2723 b' (undo %s: %s)\n'
2725 2724 ) % (oldtip, desc, detail)
2726 2725 else:
2727 2726 msg = _(
2728 2727 b'repository tip rolled back to revision %d (undo %s)\n'
2729 2728 ) % (oldtip, desc)
2730 2729 except IOError:
2731 2730 msg = _(b'rolling back unknown transaction\n')
2732 2731 desc = None
2733 2732
2734 2733 if not force and self[b'.'] != self[b'tip'] and desc == b'commit':
2735 2734 raise error.Abort(
2736 2735 _(
2737 2736 b'rollback of last commit while not checked out '
2738 2737 b'may lose data'
2739 2738 ),
2740 2739 hint=_(b'use -f to force'),
2741 2740 )
2742 2741
2743 2742 ui.status(msg)
2744 2743 if dryrun:
2745 2744 return 0
2746 2745
2747 2746 parents = self.dirstate.parents()
2748 2747 self.destroying()
2749 2748 vfsmap = {b'plain': self.vfs, b'': self.svfs}
2750 2749 transaction.rollback(
2751 2750 self.svfs, vfsmap, b'undo', ui.warn, checkambigfiles=_cachedfiles
2752 2751 )
2753 2752 bookmarksvfs = bookmarks.bookmarksvfs(self)
2754 2753 if bookmarksvfs.exists(b'undo.bookmarks'):
2755 2754 bookmarksvfs.rename(
2756 2755 b'undo.bookmarks', b'bookmarks', checkambig=True
2757 2756 )
2758 2757 if self.svfs.exists(b'undo.phaseroots'):
2759 2758 self.svfs.rename(b'undo.phaseroots', b'phaseroots', checkambig=True)
2760 2759 self.invalidate()
2761 2760
2762 2761 has_node = self.changelog.index.has_node
2763 2762 parentgone = any(not has_node(p) for p in parents)
2764 2763 if parentgone:
2765 2764 # prevent dirstateguard from overwriting already restored one
2766 2765 dsguard.close()
2767 2766
2768 2767 narrowspec.restorebackup(self, b'undo.narrowspec')
2769 2768 narrowspec.restorewcbackup(self, b'undo.narrowspec.dirstate')
2770 2769 self.dirstate.restorebackup(None, b'undo.dirstate')
2771 2770 try:
2772 2771 branch = self.vfs.read(b'undo.branch')
2773 2772 self.dirstate.setbranch(encoding.tolocal(branch))
2774 2773 except IOError:
2775 2774 ui.warn(
2776 2775 _(
2777 2776 b'named branch could not be reset: '
2778 2777 b'current branch is still \'%s\'\n'
2779 2778 )
2780 2779 % self.dirstate.branch()
2781 2780 )
2782 2781
2783 2782 parents = tuple([p.rev() for p in self[None].parents()])
2784 2783 if len(parents) > 1:
2785 2784 ui.status(
2786 2785 _(
2787 2786 b'working directory now based on '
2788 2787 b'revisions %d and %d\n'
2789 2788 )
2790 2789 % parents
2791 2790 )
2792 2791 else:
2793 2792 ui.status(
2794 2793 _(b'working directory now based on revision %d\n') % parents
2795 2794 )
2796 2795 mergestatemod.mergestate.clean(self)
2797 2796
2798 2797 # TODO: if we know which new heads may result from this rollback, pass
2799 2798 # them to destroy(), which will prevent the branchhead cache from being
2800 2799 # invalidated.
2801 2800 self.destroyed()
2802 2801 return 0
2803 2802
2804 2803 def _buildcacheupdater(self, newtransaction):
2805 2804 """called during transaction to build the callback updating cache
2806 2805
2807 2806 Lives on the repository to help extension who might want to augment
2808 2807 this logic. For this purpose, the created transaction is passed to the
2809 2808 method.
2810 2809 """
2811 2810 # we must avoid cyclic reference between repo and transaction.
2812 2811 reporef = weakref.ref(self)
2813 2812
2814 2813 def updater(tr):
2815 2814 repo = reporef()
2816 2815 assert repo is not None # help pytype
2817 2816 repo.updatecaches(tr)
2818 2817
2819 2818 return updater
2820 2819
2821 2820 @unfilteredmethod
2822 2821 def updatecaches(self, tr=None, full=False, caches=None):
2823 2822 """warm appropriate caches
2824 2823
2825 2824 If this function is called after a transaction closed. The transaction
2826 2825 will be available in the 'tr' argument. This can be used to selectively
2827 2826 update caches relevant to the changes in that transaction.
2828 2827
2829 2828 If 'full' is set, make sure all caches the function knows about have
2830 2829 up-to-date data. Even the ones usually loaded more lazily.
2831 2830
2832 2831 The `full` argument can take a special "post-clone" value. In this case
2833 2832 the cache warming is made after a clone and of the slower cache might
2834 2833 be skipped, namely the `.fnodetags` one. This argument is 5.8 specific
2835 2834 as we plan for a cleaner way to deal with this for 5.9.
2836 2835 """
2837 2836 if tr is not None and tr.hookargs.get(b'source') == b'strip':
2838 2837 # During strip, many caches are invalid but
2839 2838 # later call to `destroyed` will refresh them.
2840 2839 return
2841 2840
2842 2841 unfi = self.unfiltered()
2843 2842
2844 2843 if full:
2845 2844 msg = (
2846 2845 "`full` argument for `repo.updatecaches` is deprecated\n"
2847 2846 "(use `caches=repository.CACHE_ALL` instead)"
2848 2847 )
2849 2848 self.ui.deprecwarn(msg, b"5.9")
2850 2849 caches = repository.CACHES_ALL
2851 2850 if full == b"post-clone":
2852 2851 caches = repository.CACHES_POST_CLONE
2853 2852 caches = repository.CACHES_ALL
2854 2853 elif caches is None:
2855 2854 caches = repository.CACHES_DEFAULT
2856 2855
2857 2856 if repository.CACHE_BRANCHMAP_SERVED in caches:
2858 2857 if tr is None or tr.changes[b'origrepolen'] < len(self):
2859 2858 # accessing the 'served' branchmap should refresh all the others,
2860 2859 self.ui.debug(b'updating the branch cache\n')
2861 2860 self.filtered(b'served').branchmap()
2862 2861 self.filtered(b'served.hidden').branchmap()
2863 2862 # flush all possibly delayed write.
2864 2863 self._branchcaches.write_delayed(self)
2865 2864
2866 2865 if repository.CACHE_CHANGELOG_CACHE in caches:
2867 2866 self.changelog.update_caches(transaction=tr)
2868 2867
2869 2868 if repository.CACHE_MANIFESTLOG_CACHE in caches:
2870 2869 self.manifestlog.update_caches(transaction=tr)
2871 2870
2872 2871 if repository.CACHE_REV_BRANCH in caches:
2873 2872 rbc = unfi.revbranchcache()
2874 2873 for r in unfi.changelog:
2875 2874 rbc.branchinfo(r)
2876 2875 rbc.write()
2877 2876
2878 2877 if repository.CACHE_FULL_MANIFEST in caches:
2879 2878 # ensure the working copy parents are in the manifestfulltextcache
2880 2879 for ctx in self[b'.'].parents():
2881 2880 ctx.manifest() # accessing the manifest is enough
2882 2881
2883 2882 if repository.CACHE_FILE_NODE_TAGS in caches:
2884 2883 # accessing fnode cache warms the cache
2885 2884 tagsmod.fnoderevs(self.ui, unfi, unfi.changelog.revs())
2886 2885
2887 2886 if repository.CACHE_TAGS_DEFAULT in caches:
2888 2887 # accessing tags warm the cache
2889 2888 self.tags()
2890 2889 if repository.CACHE_TAGS_SERVED in caches:
2891 2890 self.filtered(b'served').tags()
2892 2891
2893 2892 if repository.CACHE_BRANCHMAP_ALL in caches:
2894 2893 # The CACHE_BRANCHMAP_ALL updates lazily-loaded caches immediately,
2895 2894 # so we're forcing a write to cause these caches to be warmed up
2896 2895 # even if they haven't explicitly been requested yet (if they've
2897 2896 # never been used by hg, they won't ever have been written, even if
2898 2897 # they're a subset of another kind of cache that *has* been used).
2899 2898 for filt in repoview.filtertable.keys():
2900 2899 filtered = self.filtered(filt)
2901 2900 filtered.branchmap().write(filtered)
2902 2901
2903 2902 def invalidatecaches(self):
2904 2903
2905 2904 if '_tagscache' in vars(self):
2906 2905 # can't use delattr on proxy
2907 2906 del self.__dict__['_tagscache']
2908 2907
2909 2908 self._branchcaches.clear()
2910 2909 self.invalidatevolatilesets()
2911 2910 self._sparsesignaturecache.clear()
2912 2911
2913 2912 def invalidatevolatilesets(self):
2914 2913 self.filteredrevcache.clear()
2915 2914 obsolete.clearobscaches(self)
2916 2915 self._quick_access_changeid_invalidate()
2917 2916
2918 2917 def invalidatedirstate(self):
2919 2918 """Invalidates the dirstate, causing the next call to dirstate
2920 2919 to check if it was modified since the last time it was read,
2921 2920 rereading it if it has.
2922 2921
2923 2922 This is different to dirstate.invalidate() that it doesn't always
2924 2923 rereads the dirstate. Use dirstate.invalidate() if you want to
2925 2924 explicitly read the dirstate again (i.e. restoring it to a previous
2926 2925 known good state)."""
2927 2926 if hasunfilteredcache(self, 'dirstate'):
2928 2927 for k in self.dirstate._filecache:
2929 2928 try:
2930 2929 delattr(self.dirstate, k)
2931 2930 except AttributeError:
2932 2931 pass
2933 2932 delattr(self.unfiltered(), 'dirstate')
2934 2933
2935 2934 def invalidate(self, clearfilecache=False):
2936 2935 """Invalidates both store and non-store parts other than dirstate
2937 2936
2938 2937 If a transaction is running, invalidation of store is omitted,
2939 2938 because discarding in-memory changes might cause inconsistency
2940 2939 (e.g. incomplete fncache causes unintentional failure, but
2941 2940 redundant one doesn't).
2942 2941 """
2943 2942 unfiltered = self.unfiltered() # all file caches are stored unfiltered
2944 2943 for k in list(self._filecache.keys()):
2945 2944 # dirstate is invalidated separately in invalidatedirstate()
2946 2945 if k == b'dirstate':
2947 2946 continue
2948 2947 if (
2949 2948 k == b'changelog'
2950 2949 and self.currenttransaction()
2951 2950 and self.changelog._delayed
2952 2951 ):
2953 2952 # The changelog object may store unwritten revisions. We don't
2954 2953 # want to lose them.
2955 2954 # TODO: Solve the problem instead of working around it.
2956 2955 continue
2957 2956
2958 2957 if clearfilecache:
2959 2958 del self._filecache[k]
2960 2959 try:
2961 2960 delattr(unfiltered, k)
2962 2961 except AttributeError:
2963 2962 pass
2964 2963 self.invalidatecaches()
2965 2964 if not self.currenttransaction():
2966 2965 # TODO: Changing contents of store outside transaction
2967 2966 # causes inconsistency. We should make in-memory store
2968 2967 # changes detectable, and abort if changed.
2969 2968 self.store.invalidatecaches()
2970 2969
2971 2970 def invalidateall(self):
2972 2971 """Fully invalidates both store and non-store parts, causing the
2973 2972 subsequent operation to reread any outside changes."""
2974 2973 # extension should hook this to invalidate its caches
2975 2974 self.invalidate()
2976 2975 self.invalidatedirstate()
2977 2976
2978 2977 @unfilteredmethod
2979 2978 def _refreshfilecachestats(self, tr):
2980 2979 """Reload stats of cached files so that they are flagged as valid"""
2981 2980 for k, ce in self._filecache.items():
2982 2981 k = pycompat.sysstr(k)
2983 2982 if k == 'dirstate' or k not in self.__dict__:
2984 2983 continue
2985 2984 ce.refresh()
2986 2985
2987 2986 def _lock(
2988 2987 self,
2989 2988 vfs,
2990 2989 lockname,
2991 2990 wait,
2992 2991 releasefn,
2993 2992 acquirefn,
2994 2993 desc,
2995 2994 ):
2996 2995 timeout = 0
2997 2996 warntimeout = 0
2998 2997 if wait:
2999 2998 timeout = self.ui.configint(b"ui", b"timeout")
3000 2999 warntimeout = self.ui.configint(b"ui", b"timeout.warn")
3001 3000 # internal config: ui.signal-safe-lock
3002 3001 signalsafe = self.ui.configbool(b'ui', b'signal-safe-lock')
3003 3002
3004 3003 l = lockmod.trylock(
3005 3004 self.ui,
3006 3005 vfs,
3007 3006 lockname,
3008 3007 timeout,
3009 3008 warntimeout,
3010 3009 releasefn=releasefn,
3011 3010 acquirefn=acquirefn,
3012 3011 desc=desc,
3013 3012 signalsafe=signalsafe,
3014 3013 )
3015 3014 return l
3016 3015
3017 3016 def _afterlock(self, callback):
3018 3017 """add a callback to be run when the repository is fully unlocked
3019 3018
3020 3019 The callback will be executed when the outermost lock is released
3021 3020 (with wlock being higher level than 'lock')."""
3022 3021 for ref in (self._wlockref, self._lockref):
3023 3022 l = ref and ref()
3024 3023 if l and l.held:
3025 3024 l.postrelease.append(callback)
3026 3025 break
3027 3026 else: # no lock have been found.
3028 3027 callback(True)
3029 3028
3030 3029 def lock(self, wait=True):
3031 3030 """Lock the repository store (.hg/store) and return a weak reference
3032 3031 to the lock. Use this before modifying the store (e.g. committing or
3033 3032 stripping). If you are opening a transaction, get a lock as well.)
3034 3033
3035 3034 If both 'lock' and 'wlock' must be acquired, ensure you always acquires
3036 3035 'wlock' first to avoid a dead-lock hazard."""
3037 3036 l = self._currentlock(self._lockref)
3038 3037 if l is not None:
3039 3038 l.lock()
3040 3039 return l
3041 3040
3042 3041 l = self._lock(
3043 3042 vfs=self.svfs,
3044 3043 lockname=b"lock",
3045 3044 wait=wait,
3046 3045 releasefn=None,
3047 3046 acquirefn=self.invalidate,
3048 3047 desc=_(b'repository %s') % self.origroot,
3049 3048 )
3050 3049 self._lockref = weakref.ref(l)
3051 3050 return l
3052 3051
3053 3052 def wlock(self, wait=True):
3054 3053 """Lock the non-store parts of the repository (everything under
3055 3054 .hg except .hg/store) and return a weak reference to the lock.
3056 3055
3057 3056 Use this before modifying files in .hg.
3058 3057
3059 3058 If both 'lock' and 'wlock' must be acquired, ensure you always acquires
3060 3059 'wlock' first to avoid a dead-lock hazard."""
3061 3060 l = self._wlockref() if self._wlockref else None
3062 3061 if l is not None and l.held:
3063 3062 l.lock()
3064 3063 return l
3065 3064
3066 3065 # We do not need to check for non-waiting lock acquisition. Such
3067 3066 # acquisition would not cause dead-lock as they would just fail.
3068 3067 if wait and (
3069 3068 self.ui.configbool(b'devel', b'all-warnings')
3070 3069 or self.ui.configbool(b'devel', b'check-locks')
3071 3070 ):
3072 3071 if self._currentlock(self._lockref) is not None:
3073 3072 self.ui.develwarn(b'"wlock" acquired after "lock"')
3074 3073
3075 3074 def unlock():
3076 3075 if self.dirstate.pendingparentchange():
3077 3076 self.dirstate.invalidate()
3078 3077 else:
3079 3078 self.dirstate.write(None)
3080 3079
3081 3080 self._filecache[b'dirstate'].refresh()
3082 3081
3083 3082 l = self._lock(
3084 3083 self.vfs,
3085 3084 b"wlock",
3086 3085 wait,
3087 3086 unlock,
3088 3087 self.invalidatedirstate,
3089 3088 _(b'working directory of %s') % self.origroot,
3090 3089 )
3091 3090 self._wlockref = weakref.ref(l)
3092 3091 return l
3093 3092
3094 3093 def _currentlock(self, lockref):
3095 3094 """Returns the lock if it's held, or None if it's not."""
3096 3095 if lockref is None:
3097 3096 return None
3098 3097 l = lockref()
3099 3098 if l is None or not l.held:
3100 3099 return None
3101 3100 return l
3102 3101
3103 3102 def currentwlock(self):
3104 3103 """Returns the wlock if it's held, or None if it's not."""
3105 3104 return self._currentlock(self._wlockref)
3106 3105
3107 3106 def checkcommitpatterns(self, wctx, match, status, fail):
3108 3107 """check for commit arguments that aren't committable"""
3109 3108 if match.isexact() or match.prefix():
3110 3109 matched = set(status.modified + status.added + status.removed)
3111 3110
3112 3111 for f in match.files():
3113 3112 f = self.dirstate.normalize(f)
3114 3113 if f == b'.' or f in matched or f in wctx.substate:
3115 3114 continue
3116 3115 if f in status.deleted:
3117 3116 fail(f, _(b'file not found!'))
3118 3117 # Is it a directory that exists or used to exist?
3119 3118 if self.wvfs.isdir(f) or wctx.p1().hasdir(f):
3120 3119 d = f + b'/'
3121 3120 for mf in matched:
3122 3121 if mf.startswith(d):
3123 3122 break
3124 3123 else:
3125 3124 fail(f, _(b"no match under directory!"))
3126 3125 elif f not in self.dirstate:
3127 3126 fail(f, _(b"file not tracked!"))
3128 3127
3129 3128 @unfilteredmethod
3130 3129 def commit(
3131 3130 self,
3132 3131 text=b"",
3133 3132 user=None,
3134 3133 date=None,
3135 3134 match=None,
3136 3135 force=False,
3137 3136 editor=None,
3138 3137 extra=None,
3139 3138 ):
3140 3139 """Add a new revision to current repository.
3141 3140
3142 3141 Revision information is gathered from the working directory,
3143 3142 match can be used to filter the committed files. If editor is
3144 3143 supplied, it is called to get a commit message.
3145 3144 """
3146 3145 if extra is None:
3147 3146 extra = {}
3148 3147
3149 3148 def fail(f, msg):
3150 3149 raise error.InputError(b'%s: %s' % (f, msg))
3151 3150
3152 3151 if not match:
3153 3152 match = matchmod.always()
3154 3153
3155 3154 if not force:
3156 3155 match.bad = fail
3157 3156
3158 3157 # lock() for recent changelog (see issue4368)
3159 3158 with self.wlock(), self.lock():
3160 3159 wctx = self[None]
3161 3160 merge = len(wctx.parents()) > 1
3162 3161
3163 3162 if not force and merge and not match.always():
3164 3163 raise error.Abort(
3165 3164 _(
3166 3165 b'cannot partially commit a merge '
3167 3166 b'(do not specify files or patterns)'
3168 3167 )
3169 3168 )
3170 3169
3171 3170 status = self.status(match=match, clean=force)
3172 3171 if force:
3173 3172 status.modified.extend(
3174 3173 status.clean
3175 3174 ) # mq may commit clean files
3176 3175
3177 3176 # check subrepos
3178 3177 subs, commitsubs, newstate = subrepoutil.precommit(
3179 3178 self.ui, wctx, status, match, force=force
3180 3179 )
3181 3180
3182 3181 # make sure all explicit patterns are matched
3183 3182 if not force:
3184 3183 self.checkcommitpatterns(wctx, match, status, fail)
3185 3184
3186 3185 cctx = context.workingcommitctx(
3187 3186 self, status, text, user, date, extra
3188 3187 )
3189 3188
3190 3189 ms = mergestatemod.mergestate.read(self)
3191 3190 mergeutil.checkunresolved(ms)
3192 3191
3193 3192 # internal config: ui.allowemptycommit
3194 3193 if cctx.isempty() and not self.ui.configbool(
3195 3194 b'ui', b'allowemptycommit'
3196 3195 ):
3197 3196 self.ui.debug(b'nothing to commit, clearing merge state\n')
3198 3197 ms.reset()
3199 3198 return None
3200 3199
3201 3200 if merge and cctx.deleted():
3202 3201 raise error.Abort(_(b"cannot commit merge with missing files"))
3203 3202
3204 3203 if editor:
3205 3204 cctx._text = editor(self, cctx, subs)
3206 3205 edited = text != cctx._text
3207 3206
3208 3207 # Save commit message in case this transaction gets rolled back
3209 3208 # (e.g. by a pretxncommit hook). Leave the content alone on
3210 3209 # the assumption that the user will use the same editor again.
3211 3210 msg_path = self.savecommitmessage(cctx._text)
3212 3211
3213 3212 # commit subs and write new state
3214 3213 if subs:
3215 3214 uipathfn = scmutil.getuipathfn(self)
3216 3215 for s in sorted(commitsubs):
3217 3216 sub = wctx.sub(s)
3218 3217 self.ui.status(
3219 3218 _(b'committing subrepository %s\n')
3220 3219 % uipathfn(subrepoutil.subrelpath(sub))
3221 3220 )
3222 3221 sr = sub.commit(cctx._text, user, date)
3223 3222 newstate[s] = (newstate[s][0], sr)
3224 3223 subrepoutil.writestate(self, newstate)
3225 3224
3226 3225 p1, p2 = self.dirstate.parents()
3227 3226 hookp1, hookp2 = hex(p1), (p2 != self.nullid and hex(p2) or b'')
3228 3227 try:
3229 3228 self.hook(
3230 3229 b"precommit", throw=True, parent1=hookp1, parent2=hookp2
3231 3230 )
3232 3231 with self.transaction(b'commit'):
3233 3232 ret = self.commitctx(cctx, True)
3234 3233 # update bookmarks, dirstate and mergestate
3235 3234 bookmarks.update(self, [p1, p2], ret)
3236 3235 cctx.markcommitted(ret)
3237 3236 ms.reset()
3238 3237 except: # re-raises
3239 3238 if edited:
3240 3239 self.ui.write(
3241 3240 _(b'note: commit message saved in %s\n') % msg_path
3242 3241 )
3243 3242 self.ui.write(
3244 3243 _(
3245 3244 b"note: use 'hg commit --logfile "
3246 3245 b"%s --edit' to reuse it\n"
3247 3246 )
3248 3247 % msg_path
3249 3248 )
3250 3249 raise
3251 3250
3252 3251 def commithook(unused_success):
3253 3252 # hack for command that use a temporary commit (eg: histedit)
3254 3253 # temporary commit got stripped before hook release
3255 3254 if self.changelog.hasnode(ret):
3256 3255 self.hook(
3257 3256 b"commit", node=hex(ret), parent1=hookp1, parent2=hookp2
3258 3257 )
3259 3258
3260 3259 self._afterlock(commithook)
3261 3260 return ret
3262 3261
3263 3262 @unfilteredmethod
3264 3263 def commitctx(self, ctx, error=False, origctx=None):
3265 3264 return commit.commitctx(self, ctx, error=error, origctx=origctx)
3266 3265
3267 3266 @unfilteredmethod
3268 3267 def destroying(self):
3269 3268 """Inform the repository that nodes are about to be destroyed.
3270 3269 Intended for use by strip and rollback, so there's a common
3271 3270 place for anything that has to be done before destroying history.
3272 3271
3273 3272 This is mostly useful for saving state that is in memory and waiting
3274 3273 to be flushed when the current lock is released. Because a call to
3275 3274 destroyed is imminent, the repo will be invalidated causing those
3276 3275 changes to stay in memory (waiting for the next unlock), or vanish
3277 3276 completely.
3278 3277 """
3279 3278 # When using the same lock to commit and strip, the phasecache is left
3280 3279 # dirty after committing. Then when we strip, the repo is invalidated,
3281 3280 # causing those changes to disappear.
3282 3281 if '_phasecache' in vars(self):
3283 3282 self._phasecache.write()
3284 3283
3285 3284 @unfilteredmethod
3286 3285 def destroyed(self):
3287 3286 """Inform the repository that nodes have been destroyed.
3288 3287 Intended for use by strip and rollback, so there's a common
3289 3288 place for anything that has to be done after destroying history.
3290 3289 """
3291 3290 # When one tries to:
3292 3291 # 1) destroy nodes thus calling this method (e.g. strip)
3293 3292 # 2) use phasecache somewhere (e.g. commit)
3294 3293 #
3295 3294 # then 2) will fail because the phasecache contains nodes that were
3296 3295 # removed. We can either remove phasecache from the filecache,
3297 3296 # causing it to reload next time it is accessed, or simply filter
3298 3297 # the removed nodes now and write the updated cache.
3299 3298 self._phasecache.filterunknown(self)
3300 3299 self._phasecache.write()
3301 3300
3302 3301 # refresh all repository caches
3303 3302 self.updatecaches()
3304 3303
3305 3304 # Ensure the persistent tag cache is updated. Doing it now
3306 3305 # means that the tag cache only has to worry about destroyed
3307 3306 # heads immediately after a strip/rollback. That in turn
3308 3307 # guarantees that "cachetip == currenttip" (comparing both rev
3309 3308 # and node) always means no nodes have been added or destroyed.
3310 3309
3311 3310 # XXX this is suboptimal when qrefresh'ing: we strip the current
3312 3311 # head, refresh the tag cache, then immediately add a new head.
3313 3312 # But I think doing it this way is necessary for the "instant
3314 3313 # tag cache retrieval" case to work.
3315 3314 self.invalidate()
3316 3315
3317 3316 def status(
3318 3317 self,
3319 3318 node1=b'.',
3320 3319 node2=None,
3321 3320 match=None,
3322 3321 ignored=False,
3323 3322 clean=False,
3324 3323 unknown=False,
3325 3324 listsubrepos=False,
3326 3325 ):
3327 3326 '''a convenience method that calls node1.status(node2)'''
3328 3327 return self[node1].status(
3329 3328 node2, match, ignored, clean, unknown, listsubrepos
3330 3329 )
3331 3330
3332 3331 def addpostdsstatus(self, ps):
3333 3332 """Add a callback to run within the wlock, at the point at which status
3334 3333 fixups happen.
3335 3334
3336 3335 On status completion, callback(wctx, status) will be called with the
3337 3336 wlock held, unless the dirstate has changed from underneath or the wlock
3338 3337 couldn't be grabbed.
3339 3338
3340 3339 Callbacks should not capture and use a cached copy of the dirstate --
3341 3340 it might change in the meanwhile. Instead, they should access the
3342 3341 dirstate via wctx.repo().dirstate.
3343 3342
3344 3343 This list is emptied out after each status run -- extensions should
3345 3344 make sure it adds to this list each time dirstate.status is called.
3346 3345 Extensions should also make sure they don't call this for statuses
3347 3346 that don't involve the dirstate.
3348 3347 """
3349 3348
3350 3349 # The list is located here for uniqueness reasons -- it is actually
3351 3350 # managed by the workingctx, but that isn't unique per-repo.
3352 3351 self._postdsstatus.append(ps)
3353 3352
3354 3353 def postdsstatus(self):
3355 3354 """Used by workingctx to get the list of post-dirstate-status hooks."""
3356 3355 return self._postdsstatus
3357 3356
3358 3357 def clearpostdsstatus(self):
3359 3358 """Used by workingctx to clear post-dirstate-status hooks."""
3360 3359 del self._postdsstatus[:]
3361 3360
3362 3361 def heads(self, start=None):
3363 3362 if start is None:
3364 3363 cl = self.changelog
3365 3364 headrevs = reversed(cl.headrevs())
3366 3365 return [cl.node(rev) for rev in headrevs]
3367 3366
3368 3367 heads = self.changelog.heads(start)
3369 3368 # sort the output in rev descending order
3370 3369 return sorted(heads, key=self.changelog.rev, reverse=True)
3371 3370
3372 3371 def branchheads(self, branch=None, start=None, closed=False):
3373 3372 """return a (possibly filtered) list of heads for the given branch
3374 3373
3375 3374 Heads are returned in topological order, from newest to oldest.
3376 3375 If branch is None, use the dirstate branch.
3377 3376 If start is not None, return only heads reachable from start.
3378 3377 If closed is True, return heads that are marked as closed as well.
3379 3378 """
3380 3379 if branch is None:
3381 3380 branch = self[None].branch()
3382 3381 branches = self.branchmap()
3383 3382 if not branches.hasbranch(branch):
3384 3383 return []
3385 3384 # the cache returns heads ordered lowest to highest
3386 3385 bheads = list(reversed(branches.branchheads(branch, closed=closed)))
3387 3386 if start is not None:
3388 3387 # filter out the heads that cannot be reached from startrev
3389 3388 fbheads = set(self.changelog.nodesbetween([start], bheads)[2])
3390 3389 bheads = [h for h in bheads if h in fbheads]
3391 3390 return bheads
3392 3391
3393 3392 def branches(self, nodes):
3394 3393 if not nodes:
3395 3394 nodes = [self.changelog.tip()]
3396 3395 b = []
3397 3396 for n in nodes:
3398 3397 t = n
3399 3398 while True:
3400 3399 p = self.changelog.parents(n)
3401 3400 if p[1] != self.nullid or p[0] == self.nullid:
3402 3401 b.append((t, n, p[0], p[1]))
3403 3402 break
3404 3403 n = p[0]
3405 3404 return b
3406 3405
3407 3406 def between(self, pairs):
3408 3407 r = []
3409 3408
3410 3409 for top, bottom in pairs:
3411 3410 n, l, i = top, [], 0
3412 3411 f = 1
3413 3412
3414 3413 while n != bottom and n != self.nullid:
3415 3414 p = self.changelog.parents(n)[0]
3416 3415 if i == f:
3417 3416 l.append(n)
3418 3417 f = f * 2
3419 3418 n = p
3420 3419 i += 1
3421 3420
3422 3421 r.append(l)
3423 3422
3424 3423 return r
3425 3424
3426 3425 def checkpush(self, pushop):
3427 3426 """Extensions can override this function if additional checks have
3428 3427 to be performed before pushing, or call it if they override push
3429 3428 command.
3430 3429 """
3431 3430
3432 3431 @unfilteredpropertycache
3433 3432 def prepushoutgoinghooks(self):
3434 3433 """Return util.hooks consists of a pushop with repo, remote, outgoing
3435 3434 methods, which are called before pushing changesets.
3436 3435 """
3437 3436 return util.hooks()
3438 3437
3439 3438 def pushkey(self, namespace, key, old, new):
3440 3439 try:
3441 3440 tr = self.currenttransaction()
3442 3441 hookargs = {}
3443 3442 if tr is not None:
3444 3443 hookargs.update(tr.hookargs)
3445 3444 hookargs = pycompat.strkwargs(hookargs)
3446 3445 hookargs['namespace'] = namespace
3447 3446 hookargs['key'] = key
3448 3447 hookargs['old'] = old
3449 3448 hookargs['new'] = new
3450 3449 self.hook(b'prepushkey', throw=True, **hookargs)
3451 3450 except error.HookAbort as exc:
3452 3451 self.ui.write_err(_(b"pushkey-abort: %s\n") % exc)
3453 3452 if exc.hint:
3454 3453 self.ui.write_err(_(b"(%s)\n") % exc.hint)
3455 3454 return False
3456 3455 self.ui.debug(b'pushing key for "%s:%s"\n' % (namespace, key))
3457 3456 ret = pushkey.push(self, namespace, key, old, new)
3458 3457
3459 3458 def runhook(unused_success):
3460 3459 self.hook(
3461 3460 b'pushkey',
3462 3461 namespace=namespace,
3463 3462 key=key,
3464 3463 old=old,
3465 3464 new=new,
3466 3465 ret=ret,
3467 3466 )
3468 3467
3469 3468 self._afterlock(runhook)
3470 3469 return ret
3471 3470
3472 3471 def listkeys(self, namespace):
3473 3472 self.hook(b'prelistkeys', throw=True, namespace=namespace)
3474 3473 self.ui.debug(b'listing keys for "%s"\n' % namespace)
3475 3474 values = pushkey.list(self, namespace)
3476 3475 self.hook(b'listkeys', namespace=namespace, values=values)
3477 3476 return values
3478 3477
3479 3478 def debugwireargs(self, one, two, three=None, four=None, five=None):
3480 3479 '''used to test argument passing over the wire'''
3481 3480 return b"%s %s %s %s %s" % (
3482 3481 one,
3483 3482 two,
3484 3483 pycompat.bytestr(three),
3485 3484 pycompat.bytestr(four),
3486 3485 pycompat.bytestr(five),
3487 3486 )
3488 3487
3489 3488 def savecommitmessage(self, text):
3490 3489 fp = self.vfs(b'last-message.txt', b'wb')
3491 3490 try:
3492 3491 fp.write(text)
3493 3492 finally:
3494 3493 fp.close()
3495 3494 return self.pathto(fp.name[len(self.root) + 1 :])
3496 3495
3497 3496 def register_wanted_sidedata(self, category):
3498 3497 if repository.REPO_FEATURE_SIDE_DATA not in self.features:
3499 3498 # Only revlogv2 repos can want sidedata.
3500 3499 return
3501 3500 self._wanted_sidedata.add(pycompat.bytestr(category))
3502 3501
3503 3502 def register_sidedata_computer(
3504 3503 self, kind, category, keys, computer, flags, replace=False
3505 3504 ):
3506 3505 if kind not in revlogconst.ALL_KINDS:
3507 3506 msg = _(b"unexpected revlog kind '%s'.")
3508 3507 raise error.ProgrammingError(msg % kind)
3509 3508 category = pycompat.bytestr(category)
3510 3509 already_registered = category in self._sidedata_computers.get(kind, [])
3511 3510 if already_registered and not replace:
3512 3511 msg = _(
3513 3512 b"cannot register a sidedata computer twice for category '%s'."
3514 3513 )
3515 3514 raise error.ProgrammingError(msg % category)
3516 3515 if replace and not already_registered:
3517 3516 msg = _(
3518 3517 b"cannot replace a sidedata computer that isn't registered "
3519 3518 b"for category '%s'."
3520 3519 )
3521 3520 raise error.ProgrammingError(msg % category)
3522 3521 self._sidedata_computers.setdefault(kind, {})
3523 3522 self._sidedata_computers[kind][category] = (keys, computer, flags)
3524 3523
3525 3524
3526 3525 # used to avoid circular references so destructors work
3527 3526 def aftertrans(files):
3528 3527 renamefiles = [tuple(t) for t in files]
3529 3528
3530 3529 def a():
3531 3530 for vfs, src, dest in renamefiles:
3532 3531 # if src and dest refer to a same file, vfs.rename is a no-op,
3533 3532 # leaving both src and dest on disk. delete dest to make sure
3534 3533 # the rename couldn't be such a no-op.
3535 3534 vfs.tryunlink(dest)
3536 3535 try:
3537 3536 vfs.rename(src, dest)
3538 3537 except FileNotFoundError: # journal file does not yet exist
3539 3538 pass
3540 3539
3541 3540 return a
3542 3541
3543 3542
3544 3543 def undoname(fn: bytes) -> bytes:
3545 3544 base, name = os.path.split(fn)
3546 3545 assert name.startswith(b'journal')
3547 3546 return os.path.join(base, name.replace(b'journal', b'undo', 1))
3548 3547
3549 3548
3550 3549 def instance(ui, path: bytes, create, intents=None, createopts=None):
3551 3550
3552 3551 # prevent cyclic import localrepo -> upgrade -> localrepo
3553 3552 from . import upgrade
3554 3553
3555 3554 localpath = urlutil.urllocalpath(path)
3556 3555 if create:
3557 3556 createrepository(ui, localpath, createopts=createopts)
3558 3557
3559 3558 def repo_maker():
3560 3559 return makelocalrepository(ui, localpath, intents=intents)
3561 3560
3562 3561 repo = repo_maker()
3563 3562 repo = upgrade.may_auto_upgrade(repo, repo_maker)
3564 3563 return repo
3565 3564
3566 3565
3567 3566 def islocal(path: bytes) -> bool:
3568 3567 return True
3569 3568
3570 3569
3571 3570 def defaultcreateopts(ui, createopts=None):
3572 3571 """Populate the default creation options for a repository.
3573 3572
3574 3573 A dictionary of explicitly requested creation options can be passed
3575 3574 in. Missing keys will be populated.
3576 3575 """
3577 3576 createopts = dict(createopts or {})
3578 3577
3579 3578 if b'backend' not in createopts:
3580 3579 # experimental config: storage.new-repo-backend
3581 3580 createopts[b'backend'] = ui.config(b'storage', b'new-repo-backend')
3582 3581
3583 3582 return createopts
3584 3583
3585 3584
3586 3585 def clone_requirements(ui, createopts, srcrepo):
3587 3586 """clone the requirements of a local repo for a local clone
3588 3587
3589 3588 The store requirements are unchanged while the working copy requirements
3590 3589 depends on the configuration
3591 3590 """
3592 3591 target_requirements = set()
3593 3592 if not srcrepo.requirements:
3594 3593 # this is a legacy revlog "v0" repository, we cannot do anything fancy
3595 3594 # with it.
3596 3595 return target_requirements
3597 3596 createopts = defaultcreateopts(ui, createopts=createopts)
3598 3597 for r in newreporequirements(ui, createopts):
3599 3598 if r in requirementsmod.WORKING_DIR_REQUIREMENTS:
3600 3599 target_requirements.add(r)
3601 3600
3602 3601 for r in srcrepo.requirements:
3603 3602 if r not in requirementsmod.WORKING_DIR_REQUIREMENTS:
3604 3603 target_requirements.add(r)
3605 3604 return target_requirements
3606 3605
3607 3606
3608 3607 def newreporequirements(ui, createopts):
3609 3608 """Determine the set of requirements for a new local repository.
3610 3609
3611 3610 Extensions can wrap this function to specify custom requirements for
3612 3611 new repositories.
3613 3612 """
3614 3613
3615 3614 if b'backend' not in createopts:
3616 3615 raise error.ProgrammingError(
3617 3616 b'backend key not present in createopts; '
3618 3617 b'was defaultcreateopts() called?'
3619 3618 )
3620 3619
3621 3620 if createopts[b'backend'] != b'revlogv1':
3622 3621 raise error.Abort(
3623 3622 _(
3624 3623 b'unable to determine repository requirements for '
3625 3624 b'storage backend: %s'
3626 3625 )
3627 3626 % createopts[b'backend']
3628 3627 )
3629 3628
3630 3629 requirements = {requirementsmod.REVLOGV1_REQUIREMENT}
3631 3630 if ui.configbool(b'format', b'usestore'):
3632 3631 requirements.add(requirementsmod.STORE_REQUIREMENT)
3633 3632 if ui.configbool(b'format', b'usefncache'):
3634 3633 requirements.add(requirementsmod.FNCACHE_REQUIREMENT)
3635 3634 if ui.configbool(b'format', b'dotencode'):
3636 3635 requirements.add(requirementsmod.DOTENCODE_REQUIREMENT)
3637 3636
3638 3637 compengines = ui.configlist(b'format', b'revlog-compression')
3639 3638 for compengine in compengines:
3640 3639 if compengine in util.compengines:
3641 3640 engine = util.compengines[compengine]
3642 3641 if engine.available() and engine.revlogheader():
3643 3642 break
3644 3643 else:
3645 3644 raise error.Abort(
3646 3645 _(
3647 3646 b'compression engines %s defined by '
3648 3647 b'format.revlog-compression not available'
3649 3648 )
3650 3649 % b', '.join(b'"%s"' % e for e in compengines),
3651 3650 hint=_(
3652 3651 b'run "hg debuginstall" to list available '
3653 3652 b'compression engines'
3654 3653 ),
3655 3654 )
3656 3655
3657 3656 # zlib is the historical default and doesn't need an explicit requirement.
3658 3657 if compengine == b'zstd':
3659 3658 requirements.add(b'revlog-compression-zstd')
3660 3659 elif compengine != b'zlib':
3661 3660 requirements.add(b'exp-compression-%s' % compengine)
3662 3661
3663 3662 if scmutil.gdinitconfig(ui):
3664 3663 requirements.add(requirementsmod.GENERALDELTA_REQUIREMENT)
3665 3664 if ui.configbool(b'format', b'sparse-revlog'):
3666 3665 requirements.add(requirementsmod.SPARSEREVLOG_REQUIREMENT)
3667 3666
3668 3667 # experimental config: format.use-dirstate-v2
3669 3668 # Keep this logic in sync with `has_dirstate_v2()` in `tests/hghave.py`
3670 3669 if ui.configbool(b'format', b'use-dirstate-v2'):
3671 3670 requirements.add(requirementsmod.DIRSTATE_V2_REQUIREMENT)
3672 3671
3673 3672 # experimental config: format.exp-use-copies-side-data-changeset
3674 3673 if ui.configbool(b'format', b'exp-use-copies-side-data-changeset'):
3675 3674 requirements.add(requirementsmod.CHANGELOGV2_REQUIREMENT)
3676 3675 requirements.add(requirementsmod.COPIESSDC_REQUIREMENT)
3677 3676 if ui.configbool(b'experimental', b'treemanifest'):
3678 3677 requirements.add(requirementsmod.TREEMANIFEST_REQUIREMENT)
3679 3678
3680 3679 changelogv2 = ui.config(b'format', b'exp-use-changelog-v2')
3681 3680 if changelogv2 == b'enable-unstable-format-and-corrupt-my-data':
3682 3681 requirements.add(requirementsmod.CHANGELOGV2_REQUIREMENT)
3683 3682
3684 3683 revlogv2 = ui.config(b'experimental', b'revlogv2')
3685 3684 if revlogv2 == b'enable-unstable-format-and-corrupt-my-data':
3686 3685 requirements.discard(requirementsmod.REVLOGV1_REQUIREMENT)
3687 3686 requirements.add(requirementsmod.REVLOGV2_REQUIREMENT)
3688 3687 # experimental config: format.internal-phase
3689 3688 if ui.configbool(b'format', b'use-internal-phase'):
3690 3689 requirements.add(requirementsmod.INTERNAL_PHASE_REQUIREMENT)
3691 3690
3692 3691 # experimental config: format.exp-archived-phase
3693 3692 if ui.configbool(b'format', b'exp-archived-phase'):
3694 3693 requirements.add(requirementsmod.ARCHIVED_PHASE_REQUIREMENT)
3695 3694
3696 3695 if createopts.get(b'narrowfiles'):
3697 3696 requirements.add(requirementsmod.NARROW_REQUIREMENT)
3698 3697
3699 3698 if createopts.get(b'lfs'):
3700 3699 requirements.add(b'lfs')
3701 3700
3702 3701 if ui.configbool(b'format', b'bookmarks-in-store'):
3703 3702 requirements.add(requirementsmod.BOOKMARKS_IN_STORE_REQUIREMENT)
3704 3703
3705 3704 if ui.configbool(b'format', b'use-persistent-nodemap'):
3706 3705 requirements.add(requirementsmod.NODEMAP_REQUIREMENT)
3707 3706
3708 3707 # if share-safe is enabled, let's create the new repository with the new
3709 3708 # requirement
3710 3709 if ui.configbool(b'format', b'use-share-safe'):
3711 3710 requirements.add(requirementsmod.SHARESAFE_REQUIREMENT)
3712 3711
3713 3712 # if we are creating a share-repoΒΉ we have to handle requirement
3714 3713 # differently.
3715 3714 #
3716 3715 # [1] (i.e. reusing the store from another repository, just having a
3717 3716 # working copy)
3718 3717 if b'sharedrepo' in createopts:
3719 3718 source_requirements = set(createopts[b'sharedrepo'].requirements)
3720 3719
3721 3720 if requirementsmod.SHARESAFE_REQUIREMENT not in source_requirements:
3722 3721 # share to an old school repository, we have to copy the
3723 3722 # requirements and hope for the best.
3724 3723 requirements = source_requirements
3725 3724 else:
3726 3725 # We have control on the working copy only, so "copy" the non
3727 3726 # working copy part over, ignoring previous logic.
3728 3727 to_drop = set()
3729 3728 for req in requirements:
3730 3729 if req in requirementsmod.WORKING_DIR_REQUIREMENTS:
3731 3730 continue
3732 3731 if req in source_requirements:
3733 3732 continue
3734 3733 to_drop.add(req)
3735 3734 requirements -= to_drop
3736 3735 requirements |= source_requirements
3737 3736
3738 3737 if createopts.get(b'sharedrelative'):
3739 3738 requirements.add(requirementsmod.RELATIVE_SHARED_REQUIREMENT)
3740 3739 else:
3741 3740 requirements.add(requirementsmod.SHARED_REQUIREMENT)
3742 3741
3743 3742 if ui.configbool(b'format', b'use-dirstate-tracked-hint'):
3744 3743 version = ui.configint(b'format', b'use-dirstate-tracked-hint.version')
3745 3744 msg = _(b"ignoring unknown tracked key version: %d\n")
3746 3745 hint = _(
3747 3746 b"see `hg help config.format.use-dirstate-tracked-hint-version"
3748 3747 )
3749 3748 if version != 1:
3750 3749 ui.warn(msg % version, hint=hint)
3751 3750 else:
3752 3751 requirements.add(requirementsmod.DIRSTATE_TRACKED_HINT_V1)
3753 3752
3754 3753 return requirements
3755 3754
3756 3755
3757 3756 def checkrequirementscompat(ui, requirements):
3758 3757 """Checks compatibility of repository requirements enabled and disabled.
3759 3758
3760 3759 Returns a set of requirements which needs to be dropped because dependend
3761 3760 requirements are not enabled. Also warns users about it"""
3762 3761
3763 3762 dropped = set()
3764 3763
3765 3764 if requirementsmod.STORE_REQUIREMENT not in requirements:
3766 3765 if requirementsmod.BOOKMARKS_IN_STORE_REQUIREMENT in requirements:
3767 3766 ui.warn(
3768 3767 _(
3769 3768 b'ignoring enabled \'format.bookmarks-in-store\' config '
3770 3769 b'beacuse it is incompatible with disabled '
3771 3770 b'\'format.usestore\' config\n'
3772 3771 )
3773 3772 )
3774 3773 dropped.add(requirementsmod.BOOKMARKS_IN_STORE_REQUIREMENT)
3775 3774
3776 3775 if (
3777 3776 requirementsmod.SHARED_REQUIREMENT in requirements
3778 3777 or requirementsmod.RELATIVE_SHARED_REQUIREMENT in requirements
3779 3778 ):
3780 3779 raise error.Abort(
3781 3780 _(
3782 3781 b"cannot create shared repository as source was created"
3783 3782 b" with 'format.usestore' config disabled"
3784 3783 )
3785 3784 )
3786 3785
3787 3786 if requirementsmod.SHARESAFE_REQUIREMENT in requirements:
3788 3787 if ui.hasconfig(b'format', b'use-share-safe'):
3789 3788 msg = _(
3790 3789 b"ignoring enabled 'format.use-share-safe' config because "
3791 3790 b"it is incompatible with disabled 'format.usestore'"
3792 3791 b" config\n"
3793 3792 )
3794 3793 ui.warn(msg)
3795 3794 dropped.add(requirementsmod.SHARESAFE_REQUIREMENT)
3796 3795
3797 3796 return dropped
3798 3797
3799 3798
3800 3799 def filterknowncreateopts(ui, createopts):
3801 3800 """Filters a dict of repo creation options against options that are known.
3802 3801
3803 3802 Receives a dict of repo creation options and returns a dict of those
3804 3803 options that we don't know how to handle.
3805 3804
3806 3805 This function is called as part of repository creation. If the
3807 3806 returned dict contains any items, repository creation will not
3808 3807 be allowed, as it means there was a request to create a repository
3809 3808 with options not recognized by loaded code.
3810 3809
3811 3810 Extensions can wrap this function to filter out creation options
3812 3811 they know how to handle.
3813 3812 """
3814 3813 known = {
3815 3814 b'backend',
3816 3815 b'lfs',
3817 3816 b'narrowfiles',
3818 3817 b'sharedrepo',
3819 3818 b'sharedrelative',
3820 3819 b'shareditems',
3821 3820 b'shallowfilestore',
3822 3821 }
3823 3822
3824 3823 return {k: v for k, v in createopts.items() if k not in known}
3825 3824
3826 3825
3827 3826 def createrepository(ui, path: bytes, createopts=None, requirements=None):
3828 3827 """Create a new repository in a vfs.
3829 3828
3830 3829 ``path`` path to the new repo's working directory.
3831 3830 ``createopts`` options for the new repository.
3832 3831 ``requirement`` predefined set of requirements.
3833 3832 (incompatible with ``createopts``)
3834 3833
3835 3834 The following keys for ``createopts`` are recognized:
3836 3835
3837 3836 backend
3838 3837 The storage backend to use.
3839 3838 lfs
3840 3839 Repository will be created with ``lfs`` requirement. The lfs extension
3841 3840 will automatically be loaded when the repository is accessed.
3842 3841 narrowfiles
3843 3842 Set up repository to support narrow file storage.
3844 3843 sharedrepo
3845 3844 Repository object from which storage should be shared.
3846 3845 sharedrelative
3847 3846 Boolean indicating if the path to the shared repo should be
3848 3847 stored as relative. By default, the pointer to the "parent" repo
3849 3848 is stored as an absolute path.
3850 3849 shareditems
3851 3850 Set of items to share to the new repository (in addition to storage).
3852 3851 shallowfilestore
3853 3852 Indicates that storage for files should be shallow (not all ancestor
3854 3853 revisions are known).
3855 3854 """
3856 3855
3857 3856 if requirements is not None:
3858 3857 if createopts is not None:
3859 3858 msg = b'cannot specify both createopts and requirements'
3860 3859 raise error.ProgrammingError(msg)
3861 3860 createopts = {}
3862 3861 else:
3863 3862 createopts = defaultcreateopts(ui, createopts=createopts)
3864 3863
3865 3864 unknownopts = filterknowncreateopts(ui, createopts)
3866 3865
3867 3866 if not isinstance(unknownopts, dict):
3868 3867 raise error.ProgrammingError(
3869 3868 b'filterknowncreateopts() did not return a dict'
3870 3869 )
3871 3870
3872 3871 if unknownopts:
3873 3872 raise error.Abort(
3874 3873 _(
3875 3874 b'unable to create repository because of unknown '
3876 3875 b'creation option: %s'
3877 3876 )
3878 3877 % b', '.join(sorted(unknownopts)),
3879 3878 hint=_(b'is a required extension not loaded?'),
3880 3879 )
3881 3880
3882 3881 requirements = newreporequirements(ui, createopts=createopts)
3883 3882 requirements -= checkrequirementscompat(ui, requirements)
3884 3883
3885 3884 wdirvfs = vfsmod.vfs(path, expandpath=True, realpath=True)
3886 3885
3887 3886 hgvfs = vfsmod.vfs(wdirvfs.join(b'.hg'))
3888 3887 if hgvfs.exists():
3889 3888 raise error.RepoError(_(b'repository %s already exists') % path)
3890 3889
3891 3890 if b'sharedrepo' in createopts:
3892 3891 sharedpath = createopts[b'sharedrepo'].sharedpath
3893 3892
3894 3893 if createopts.get(b'sharedrelative'):
3895 3894 try:
3896 3895 sharedpath = os.path.relpath(sharedpath, hgvfs.base)
3897 3896 sharedpath = util.pconvert(sharedpath)
3898 3897 except (IOError, ValueError) as e:
3899 3898 # ValueError is raised on Windows if the drive letters differ
3900 3899 # on each path.
3901 3900 raise error.Abort(
3902 3901 _(b'cannot calculate relative path'),
3903 3902 hint=stringutil.forcebytestr(e),
3904 3903 )
3905 3904
3906 3905 if not wdirvfs.exists():
3907 3906 wdirvfs.makedirs()
3908 3907
3909 3908 hgvfs.makedir(notindexed=True)
3910 3909 if b'sharedrepo' not in createopts:
3911 3910 hgvfs.mkdir(b'cache')
3912 3911 hgvfs.mkdir(b'wcache')
3913 3912
3914 3913 has_store = requirementsmod.STORE_REQUIREMENT in requirements
3915 3914 if has_store and b'sharedrepo' not in createopts:
3916 3915 hgvfs.mkdir(b'store')
3917 3916
3918 3917 # We create an invalid changelog outside the store so very old
3919 3918 # Mercurial versions (which didn't know about the requirements
3920 3919 # file) encounter an error on reading the changelog. This
3921 3920 # effectively locks out old clients and prevents them from
3922 3921 # mucking with a repo in an unknown format.
3923 3922 #
3924 3923 # The revlog header has version 65535, which won't be recognized by
3925 3924 # such old clients.
3926 3925 hgvfs.append(
3927 3926 b'00changelog.i',
3928 3927 b'\0\0\xFF\xFF dummy changelog to prevent using the old repo '
3929 3928 b'layout',
3930 3929 )
3931 3930
3932 3931 # Filter the requirements into working copy and store ones
3933 3932 wcreq, storereq = scmutil.filterrequirements(requirements)
3934 3933 # write working copy ones
3935 3934 scmutil.writerequires(hgvfs, wcreq)
3936 3935 # If there are store requirements and the current repository
3937 3936 # is not a shared one, write stored requirements
3938 3937 # For new shared repository, we don't need to write the store
3939 3938 # requirements as they are already present in store requires
3940 3939 if storereq and b'sharedrepo' not in createopts:
3941 3940 storevfs = vfsmod.vfs(hgvfs.join(b'store'), cacheaudited=True)
3942 3941 scmutil.writerequires(storevfs, storereq)
3943 3942
3944 3943 # Write out file telling readers where to find the shared store.
3945 3944 if b'sharedrepo' in createopts:
3946 3945 hgvfs.write(b'sharedpath', sharedpath)
3947 3946
3948 3947 if createopts.get(b'shareditems'):
3949 3948 shared = b'\n'.join(sorted(createopts[b'shareditems'])) + b'\n'
3950 3949 hgvfs.write(b'shared', shared)
3951 3950
3952 3951
3953 3952 def poisonrepository(repo):
3954 3953 """Poison a repository instance so it can no longer be used."""
3955 3954 # Perform any cleanup on the instance.
3956 3955 repo.close()
3957 3956
3958 3957 # Our strategy is to replace the type of the object with one that
3959 3958 # has all attribute lookups result in error.
3960 3959 #
3961 3960 # But we have to allow the close() method because some constructors
3962 3961 # of repos call close() on repo references.
3963 3962 class poisonedrepository:
3964 3963 def __getattribute__(self, item):
3965 3964 if item == 'close':
3966 3965 return object.__getattribute__(self, item)
3967 3966
3968 3967 raise error.ProgrammingError(
3969 3968 b'repo instances should not be used after unshare'
3970 3969 )
3971 3970
3972 3971 def close(self):
3973 3972 pass
3974 3973
3975 3974 # We may have a repoview, which intercepts __setattr__. So be sure
3976 3975 # we operate at the lowest level possible.
3977 3976 object.__setattr__(repo, '__class__', poisonedrepository)
@@ -1,675 +1,675 b''
1 1 # sshpeer.py - ssh repository proxy class for mercurial
2 2 #
3 3 # Copyright 2005, 2006 Olivia Mackall <olivia@selenic.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
9 9 import re
10 10 import uuid
11 11
12 12 from .i18n import _
13 13 from .pycompat import getattr
14 14 from . import (
15 15 error,
16 16 pycompat,
17 17 util,
18 18 wireprototypes,
19 19 wireprotov1peer,
20 20 wireprotov1server,
21 21 )
22 22 from .utils import (
23 23 procutil,
24 24 stringutil,
25 25 urlutil,
26 26 )
27 27
28 28
29 29 def _serverquote(s):
30 30 """quote a string for the remote shell ... which we assume is sh"""
31 31 if not s:
32 32 return s
33 33 if re.match(b'[a-zA-Z0-9@%_+=:,./-]*$', s):
34 34 return s
35 35 return b"'%s'" % s.replace(b"'", b"'\\''")
36 36
37 37
38 38 def _forwardoutput(ui, pipe, warn=False):
39 39 """display all data currently available on pipe as remote output.
40 40
41 41 This is non blocking."""
42 42 if pipe and not pipe.closed:
43 43 s = procutil.readpipe(pipe)
44 44 if s:
45 45 display = ui.warn if warn else ui.status
46 46 for l in s.splitlines():
47 47 display(_(b"remote: "), l, b'\n')
48 48
49 49
50 50 class doublepipe:
51 51 """Operate a side-channel pipe in addition of a main one
52 52
53 53 The side-channel pipe contains server output to be forwarded to the user
54 54 input. The double pipe will behave as the "main" pipe, but will ensure the
55 55 content of the "side" pipe is properly processed while we wait for blocking
56 56 call on the "main" pipe.
57 57
58 58 If large amounts of data are read from "main", the forward will cease after
59 59 the first bytes start to appear. This simplifies the implementation
60 60 without affecting actual output of sshpeer too much as we rarely issue
61 61 large read for data not yet emitted by the server.
62 62
63 63 The main pipe is expected to be a 'bufferedinputpipe' from the util module
64 64 that handle all the os specific bits. This class lives in this module
65 65 because it focus on behavior specific to the ssh protocol."""
66 66
67 67 def __init__(self, ui, main, side):
68 68 self._ui = ui
69 69 self._main = main
70 70 self._side = side
71 71
72 72 def _wait(self):
73 73 """wait until some data are available on main or side
74 74
75 75 return a pair of boolean (ismainready, issideready)
76 76
77 77 (This will only wait for data if the setup is supported by `util.poll`)
78 78 """
79 79 if (
80 80 isinstance(self._main, util.bufferedinputpipe)
81 81 and self._main.hasbuffer
82 82 ):
83 83 # Main has data. Assume side is worth poking at.
84 84 return True, True
85 85
86 86 fds = [self._main.fileno(), self._side.fileno()]
87 87 try:
88 88 act = util.poll(fds)
89 89 except NotImplementedError:
90 90 # non supported yet case, assume all have data.
91 91 act = fds
92 92 return (self._main.fileno() in act, self._side.fileno() in act)
93 93
94 94 def write(self, data):
95 95 return self._call(b'write', data)
96 96
97 97 def read(self, size):
98 98 r = self._call(b'read', size)
99 99 if size != 0 and not r:
100 100 # We've observed a condition that indicates the
101 101 # stdout closed unexpectedly. Check stderr one
102 102 # more time and snag anything that's there before
103 103 # letting anyone know the main part of the pipe
104 104 # closed prematurely.
105 105 _forwardoutput(self._ui, self._side)
106 106 return r
107 107
108 108 def unbufferedread(self, size):
109 109 r = self._call(b'unbufferedread', size)
110 110 if size != 0 and not r:
111 111 # We've observed a condition that indicates the
112 112 # stdout closed unexpectedly. Check stderr one
113 113 # more time and snag anything that's there before
114 114 # letting anyone know the main part of the pipe
115 115 # closed prematurely.
116 116 _forwardoutput(self._ui, self._side)
117 117 return r
118 118
119 119 def readline(self):
120 120 return self._call(b'readline')
121 121
122 122 def _call(self, methname, data=None):
123 123 """call <methname> on "main", forward output of "side" while blocking"""
124 124 # data can be '' or 0
125 125 if (data is not None and not data) or self._main.closed:
126 126 _forwardoutput(self._ui, self._side)
127 127 return b''
128 128 while True:
129 129 mainready, sideready = self._wait()
130 130 if sideready:
131 131 _forwardoutput(self._ui, self._side)
132 132 if mainready:
133 133 meth = getattr(self._main, methname)
134 134 if data is None:
135 135 return meth()
136 136 else:
137 137 return meth(data)
138 138
139 139 def close(self):
140 140 return self._main.close()
141 141
142 142 @property
143 143 def closed(self):
144 144 return self._main.closed
145 145
146 146 def flush(self):
147 147 return self._main.flush()
148 148
149 149
150 150 def _cleanuppipes(ui, pipei, pipeo, pipee, warn):
151 151 """Clean up pipes used by an SSH connection."""
152 152 didsomething = False
153 153 if pipeo and not pipeo.closed:
154 154 didsomething = True
155 155 pipeo.close()
156 156 if pipei and not pipei.closed:
157 157 didsomething = True
158 158 pipei.close()
159 159
160 160 if pipee and not pipee.closed:
161 161 didsomething = True
162 162 # Try to read from the err descriptor until EOF.
163 163 try:
164 164 for l in pipee:
165 165 ui.status(_(b'remote: '), l)
166 166 except (IOError, ValueError):
167 167 pass
168 168
169 169 pipee.close()
170 170
171 171 if didsomething and warn is not None:
172 172 # Encourage explicit close of sshpeers. Closing via __del__ is
173 173 # not very predictable when exceptions are thrown, which has led
174 174 # to deadlocks due to a peer get gc'ed in a fork
175 175 # We add our own stack trace, because the stacktrace when called
176 176 # from __del__ is useless.
177 177 ui.develwarn(b'missing close on SSH connection created at:\n%s' % warn)
178 178
179 179
180 180 def _makeconnection(ui, sshcmd, args, remotecmd, path, sshenv=None):
181 181 """Create an SSH connection to a server.
182 182
183 183 Returns a tuple of (process, stdin, stdout, stderr) for the
184 184 spawned process.
185 185 """
186 186 cmd = b'%s %s %s' % (
187 187 sshcmd,
188 188 args,
189 189 procutil.shellquote(
190 190 b'%s -R %s serve --stdio'
191 191 % (_serverquote(remotecmd), _serverquote(path))
192 192 ),
193 193 )
194 194
195 195 ui.debug(b'running %s\n' % cmd)
196 196
197 197 # no buffer allow the use of 'select'
198 198 # feel free to remove buffering and select usage when we ultimately
199 199 # move to threading.
200 200 stdin, stdout, stderr, proc = procutil.popen4(cmd, bufsize=0, env=sshenv)
201 201
202 202 return proc, stdin, stdout, stderr
203 203
204 204
205 205 def _clientcapabilities():
206 206 """Return list of capabilities of this client.
207 207
208 208 Returns a list of capabilities that are supported by this client.
209 209 """
210 210 protoparams = {b'partial-pull'}
211 211 comps = [
212 212 e.wireprotosupport().name
213 213 for e in util.compengines.supportedwireengines(util.CLIENTROLE)
214 214 ]
215 215 protoparams.add(b'comp=%s' % b','.join(comps))
216 216 return protoparams
217 217
218 218
219 219 def _performhandshake(ui, stdin, stdout, stderr):
220 220 def badresponse():
221 221 # Flush any output on stderr. In general, the stderr contains errors
222 222 # from the remote (ssh errors, some hg errors), and status indications
223 223 # (like "adding changes"), with no current way to tell them apart.
224 224 # Here we failed so early that it's almost certainly only errors, so
225 225 # use warn=True so -q doesn't hide them.
226 226 _forwardoutput(ui, stderr, warn=True)
227 227
228 228 msg = _(b'no suitable response from remote hg')
229 229 hint = ui.config(b'ui', b'ssherrorhint')
230 230 raise error.RepoError(msg, hint=hint)
231 231
232 232 # The handshake consists of sending wire protocol commands in reverse
233 233 # order of protocol implementation and then sniffing for a response
234 234 # to one of them.
235 235 #
236 236 # Those commands (from oldest to newest) are:
237 237 #
238 238 # ``between``
239 239 # Asks for the set of revisions between a pair of revisions. Command
240 240 # present in all Mercurial server implementations.
241 241 #
242 242 # ``hello``
243 243 # Instructs the server to advertise its capabilities. Introduced in
244 244 # Mercurial 0.9.1.
245 245 #
246 246 # ``upgrade``
247 247 # Requests upgrade from default transport protocol version 1 to
248 248 # a newer version. Introduced in Mercurial 4.6 as an experimental
249 249 # feature.
250 250 #
251 251 # The ``between`` command is issued with a request for the null
252 252 # range. If the remote is a Mercurial server, this request will
253 253 # generate a specific response: ``1\n\n``. This represents the
254 254 # wire protocol encoded value for ``\n``. We look for ``1\n\n``
255 255 # in the output stream and know this is the response to ``between``
256 256 # and we're at the end of our handshake reply.
257 257 #
258 258 # The response to the ``hello`` command will be a line with the
259 259 # length of the value returned by that command followed by that
260 260 # value. If the server doesn't support ``hello`` (which should be
261 261 # rare), that line will be ``0\n``. Otherwise, the value will contain
262 262 # RFC 822 like lines. Of these, the ``capabilities:`` line contains
263 263 # the capabilities of the server.
264 264 #
265 265 # The ``upgrade`` command isn't really a command in the traditional
266 266 # sense of version 1 of the transport because it isn't using the
267 267 # proper mechanism for formatting insteads: instead, it just encodes
268 268 # arguments on the line, delimited by spaces.
269 269 #
270 270 # The ``upgrade`` line looks like ``upgrade <token> <capabilities>``.
271 271 # If the server doesn't support protocol upgrades, it will reply to
272 272 # this line with ``0\n``. Otherwise, it emits an
273 273 # ``upgraded <token> <protocol>`` line to both stdout and stderr.
274 274 # Content immediately following this line describes additional
275 275 # protocol and server state.
276 276 #
277 277 # In addition to the responses to our command requests, the server
278 278 # may emit "banner" output on stdout. SSH servers are allowed to
279 279 # print messages to stdout on login. Issuing commands on connection
280 280 # allows us to flush this banner output from the server by scanning
281 281 # for output to our well-known ``between`` command. Of course, if
282 282 # the banner contains ``1\n\n``, this will throw off our detection.
283 283
284 284 requestlog = ui.configbool(b'devel', b'debug.peer-request')
285 285
286 286 # Generate a random token to help identify responses to version 2
287 287 # upgrade request.
288 288 token = pycompat.sysbytes(str(uuid.uuid4()))
289 289
290 290 try:
291 291 pairsarg = b'%s-%s' % (b'0' * 40, b'0' * 40)
292 292 handshake = [
293 293 b'hello\n',
294 294 b'between\n',
295 295 b'pairs %d\n' % len(pairsarg),
296 296 pairsarg,
297 297 ]
298 298
299 299 if requestlog:
300 300 ui.debug(b'devel-peer-request: hello+between\n')
301 301 ui.debug(b'devel-peer-request: pairs: %d bytes\n' % len(pairsarg))
302 302 ui.debug(b'sending hello command\n')
303 303 ui.debug(b'sending between command\n')
304 304
305 305 stdin.write(b''.join(handshake))
306 306 stdin.flush()
307 307 except IOError:
308 308 badresponse()
309 309
310 310 # Assume version 1 of wire protocol by default.
311 311 protoname = wireprototypes.SSHV1
312 312 reupgraded = re.compile(b'^upgraded %s (.*)$' % stringutil.reescape(token))
313 313
314 314 lines = [b'', b'dummy']
315 315 max_noise = 500
316 316 while lines[-1] and max_noise:
317 317 try:
318 318 l = stdout.readline()
319 319 _forwardoutput(ui, stderr, warn=True)
320 320
321 321 # Look for reply to protocol upgrade request. It has a token
322 322 # in it, so there should be no false positives.
323 323 m = reupgraded.match(l)
324 324 if m:
325 325 protoname = m.group(1)
326 326 ui.debug(b'protocol upgraded to %s\n' % protoname)
327 327 # If an upgrade was handled, the ``hello`` and ``between``
328 328 # requests are ignored. The next output belongs to the
329 329 # protocol, so stop scanning lines.
330 330 break
331 331
332 332 # Otherwise it could be a banner, ``0\n`` response if server
333 333 # doesn't support upgrade.
334 334
335 335 if lines[-1] == b'1\n' and l == b'\n':
336 336 break
337 337 if l:
338 338 ui.debug(b'remote: ', l)
339 339 lines.append(l)
340 340 max_noise -= 1
341 341 except IOError:
342 342 badresponse()
343 343 else:
344 344 badresponse()
345 345
346 346 caps = set()
347 347
348 348 # For version 1, we should see a ``capabilities`` line in response to the
349 349 # ``hello`` command.
350 350 if protoname == wireprototypes.SSHV1:
351 351 for l in reversed(lines):
352 352 # Look for response to ``hello`` command. Scan from the back so
353 353 # we don't misinterpret banner output as the command reply.
354 354 if l.startswith(b'capabilities:'):
355 355 caps.update(l[:-1].split(b':')[1].split())
356 356 break
357 357
358 358 # Error if we couldn't find capabilities, this means:
359 359 #
360 360 # 1. Remote isn't a Mercurial server
361 361 # 2. Remote is a <0.9.1 Mercurial server
362 362 # 3. Remote is a future Mercurial server that dropped ``hello``
363 363 # and other attempted handshake mechanisms.
364 364 if not caps:
365 365 badresponse()
366 366
367 367 # Flush any output on stderr before proceeding.
368 368 _forwardoutput(ui, stderr, warn=True)
369 369
370 370 return protoname, caps
371 371
372 372
373 373 class sshv1peer(wireprotov1peer.wirepeer):
374 374 def __init__(
375 375 self, ui, url, proc, stdin, stdout, stderr, caps, autoreadstderr=True
376 376 ):
377 377 """Create a peer from an existing SSH connection.
378 378
379 379 ``proc`` is a handle on the underlying SSH process.
380 380 ``stdin``, ``stdout``, and ``stderr`` are handles on the stdio
381 381 pipes for that process.
382 382 ``caps`` is a set of capabilities supported by the remote.
383 383 ``autoreadstderr`` denotes whether to automatically read from
384 384 stderr and to forward its output.
385 385 """
386 super().__init__(ui)
386 387 self._url = url
387 self.ui = ui
388 388 # self._subprocess is unused. Keeping a handle on the process
389 389 # holds a reference and prevents it from being garbage collected.
390 390 self._subprocess = proc
391 391
392 392 # And we hook up our "doublepipe" wrapper to allow querying
393 393 # stderr any time we perform I/O.
394 394 if autoreadstderr:
395 395 stdout = doublepipe(ui, util.bufferedinputpipe(stdout), stderr)
396 396 stdin = doublepipe(ui, stdin, stderr)
397 397
398 398 self._pipeo = stdin
399 399 self._pipei = stdout
400 400 self._pipee = stderr
401 401 self._caps = caps
402 402 self._autoreadstderr = autoreadstderr
403 403 self._initstack = b''.join(util.getstackframes(1))
404 404
405 405 # Commands that have a "framed" response where the first line of the
406 406 # response contains the length of that response.
407 407 _FRAMED_COMMANDS = {
408 408 b'batch',
409 409 }
410 410
411 411 # Begin of ipeerconnection interface.
412 412
413 413 def url(self):
414 414 return self._url
415 415
416 416 def local(self):
417 417 return None
418 418
419 419 def canpush(self):
420 420 return True
421 421
422 422 def close(self):
423 423 self._cleanup()
424 424
425 425 # End of ipeerconnection interface.
426 426
427 427 # Begin of ipeercommands interface.
428 428
429 429 def capabilities(self):
430 430 return self._caps
431 431
432 432 # End of ipeercommands interface.
433 433
434 434 def _readerr(self):
435 435 _forwardoutput(self.ui, self._pipee)
436 436
437 437 def _abort(self, exception):
438 438 self._cleanup()
439 439 raise exception
440 440
441 441 def _cleanup(self, warn=None):
442 442 _cleanuppipes(self.ui, self._pipei, self._pipeo, self._pipee, warn=warn)
443 443
444 444 def __del__(self):
445 445 self._cleanup(warn=self._initstack)
446 446
447 447 def _sendrequest(self, cmd, args, framed=False):
448 448 if self.ui.debugflag and self.ui.configbool(
449 449 b'devel', b'debug.peer-request'
450 450 ):
451 451 dbg = self.ui.debug
452 452 line = b'devel-peer-request: %s\n'
453 453 dbg(line % cmd)
454 454 for key, value in sorted(args.items()):
455 455 if not isinstance(value, dict):
456 456 dbg(line % b' %s: %d bytes' % (key, len(value)))
457 457 else:
458 458 for dk, dv in sorted(value.items()):
459 459 dbg(line % b' %s-%s: %d' % (key, dk, len(dv)))
460 460 self.ui.debug(b"sending %s command\n" % cmd)
461 461 self._pipeo.write(b"%s\n" % cmd)
462 462 _func, names = wireprotov1server.commands[cmd]
463 463 keys = names.split()
464 464 wireargs = {}
465 465 for k in keys:
466 466 if k == b'*':
467 467 wireargs[b'*'] = args
468 468 break
469 469 else:
470 470 wireargs[k] = args[k]
471 471 del args[k]
472 472 for k, v in sorted(wireargs.items()):
473 473 self._pipeo.write(b"%s %d\n" % (k, len(v)))
474 474 if isinstance(v, dict):
475 475 for dk, dv in v.items():
476 476 self._pipeo.write(b"%s %d\n" % (dk, len(dv)))
477 477 self._pipeo.write(dv)
478 478 else:
479 479 self._pipeo.write(v)
480 480 self._pipeo.flush()
481 481
482 482 # We know exactly how many bytes are in the response. So return a proxy
483 483 # around the raw output stream that allows reading exactly this many
484 484 # bytes. Callers then can read() without fear of overrunning the
485 485 # response.
486 486 if framed:
487 487 amount = self._getamount()
488 488 return util.cappedreader(self._pipei, amount)
489 489
490 490 return self._pipei
491 491
492 492 def _callstream(self, cmd, **args):
493 493 args = pycompat.byteskwargs(args)
494 494 return self._sendrequest(cmd, args, framed=cmd in self._FRAMED_COMMANDS)
495 495
496 496 def _callcompressable(self, cmd, **args):
497 497 args = pycompat.byteskwargs(args)
498 498 return self._sendrequest(cmd, args, framed=cmd in self._FRAMED_COMMANDS)
499 499
500 500 def _call(self, cmd, **args):
501 501 args = pycompat.byteskwargs(args)
502 502 return self._sendrequest(cmd, args, framed=True).read()
503 503
504 504 def _callpush(self, cmd, fp, **args):
505 505 # The server responds with an empty frame if the client should
506 506 # continue submitting the payload.
507 507 r = self._call(cmd, **args)
508 508 if r:
509 509 return b'', r
510 510
511 511 # The payload consists of frames with content followed by an empty
512 512 # frame.
513 513 for d in iter(lambda: fp.read(4096), b''):
514 514 self._writeframed(d)
515 515 self._writeframed(b"", flush=True)
516 516
517 517 # In case of success, there is an empty frame and a frame containing
518 518 # the integer result (as a string).
519 519 # In case of error, there is a non-empty frame containing the error.
520 520 r = self._readframed()
521 521 if r:
522 522 return b'', r
523 523 return self._readframed(), b''
524 524
525 525 def _calltwowaystream(self, cmd, fp, **args):
526 526 # The server responds with an empty frame if the client should
527 527 # continue submitting the payload.
528 528 r = self._call(cmd, **args)
529 529 if r:
530 530 # XXX needs to be made better
531 531 raise error.Abort(_(b'unexpected remote reply: %s') % r)
532 532
533 533 # The payload consists of frames with content followed by an empty
534 534 # frame.
535 535 for d in iter(lambda: fp.read(4096), b''):
536 536 self._writeframed(d)
537 537 self._writeframed(b"", flush=True)
538 538
539 539 return self._pipei
540 540
541 541 def _getamount(self):
542 542 l = self._pipei.readline()
543 543 if l == b'\n':
544 544 if self._autoreadstderr:
545 545 self._readerr()
546 546 msg = _(b'check previous remote output')
547 547 self._abort(error.OutOfBandError(hint=msg))
548 548 if self._autoreadstderr:
549 549 self._readerr()
550 550 try:
551 551 return int(l)
552 552 except ValueError:
553 553 self._abort(error.ResponseError(_(b"unexpected response:"), l))
554 554
555 555 def _readframed(self):
556 556 size = self._getamount()
557 557 if not size:
558 558 return b''
559 559
560 560 return self._pipei.read(size)
561 561
562 562 def _writeframed(self, data, flush=False):
563 563 self._pipeo.write(b"%d\n" % len(data))
564 564 if data:
565 565 self._pipeo.write(data)
566 566 if flush:
567 567 self._pipeo.flush()
568 568 if self._autoreadstderr:
569 569 self._readerr()
570 570
571 571
572 572 def makepeer(ui, path, proc, stdin, stdout, stderr, autoreadstderr=True):
573 573 """Make a peer instance from existing pipes.
574 574
575 575 ``path`` and ``proc`` are stored on the eventual peer instance and may
576 576 not be used for anything meaningful.
577 577
578 578 ``stdin``, ``stdout``, and ``stderr`` are the pipes connected to the
579 579 SSH server's stdio handles.
580 580
581 581 This function is factored out to allow creating peers that don't
582 582 actually spawn a new process. It is useful for starting SSH protocol
583 583 servers and clients via non-standard means, which can be useful for
584 584 testing.
585 585 """
586 586 try:
587 587 protoname, caps = _performhandshake(ui, stdin, stdout, stderr)
588 588 except Exception:
589 589 _cleanuppipes(ui, stdout, stdin, stderr, warn=None)
590 590 raise
591 591
592 592 if protoname == wireprototypes.SSHV1:
593 593 return sshv1peer(
594 594 ui,
595 595 path,
596 596 proc,
597 597 stdin,
598 598 stdout,
599 599 stderr,
600 600 caps,
601 601 autoreadstderr=autoreadstderr,
602 602 )
603 603 else:
604 604 _cleanuppipes(ui, stdout, stdin, stderr, warn=None)
605 605 raise error.RepoError(
606 606 _(b'unknown version of SSH protocol: %s') % protoname
607 607 )
608 608
609 609
610 610 def make_peer(ui, path, create, intents=None, createopts=None):
611 611 """Create an SSH peer.
612 612
613 613 The returned object conforms to the ``wireprotov1peer.wirepeer`` interface.
614 614 """
615 615 u = urlutil.url(path, parsequery=False, parsefragment=False)
616 616 if u.scheme != b'ssh' or not u.host or u.path is None:
617 617 raise error.RepoError(_(b"couldn't parse location %s") % path)
618 618
619 619 urlutil.checksafessh(path)
620 620
621 621 if u.passwd is not None:
622 622 raise error.RepoError(_(b'password in URL not supported'))
623 623
624 624 sshcmd = ui.config(b'ui', b'ssh')
625 625 remotecmd = ui.config(b'ui', b'remotecmd')
626 626 sshaddenv = dict(ui.configitems(b'sshenv'))
627 627 sshenv = procutil.shellenviron(sshaddenv)
628 628 remotepath = u.path or b'.'
629 629
630 630 args = procutil.sshargs(sshcmd, u.host, u.user, u.port)
631 631
632 632 if create:
633 633 # We /could/ do this, but only if the remote init command knows how to
634 634 # handle them. We don't yet make any assumptions about that. And without
635 635 # querying the remote, there's no way of knowing if the remote even
636 636 # supports said requested feature.
637 637 if createopts:
638 638 raise error.RepoError(
639 639 _(
640 640 b'cannot create remote SSH repositories '
641 641 b'with extra options'
642 642 )
643 643 )
644 644
645 645 cmd = b'%s %s %s' % (
646 646 sshcmd,
647 647 args,
648 648 procutil.shellquote(
649 649 b'%s init %s'
650 650 % (_serverquote(remotecmd), _serverquote(remotepath))
651 651 ),
652 652 )
653 653 ui.debug(b'running %s\n' % cmd)
654 654 res = ui.system(cmd, blockedtag=b'sshpeer', environ=sshenv)
655 655 if res != 0:
656 656 raise error.RepoError(_(b'could not create remote repo'))
657 657
658 658 proc, stdin, stdout, stderr = _makeconnection(
659 659 ui, sshcmd, args, remotecmd, remotepath, sshenv
660 660 )
661 661
662 662 peer = makepeer(ui, path, proc, stdin, stdout, stderr)
663 663
664 664 # Finally, if supported by the server, notify it about our own
665 665 # capabilities.
666 666 if b'protocaps' in peer.capabilities():
667 667 try:
668 668 peer._call(
669 669 b"protocaps", caps=b' '.join(sorted(_clientcapabilities()))
670 670 )
671 671 except IOError:
672 672 peer._cleanup()
673 673 raise error.RepoError(_(b'capability exchange failed'))
674 674
675 675 return peer
General Comments 0
You need to be logged in to leave comments. Login now