##// END OF EJS Templates
merge with stable
Yuya Nishihara -
r41387:0ae3ddb4 merge default
parent child Browse files
Show More
@@ -1,2643 +1,2646
1 1 # revlog.py - storage back-end for mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@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 """Storage back-end for Mercurial.
9 9
10 10 This provides efficient delta storage with O(1) retrieve and append
11 11 and O(changes) merge between branches.
12 12 """
13 13
14 14 from __future__ import absolute_import
15 15
16 16 import collections
17 17 import contextlib
18 18 import errno
19 19 import os
20 20 import struct
21 21 import zlib
22 22
23 23 # import stuff from node for others to import from revlog
24 24 from .node import (
25 25 bin,
26 26 hex,
27 27 nullhex,
28 28 nullid,
29 29 nullrev,
30 30 short,
31 31 wdirfilenodeids,
32 32 wdirhex,
33 33 wdirid,
34 34 wdirrev,
35 35 )
36 36 from .i18n import _
37 37 from .revlogutils.constants import (
38 38 FLAG_GENERALDELTA,
39 39 FLAG_INLINE_DATA,
40 40 REVIDX_DEFAULT_FLAGS,
41 41 REVIDX_ELLIPSIS,
42 42 REVIDX_EXTSTORED,
43 43 REVIDX_FLAGS_ORDER,
44 44 REVIDX_ISCENSORED,
45 45 REVIDX_KNOWN_FLAGS,
46 46 REVIDX_RAWTEXT_CHANGING_FLAGS,
47 47 REVLOGV0,
48 48 REVLOGV1,
49 49 REVLOGV1_FLAGS,
50 50 REVLOGV2,
51 51 REVLOGV2_FLAGS,
52 52 REVLOG_DEFAULT_FLAGS,
53 53 REVLOG_DEFAULT_FORMAT,
54 54 REVLOG_DEFAULT_VERSION,
55 55 )
56 56 from .thirdparty import (
57 57 attr,
58 58 )
59 59 from . import (
60 60 ancestor,
61 61 dagop,
62 62 error,
63 63 mdiff,
64 64 policy,
65 65 pycompat,
66 66 repository,
67 67 templatefilters,
68 68 util,
69 69 )
70 70 from .revlogutils import (
71 71 deltas as deltautil,
72 72 )
73 73 from .utils import (
74 74 interfaceutil,
75 75 storageutil,
76 76 stringutil,
77 77 )
78 78
79 79 # blanked usage of all the name to prevent pyflakes constraints
80 80 # We need these name available in the module for extensions.
81 81 REVLOGV0
82 82 REVLOGV1
83 83 REVLOGV2
84 84 FLAG_INLINE_DATA
85 85 FLAG_GENERALDELTA
86 86 REVLOG_DEFAULT_FLAGS
87 87 REVLOG_DEFAULT_FORMAT
88 88 REVLOG_DEFAULT_VERSION
89 89 REVLOGV1_FLAGS
90 90 REVLOGV2_FLAGS
91 91 REVIDX_ISCENSORED
92 92 REVIDX_ELLIPSIS
93 93 REVIDX_EXTSTORED
94 94 REVIDX_DEFAULT_FLAGS
95 95 REVIDX_FLAGS_ORDER
96 96 REVIDX_KNOWN_FLAGS
97 97 REVIDX_RAWTEXT_CHANGING_FLAGS
98 98
99 99 parsers = policy.importmod(r'parsers')
100 100 try:
101 101 from . import rustext
102 102 rustext.__name__ # force actual import (see hgdemandimport)
103 103 except ImportError:
104 104 rustext = None
105 105
106 106 # Aliased for performance.
107 107 _zlibdecompress = zlib.decompress
108 108
109 109 # max size of revlog with inline data
110 110 _maxinline = 131072
111 111 _chunksize = 1048576
112 112
113 113 # Store flag processors (cf. 'addflagprocessor()' to register)
114 114 _flagprocessors = {
115 115 REVIDX_ISCENSORED: None,
116 116 }
117 117
118 118 # Flag processors for REVIDX_ELLIPSIS.
119 119 def ellipsisreadprocessor(rl, text):
120 120 return text, False
121 121
122 122 def ellipsiswriteprocessor(rl, text):
123 123 return text, False
124 124
125 125 def ellipsisrawprocessor(rl, text):
126 126 return False
127 127
128 128 ellipsisprocessor = (
129 129 ellipsisreadprocessor,
130 130 ellipsiswriteprocessor,
131 131 ellipsisrawprocessor,
132 132 )
133 133
134 134 def addflagprocessor(flag, processor):
135 135 """Register a flag processor on a revision data flag.
136 136
137 137 Invariant:
138 138 - Flags need to be defined in REVIDX_KNOWN_FLAGS and REVIDX_FLAGS_ORDER,
139 139 and REVIDX_RAWTEXT_CHANGING_FLAGS if they can alter rawtext.
140 140 - Only one flag processor can be registered on a specific flag.
141 141 - flagprocessors must be 3-tuples of functions (read, write, raw) with the
142 142 following signatures:
143 143 - (read) f(self, rawtext) -> text, bool
144 144 - (write) f(self, text) -> rawtext, bool
145 145 - (raw) f(self, rawtext) -> bool
146 146 "text" is presented to the user. "rawtext" is stored in revlog data, not
147 147 directly visible to the user.
148 148 The boolean returned by these transforms is used to determine whether
149 149 the returned text can be used for hash integrity checking. For example,
150 150 if "write" returns False, then "text" is used to generate hash. If
151 151 "write" returns True, that basically means "rawtext" returned by "write"
152 152 should be used to generate hash. Usually, "write" and "read" return
153 153 different booleans. And "raw" returns a same boolean as "write".
154 154
155 155 Note: The 'raw' transform is used for changegroup generation and in some
156 156 debug commands. In this case the transform only indicates whether the
157 157 contents can be used for hash integrity checks.
158 158 """
159 159 _insertflagprocessor(flag, processor, _flagprocessors)
160 160
161 161 def _insertflagprocessor(flag, processor, flagprocessors):
162 162 if not flag & REVIDX_KNOWN_FLAGS:
163 163 msg = _("cannot register processor on unknown flag '%#x'.") % (flag)
164 164 raise error.ProgrammingError(msg)
165 165 if flag not in REVIDX_FLAGS_ORDER:
166 166 msg = _("flag '%#x' undefined in REVIDX_FLAGS_ORDER.") % (flag)
167 167 raise error.ProgrammingError(msg)
168 168 if flag in flagprocessors:
169 169 msg = _("cannot register multiple processors on flag '%#x'.") % (flag)
170 170 raise error.Abort(msg)
171 171 flagprocessors[flag] = processor
172 172
173 173 def getoffset(q):
174 174 return int(q >> 16)
175 175
176 176 def gettype(q):
177 177 return int(q & 0xFFFF)
178 178
179 179 def offset_type(offset, type):
180 180 if (type & ~REVIDX_KNOWN_FLAGS) != 0:
181 181 raise ValueError('unknown revlog index flags')
182 182 return int(int(offset) << 16 | type)
183 183
184 184 @attr.s(slots=True, frozen=True)
185 185 class _revisioninfo(object):
186 186 """Information about a revision that allows building its fulltext
187 187 node: expected hash of the revision
188 188 p1, p2: parent revs of the revision
189 189 btext: built text cache consisting of a one-element list
190 190 cachedelta: (baserev, uncompressed_delta) or None
191 191 flags: flags associated to the revision storage
192 192
193 193 One of btext[0] or cachedelta must be set.
194 194 """
195 195 node = attr.ib()
196 196 p1 = attr.ib()
197 197 p2 = attr.ib()
198 198 btext = attr.ib()
199 199 textlen = attr.ib()
200 200 cachedelta = attr.ib()
201 201 flags = attr.ib()
202 202
203 203 @interfaceutil.implementer(repository.irevisiondelta)
204 204 @attr.s(slots=True)
205 205 class revlogrevisiondelta(object):
206 206 node = attr.ib()
207 207 p1node = attr.ib()
208 208 p2node = attr.ib()
209 209 basenode = attr.ib()
210 210 flags = attr.ib()
211 211 baserevisionsize = attr.ib()
212 212 revision = attr.ib()
213 213 delta = attr.ib()
214 214 linknode = attr.ib(default=None)
215 215
216 216 @interfaceutil.implementer(repository.iverifyproblem)
217 217 @attr.s(frozen=True)
218 218 class revlogproblem(object):
219 219 warning = attr.ib(default=None)
220 220 error = attr.ib(default=None)
221 221 node = attr.ib(default=None)
222 222
223 223 # index v0:
224 224 # 4 bytes: offset
225 225 # 4 bytes: compressed length
226 226 # 4 bytes: base rev
227 227 # 4 bytes: link rev
228 228 # 20 bytes: parent 1 nodeid
229 229 # 20 bytes: parent 2 nodeid
230 230 # 20 bytes: nodeid
231 231 indexformatv0 = struct.Struct(">4l20s20s20s")
232 232 indexformatv0_pack = indexformatv0.pack
233 233 indexformatv0_unpack = indexformatv0.unpack
234 234
235 235 class revlogoldindex(list):
236 236 def __getitem__(self, i):
237 237 if i == -1:
238 238 return (0, 0, 0, -1, -1, -1, -1, nullid)
239 239 return list.__getitem__(self, i)
240 240
241 241 class revlogoldio(object):
242 242 def __init__(self):
243 243 self.size = indexformatv0.size
244 244
245 245 def parseindex(self, data, inline):
246 246 s = self.size
247 247 index = []
248 248 nodemap = {nullid: nullrev}
249 249 n = off = 0
250 250 l = len(data)
251 251 while off + s <= l:
252 252 cur = data[off:off + s]
253 253 off += s
254 254 e = indexformatv0_unpack(cur)
255 255 # transform to revlogv1 format
256 256 e2 = (offset_type(e[0], 0), e[1], -1, e[2], e[3],
257 257 nodemap.get(e[4], nullrev), nodemap.get(e[5], nullrev), e[6])
258 258 index.append(e2)
259 259 nodemap[e[6]] = n
260 260 n += 1
261 261
262 262 return revlogoldindex(index), nodemap, None
263 263
264 264 def packentry(self, entry, node, version, rev):
265 265 if gettype(entry[0]):
266 266 raise error.RevlogError(_('index entry flags need revlog '
267 267 'version 1'))
268 268 e2 = (getoffset(entry[0]), entry[1], entry[3], entry[4],
269 269 node(entry[5]), node(entry[6]), entry[7])
270 270 return indexformatv0_pack(*e2)
271 271
272 272 # index ng:
273 273 # 6 bytes: offset
274 274 # 2 bytes: flags
275 275 # 4 bytes: compressed length
276 276 # 4 bytes: uncompressed length
277 277 # 4 bytes: base rev
278 278 # 4 bytes: link rev
279 279 # 4 bytes: parent 1 rev
280 280 # 4 bytes: parent 2 rev
281 281 # 32 bytes: nodeid
282 282 indexformatng = struct.Struct(">Qiiiiii20s12x")
283 283 indexformatng_pack = indexformatng.pack
284 284 versionformat = struct.Struct(">I")
285 285 versionformat_pack = versionformat.pack
286 286 versionformat_unpack = versionformat.unpack
287 287
288 288 # corresponds to uncompressed length of indexformatng (2 gigs, 4-byte
289 289 # signed integer)
290 290 _maxentrysize = 0x7fffffff
291 291
292 292 class revlogio(object):
293 293 def __init__(self):
294 294 self.size = indexformatng.size
295 295
296 296 def parseindex(self, data, inline):
297 297 # call the C implementation to parse the index data
298 298 index, cache = parsers.parse_index2(data, inline)
299 299 return index, getattr(index, 'nodemap', None), cache
300 300
301 301 def packentry(self, entry, node, version, rev):
302 302 p = indexformatng_pack(*entry)
303 303 if rev == 0:
304 304 p = versionformat_pack(version) + p[4:]
305 305 return p
306 306
307 307 class revlog(object):
308 308 """
309 309 the underlying revision storage object
310 310
311 311 A revlog consists of two parts, an index and the revision data.
312 312
313 313 The index is a file with a fixed record size containing
314 314 information on each revision, including its nodeid (hash), the
315 315 nodeids of its parents, the position and offset of its data within
316 316 the data file, and the revision it's based on. Finally, each entry
317 317 contains a linkrev entry that can serve as a pointer to external
318 318 data.
319 319
320 320 The revision data itself is a linear collection of data chunks.
321 321 Each chunk represents a revision and is usually represented as a
322 322 delta against the previous chunk. To bound lookup time, runs of
323 323 deltas are limited to about 2 times the length of the original
324 324 version data. This makes retrieval of a version proportional to
325 325 its size, or O(1) relative to the number of revisions.
326 326
327 327 Both pieces of the revlog are written to in an append-only
328 328 fashion, which means we never need to rewrite a file to insert or
329 329 remove data, and can use some simple techniques to avoid the need
330 330 for locking while reading.
331 331
332 332 If checkambig, indexfile is opened with checkambig=True at
333 333 writing, to avoid file stat ambiguity.
334 334
335 335 If mmaplargeindex is True, and an mmapindexthreshold is set, the
336 336 index will be mmapped rather than read if it is larger than the
337 337 configured threshold.
338 338
339 339 If censorable is True, the revlog can have censored revisions.
340 340 """
341 341 def __init__(self, opener, indexfile, datafile=None, checkambig=False,
342 342 mmaplargeindex=False, censorable=False):
343 343 """
344 344 create a revlog object
345 345
346 346 opener is a function that abstracts the file opening operation
347 347 and can be used to implement COW semantics or the like.
348 348 """
349 349 self.indexfile = indexfile
350 350 self.datafile = datafile or (indexfile[:-2] + ".d")
351 351 self.opener = opener
352 352 # When True, indexfile is opened with checkambig=True at writing, to
353 353 # avoid file stat ambiguity.
354 354 self._checkambig = checkambig
355 355 self._mmaplargeindex = mmaplargeindex
356 356 self._censorable = censorable
357 357 # 3-tuple of (node, rev, text) for a raw revision.
358 358 self._revisioncache = None
359 359 # Maps rev to chain base rev.
360 360 self._chainbasecache = util.lrucachedict(100)
361 361 # 2-tuple of (offset, data) of raw data from the revlog at an offset.
362 362 self._chunkcache = (0, '')
363 363 # How much data to read and cache into the raw revlog data cache.
364 364 self._chunkcachesize = 65536
365 365 self._maxchainlen = None
366 366 self._deltabothparents = True
367 367 self.index = []
368 368 # Mapping of partial identifiers to full nodes.
369 369 self._pcache = {}
370 370 # Mapping of revision integer to full node.
371 371 self._nodecache = {nullid: nullrev}
372 372 self._nodepos = None
373 373 self._compengine = 'zlib'
374 374 self._maxdeltachainspan = -1
375 375 self._withsparseread = False
376 376 self._sparserevlog = False
377 377 self._srdensitythreshold = 0.50
378 378 self._srmingapsize = 262144
379 379
380 380 # Make copy of flag processors so each revlog instance can support
381 381 # custom flags.
382 382 self._flagprocessors = dict(_flagprocessors)
383 383
384 384 # 2-tuple of file handles being used for active writing.
385 385 self._writinghandles = None
386 386
387 387 self._loadindex()
388 388
389 389 def _loadindex(self):
390 390 mmapindexthreshold = None
391 391 opts = getattr(self.opener, 'options', {}) or {}
392 392
393 393 if 'revlogv2' in opts:
394 394 newversionflags = REVLOGV2 | FLAG_INLINE_DATA
395 395 elif 'revlogv1' in opts:
396 396 newversionflags = REVLOGV1 | FLAG_INLINE_DATA
397 397 if 'generaldelta' in opts:
398 398 newversionflags |= FLAG_GENERALDELTA
399 elif getattr(self.opener, 'options', None) is not None:
400 # If options provided but no 'revlog*' found, the repository
401 # would have no 'requires' file in it, which means we have to
402 # stick to the old format.
403 newversionflags = REVLOGV0
399 404 else:
400 405 newversionflags = REVLOG_DEFAULT_VERSION
401 406
402 407 if 'chunkcachesize' in opts:
403 408 self._chunkcachesize = opts['chunkcachesize']
404 409 if 'maxchainlen' in opts:
405 410 self._maxchainlen = opts['maxchainlen']
406 411 if 'deltabothparents' in opts:
407 412 self._deltabothparents = opts['deltabothparents']
408 413 self._lazydeltabase = bool(opts.get('lazydeltabase', False))
409 414 if 'compengine' in opts:
410 415 self._compengine = opts['compengine']
411 416 if 'maxdeltachainspan' in opts:
412 417 self._maxdeltachainspan = opts['maxdeltachainspan']
413 418 if self._mmaplargeindex and 'mmapindexthreshold' in opts:
414 419 mmapindexthreshold = opts['mmapindexthreshold']
415 420 self._sparserevlog = bool(opts.get('sparse-revlog', False))
416 421 withsparseread = bool(opts.get('with-sparse-read', False))
417 422 # sparse-revlog forces sparse-read
418 423 self._withsparseread = self._sparserevlog or withsparseread
419 424 if 'sparse-read-density-threshold' in opts:
420 425 self._srdensitythreshold = opts['sparse-read-density-threshold']
421 426 if 'sparse-read-min-gap-size' in opts:
422 427 self._srmingapsize = opts['sparse-read-min-gap-size']
423 428 if opts.get('enableellipsis'):
424 429 self._flagprocessors[REVIDX_ELLIPSIS] = ellipsisprocessor
425 430
426 431 # revlog v0 doesn't have flag processors
427 432 for flag, processor in opts.get(b'flagprocessors', {}).iteritems():
428 433 _insertflagprocessor(flag, processor, self._flagprocessors)
429 434
430 435 if self._chunkcachesize <= 0:
431 436 raise error.RevlogError(_('revlog chunk cache size %r is not '
432 437 'greater than 0') % self._chunkcachesize)
433 438 elif self._chunkcachesize & (self._chunkcachesize - 1):
434 439 raise error.RevlogError(_('revlog chunk cache size %r is not a '
435 440 'power of 2') % self._chunkcachesize)
436 441
437 442 indexdata = ''
438 443 self._initempty = True
439 444 try:
440 445 with self._indexfp() as f:
441 446 if (mmapindexthreshold is not None and
442 447 self.opener.fstat(f).st_size >= mmapindexthreshold):
443 448 # TODO: should .close() to release resources without
444 449 # relying on Python GC
445 450 indexdata = util.buffer(util.mmapread(f))
446 451 else:
447 452 indexdata = f.read()
448 453 if len(indexdata) > 0:
449 454 versionflags = versionformat_unpack(indexdata[:4])[0]
450 455 self._initempty = False
451 456 else:
452 457 versionflags = newversionflags
453 458 except IOError as inst:
454 459 if inst.errno != errno.ENOENT:
455 460 raise
456 461
457 462 versionflags = newversionflags
458 463
459 464 self.version = versionflags
460 465
461 466 flags = versionflags & ~0xFFFF
462 467 fmt = versionflags & 0xFFFF
463 468
464 469 if fmt == REVLOGV0:
465 470 if flags:
466 471 raise error.RevlogError(_('unknown flags (%#04x) in version %d '
467 472 'revlog %s') %
468 473 (flags >> 16, fmt, self.indexfile))
469 474
470 475 self._inline = False
471 476 self._generaldelta = False
472 477
473 478 elif fmt == REVLOGV1:
474 479 if flags & ~REVLOGV1_FLAGS:
475 480 raise error.RevlogError(_('unknown flags (%#04x) in version %d '
476 481 'revlog %s') %
477 482 (flags >> 16, fmt, self.indexfile))
478 483
479 484 self._inline = versionflags & FLAG_INLINE_DATA
480 485 self._generaldelta = versionflags & FLAG_GENERALDELTA
481 486
482 487 elif fmt == REVLOGV2:
483 488 if flags & ~REVLOGV2_FLAGS:
484 489 raise error.RevlogError(_('unknown flags (%#04x) in version %d '
485 490 'revlog %s') %
486 491 (flags >> 16, fmt, self.indexfile))
487 492
488 493 self._inline = versionflags & FLAG_INLINE_DATA
489 494 # generaldelta implied by version 2 revlogs.
490 495 self._generaldelta = True
491 496
492 497 else:
493 498 raise error.RevlogError(_('unknown version (%d) in revlog %s') %
494 499 (fmt, self.indexfile))
495 500
496 501 self._storedeltachains = True
497 502
498 503 self._io = revlogio()
499 504 if self.version == REVLOGV0:
500 505 self._io = revlogoldio()
501 506 try:
502 507 d = self._io.parseindex(indexdata, self._inline)
503 508 except (ValueError, IndexError):
504 509 raise error.RevlogError(_("index %s is corrupted") %
505 510 self.indexfile)
506 511 self.index, nodemap, self._chunkcache = d
507 512 if nodemap is not None:
508 513 self.nodemap = self._nodecache = nodemap
509 514 if not self._chunkcache:
510 515 self._chunkclear()
511 516 # revnum -> (chain-length, sum-delta-length)
512 517 self._chaininfocache = {}
513 518 # revlog header -> revlog compressor
514 519 self._decompressors = {}
515 520
516 521 @util.propertycache
517 522 def _compressor(self):
518 523 return util.compengines[self._compengine].revlogcompressor()
519 524
520 525 def _indexfp(self, mode='r'):
521 526 """file object for the revlog's index file"""
522 527 args = {r'mode': mode}
523 528 if mode != 'r':
524 529 args[r'checkambig'] = self._checkambig
525 530 if mode == 'w':
526 531 args[r'atomictemp'] = True
527 532 return self.opener(self.indexfile, **args)
528 533
529 534 def _datafp(self, mode='r'):
530 535 """file object for the revlog's data file"""
531 536 return self.opener(self.datafile, mode=mode)
532 537
533 538 @contextlib.contextmanager
534 539 def _datareadfp(self, existingfp=None):
535 540 """file object suitable to read data"""
536 541 # Use explicit file handle, if given.
537 542 if existingfp is not None:
538 543 yield existingfp
539 544
540 545 # Use a file handle being actively used for writes, if available.
541 546 # There is some danger to doing this because reads will seek the
542 547 # file. However, _writeentry() performs a SEEK_END before all writes,
543 548 # so we should be safe.
544 549 elif self._writinghandles:
545 550 if self._inline:
546 551 yield self._writinghandles[0]
547 552 else:
548 553 yield self._writinghandles[1]
549 554
550 555 # Otherwise open a new file handle.
551 556 else:
552 557 if self._inline:
553 558 func = self._indexfp
554 559 else:
555 560 func = self._datafp
556 561 with func() as fp:
557 562 yield fp
558 563
559 564 def tip(self):
560 565 return self.node(len(self.index) - 1)
561 566 def __contains__(self, rev):
562 567 return 0 <= rev < len(self)
563 568 def __len__(self):
564 569 return len(self.index)
565 570 def __iter__(self):
566 571 return iter(pycompat.xrange(len(self)))
567 572 def revs(self, start=0, stop=None):
568 573 """iterate over all rev in this revlog (from start to stop)"""
569 574 return storageutil.iterrevs(len(self), start=start, stop=stop)
570 575
571 576 @util.propertycache
572 577 def nodemap(self):
573 578 if self.index:
574 579 # populate mapping down to the initial node
575 580 node0 = self.index[0][7] # get around changelog filtering
576 581 self.rev(node0)
577 582 return self._nodecache
578 583
579 584 def hasnode(self, node):
580 585 try:
581 586 self.rev(node)
582 587 return True
583 588 except KeyError:
584 589 return False
585 590
586 591 def candelta(self, baserev, rev):
587 592 """whether two revisions (baserev, rev) can be delta-ed or not"""
588 593 # Disable delta if either rev requires a content-changing flag
589 594 # processor (ex. LFS). This is because such flag processor can alter
590 595 # the rawtext content that the delta will be based on, and two clients
591 596 # could have a same revlog node with different flags (i.e. different
592 597 # rawtext contents) and the delta could be incompatible.
593 598 if ((self.flags(baserev) & REVIDX_RAWTEXT_CHANGING_FLAGS)
594 599 or (self.flags(rev) & REVIDX_RAWTEXT_CHANGING_FLAGS)):
595 600 return False
596 601 return True
597 602
598 603 def clearcaches(self):
599 604 self._revisioncache = None
600 605 self._chainbasecache.clear()
601 606 self._chunkcache = (0, '')
602 607 self._pcache = {}
603 608
604 609 try:
605 610 self._nodecache.clearcaches()
606 611 except AttributeError:
607 612 self._nodecache = {nullid: nullrev}
608 613 self._nodepos = None
609 614
610 615 def rev(self, node):
611 616 try:
612 617 return self._nodecache[node]
613 618 except TypeError:
614 619 raise
615 620 except error.RevlogError:
616 621 # parsers.c radix tree lookup failed
617 622 if node == wdirid or node in wdirfilenodeids:
618 623 raise error.WdirUnsupported
619 624 raise error.LookupError(node, self.indexfile, _('no node'))
620 625 except KeyError:
621 626 # pure python cache lookup failed
622 627 n = self._nodecache
623 628 i = self.index
624 629 p = self._nodepos
625 630 if p is None:
626 631 p = len(i) - 1
627 632 else:
628 633 assert p < len(i)
629 634 for r in pycompat.xrange(p, -1, -1):
630 635 v = i[r][7]
631 636 n[v] = r
632 637 if v == node:
633 638 self._nodepos = r - 1
634 639 return r
635 640 if node == wdirid or node in wdirfilenodeids:
636 641 raise error.WdirUnsupported
637 642 raise error.LookupError(node, self.indexfile, _('no node'))
638 643
639 644 # Accessors for index entries.
640 645
641 646 # First tuple entry is 8 bytes. First 6 bytes are offset. Last 2 bytes
642 647 # are flags.
643 648 def start(self, rev):
644 649 return int(self.index[rev][0] >> 16)
645 650
646 651 def flags(self, rev):
647 652 return self.index[rev][0] & 0xFFFF
648 653
649 654 def length(self, rev):
650 655 return self.index[rev][1]
651 656
652 657 def rawsize(self, rev):
653 658 """return the length of the uncompressed text for a given revision"""
654 659 l = self.index[rev][2]
655 660 if l >= 0:
656 661 return l
657 662
658 663 t = self.revision(rev, raw=True)
659 664 return len(t)
660 665
661 666 def size(self, rev):
662 667 """length of non-raw text (processed by a "read" flag processor)"""
663 668 # fast path: if no "read" flag processor could change the content,
664 669 # size is rawsize. note: ELLIPSIS is known to not change the content.
665 670 flags = self.flags(rev)
666 671 if flags & (REVIDX_KNOWN_FLAGS ^ REVIDX_ELLIPSIS) == 0:
667 672 return self.rawsize(rev)
668 673
669 674 return len(self.revision(rev, raw=False))
670 675
671 676 def chainbase(self, rev):
672 677 base = self._chainbasecache.get(rev)
673 678 if base is not None:
674 679 return base
675 680
676 681 index = self.index
677 682 iterrev = rev
678 683 base = index[iterrev][3]
679 684 while base != iterrev:
680 685 iterrev = base
681 686 base = index[iterrev][3]
682 687
683 688 self._chainbasecache[rev] = base
684 689 return base
685 690
686 691 def linkrev(self, rev):
687 692 return self.index[rev][4]
688 693
689 694 def parentrevs(self, rev):
690 695 try:
691 696 entry = self.index[rev]
692 697 except IndexError:
693 698 if rev == wdirrev:
694 699 raise error.WdirUnsupported
695 700 raise
696 701
697 702 return entry[5], entry[6]
698 703
699 704 # fast parentrevs(rev) where rev isn't filtered
700 705 _uncheckedparentrevs = parentrevs
701 706
702 707 def node(self, rev):
703 708 try:
704 709 return self.index[rev][7]
705 710 except IndexError:
706 711 if rev == wdirrev:
707 712 raise error.WdirUnsupported
708 713 raise
709 714
710 715 # Derived from index values.
711 716
712 717 def end(self, rev):
713 718 return self.start(rev) + self.length(rev)
714 719
715 720 def parents(self, node):
716 721 i = self.index
717 722 d = i[self.rev(node)]
718 723 return i[d[5]][7], i[d[6]][7] # map revisions to nodes inline
719 724
720 725 def chainlen(self, rev):
721 726 return self._chaininfo(rev)[0]
722 727
723 728 def _chaininfo(self, rev):
724 729 chaininfocache = self._chaininfocache
725 730 if rev in chaininfocache:
726 731 return chaininfocache[rev]
727 732 index = self.index
728 733 generaldelta = self._generaldelta
729 734 iterrev = rev
730 735 e = index[iterrev]
731 736 clen = 0
732 737 compresseddeltalen = 0
733 738 while iterrev != e[3]:
734 739 clen += 1
735 740 compresseddeltalen += e[1]
736 741 if generaldelta:
737 742 iterrev = e[3]
738 743 else:
739 744 iterrev -= 1
740 745 if iterrev in chaininfocache:
741 746 t = chaininfocache[iterrev]
742 747 clen += t[0]
743 748 compresseddeltalen += t[1]
744 749 break
745 750 e = index[iterrev]
746 751 else:
747 752 # Add text length of base since decompressing that also takes
748 753 # work. For cache hits the length is already included.
749 754 compresseddeltalen += e[1]
750 755 r = (clen, compresseddeltalen)
751 756 chaininfocache[rev] = r
752 757 return r
753 758
754 759 def _deltachain(self, rev, stoprev=None):
755 760 """Obtain the delta chain for a revision.
756 761
757 762 ``stoprev`` specifies a revision to stop at. If not specified, we
758 763 stop at the base of the chain.
759 764
760 765 Returns a 2-tuple of (chain, stopped) where ``chain`` is a list of
761 766 revs in ascending order and ``stopped`` is a bool indicating whether
762 767 ``stoprev`` was hit.
763 768 """
764 769 # Try C implementation.
765 770 try:
766 771 return self.index.deltachain(rev, stoprev, self._generaldelta)
767 772 except AttributeError:
768 773 pass
769 774
770 775 chain = []
771 776
772 777 # Alias to prevent attribute lookup in tight loop.
773 778 index = self.index
774 779 generaldelta = self._generaldelta
775 780
776 781 iterrev = rev
777 782 e = index[iterrev]
778 783 while iterrev != e[3] and iterrev != stoprev:
779 784 chain.append(iterrev)
780 785 if generaldelta:
781 786 iterrev = e[3]
782 787 else:
783 788 iterrev -= 1
784 789 e = index[iterrev]
785 790
786 791 if iterrev == stoprev:
787 792 stopped = True
788 793 else:
789 794 chain.append(iterrev)
790 795 stopped = False
791 796
792 797 chain.reverse()
793 798 return chain, stopped
794 799
795 800 def ancestors(self, revs, stoprev=0, inclusive=False):
796 801 """Generate the ancestors of 'revs' in reverse revision order.
797 802 Does not generate revs lower than stoprev.
798 803
799 804 See the documentation for ancestor.lazyancestors for more details."""
800 805
801 806 # first, make sure start revisions aren't filtered
802 807 revs = list(revs)
803 808 checkrev = self.node
804 809 for r in revs:
805 810 checkrev(r)
806 811 # and we're sure ancestors aren't filtered as well
807 812
808 813 if rustext is not None:
809 814 lazyancestors = rustext.ancestor.LazyAncestors
810 815 arg = self.index
811 816 elif util.safehasattr(parsers, 'rustlazyancestors'):
812 817 lazyancestors = ancestor.rustlazyancestors
813 818 arg = self.index
814 819 else:
815 820 lazyancestors = ancestor.lazyancestors
816 821 arg = self._uncheckedparentrevs
817 822 return lazyancestors(arg, revs, stoprev=stoprev, inclusive=inclusive)
818 823
819 824 def descendants(self, revs):
820 825 return dagop.descendantrevs(revs, self.revs, self.parentrevs)
821 826
822 827 def findcommonmissing(self, common=None, heads=None):
823 828 """Return a tuple of the ancestors of common and the ancestors of heads
824 829 that are not ancestors of common. In revset terminology, we return the
825 830 tuple:
826 831
827 832 ::common, (::heads) - (::common)
828 833
829 834 The list is sorted by revision number, meaning it is
830 835 topologically sorted.
831 836
832 837 'heads' and 'common' are both lists of node IDs. If heads is
833 838 not supplied, uses all of the revlog's heads. If common is not
834 839 supplied, uses nullid."""
835 840 if common is None:
836 841 common = [nullid]
837 842 if heads is None:
838 843 heads = self.heads()
839 844
840 845 common = [self.rev(n) for n in common]
841 846 heads = [self.rev(n) for n in heads]
842 847
843 848 # we want the ancestors, but inclusive
844 849 class lazyset(object):
845 850 def __init__(self, lazyvalues):
846 851 self.addedvalues = set()
847 852 self.lazyvalues = lazyvalues
848 853
849 854 def __contains__(self, value):
850 855 return value in self.addedvalues or value in self.lazyvalues
851 856
852 857 def __iter__(self):
853 858 added = self.addedvalues
854 859 for r in added:
855 860 yield r
856 861 for r in self.lazyvalues:
857 862 if not r in added:
858 863 yield r
859 864
860 865 def add(self, value):
861 866 self.addedvalues.add(value)
862 867
863 868 def update(self, values):
864 869 self.addedvalues.update(values)
865 870
866 871 has = lazyset(self.ancestors(common))
867 872 has.add(nullrev)
868 873 has.update(common)
869 874
870 875 # take all ancestors from heads that aren't in has
871 876 missing = set()
872 877 visit = collections.deque(r for r in heads if r not in has)
873 878 while visit:
874 879 r = visit.popleft()
875 880 if r in missing:
876 881 continue
877 882 else:
878 883 missing.add(r)
879 884 for p in self.parentrevs(r):
880 885 if p not in has:
881 886 visit.append(p)
882 887 missing = list(missing)
883 888 missing.sort()
884 889 return has, [self.node(miss) for miss in missing]
885 890
886 891 def incrementalmissingrevs(self, common=None):
887 892 """Return an object that can be used to incrementally compute the
888 893 revision numbers of the ancestors of arbitrary sets that are not
889 894 ancestors of common. This is an ancestor.incrementalmissingancestors
890 895 object.
891 896
892 897 'common' is a list of revision numbers. If common is not supplied, uses
893 898 nullrev.
894 899 """
895 900 if common is None:
896 901 common = [nullrev]
897 902
898 903 if rustext is not None:
899 # TODO: WdirUnsupported should be raised instead of GraphError
900 # if common includes wdirrev
901 904 return rustext.ancestor.MissingAncestors(self.index, common)
902 905 return ancestor.incrementalmissingancestors(self.parentrevs, common)
903 906
904 907 def findmissingrevs(self, common=None, heads=None):
905 908 """Return the revision numbers of the ancestors of heads that
906 909 are not ancestors of common.
907 910
908 911 More specifically, return a list of revision numbers corresponding to
909 912 nodes N such that every N satisfies the following constraints:
910 913
911 914 1. N is an ancestor of some node in 'heads'
912 915 2. N is not an ancestor of any node in 'common'
913 916
914 917 The list is sorted by revision number, meaning it is
915 918 topologically sorted.
916 919
917 920 'heads' and 'common' are both lists of revision numbers. If heads is
918 921 not supplied, uses all of the revlog's heads. If common is not
919 922 supplied, uses nullid."""
920 923 if common is None:
921 924 common = [nullrev]
922 925 if heads is None:
923 926 heads = self.headrevs()
924 927
925 928 inc = self.incrementalmissingrevs(common=common)
926 929 return inc.missingancestors(heads)
927 930
928 931 def findmissing(self, common=None, heads=None):
929 932 """Return the ancestors of heads that are not ancestors of common.
930 933
931 934 More specifically, return a list of nodes N such that every N
932 935 satisfies the following constraints:
933 936
934 937 1. N is an ancestor of some node in 'heads'
935 938 2. N is not an ancestor of any node in 'common'
936 939
937 940 The list is sorted by revision number, meaning it is
938 941 topologically sorted.
939 942
940 943 'heads' and 'common' are both lists of node IDs. If heads is
941 944 not supplied, uses all of the revlog's heads. If common is not
942 945 supplied, uses nullid."""
943 946 if common is None:
944 947 common = [nullid]
945 948 if heads is None:
946 949 heads = self.heads()
947 950
948 951 common = [self.rev(n) for n in common]
949 952 heads = [self.rev(n) for n in heads]
950 953
951 954 inc = self.incrementalmissingrevs(common=common)
952 955 return [self.node(r) for r in inc.missingancestors(heads)]
953 956
954 957 def nodesbetween(self, roots=None, heads=None):
955 958 """Return a topological path from 'roots' to 'heads'.
956 959
957 960 Return a tuple (nodes, outroots, outheads) where 'nodes' is a
958 961 topologically sorted list of all nodes N that satisfy both of
959 962 these constraints:
960 963
961 964 1. N is a descendant of some node in 'roots'
962 965 2. N is an ancestor of some node in 'heads'
963 966
964 967 Every node is considered to be both a descendant and an ancestor
965 968 of itself, so every reachable node in 'roots' and 'heads' will be
966 969 included in 'nodes'.
967 970
968 971 'outroots' is the list of reachable nodes in 'roots', i.e., the
969 972 subset of 'roots' that is returned in 'nodes'. Likewise,
970 973 'outheads' is the subset of 'heads' that is also in 'nodes'.
971 974
972 975 'roots' and 'heads' are both lists of node IDs. If 'roots' is
973 976 unspecified, uses nullid as the only root. If 'heads' is
974 977 unspecified, uses list of all of the revlog's heads."""
975 978 nonodes = ([], [], [])
976 979 if roots is not None:
977 980 roots = list(roots)
978 981 if not roots:
979 982 return nonodes
980 983 lowestrev = min([self.rev(n) for n in roots])
981 984 else:
982 985 roots = [nullid] # Everybody's a descendant of nullid
983 986 lowestrev = nullrev
984 987 if (lowestrev == nullrev) and (heads is None):
985 988 # We want _all_ the nodes!
986 989 return ([self.node(r) for r in self], [nullid], list(self.heads()))
987 990 if heads is None:
988 991 # All nodes are ancestors, so the latest ancestor is the last
989 992 # node.
990 993 highestrev = len(self) - 1
991 994 # Set ancestors to None to signal that every node is an ancestor.
992 995 ancestors = None
993 996 # Set heads to an empty dictionary for later discovery of heads
994 997 heads = {}
995 998 else:
996 999 heads = list(heads)
997 1000 if not heads:
998 1001 return nonodes
999 1002 ancestors = set()
1000 1003 # Turn heads into a dictionary so we can remove 'fake' heads.
1001 1004 # Also, later we will be using it to filter out the heads we can't
1002 1005 # find from roots.
1003 1006 heads = dict.fromkeys(heads, False)
1004 1007 # Start at the top and keep marking parents until we're done.
1005 1008 nodestotag = set(heads)
1006 1009 # Remember where the top was so we can use it as a limit later.
1007 1010 highestrev = max([self.rev(n) for n in nodestotag])
1008 1011 while nodestotag:
1009 1012 # grab a node to tag
1010 1013 n = nodestotag.pop()
1011 1014 # Never tag nullid
1012 1015 if n == nullid:
1013 1016 continue
1014 1017 # A node's revision number represents its place in a
1015 1018 # topologically sorted list of nodes.
1016 1019 r = self.rev(n)
1017 1020 if r >= lowestrev:
1018 1021 if n not in ancestors:
1019 1022 # If we are possibly a descendant of one of the roots
1020 1023 # and we haven't already been marked as an ancestor
1021 1024 ancestors.add(n) # Mark as ancestor
1022 1025 # Add non-nullid parents to list of nodes to tag.
1023 1026 nodestotag.update([p for p in self.parents(n) if
1024 1027 p != nullid])
1025 1028 elif n in heads: # We've seen it before, is it a fake head?
1026 1029 # So it is, real heads should not be the ancestors of
1027 1030 # any other heads.
1028 1031 heads.pop(n)
1029 1032 if not ancestors:
1030 1033 return nonodes
1031 1034 # Now that we have our set of ancestors, we want to remove any
1032 1035 # roots that are not ancestors.
1033 1036
1034 1037 # If one of the roots was nullid, everything is included anyway.
1035 1038 if lowestrev > nullrev:
1036 1039 # But, since we weren't, let's recompute the lowest rev to not
1037 1040 # include roots that aren't ancestors.
1038 1041
1039 1042 # Filter out roots that aren't ancestors of heads
1040 1043 roots = [root for root in roots if root in ancestors]
1041 1044 # Recompute the lowest revision
1042 1045 if roots:
1043 1046 lowestrev = min([self.rev(root) for root in roots])
1044 1047 else:
1045 1048 # No more roots? Return empty list
1046 1049 return nonodes
1047 1050 else:
1048 1051 # We are descending from nullid, and don't need to care about
1049 1052 # any other roots.
1050 1053 lowestrev = nullrev
1051 1054 roots = [nullid]
1052 1055 # Transform our roots list into a set.
1053 1056 descendants = set(roots)
1054 1057 # Also, keep the original roots so we can filter out roots that aren't
1055 1058 # 'real' roots (i.e. are descended from other roots).
1056 1059 roots = descendants.copy()
1057 1060 # Our topologically sorted list of output nodes.
1058 1061 orderedout = []
1059 1062 # Don't start at nullid since we don't want nullid in our output list,
1060 1063 # and if nullid shows up in descendants, empty parents will look like
1061 1064 # they're descendants.
1062 1065 for r in self.revs(start=max(lowestrev, 0), stop=highestrev + 1):
1063 1066 n = self.node(r)
1064 1067 isdescendant = False
1065 1068 if lowestrev == nullrev: # Everybody is a descendant of nullid
1066 1069 isdescendant = True
1067 1070 elif n in descendants:
1068 1071 # n is already a descendant
1069 1072 isdescendant = True
1070 1073 # This check only needs to be done here because all the roots
1071 1074 # will start being marked is descendants before the loop.
1072 1075 if n in roots:
1073 1076 # If n was a root, check if it's a 'real' root.
1074 1077 p = tuple(self.parents(n))
1075 1078 # If any of its parents are descendants, it's not a root.
1076 1079 if (p[0] in descendants) or (p[1] in descendants):
1077 1080 roots.remove(n)
1078 1081 else:
1079 1082 p = tuple(self.parents(n))
1080 1083 # A node is a descendant if either of its parents are
1081 1084 # descendants. (We seeded the dependents list with the roots
1082 1085 # up there, remember?)
1083 1086 if (p[0] in descendants) or (p[1] in descendants):
1084 1087 descendants.add(n)
1085 1088 isdescendant = True
1086 1089 if isdescendant and ((ancestors is None) or (n in ancestors)):
1087 1090 # Only include nodes that are both descendants and ancestors.
1088 1091 orderedout.append(n)
1089 1092 if (ancestors is not None) and (n in heads):
1090 1093 # We're trying to figure out which heads are reachable
1091 1094 # from roots.
1092 1095 # Mark this head as having been reached
1093 1096 heads[n] = True
1094 1097 elif ancestors is None:
1095 1098 # Otherwise, we're trying to discover the heads.
1096 1099 # Assume this is a head because if it isn't, the next step
1097 1100 # will eventually remove it.
1098 1101 heads[n] = True
1099 1102 # But, obviously its parents aren't.
1100 1103 for p in self.parents(n):
1101 1104 heads.pop(p, None)
1102 1105 heads = [head for head, flag in heads.iteritems() if flag]
1103 1106 roots = list(roots)
1104 1107 assert orderedout
1105 1108 assert roots
1106 1109 assert heads
1107 1110 return (orderedout, roots, heads)
1108 1111
1109 1112 def headrevs(self, revs=None):
1110 1113 if revs is None:
1111 1114 try:
1112 1115 return self.index.headrevs()
1113 1116 except AttributeError:
1114 1117 return self._headrevs()
1115 1118 return dagop.headrevs(revs, self.parentrevs)
1116 1119
1117 1120 def computephases(self, roots):
1118 1121 return self.index.computephasesmapsets(roots)
1119 1122
1120 1123 def _headrevs(self):
1121 1124 count = len(self)
1122 1125 if not count:
1123 1126 return [nullrev]
1124 1127 # we won't iter over filtered rev so nobody is a head at start
1125 1128 ishead = [0] * (count + 1)
1126 1129 index = self.index
1127 1130 for r in self:
1128 1131 ishead[r] = 1 # I may be an head
1129 1132 e = index[r]
1130 1133 ishead[e[5]] = ishead[e[6]] = 0 # my parent are not
1131 1134 return [r for r, val in enumerate(ishead) if val]
1132 1135
1133 1136 def heads(self, start=None, stop=None):
1134 1137 """return the list of all nodes that have no children
1135 1138
1136 1139 if start is specified, only heads that are descendants of
1137 1140 start will be returned
1138 1141 if stop is specified, it will consider all the revs from stop
1139 1142 as if they had no children
1140 1143 """
1141 1144 if start is None and stop is None:
1142 1145 if not len(self):
1143 1146 return [nullid]
1144 1147 return [self.node(r) for r in self.headrevs()]
1145 1148
1146 1149 if start is None:
1147 1150 start = nullrev
1148 1151 else:
1149 1152 start = self.rev(start)
1150 1153
1151 1154 stoprevs = set(self.rev(n) for n in stop or [])
1152 1155
1153 1156 revs = dagop.headrevssubset(self.revs, self.parentrevs, startrev=start,
1154 1157 stoprevs=stoprevs)
1155 1158
1156 1159 return [self.node(rev) for rev in revs]
1157 1160
1158 1161 def children(self, node):
1159 1162 """find the children of a given node"""
1160 1163 c = []
1161 1164 p = self.rev(node)
1162 1165 for r in self.revs(start=p + 1):
1163 1166 prevs = [pr for pr in self.parentrevs(r) if pr != nullrev]
1164 1167 if prevs:
1165 1168 for pr in prevs:
1166 1169 if pr == p:
1167 1170 c.append(self.node(r))
1168 1171 elif p == nullrev:
1169 1172 c.append(self.node(r))
1170 1173 return c
1171 1174
1172 1175 def commonancestorsheads(self, a, b):
1173 1176 """calculate all the heads of the common ancestors of nodes a and b"""
1174 1177 a, b = self.rev(a), self.rev(b)
1175 1178 ancs = self._commonancestorsheads(a, b)
1176 1179 return pycompat.maplist(self.node, ancs)
1177 1180
1178 1181 def _commonancestorsheads(self, *revs):
1179 1182 """calculate all the heads of the common ancestors of revs"""
1180 1183 try:
1181 1184 ancs = self.index.commonancestorsheads(*revs)
1182 1185 except (AttributeError, OverflowError): # C implementation failed
1183 1186 ancs = ancestor.commonancestorsheads(self.parentrevs, *revs)
1184 1187 return ancs
1185 1188
1186 1189 def isancestor(self, a, b):
1187 1190 """return True if node a is an ancestor of node b
1188 1191
1189 1192 A revision is considered an ancestor of itself."""
1190 1193 a, b = self.rev(a), self.rev(b)
1191 1194 return self.isancestorrev(a, b)
1192 1195
1193 1196 def isancestorrev(self, a, b):
1194 1197 """return True if revision a is an ancestor of revision b
1195 1198
1196 1199 A revision is considered an ancestor of itself.
1197 1200
1198 1201 The implementation of this is trivial but the use of
1199 1202 commonancestorsheads is not."""
1200 1203 if a == nullrev:
1201 1204 return True
1202 1205 elif a == b:
1203 1206 return True
1204 1207 elif a > b:
1205 1208 return False
1206 1209 return a in self._commonancestorsheads(a, b)
1207 1210
1208 1211 def ancestor(self, a, b):
1209 1212 """calculate the "best" common ancestor of nodes a and b"""
1210 1213
1211 1214 a, b = self.rev(a), self.rev(b)
1212 1215 try:
1213 1216 ancs = self.index.ancestors(a, b)
1214 1217 except (AttributeError, OverflowError):
1215 1218 ancs = ancestor.ancestors(self.parentrevs, a, b)
1216 1219 if ancs:
1217 1220 # choose a consistent winner when there's a tie
1218 1221 return min(map(self.node, ancs))
1219 1222 return nullid
1220 1223
1221 1224 def _match(self, id):
1222 1225 if isinstance(id, int):
1223 1226 # rev
1224 1227 return self.node(id)
1225 1228 if len(id) == 20:
1226 1229 # possibly a binary node
1227 1230 # odds of a binary node being all hex in ASCII are 1 in 10**25
1228 1231 try:
1229 1232 node = id
1230 1233 self.rev(node) # quick search the index
1231 1234 return node
1232 1235 except error.LookupError:
1233 1236 pass # may be partial hex id
1234 1237 try:
1235 1238 # str(rev)
1236 1239 rev = int(id)
1237 1240 if "%d" % rev != id:
1238 1241 raise ValueError
1239 1242 if rev < 0:
1240 1243 rev = len(self) + rev
1241 1244 if rev < 0 or rev >= len(self):
1242 1245 raise ValueError
1243 1246 return self.node(rev)
1244 1247 except (ValueError, OverflowError):
1245 1248 pass
1246 1249 if len(id) == 40:
1247 1250 try:
1248 1251 # a full hex nodeid?
1249 1252 node = bin(id)
1250 1253 self.rev(node)
1251 1254 return node
1252 1255 except (TypeError, error.LookupError):
1253 1256 pass
1254 1257
1255 1258 def _partialmatch(self, id):
1256 1259 # we don't care wdirfilenodeids as they should be always full hash
1257 1260 maybewdir = wdirhex.startswith(id)
1258 1261 try:
1259 1262 partial = self.index.partialmatch(id)
1260 1263 if partial and self.hasnode(partial):
1261 1264 if maybewdir:
1262 1265 # single 'ff...' match in radix tree, ambiguous with wdir
1263 1266 raise error.RevlogError
1264 1267 return partial
1265 1268 if maybewdir:
1266 1269 # no 'ff...' match in radix tree, wdir identified
1267 1270 raise error.WdirUnsupported
1268 1271 return None
1269 1272 except error.RevlogError:
1270 1273 # parsers.c radix tree lookup gave multiple matches
1271 1274 # fast path: for unfiltered changelog, radix tree is accurate
1272 1275 if not getattr(self, 'filteredrevs', None):
1273 1276 raise error.AmbiguousPrefixLookupError(
1274 1277 id, self.indexfile, _('ambiguous identifier'))
1275 1278 # fall through to slow path that filters hidden revisions
1276 1279 except (AttributeError, ValueError):
1277 1280 # we are pure python, or key was too short to search radix tree
1278 1281 pass
1279 1282
1280 1283 if id in self._pcache:
1281 1284 return self._pcache[id]
1282 1285
1283 1286 if len(id) <= 40:
1284 1287 try:
1285 1288 # hex(node)[:...]
1286 1289 l = len(id) // 2 # grab an even number of digits
1287 1290 prefix = bin(id[:l * 2])
1288 1291 nl = [e[7] for e in self.index if e[7].startswith(prefix)]
1289 1292 nl = [n for n in nl if hex(n).startswith(id) and
1290 1293 self.hasnode(n)]
1291 1294 if nullhex.startswith(id):
1292 1295 nl.append(nullid)
1293 1296 if len(nl) > 0:
1294 1297 if len(nl) == 1 and not maybewdir:
1295 1298 self._pcache[id] = nl[0]
1296 1299 return nl[0]
1297 1300 raise error.AmbiguousPrefixLookupError(
1298 1301 id, self.indexfile, _('ambiguous identifier'))
1299 1302 if maybewdir:
1300 1303 raise error.WdirUnsupported
1301 1304 return None
1302 1305 except TypeError:
1303 1306 pass
1304 1307
1305 1308 def lookup(self, id):
1306 1309 """locate a node based on:
1307 1310 - revision number or str(revision number)
1308 1311 - nodeid or subset of hex nodeid
1309 1312 """
1310 1313 n = self._match(id)
1311 1314 if n is not None:
1312 1315 return n
1313 1316 n = self._partialmatch(id)
1314 1317 if n:
1315 1318 return n
1316 1319
1317 1320 raise error.LookupError(id, self.indexfile, _('no match found'))
1318 1321
1319 1322 def shortest(self, node, minlength=1):
1320 1323 """Find the shortest unambiguous prefix that matches node."""
1321 1324 def isvalid(prefix):
1322 1325 try:
1323 1326 node = self._partialmatch(prefix)
1324 1327 except error.AmbiguousPrefixLookupError:
1325 1328 return False
1326 1329 except error.WdirUnsupported:
1327 1330 # single 'ff...' match
1328 1331 return True
1329 1332 if node is None:
1330 1333 raise error.LookupError(node, self.indexfile, _('no node'))
1331 1334 return True
1332 1335
1333 1336 def maybewdir(prefix):
1334 1337 return all(c == 'f' for c in prefix)
1335 1338
1336 1339 hexnode = hex(node)
1337 1340
1338 1341 def disambiguate(hexnode, minlength):
1339 1342 """Disambiguate against wdirid."""
1340 1343 for length in range(minlength, 41):
1341 1344 prefix = hexnode[:length]
1342 1345 if not maybewdir(prefix):
1343 1346 return prefix
1344 1347
1345 1348 if not getattr(self, 'filteredrevs', None):
1346 1349 try:
1347 1350 length = max(self.index.shortest(node), minlength)
1348 1351 return disambiguate(hexnode, length)
1349 1352 except error.RevlogError:
1350 1353 if node != wdirid:
1351 1354 raise error.LookupError(node, self.indexfile, _('no node'))
1352 1355 except AttributeError:
1353 1356 # Fall through to pure code
1354 1357 pass
1355 1358
1356 1359 if node == wdirid:
1357 1360 for length in range(minlength, 41):
1358 1361 prefix = hexnode[:length]
1359 1362 if isvalid(prefix):
1360 1363 return prefix
1361 1364
1362 1365 for length in range(minlength, 41):
1363 1366 prefix = hexnode[:length]
1364 1367 if isvalid(prefix):
1365 1368 return disambiguate(hexnode, length)
1366 1369
1367 1370 def cmp(self, node, text):
1368 1371 """compare text with a given file revision
1369 1372
1370 1373 returns True if text is different than what is stored.
1371 1374 """
1372 1375 p1, p2 = self.parents(node)
1373 1376 return storageutil.hashrevisionsha1(text, p1, p2) != node
1374 1377
1375 1378 def _cachesegment(self, offset, data):
1376 1379 """Add a segment to the revlog cache.
1377 1380
1378 1381 Accepts an absolute offset and the data that is at that location.
1379 1382 """
1380 1383 o, d = self._chunkcache
1381 1384 # try to add to existing cache
1382 1385 if o + len(d) == offset and len(d) + len(data) < _chunksize:
1383 1386 self._chunkcache = o, d + data
1384 1387 else:
1385 1388 self._chunkcache = offset, data
1386 1389
1387 1390 def _readsegment(self, offset, length, df=None):
1388 1391 """Load a segment of raw data from the revlog.
1389 1392
1390 1393 Accepts an absolute offset, length to read, and an optional existing
1391 1394 file handle to read from.
1392 1395
1393 1396 If an existing file handle is passed, it will be seeked and the
1394 1397 original seek position will NOT be restored.
1395 1398
1396 1399 Returns a str or buffer of raw byte data.
1397 1400
1398 1401 Raises if the requested number of bytes could not be read.
1399 1402 """
1400 1403 # Cache data both forward and backward around the requested
1401 1404 # data, in a fixed size window. This helps speed up operations
1402 1405 # involving reading the revlog backwards.
1403 1406 cachesize = self._chunkcachesize
1404 1407 realoffset = offset & ~(cachesize - 1)
1405 1408 reallength = (((offset + length + cachesize) & ~(cachesize - 1))
1406 1409 - realoffset)
1407 1410 with self._datareadfp(df) as df:
1408 1411 df.seek(realoffset)
1409 1412 d = df.read(reallength)
1410 1413
1411 1414 self._cachesegment(realoffset, d)
1412 1415 if offset != realoffset or reallength != length:
1413 1416 startoffset = offset - realoffset
1414 1417 if len(d) - startoffset < length:
1415 1418 raise error.RevlogError(
1416 1419 _('partial read of revlog %s; expected %d bytes from '
1417 1420 'offset %d, got %d') %
1418 1421 (self.indexfile if self._inline else self.datafile,
1419 1422 length, realoffset, len(d) - startoffset))
1420 1423
1421 1424 return util.buffer(d, startoffset, length)
1422 1425
1423 1426 if len(d) < length:
1424 1427 raise error.RevlogError(
1425 1428 _('partial read of revlog %s; expected %d bytes from offset '
1426 1429 '%d, got %d') %
1427 1430 (self.indexfile if self._inline else self.datafile,
1428 1431 length, offset, len(d)))
1429 1432
1430 1433 return d
1431 1434
1432 1435 def _getsegment(self, offset, length, df=None):
1433 1436 """Obtain a segment of raw data from the revlog.
1434 1437
1435 1438 Accepts an absolute offset, length of bytes to obtain, and an
1436 1439 optional file handle to the already-opened revlog. If the file
1437 1440 handle is used, it's original seek position will not be preserved.
1438 1441
1439 1442 Requests for data may be returned from a cache.
1440 1443
1441 1444 Returns a str or a buffer instance of raw byte data.
1442 1445 """
1443 1446 o, d = self._chunkcache
1444 1447 l = len(d)
1445 1448
1446 1449 # is it in the cache?
1447 1450 cachestart = offset - o
1448 1451 cacheend = cachestart + length
1449 1452 if cachestart >= 0 and cacheend <= l:
1450 1453 if cachestart == 0 and cacheend == l:
1451 1454 return d # avoid a copy
1452 1455 return util.buffer(d, cachestart, cacheend - cachestart)
1453 1456
1454 1457 return self._readsegment(offset, length, df=df)
1455 1458
1456 1459 def _getsegmentforrevs(self, startrev, endrev, df=None):
1457 1460 """Obtain a segment of raw data corresponding to a range of revisions.
1458 1461
1459 1462 Accepts the start and end revisions and an optional already-open
1460 1463 file handle to be used for reading. If the file handle is read, its
1461 1464 seek position will not be preserved.
1462 1465
1463 1466 Requests for data may be satisfied by a cache.
1464 1467
1465 1468 Returns a 2-tuple of (offset, data) for the requested range of
1466 1469 revisions. Offset is the integer offset from the beginning of the
1467 1470 revlog and data is a str or buffer of the raw byte data.
1468 1471
1469 1472 Callers will need to call ``self.start(rev)`` and ``self.length(rev)``
1470 1473 to determine where each revision's data begins and ends.
1471 1474 """
1472 1475 # Inlined self.start(startrev) & self.end(endrev) for perf reasons
1473 1476 # (functions are expensive).
1474 1477 index = self.index
1475 1478 istart = index[startrev]
1476 1479 start = int(istart[0] >> 16)
1477 1480 if startrev == endrev:
1478 1481 end = start + istart[1]
1479 1482 else:
1480 1483 iend = index[endrev]
1481 1484 end = int(iend[0] >> 16) + iend[1]
1482 1485
1483 1486 if self._inline:
1484 1487 start += (startrev + 1) * self._io.size
1485 1488 end += (endrev + 1) * self._io.size
1486 1489 length = end - start
1487 1490
1488 1491 return start, self._getsegment(start, length, df=df)
1489 1492
1490 1493 def _chunk(self, rev, df=None):
1491 1494 """Obtain a single decompressed chunk for a revision.
1492 1495
1493 1496 Accepts an integer revision and an optional already-open file handle
1494 1497 to be used for reading. If used, the seek position of the file will not
1495 1498 be preserved.
1496 1499
1497 1500 Returns a str holding uncompressed data for the requested revision.
1498 1501 """
1499 1502 return self.decompress(self._getsegmentforrevs(rev, rev, df=df)[1])
1500 1503
1501 1504 def _chunks(self, revs, df=None, targetsize=None):
1502 1505 """Obtain decompressed chunks for the specified revisions.
1503 1506
1504 1507 Accepts an iterable of numeric revisions that are assumed to be in
1505 1508 ascending order. Also accepts an optional already-open file handle
1506 1509 to be used for reading. If used, the seek position of the file will
1507 1510 not be preserved.
1508 1511
1509 1512 This function is similar to calling ``self._chunk()`` multiple times,
1510 1513 but is faster.
1511 1514
1512 1515 Returns a list with decompressed data for each requested revision.
1513 1516 """
1514 1517 if not revs:
1515 1518 return []
1516 1519 start = self.start
1517 1520 length = self.length
1518 1521 inline = self._inline
1519 1522 iosize = self._io.size
1520 1523 buffer = util.buffer
1521 1524
1522 1525 l = []
1523 1526 ladd = l.append
1524 1527
1525 1528 if not self._withsparseread:
1526 1529 slicedchunks = (revs,)
1527 1530 else:
1528 1531 slicedchunks = deltautil.slicechunk(self, revs,
1529 1532 targetsize=targetsize)
1530 1533
1531 1534 for revschunk in slicedchunks:
1532 1535 firstrev = revschunk[0]
1533 1536 # Skip trailing revisions with empty diff
1534 1537 for lastrev in revschunk[::-1]:
1535 1538 if length(lastrev) != 0:
1536 1539 break
1537 1540
1538 1541 try:
1539 1542 offset, data = self._getsegmentforrevs(firstrev, lastrev, df=df)
1540 1543 except OverflowError:
1541 1544 # issue4215 - we can't cache a run of chunks greater than
1542 1545 # 2G on Windows
1543 1546 return [self._chunk(rev, df=df) for rev in revschunk]
1544 1547
1545 1548 decomp = self.decompress
1546 1549 for rev in revschunk:
1547 1550 chunkstart = start(rev)
1548 1551 if inline:
1549 1552 chunkstart += (rev + 1) * iosize
1550 1553 chunklength = length(rev)
1551 1554 ladd(decomp(buffer(data, chunkstart - offset, chunklength)))
1552 1555
1553 1556 return l
1554 1557
1555 1558 def _chunkclear(self):
1556 1559 """Clear the raw chunk cache."""
1557 1560 self._chunkcache = (0, '')
1558 1561
1559 1562 def deltaparent(self, rev):
1560 1563 """return deltaparent of the given revision"""
1561 1564 base = self.index[rev][3]
1562 1565 if base == rev:
1563 1566 return nullrev
1564 1567 elif self._generaldelta:
1565 1568 return base
1566 1569 else:
1567 1570 return rev - 1
1568 1571
1569 1572 def issnapshot(self, rev):
1570 1573 """tells whether rev is a snapshot
1571 1574 """
1572 1575 if not self._sparserevlog:
1573 1576 return self.deltaparent(rev) == nullrev
1574 1577 elif util.safehasattr(self.index, 'issnapshot'):
1575 1578 # directly assign the method to cache the testing and access
1576 1579 self.issnapshot = self.index.issnapshot
1577 1580 return self.issnapshot(rev)
1578 1581 if rev == nullrev:
1579 1582 return True
1580 1583 entry = self.index[rev]
1581 1584 base = entry[3]
1582 1585 if base == rev:
1583 1586 return True
1584 1587 if base == nullrev:
1585 1588 return True
1586 1589 p1 = entry[5]
1587 1590 p2 = entry[6]
1588 1591 if base == p1 or base == p2:
1589 1592 return False
1590 1593 return self.issnapshot(base)
1591 1594
1592 1595 def snapshotdepth(self, rev):
1593 1596 """number of snapshot in the chain before this one"""
1594 1597 if not self.issnapshot(rev):
1595 1598 raise error.ProgrammingError('revision %d not a snapshot')
1596 1599 return len(self._deltachain(rev)[0]) - 1
1597 1600
1598 1601 def revdiff(self, rev1, rev2):
1599 1602 """return or calculate a delta between two revisions
1600 1603
1601 1604 The delta calculated is in binary form and is intended to be written to
1602 1605 revlog data directly. So this function needs raw revision data.
1603 1606 """
1604 1607 if rev1 != nullrev and self.deltaparent(rev2) == rev1:
1605 1608 return bytes(self._chunk(rev2))
1606 1609
1607 1610 return mdiff.textdiff(self.revision(rev1, raw=True),
1608 1611 self.revision(rev2, raw=True))
1609 1612
1610 1613 def revision(self, nodeorrev, _df=None, raw=False):
1611 1614 """return an uncompressed revision of a given node or revision
1612 1615 number.
1613 1616
1614 1617 _df - an existing file handle to read from. (internal-only)
1615 1618 raw - an optional argument specifying if the revision data is to be
1616 1619 treated as raw data when applying flag transforms. 'raw' should be set
1617 1620 to True when generating changegroups or in debug commands.
1618 1621 """
1619 1622 if isinstance(nodeorrev, int):
1620 1623 rev = nodeorrev
1621 1624 node = self.node(rev)
1622 1625 else:
1623 1626 node = nodeorrev
1624 1627 rev = None
1625 1628
1626 1629 cachedrev = None
1627 1630 flags = None
1628 1631 rawtext = None
1629 1632 if node == nullid:
1630 1633 return ""
1631 1634 if self._revisioncache:
1632 1635 if self._revisioncache[0] == node:
1633 1636 # _cache only stores rawtext
1634 1637 if raw:
1635 1638 return self._revisioncache[2]
1636 1639 # duplicated, but good for perf
1637 1640 if rev is None:
1638 1641 rev = self.rev(node)
1639 1642 if flags is None:
1640 1643 flags = self.flags(rev)
1641 1644 # no extra flags set, no flag processor runs, text = rawtext
1642 1645 if flags == REVIDX_DEFAULT_FLAGS:
1643 1646 return self._revisioncache[2]
1644 1647 # rawtext is reusable. need to run flag processor
1645 1648 rawtext = self._revisioncache[2]
1646 1649
1647 1650 cachedrev = self._revisioncache[1]
1648 1651
1649 1652 # look up what we need to read
1650 1653 if rawtext is None:
1651 1654 if rev is None:
1652 1655 rev = self.rev(node)
1653 1656
1654 1657 chain, stopped = self._deltachain(rev, stoprev=cachedrev)
1655 1658 if stopped:
1656 1659 rawtext = self._revisioncache[2]
1657 1660
1658 1661 # drop cache to save memory
1659 1662 self._revisioncache = None
1660 1663
1661 1664 targetsize = None
1662 1665 rawsize = self.index[rev][2]
1663 1666 if 0 <= rawsize:
1664 1667 targetsize = 4 * rawsize
1665 1668
1666 1669 bins = self._chunks(chain, df=_df, targetsize=targetsize)
1667 1670 if rawtext is None:
1668 1671 rawtext = bytes(bins[0])
1669 1672 bins = bins[1:]
1670 1673
1671 1674 rawtext = mdiff.patches(rawtext, bins)
1672 1675 self._revisioncache = (node, rev, rawtext)
1673 1676
1674 1677 if flags is None:
1675 1678 if rev is None:
1676 1679 rev = self.rev(node)
1677 1680 flags = self.flags(rev)
1678 1681
1679 1682 text, validatehash = self._processflags(rawtext, flags, 'read', raw=raw)
1680 1683 if validatehash:
1681 1684 self.checkhash(text, node, rev=rev)
1682 1685
1683 1686 return text
1684 1687
1685 1688 def hash(self, text, p1, p2):
1686 1689 """Compute a node hash.
1687 1690
1688 1691 Available as a function so that subclasses can replace the hash
1689 1692 as needed.
1690 1693 """
1691 1694 return storageutil.hashrevisionsha1(text, p1, p2)
1692 1695
1693 1696 def _processflags(self, text, flags, operation, raw=False):
1694 1697 """Inspect revision data flags and applies transforms defined by
1695 1698 registered flag processors.
1696 1699
1697 1700 ``text`` - the revision data to process
1698 1701 ``flags`` - the revision flags
1699 1702 ``operation`` - the operation being performed (read or write)
1700 1703 ``raw`` - an optional argument describing if the raw transform should be
1701 1704 applied.
1702 1705
1703 1706 This method processes the flags in the order (or reverse order if
1704 1707 ``operation`` is 'write') defined by REVIDX_FLAGS_ORDER, applying the
1705 1708 flag processors registered for present flags. The order of flags defined
1706 1709 in REVIDX_FLAGS_ORDER needs to be stable to allow non-commutativity.
1707 1710
1708 1711 Returns a 2-tuple of ``(text, validatehash)`` where ``text`` is the
1709 1712 processed text and ``validatehash`` is a bool indicating whether the
1710 1713 returned text should be checked for hash integrity.
1711 1714
1712 1715 Note: If the ``raw`` argument is set, it has precedence over the
1713 1716 operation and will only update the value of ``validatehash``.
1714 1717 """
1715 1718 # fast path: no flag processors will run
1716 1719 if flags == 0:
1717 1720 return text, True
1718 1721 if not operation in ('read', 'write'):
1719 1722 raise error.ProgrammingError(_("invalid '%s' operation") %
1720 1723 operation)
1721 1724 # Check all flags are known.
1722 1725 if flags & ~REVIDX_KNOWN_FLAGS:
1723 1726 raise error.RevlogError(_("incompatible revision flag '%#x'") %
1724 1727 (flags & ~REVIDX_KNOWN_FLAGS))
1725 1728 validatehash = True
1726 1729 # Depending on the operation (read or write), the order might be
1727 1730 # reversed due to non-commutative transforms.
1728 1731 orderedflags = REVIDX_FLAGS_ORDER
1729 1732 if operation == 'write':
1730 1733 orderedflags = reversed(orderedflags)
1731 1734
1732 1735 for flag in orderedflags:
1733 1736 # If a flagprocessor has been registered for a known flag, apply the
1734 1737 # related operation transform and update result tuple.
1735 1738 if flag & flags:
1736 1739 vhash = True
1737 1740
1738 1741 if flag not in self._flagprocessors:
1739 1742 message = _("missing processor for flag '%#x'") % (flag)
1740 1743 raise error.RevlogError(message)
1741 1744
1742 1745 processor = self._flagprocessors[flag]
1743 1746 if processor is not None:
1744 1747 readtransform, writetransform, rawtransform = processor
1745 1748
1746 1749 if raw:
1747 1750 vhash = rawtransform(self, text)
1748 1751 elif operation == 'read':
1749 1752 text, vhash = readtransform(self, text)
1750 1753 else: # write operation
1751 1754 text, vhash = writetransform(self, text)
1752 1755 validatehash = validatehash and vhash
1753 1756
1754 1757 return text, validatehash
1755 1758
1756 1759 def checkhash(self, text, node, p1=None, p2=None, rev=None):
1757 1760 """Check node hash integrity.
1758 1761
1759 1762 Available as a function so that subclasses can extend hash mismatch
1760 1763 behaviors as needed.
1761 1764 """
1762 1765 try:
1763 1766 if p1 is None and p2 is None:
1764 1767 p1, p2 = self.parents(node)
1765 1768 if node != self.hash(text, p1, p2):
1766 1769 # Clear the revision cache on hash failure. The revision cache
1767 1770 # only stores the raw revision and clearing the cache does have
1768 1771 # the side-effect that we won't have a cache hit when the raw
1769 1772 # revision data is accessed. But this case should be rare and
1770 1773 # it is extra work to teach the cache about the hash
1771 1774 # verification state.
1772 1775 if self._revisioncache and self._revisioncache[0] == node:
1773 1776 self._revisioncache = None
1774 1777
1775 1778 revornode = rev
1776 1779 if revornode is None:
1777 1780 revornode = templatefilters.short(hex(node))
1778 1781 raise error.RevlogError(_("integrity check failed on %s:%s")
1779 1782 % (self.indexfile, pycompat.bytestr(revornode)))
1780 1783 except error.RevlogError:
1781 1784 if self._censorable and storageutil.iscensoredtext(text):
1782 1785 raise error.CensoredNodeError(self.indexfile, node, text)
1783 1786 raise
1784 1787
1785 1788 def _enforceinlinesize(self, tr, fp=None):
1786 1789 """Check if the revlog is too big for inline and convert if so.
1787 1790
1788 1791 This should be called after revisions are added to the revlog. If the
1789 1792 revlog has grown too large to be an inline revlog, it will convert it
1790 1793 to use multiple index and data files.
1791 1794 """
1792 1795 tiprev = len(self) - 1
1793 1796 if (not self._inline or
1794 1797 (self.start(tiprev) + self.length(tiprev)) < _maxinline):
1795 1798 return
1796 1799
1797 1800 trinfo = tr.find(self.indexfile)
1798 1801 if trinfo is None:
1799 1802 raise error.RevlogError(_("%s not found in the transaction")
1800 1803 % self.indexfile)
1801 1804
1802 1805 trindex = trinfo[2]
1803 1806 if trindex is not None:
1804 1807 dataoff = self.start(trindex)
1805 1808 else:
1806 1809 # revlog was stripped at start of transaction, use all leftover data
1807 1810 trindex = len(self) - 1
1808 1811 dataoff = self.end(tiprev)
1809 1812
1810 1813 tr.add(self.datafile, dataoff)
1811 1814
1812 1815 if fp:
1813 1816 fp.flush()
1814 1817 fp.close()
1815 1818 # We can't use the cached file handle after close(). So prevent
1816 1819 # its usage.
1817 1820 self._writinghandles = None
1818 1821
1819 1822 with self._indexfp('r') as ifh, self._datafp('w') as dfh:
1820 1823 for r in self:
1821 1824 dfh.write(self._getsegmentforrevs(r, r, df=ifh)[1])
1822 1825
1823 1826 with self._indexfp('w') as fp:
1824 1827 self.version &= ~FLAG_INLINE_DATA
1825 1828 self._inline = False
1826 1829 io = self._io
1827 1830 for i in self:
1828 1831 e = io.packentry(self.index[i], self.node, self.version, i)
1829 1832 fp.write(e)
1830 1833
1831 1834 # the temp file replace the real index when we exit the context
1832 1835 # manager
1833 1836
1834 1837 tr.replace(self.indexfile, trindex * self._io.size)
1835 1838 self._chunkclear()
1836 1839
1837 1840 def _nodeduplicatecallback(self, transaction, node):
1838 1841 """called when trying to add a node already stored.
1839 1842 """
1840 1843
1841 1844 def addrevision(self, text, transaction, link, p1, p2, cachedelta=None,
1842 1845 node=None, flags=REVIDX_DEFAULT_FLAGS, deltacomputer=None):
1843 1846 """add a revision to the log
1844 1847
1845 1848 text - the revision data to add
1846 1849 transaction - the transaction object used for rollback
1847 1850 link - the linkrev data to add
1848 1851 p1, p2 - the parent nodeids of the revision
1849 1852 cachedelta - an optional precomputed delta
1850 1853 node - nodeid of revision; typically node is not specified, and it is
1851 1854 computed by default as hash(text, p1, p2), however subclasses might
1852 1855 use different hashing method (and override checkhash() in such case)
1853 1856 flags - the known flags to set on the revision
1854 1857 deltacomputer - an optional deltacomputer instance shared between
1855 1858 multiple calls
1856 1859 """
1857 1860 if link == nullrev:
1858 1861 raise error.RevlogError(_("attempted to add linkrev -1 to %s")
1859 1862 % self.indexfile)
1860 1863
1861 1864 if flags:
1862 1865 node = node or self.hash(text, p1, p2)
1863 1866
1864 1867 rawtext, validatehash = self._processflags(text, flags, 'write')
1865 1868
1866 1869 # If the flag processor modifies the revision data, ignore any provided
1867 1870 # cachedelta.
1868 1871 if rawtext != text:
1869 1872 cachedelta = None
1870 1873
1871 1874 if len(rawtext) > _maxentrysize:
1872 1875 raise error.RevlogError(
1873 1876 _("%s: size of %d bytes exceeds maximum revlog storage of 2GiB")
1874 1877 % (self.indexfile, len(rawtext)))
1875 1878
1876 1879 node = node or self.hash(rawtext, p1, p2)
1877 1880 if node in self.nodemap:
1878 1881 return node
1879 1882
1880 1883 if validatehash:
1881 1884 self.checkhash(rawtext, node, p1=p1, p2=p2)
1882 1885
1883 1886 return self.addrawrevision(rawtext, transaction, link, p1, p2, node,
1884 1887 flags, cachedelta=cachedelta,
1885 1888 deltacomputer=deltacomputer)
1886 1889
1887 1890 def addrawrevision(self, rawtext, transaction, link, p1, p2, node, flags,
1888 1891 cachedelta=None, deltacomputer=None):
1889 1892 """add a raw revision with known flags, node and parents
1890 1893 useful when reusing a revision not stored in this revlog (ex: received
1891 1894 over wire, or read from an external bundle).
1892 1895 """
1893 1896 dfh = None
1894 1897 if not self._inline:
1895 1898 dfh = self._datafp("a+")
1896 1899 ifh = self._indexfp("a+")
1897 1900 try:
1898 1901 return self._addrevision(node, rawtext, transaction, link, p1, p2,
1899 1902 flags, cachedelta, ifh, dfh,
1900 1903 deltacomputer=deltacomputer)
1901 1904 finally:
1902 1905 if dfh:
1903 1906 dfh.close()
1904 1907 ifh.close()
1905 1908
1906 1909 def compress(self, data):
1907 1910 """Generate a possibly-compressed representation of data."""
1908 1911 if not data:
1909 1912 return '', data
1910 1913
1911 1914 compressed = self._compressor.compress(data)
1912 1915
1913 1916 if compressed:
1914 1917 # The revlog compressor added the header in the returned data.
1915 1918 return '', compressed
1916 1919
1917 1920 if data[0:1] == '\0':
1918 1921 return '', data
1919 1922 return 'u', data
1920 1923
1921 1924 def decompress(self, data):
1922 1925 """Decompress a revlog chunk.
1923 1926
1924 1927 The chunk is expected to begin with a header identifying the
1925 1928 format type so it can be routed to an appropriate decompressor.
1926 1929 """
1927 1930 if not data:
1928 1931 return data
1929 1932
1930 1933 # Revlogs are read much more frequently than they are written and many
1931 1934 # chunks only take microseconds to decompress, so performance is
1932 1935 # important here.
1933 1936 #
1934 1937 # We can make a few assumptions about revlogs:
1935 1938 #
1936 1939 # 1) the majority of chunks will be compressed (as opposed to inline
1937 1940 # raw data).
1938 1941 # 2) decompressing *any* data will likely by at least 10x slower than
1939 1942 # returning raw inline data.
1940 1943 # 3) we want to prioritize common and officially supported compression
1941 1944 # engines
1942 1945 #
1943 1946 # It follows that we want to optimize for "decompress compressed data
1944 1947 # when encoded with common and officially supported compression engines"
1945 1948 # case over "raw data" and "data encoded by less common or non-official
1946 1949 # compression engines." That is why we have the inline lookup first
1947 1950 # followed by the compengines lookup.
1948 1951 #
1949 1952 # According to `hg perfrevlogchunks`, this is ~0.5% faster for zlib
1950 1953 # compressed chunks. And this matters for changelog and manifest reads.
1951 1954 t = data[0:1]
1952 1955
1953 1956 if t == 'x':
1954 1957 try:
1955 1958 return _zlibdecompress(data)
1956 1959 except zlib.error as e:
1957 1960 raise error.RevlogError(_('revlog decompress error: %s') %
1958 1961 stringutil.forcebytestr(e))
1959 1962 # '\0' is more common than 'u' so it goes first.
1960 1963 elif t == '\0':
1961 1964 return data
1962 1965 elif t == 'u':
1963 1966 return util.buffer(data, 1)
1964 1967
1965 1968 try:
1966 1969 compressor = self._decompressors[t]
1967 1970 except KeyError:
1968 1971 try:
1969 1972 engine = util.compengines.forrevlogheader(t)
1970 1973 compressor = engine.revlogcompressor()
1971 1974 self._decompressors[t] = compressor
1972 1975 except KeyError:
1973 1976 raise error.RevlogError(_('unknown compression type %r') % t)
1974 1977
1975 1978 return compressor.decompress(data)
1976 1979
1977 1980 def _addrevision(self, node, rawtext, transaction, link, p1, p2, flags,
1978 1981 cachedelta, ifh, dfh, alwayscache=False,
1979 1982 deltacomputer=None):
1980 1983 """internal function to add revisions to the log
1981 1984
1982 1985 see addrevision for argument descriptions.
1983 1986
1984 1987 note: "addrevision" takes non-raw text, "_addrevision" takes raw text.
1985 1988
1986 1989 if "deltacomputer" is not provided or None, a defaultdeltacomputer will
1987 1990 be used.
1988 1991
1989 1992 invariants:
1990 1993 - rawtext is optional (can be None); if not set, cachedelta must be set.
1991 1994 if both are set, they must correspond to each other.
1992 1995 """
1993 1996 if node == nullid:
1994 1997 raise error.RevlogError(_("%s: attempt to add null revision") %
1995 1998 self.indexfile)
1996 1999 if node == wdirid or node in wdirfilenodeids:
1997 2000 raise error.RevlogError(_("%s: attempt to add wdir revision") %
1998 2001 self.indexfile)
1999 2002
2000 2003 if self._inline:
2001 2004 fh = ifh
2002 2005 else:
2003 2006 fh = dfh
2004 2007
2005 2008 btext = [rawtext]
2006 2009
2007 2010 curr = len(self)
2008 2011 prev = curr - 1
2009 2012 offset = self.end(prev)
2010 2013 p1r, p2r = self.rev(p1), self.rev(p2)
2011 2014
2012 2015 # full versions are inserted when the needed deltas
2013 2016 # become comparable to the uncompressed text
2014 2017 if rawtext is None:
2015 2018 # need rawtext size, before changed by flag processors, which is
2016 2019 # the non-raw size. use revlog explicitly to avoid filelog's extra
2017 2020 # logic that might remove metadata size.
2018 2021 textlen = mdiff.patchedsize(revlog.size(self, cachedelta[0]),
2019 2022 cachedelta[1])
2020 2023 else:
2021 2024 textlen = len(rawtext)
2022 2025
2023 2026 if deltacomputer is None:
2024 2027 deltacomputer = deltautil.deltacomputer(self)
2025 2028
2026 2029 revinfo = _revisioninfo(node, p1, p2, btext, textlen, cachedelta, flags)
2027 2030
2028 2031 deltainfo = deltacomputer.finddeltainfo(revinfo, fh)
2029 2032
2030 2033 e = (offset_type(offset, flags), deltainfo.deltalen, textlen,
2031 2034 deltainfo.base, link, p1r, p2r, node)
2032 2035 self.index.append(e)
2033 2036 self.nodemap[node] = curr
2034 2037
2035 2038 # Reset the pure node cache start lookup offset to account for new
2036 2039 # revision.
2037 2040 if self._nodepos is not None:
2038 2041 self._nodepos = curr
2039 2042
2040 2043 entry = self._io.packentry(e, self.node, self.version, curr)
2041 2044 self._writeentry(transaction, ifh, dfh, entry, deltainfo.data,
2042 2045 link, offset)
2043 2046
2044 2047 rawtext = btext[0]
2045 2048
2046 2049 if alwayscache and rawtext is None:
2047 2050 rawtext = deltacomputer.buildtext(revinfo, fh)
2048 2051
2049 2052 if type(rawtext) == bytes: # only accept immutable objects
2050 2053 self._revisioncache = (node, curr, rawtext)
2051 2054 self._chainbasecache[curr] = deltainfo.chainbase
2052 2055 return node
2053 2056
2054 2057 def _writeentry(self, transaction, ifh, dfh, entry, data, link, offset):
2055 2058 # Files opened in a+ mode have inconsistent behavior on various
2056 2059 # platforms. Windows requires that a file positioning call be made
2057 2060 # when the file handle transitions between reads and writes. See
2058 2061 # 3686fa2b8eee and the mixedfilemodewrapper in windows.py. On other
2059 2062 # platforms, Python or the platform itself can be buggy. Some versions
2060 2063 # of Solaris have been observed to not append at the end of the file
2061 2064 # if the file was seeked to before the end. See issue4943 for more.
2062 2065 #
2063 2066 # We work around this issue by inserting a seek() before writing.
2064 2067 # Note: This is likely not necessary on Python 3. However, because
2065 2068 # the file handle is reused for reads and may be seeked there, we need
2066 2069 # to be careful before changing this.
2067 2070 ifh.seek(0, os.SEEK_END)
2068 2071 if dfh:
2069 2072 dfh.seek(0, os.SEEK_END)
2070 2073
2071 2074 curr = len(self) - 1
2072 2075 if not self._inline:
2073 2076 transaction.add(self.datafile, offset)
2074 2077 transaction.add(self.indexfile, curr * len(entry))
2075 2078 if data[0]:
2076 2079 dfh.write(data[0])
2077 2080 dfh.write(data[1])
2078 2081 ifh.write(entry)
2079 2082 else:
2080 2083 offset += curr * self._io.size
2081 2084 transaction.add(self.indexfile, offset, curr)
2082 2085 ifh.write(entry)
2083 2086 ifh.write(data[0])
2084 2087 ifh.write(data[1])
2085 2088 self._enforceinlinesize(transaction, ifh)
2086 2089
2087 2090 def addgroup(self, deltas, linkmapper, transaction, addrevisioncb=None):
2088 2091 """
2089 2092 add a delta group
2090 2093
2091 2094 given a set of deltas, add them to the revision log. the
2092 2095 first delta is against its parent, which should be in our
2093 2096 log, the rest are against the previous delta.
2094 2097
2095 2098 If ``addrevisioncb`` is defined, it will be called with arguments of
2096 2099 this revlog and the node that was added.
2097 2100 """
2098 2101
2099 2102 if self._writinghandles:
2100 2103 raise error.ProgrammingError('cannot nest addgroup() calls')
2101 2104
2102 2105 nodes = []
2103 2106
2104 2107 r = len(self)
2105 2108 end = 0
2106 2109 if r:
2107 2110 end = self.end(r - 1)
2108 2111 ifh = self._indexfp("a+")
2109 2112 isize = r * self._io.size
2110 2113 if self._inline:
2111 2114 transaction.add(self.indexfile, end + isize, r)
2112 2115 dfh = None
2113 2116 else:
2114 2117 transaction.add(self.indexfile, isize, r)
2115 2118 transaction.add(self.datafile, end)
2116 2119 dfh = self._datafp("a+")
2117 2120 def flush():
2118 2121 if dfh:
2119 2122 dfh.flush()
2120 2123 ifh.flush()
2121 2124
2122 2125 self._writinghandles = (ifh, dfh)
2123 2126
2124 2127 try:
2125 2128 deltacomputer = deltautil.deltacomputer(self)
2126 2129 # loop through our set of deltas
2127 2130 for data in deltas:
2128 2131 node, p1, p2, linknode, deltabase, delta, flags = data
2129 2132 link = linkmapper(linknode)
2130 2133 flags = flags or REVIDX_DEFAULT_FLAGS
2131 2134
2132 2135 nodes.append(node)
2133 2136
2134 2137 if node in self.nodemap:
2135 2138 self._nodeduplicatecallback(transaction, node)
2136 2139 # this can happen if two branches make the same change
2137 2140 continue
2138 2141
2139 2142 for p in (p1, p2):
2140 2143 if p not in self.nodemap:
2141 2144 raise error.LookupError(p, self.indexfile,
2142 2145 _('unknown parent'))
2143 2146
2144 2147 if deltabase not in self.nodemap:
2145 2148 raise error.LookupError(deltabase, self.indexfile,
2146 2149 _('unknown delta base'))
2147 2150
2148 2151 baserev = self.rev(deltabase)
2149 2152
2150 2153 if baserev != nullrev and self.iscensored(baserev):
2151 2154 # if base is censored, delta must be full replacement in a
2152 2155 # single patch operation
2153 2156 hlen = struct.calcsize(">lll")
2154 2157 oldlen = self.rawsize(baserev)
2155 2158 newlen = len(delta) - hlen
2156 2159 if delta[:hlen] != mdiff.replacediffheader(oldlen, newlen):
2157 2160 raise error.CensoredBaseError(self.indexfile,
2158 2161 self.node(baserev))
2159 2162
2160 2163 if not flags and self._peek_iscensored(baserev, delta, flush):
2161 2164 flags |= REVIDX_ISCENSORED
2162 2165
2163 2166 # We assume consumers of addrevisioncb will want to retrieve
2164 2167 # the added revision, which will require a call to
2165 2168 # revision(). revision() will fast path if there is a cache
2166 2169 # hit. So, we tell _addrevision() to always cache in this case.
2167 2170 # We're only using addgroup() in the context of changegroup
2168 2171 # generation so the revision data can always be handled as raw
2169 2172 # by the flagprocessor.
2170 2173 self._addrevision(node, None, transaction, link,
2171 2174 p1, p2, flags, (baserev, delta),
2172 2175 ifh, dfh,
2173 2176 alwayscache=bool(addrevisioncb),
2174 2177 deltacomputer=deltacomputer)
2175 2178
2176 2179 if addrevisioncb:
2177 2180 addrevisioncb(self, node)
2178 2181
2179 2182 if not dfh and not self._inline:
2180 2183 # addrevision switched from inline to conventional
2181 2184 # reopen the index
2182 2185 ifh.close()
2183 2186 dfh = self._datafp("a+")
2184 2187 ifh = self._indexfp("a+")
2185 2188 self._writinghandles = (ifh, dfh)
2186 2189 finally:
2187 2190 self._writinghandles = None
2188 2191
2189 2192 if dfh:
2190 2193 dfh.close()
2191 2194 ifh.close()
2192 2195
2193 2196 return nodes
2194 2197
2195 2198 def iscensored(self, rev):
2196 2199 """Check if a file revision is censored."""
2197 2200 if not self._censorable:
2198 2201 return False
2199 2202
2200 2203 return self.flags(rev) & REVIDX_ISCENSORED
2201 2204
2202 2205 def _peek_iscensored(self, baserev, delta, flush):
2203 2206 """Quickly check if a delta produces a censored revision."""
2204 2207 if not self._censorable:
2205 2208 return False
2206 2209
2207 2210 return storageutil.deltaiscensored(delta, baserev, self.rawsize)
2208 2211
2209 2212 def getstrippoint(self, minlink):
2210 2213 """find the minimum rev that must be stripped to strip the linkrev
2211 2214
2212 2215 Returns a tuple containing the minimum rev and a set of all revs that
2213 2216 have linkrevs that will be broken by this strip.
2214 2217 """
2215 2218 return storageutil.resolvestripinfo(minlink, len(self) - 1,
2216 2219 self.headrevs(),
2217 2220 self.linkrev, self.parentrevs)
2218 2221
2219 2222 def strip(self, minlink, transaction):
2220 2223 """truncate the revlog on the first revision with a linkrev >= minlink
2221 2224
2222 2225 This function is called when we're stripping revision minlink and
2223 2226 its descendants from the repository.
2224 2227
2225 2228 We have to remove all revisions with linkrev >= minlink, because
2226 2229 the equivalent changelog revisions will be renumbered after the
2227 2230 strip.
2228 2231
2229 2232 So we truncate the revlog on the first of these revisions, and
2230 2233 trust that the caller has saved the revisions that shouldn't be
2231 2234 removed and that it'll re-add them after this truncation.
2232 2235 """
2233 2236 if len(self) == 0:
2234 2237 return
2235 2238
2236 2239 rev, _ = self.getstrippoint(minlink)
2237 2240 if rev == len(self):
2238 2241 return
2239 2242
2240 2243 # first truncate the files on disk
2241 2244 end = self.start(rev)
2242 2245 if not self._inline:
2243 2246 transaction.add(self.datafile, end)
2244 2247 end = rev * self._io.size
2245 2248 else:
2246 2249 end += rev * self._io.size
2247 2250
2248 2251 transaction.add(self.indexfile, end)
2249 2252
2250 2253 # then reset internal state in memory to forget those revisions
2251 2254 self._revisioncache = None
2252 2255 self._chaininfocache = {}
2253 2256 self._chunkclear()
2254 2257 for x in pycompat.xrange(rev, len(self)):
2255 2258 del self.nodemap[self.node(x)]
2256 2259
2257 2260 del self.index[rev:-1]
2258 2261 self._nodepos = None
2259 2262
2260 2263 def checksize(self):
2261 2264 expected = 0
2262 2265 if len(self):
2263 2266 expected = max(0, self.end(len(self) - 1))
2264 2267
2265 2268 try:
2266 2269 with self._datafp() as f:
2267 2270 f.seek(0, 2)
2268 2271 actual = f.tell()
2269 2272 dd = actual - expected
2270 2273 except IOError as inst:
2271 2274 if inst.errno != errno.ENOENT:
2272 2275 raise
2273 2276 dd = 0
2274 2277
2275 2278 try:
2276 2279 f = self.opener(self.indexfile)
2277 2280 f.seek(0, 2)
2278 2281 actual = f.tell()
2279 2282 f.close()
2280 2283 s = self._io.size
2281 2284 i = max(0, actual // s)
2282 2285 di = actual - (i * s)
2283 2286 if self._inline:
2284 2287 databytes = 0
2285 2288 for r in self:
2286 2289 databytes += max(0, self.length(r))
2287 2290 dd = 0
2288 2291 di = actual - len(self) * s - databytes
2289 2292 except IOError as inst:
2290 2293 if inst.errno != errno.ENOENT:
2291 2294 raise
2292 2295 di = 0
2293 2296
2294 2297 return (dd, di)
2295 2298
2296 2299 def files(self):
2297 2300 res = [self.indexfile]
2298 2301 if not self._inline:
2299 2302 res.append(self.datafile)
2300 2303 return res
2301 2304
2302 2305 def emitrevisions(self, nodes, nodesorder=None, revisiondata=False,
2303 2306 assumehaveparentrevisions=False,
2304 2307 deltamode=repository.CG_DELTAMODE_STD):
2305 2308 if nodesorder not in ('nodes', 'storage', 'linear', None):
2306 2309 raise error.ProgrammingError('unhandled value for nodesorder: %s' %
2307 2310 nodesorder)
2308 2311
2309 2312 if nodesorder is None and not self._generaldelta:
2310 2313 nodesorder = 'storage'
2311 2314
2312 2315 if (not self._storedeltachains and
2313 2316 deltamode != repository.CG_DELTAMODE_PREV):
2314 2317 deltamode = repository.CG_DELTAMODE_FULL
2315 2318
2316 2319 return storageutil.emitrevisions(
2317 2320 self, nodes, nodesorder, revlogrevisiondelta,
2318 2321 deltaparentfn=self.deltaparent,
2319 2322 candeltafn=self.candelta,
2320 2323 rawsizefn=self.rawsize,
2321 2324 revdifffn=self.revdiff,
2322 2325 flagsfn=self.flags,
2323 2326 deltamode=deltamode,
2324 2327 revisiondata=revisiondata,
2325 2328 assumehaveparentrevisions=assumehaveparentrevisions)
2326 2329
2327 2330 DELTAREUSEALWAYS = 'always'
2328 2331 DELTAREUSESAMEREVS = 'samerevs'
2329 2332 DELTAREUSENEVER = 'never'
2330 2333
2331 2334 DELTAREUSEFULLADD = 'fulladd'
2332 2335
2333 2336 DELTAREUSEALL = {'always', 'samerevs', 'never', 'fulladd'}
2334 2337
2335 2338 def clone(self, tr, destrevlog, addrevisioncb=None,
2336 2339 deltareuse=DELTAREUSESAMEREVS, forcedeltabothparents=None):
2337 2340 """Copy this revlog to another, possibly with format changes.
2338 2341
2339 2342 The destination revlog will contain the same revisions and nodes.
2340 2343 However, it may not be bit-for-bit identical due to e.g. delta encoding
2341 2344 differences.
2342 2345
2343 2346 The ``deltareuse`` argument control how deltas from the existing revlog
2344 2347 are preserved in the destination revlog. The argument can have the
2345 2348 following values:
2346 2349
2347 2350 DELTAREUSEALWAYS
2348 2351 Deltas will always be reused (if possible), even if the destination
2349 2352 revlog would not select the same revisions for the delta. This is the
2350 2353 fastest mode of operation.
2351 2354 DELTAREUSESAMEREVS
2352 2355 Deltas will be reused if the destination revlog would pick the same
2353 2356 revisions for the delta. This mode strikes a balance between speed
2354 2357 and optimization.
2355 2358 DELTAREUSENEVER
2356 2359 Deltas will never be reused. This is the slowest mode of execution.
2357 2360 This mode can be used to recompute deltas (e.g. if the diff/delta
2358 2361 algorithm changes).
2359 2362
2360 2363 Delta computation can be slow, so the choice of delta reuse policy can
2361 2364 significantly affect run time.
2362 2365
2363 2366 The default policy (``DELTAREUSESAMEREVS``) strikes a balance between
2364 2367 two extremes. Deltas will be reused if they are appropriate. But if the
2365 2368 delta could choose a better revision, it will do so. This means if you
2366 2369 are converting a non-generaldelta revlog to a generaldelta revlog,
2367 2370 deltas will be recomputed if the delta's parent isn't a parent of the
2368 2371 revision.
2369 2372
2370 2373 In addition to the delta policy, the ``forcedeltabothparents``
2371 2374 argument controls whether to force compute deltas against both parents
2372 2375 for merges. By default, the current default is used.
2373 2376 """
2374 2377 if deltareuse not in self.DELTAREUSEALL:
2375 2378 raise ValueError(_('value for deltareuse invalid: %s') % deltareuse)
2376 2379
2377 2380 if len(destrevlog):
2378 2381 raise ValueError(_('destination revlog is not empty'))
2379 2382
2380 2383 if getattr(self, 'filteredrevs', None):
2381 2384 raise ValueError(_('source revlog has filtered revisions'))
2382 2385 if getattr(destrevlog, 'filteredrevs', None):
2383 2386 raise ValueError(_('destination revlog has filtered revisions'))
2384 2387
2385 2388 # lazydeltabase controls whether to reuse a cached delta, if possible.
2386 2389 oldlazydeltabase = destrevlog._lazydeltabase
2387 2390 oldamd = destrevlog._deltabothparents
2388 2391
2389 2392 try:
2390 2393 if deltareuse == self.DELTAREUSEALWAYS:
2391 2394 destrevlog._lazydeltabase = True
2392 2395 elif deltareuse == self.DELTAREUSESAMEREVS:
2393 2396 destrevlog._lazydeltabase = False
2394 2397
2395 2398 destrevlog._deltabothparents = forcedeltabothparents or oldamd
2396 2399
2397 2400 populatecachedelta = deltareuse in (self.DELTAREUSEALWAYS,
2398 2401 self.DELTAREUSESAMEREVS)
2399 2402
2400 2403 deltacomputer = deltautil.deltacomputer(destrevlog)
2401 2404 index = self.index
2402 2405 for rev in self:
2403 2406 entry = index[rev]
2404 2407
2405 2408 # Some classes override linkrev to take filtered revs into
2406 2409 # account. Use raw entry from index.
2407 2410 flags = entry[0] & 0xffff
2408 2411 linkrev = entry[4]
2409 2412 p1 = index[entry[5]][7]
2410 2413 p2 = index[entry[6]][7]
2411 2414 node = entry[7]
2412 2415
2413 2416 # (Possibly) reuse the delta from the revlog if allowed and
2414 2417 # the revlog chunk is a delta.
2415 2418 cachedelta = None
2416 2419 rawtext = None
2417 2420 if populatecachedelta:
2418 2421 dp = self.deltaparent(rev)
2419 2422 if dp != nullrev:
2420 2423 cachedelta = (dp, bytes(self._chunk(rev)))
2421 2424
2422 2425 if not cachedelta:
2423 2426 rawtext = self.revision(rev, raw=True)
2424 2427
2425 2428
2426 2429 if deltareuse == self.DELTAREUSEFULLADD:
2427 2430 destrevlog.addrevision(rawtext, tr, linkrev, p1, p2,
2428 2431 cachedelta=cachedelta,
2429 2432 node=node, flags=flags,
2430 2433 deltacomputer=deltacomputer)
2431 2434 else:
2432 2435 ifh = destrevlog.opener(destrevlog.indexfile, 'a+',
2433 2436 checkambig=False)
2434 2437 dfh = None
2435 2438 if not destrevlog._inline:
2436 2439 dfh = destrevlog.opener(destrevlog.datafile, 'a+')
2437 2440 try:
2438 2441 destrevlog._addrevision(node, rawtext, tr, linkrev, p1,
2439 2442 p2, flags, cachedelta, ifh, dfh,
2440 2443 deltacomputer=deltacomputer)
2441 2444 finally:
2442 2445 if dfh:
2443 2446 dfh.close()
2444 2447 ifh.close()
2445 2448
2446 2449 if addrevisioncb:
2447 2450 addrevisioncb(self, rev, node)
2448 2451 finally:
2449 2452 destrevlog._lazydeltabase = oldlazydeltabase
2450 2453 destrevlog._deltabothparents = oldamd
2451 2454
2452 2455 def censorrevision(self, tr, censornode, tombstone=b''):
2453 2456 if (self.version & 0xFFFF) == REVLOGV0:
2454 2457 raise error.RevlogError(_('cannot censor with version %d revlogs') %
2455 2458 self.version)
2456 2459
2457 2460 censorrev = self.rev(censornode)
2458 2461 tombstone = storageutil.packmeta({b'censored': tombstone}, b'')
2459 2462
2460 2463 if len(tombstone) > self.rawsize(censorrev):
2461 2464 raise error.Abort(_('censor tombstone must be no longer than '
2462 2465 'censored data'))
2463 2466
2464 2467 # Rewriting the revlog in place is hard. Our strategy for censoring is
2465 2468 # to create a new revlog, copy all revisions to it, then replace the
2466 2469 # revlogs on transaction close.
2467 2470
2468 2471 newindexfile = self.indexfile + b'.tmpcensored'
2469 2472 newdatafile = self.datafile + b'.tmpcensored'
2470 2473
2471 2474 # This is a bit dangerous. We could easily have a mismatch of state.
2472 2475 newrl = revlog(self.opener, newindexfile, newdatafile,
2473 2476 censorable=True)
2474 2477 newrl.version = self.version
2475 2478 newrl._generaldelta = self._generaldelta
2476 2479 newrl._io = self._io
2477 2480
2478 2481 for rev in self.revs():
2479 2482 node = self.node(rev)
2480 2483 p1, p2 = self.parents(node)
2481 2484
2482 2485 if rev == censorrev:
2483 2486 newrl.addrawrevision(tombstone, tr, self.linkrev(censorrev),
2484 2487 p1, p2, censornode, REVIDX_ISCENSORED)
2485 2488
2486 2489 if newrl.deltaparent(rev) != nullrev:
2487 2490 raise error.Abort(_('censored revision stored as delta; '
2488 2491 'cannot censor'),
2489 2492 hint=_('censoring of revlogs is not '
2490 2493 'fully implemented; please report '
2491 2494 'this bug'))
2492 2495 continue
2493 2496
2494 2497 if self.iscensored(rev):
2495 2498 if self.deltaparent(rev) != nullrev:
2496 2499 raise error.Abort(_('cannot censor due to censored '
2497 2500 'revision having delta stored'))
2498 2501 rawtext = self._chunk(rev)
2499 2502 else:
2500 2503 rawtext = self.revision(rev, raw=True)
2501 2504
2502 2505 newrl.addrawrevision(rawtext, tr, self.linkrev(rev), p1, p2, node,
2503 2506 self.flags(rev))
2504 2507
2505 2508 tr.addbackup(self.indexfile, location='store')
2506 2509 if not self._inline:
2507 2510 tr.addbackup(self.datafile, location='store')
2508 2511
2509 2512 self.opener.rename(newrl.indexfile, self.indexfile)
2510 2513 if not self._inline:
2511 2514 self.opener.rename(newrl.datafile, self.datafile)
2512 2515
2513 2516 self.clearcaches()
2514 2517 self._loadindex()
2515 2518
2516 2519 def verifyintegrity(self, state):
2517 2520 """Verifies the integrity of the revlog.
2518 2521
2519 2522 Yields ``revlogproblem`` instances describing problems that are
2520 2523 found.
2521 2524 """
2522 2525 dd, di = self.checksize()
2523 2526 if dd:
2524 2527 yield revlogproblem(error=_('data length off by %d bytes') % dd)
2525 2528 if di:
2526 2529 yield revlogproblem(error=_('index contains %d extra bytes') % di)
2527 2530
2528 2531 version = self.version & 0xFFFF
2529 2532
2530 2533 # The verifier tells us what version revlog we should be.
2531 2534 if version != state['expectedversion']:
2532 2535 yield revlogproblem(
2533 2536 warning=_("warning: '%s' uses revlog format %d; expected %d") %
2534 2537 (self.indexfile, version, state['expectedversion']))
2535 2538
2536 2539 state['skipread'] = set()
2537 2540
2538 2541 for rev in self:
2539 2542 node = self.node(rev)
2540 2543
2541 2544 # Verify contents. 4 cases to care about:
2542 2545 #
2543 2546 # common: the most common case
2544 2547 # rename: with a rename
2545 2548 # meta: file content starts with b'\1\n', the metadata
2546 2549 # header defined in filelog.py, but without a rename
2547 2550 # ext: content stored externally
2548 2551 #
2549 2552 # More formally, their differences are shown below:
2550 2553 #
2551 2554 # | common | rename | meta | ext
2552 2555 # -------------------------------------------------------
2553 2556 # flags() | 0 | 0 | 0 | not 0
2554 2557 # renamed() | False | True | False | ?
2555 2558 # rawtext[0:2]=='\1\n'| False | True | True | ?
2556 2559 #
2557 2560 # "rawtext" means the raw text stored in revlog data, which
2558 2561 # could be retrieved by "revision(rev, raw=True)". "text"
2559 2562 # mentioned below is "revision(rev, raw=False)".
2560 2563 #
2561 2564 # There are 3 different lengths stored physically:
2562 2565 # 1. L1: rawsize, stored in revlog index
2563 2566 # 2. L2: len(rawtext), stored in revlog data
2564 2567 # 3. L3: len(text), stored in revlog data if flags==0, or
2565 2568 # possibly somewhere else if flags!=0
2566 2569 #
2567 2570 # L1 should be equal to L2. L3 could be different from them.
2568 2571 # "text" may or may not affect commit hash depending on flag
2569 2572 # processors (see revlog.addflagprocessor).
2570 2573 #
2571 2574 # | common | rename | meta | ext
2572 2575 # -------------------------------------------------
2573 2576 # rawsize() | L1 | L1 | L1 | L1
2574 2577 # size() | L1 | L2-LM | L1(*) | L1 (?)
2575 2578 # len(rawtext) | L2 | L2 | L2 | L2
2576 2579 # len(text) | L2 | L2 | L2 | L3
2577 2580 # len(read()) | L2 | L2-LM | L2-LM | L3 (?)
2578 2581 #
2579 2582 # LM: length of metadata, depending on rawtext
2580 2583 # (*): not ideal, see comment in filelog.size
2581 2584 # (?): could be "- len(meta)" if the resolved content has
2582 2585 # rename metadata
2583 2586 #
2584 2587 # Checks needed to be done:
2585 2588 # 1. length check: L1 == L2, in all cases.
2586 2589 # 2. hash check: depending on flag processor, we may need to
2587 2590 # use either "text" (external), or "rawtext" (in revlog).
2588 2591
2589 2592 try:
2590 2593 skipflags = state.get('skipflags', 0)
2591 2594 if skipflags:
2592 2595 skipflags &= self.flags(rev)
2593 2596
2594 2597 if skipflags:
2595 2598 state['skipread'].add(node)
2596 2599 else:
2597 2600 # Side-effect: read content and verify hash.
2598 2601 self.revision(node)
2599 2602
2600 2603 l1 = self.rawsize(rev)
2601 2604 l2 = len(self.revision(node, raw=True))
2602 2605
2603 2606 if l1 != l2:
2604 2607 yield revlogproblem(
2605 2608 error=_('unpacked size is %d, %d expected') % (l2, l1),
2606 2609 node=node)
2607 2610
2608 2611 except error.CensoredNodeError:
2609 2612 if state['erroroncensored']:
2610 2613 yield revlogproblem(error=_('censored file data'),
2611 2614 node=node)
2612 2615 state['skipread'].add(node)
2613 2616 except Exception as e:
2614 2617 yield revlogproblem(
2615 2618 error=_('unpacking %s: %s') % (short(node),
2616 2619 stringutil.forcebytestr(e)),
2617 2620 node=node)
2618 2621 state['skipread'].add(node)
2619 2622
2620 2623 def storageinfo(self, exclusivefiles=False, sharedfiles=False,
2621 2624 revisionscount=False, trackedsize=False,
2622 2625 storedsize=False):
2623 2626 d = {}
2624 2627
2625 2628 if exclusivefiles:
2626 2629 d['exclusivefiles'] = [(self.opener, self.indexfile)]
2627 2630 if not self._inline:
2628 2631 d['exclusivefiles'].append((self.opener, self.datafile))
2629 2632
2630 2633 if sharedfiles:
2631 2634 d['sharedfiles'] = []
2632 2635
2633 2636 if revisionscount:
2634 2637 d['revisionscount'] = len(self)
2635 2638
2636 2639 if trackedsize:
2637 2640 d['trackedsize'] = sum(map(self.rawsize, iter(self)))
2638 2641
2639 2642 if storedsize:
2640 2643 d['storedsize'] = sum(self.opener.stat(path).st_size
2641 2644 for path in self.files())
2642 2645
2643 2646 return d
@@ -1,355 +1,356
1 1 # setdiscovery.py - improved discovery of common nodeset for mercurial
2 2 #
3 3 # Copyright 2010 Benoit Boissinot <bboissin@gmail.com>
4 4 # and Peter Arrenbrecht <peter@arrenbrecht.ch>
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 Algorithm works in the following way. You have two repository: local and
10 10 remote. They both contains a DAG of changelists.
11 11
12 12 The goal of the discovery protocol is to find one set of node *common*,
13 13 the set of nodes shared by local and remote.
14 14
15 15 One of the issue with the original protocol was latency, it could
16 16 potentially require lots of roundtrips to discover that the local repo was a
17 17 subset of remote (which is a very common case, you usually have few changes
18 18 compared to upstream, while upstream probably had lots of development).
19 19
20 20 The new protocol only requires one interface for the remote repo: `known()`,
21 21 which given a set of changelists tells you if they are present in the DAG.
22 22
23 23 The algorithm then works as follow:
24 24
25 25 - We will be using three sets, `common`, `missing`, `unknown`. Originally
26 26 all nodes are in `unknown`.
27 27 - Take a sample from `unknown`, call `remote.known(sample)`
28 28 - For each node that remote knows, move it and all its ancestors to `common`
29 29 - For each node that remote doesn't know, move it and all its descendants
30 30 to `missing`
31 31 - Iterate until `unknown` is empty
32 32
33 33 There are a couple optimizations, first is instead of starting with a random
34 34 sample of missing, start by sending all heads, in the case where the local
35 35 repo is a subset, you computed the answer in one round trip.
36 36
37 37 Then you can do something similar to the bisecting strategy used when
38 38 finding faulty changesets. Instead of random samples, you can try picking
39 39 nodes that will maximize the number of nodes that will be
40 40 classified with it (since all ancestors or descendants will be marked as well).
41 41 """
42 42
43 43 from __future__ import absolute_import
44 44
45 45 import collections
46 46 import random
47 47
48 48 from .i18n import _
49 49 from .node import (
50 50 nullid,
51 51 nullrev,
52 52 )
53 53 from . import (
54 54 error,
55 55 util,
56 56 )
57 57
58 58 def _updatesample(revs, heads, sample, parentfn, quicksamplesize=0):
59 59 """update an existing sample to match the expected size
60 60
61 61 The sample is updated with revs exponentially distant from each head of the
62 62 <revs> set. (H~1, H~2, H~4, H~8, etc).
63 63
64 64 If a target size is specified, the sampling will stop once this size is
65 65 reached. Otherwise sampling will happen until roots of the <revs> set are
66 66 reached.
67 67
68 68 :revs: set of revs we want to discover (if None, assume the whole dag)
69 69 :heads: set of DAG head revs
70 70 :sample: a sample to update
71 71 :parentfn: a callable to resolve parents for a revision
72 72 :quicksamplesize: optional target size of the sample"""
73 73 dist = {}
74 74 visit = collections.deque(heads)
75 75 seen = set()
76 76 factor = 1
77 77 while visit:
78 78 curr = visit.popleft()
79 79 if curr in seen:
80 80 continue
81 81 d = dist.setdefault(curr, 1)
82 82 if d > factor:
83 83 factor *= 2
84 84 if d == factor:
85 85 sample.add(curr)
86 86 if quicksamplesize and (len(sample) >= quicksamplesize):
87 87 return
88 88 seen.add(curr)
89 89
90 90 for p in parentfn(curr):
91 91 if p != nullrev and (not revs or p in revs):
92 92 dist.setdefault(p, d + 1)
93 93 visit.append(p)
94 94
95 95 def _takequicksample(repo, headrevs, revs, size):
96 96 """takes a quick sample of size <size>
97 97
98 98 It is meant for initial sampling and focuses on querying heads and close
99 99 ancestors of heads.
100 100
101 101 :dag: a dag object
102 102 :headrevs: set of head revisions in local DAG to consider
103 103 :revs: set of revs to discover
104 104 :size: the maximum size of the sample"""
105 105 if len(revs) <= size:
106 106 return list(revs)
107 107 sample = set(repo.revs('heads(%ld)', revs))
108 108
109 109 if len(sample) >= size:
110 110 return _limitsample(sample, size)
111 111
112 112 _updatesample(None, headrevs, sample, repo.changelog.parentrevs,
113 113 quicksamplesize=size)
114 114 return sample
115 115
116 116 def _takefullsample(repo, headrevs, revs, size):
117 117 if len(revs) <= size:
118 118 return list(revs)
119 119 sample = set(repo.revs('heads(%ld)', revs))
120 120
121 121 # update from heads
122 122 revsheads = set(repo.revs('heads(%ld)', revs))
123 123 _updatesample(revs, revsheads, sample, repo.changelog.parentrevs)
124 124
125 125 # update from roots
126 126 revsroots = set(repo.revs('roots(%ld)', revs))
127 127
128 128 # _updatesample() essentially does interaction over revisions to look up
129 129 # their children. This lookup is expensive and doing it in a loop is
130 130 # quadratic. We precompute the children for all relevant revisions and
131 131 # make the lookup in _updatesample() a simple dict lookup.
132 132 #
133 133 # Because this function can be called multiple times during discovery, we
134 134 # may still perform redundant work and there is room to optimize this by
135 135 # keeping a persistent cache of children across invocations.
136 136 children = {}
137 137
138 138 parentrevs = repo.changelog.parentrevs
139 139 for rev in repo.changelog.revs(start=min(revsroots)):
140 140 # Always ensure revision has an entry so we don't need to worry about
141 141 # missing keys.
142 142 children.setdefault(rev, [])
143 143
144 144 for prev in parentrevs(rev):
145 145 if prev == nullrev:
146 146 continue
147 147
148 148 children.setdefault(prev, []).append(rev)
149 149
150 150 _updatesample(revs, revsroots, sample, children.__getitem__)
151 151 assert sample
152 152 sample = _limitsample(sample, size)
153 153 if len(sample) < size:
154 154 more = size - len(sample)
155 155 sample.update(random.sample(list(revs - sample), more))
156 156 return sample
157 157
158 158 def _limitsample(sample, desiredlen):
159 159 """return a random subset of sample of at most desiredlen item"""
160 160 if len(sample) > desiredlen:
161 161 sample = set(random.sample(sample, desiredlen))
162 162 return sample
163 163
164 164 class partialdiscovery(object):
165 165 """an object representing ongoing discovery
166 166
167 167 Feed with data from the remote repository, this object keep track of the
168 168 current set of changeset in various states:
169 169
170 170 - common: revs also known remotely
171 171 - undecided: revs we don't have information on yet
172 172 - missing: revs missing remotely
173 173 (all tracked revisions are known locally)
174 174 """
175 175
176 176 def __init__(self, repo, targetheads):
177 177 self._repo = repo
178 178 self._targetheads = targetheads
179 179 self._common = repo.changelog.incrementalmissingrevs()
180 180 self._undecided = None
181 181 self.missing = set()
182 182
183 183 def addcommons(self, commons):
184 184 """registrer nodes known as common"""
185 185 self._common.addbases(commons)
186 self._common.removeancestorsfrom(self.undecided)
186 if self._undecided is not None:
187 self._common.removeancestorsfrom(self._undecided)
187 188
188 189 def addmissings(self, missings):
189 190 """registrer some nodes as missing"""
190 191 newmissing = self._repo.revs('%ld::%ld', missings, self.undecided)
191 192 if newmissing:
192 193 self.missing.update(newmissing)
193 194 self.undecided.difference_update(newmissing)
194 195
195 196 def addinfo(self, sample):
196 197 """consume an iterable of (rev, known) tuples"""
197 198 common = set()
198 199 missing = set()
199 200 for rev, known in sample:
200 201 if known:
201 202 common.add(rev)
202 203 else:
203 204 missing.add(rev)
204 205 if common:
205 206 self.addcommons(common)
206 207 if missing:
207 208 self.addmissings(missing)
208 209
209 210 def hasinfo(self):
210 211 """return True is we have any clue about the remote state"""
211 212 return self._common.hasbases()
212 213
213 214 def iscomplete(self):
214 215 """True if all the necessary data have been gathered"""
215 216 return self._undecided is not None and not self._undecided
216 217
217 218 @property
218 219 def undecided(self):
219 220 if self._undecided is not None:
220 221 return self._undecided
221 222 self._undecided = set(self._common.missingancestors(self._targetheads))
222 223 return self._undecided
223 224
224 225 def commonheads(self):
225 226 """the heads of the known common set"""
226 227 # heads(common) == heads(common.bases) since common represents
227 228 # common.bases and all its ancestors
228 229 return self._common.basesheads()
229 230
230 231 def findcommonheads(ui, local, remote,
231 232 initialsamplesize=100,
232 233 fullsamplesize=200,
233 234 abortwhenunrelated=True,
234 235 ancestorsof=None):
235 236 '''Return a tuple (common, anyincoming, remoteheads) used to identify
236 237 missing nodes from or in remote.
237 238 '''
238 239 start = util.timer()
239 240
240 241 roundtrips = 0
241 242 cl = local.changelog
242 243 clnode = cl.node
243 244 clrev = cl.rev
244 245
245 246 if ancestorsof is not None:
246 247 ownheads = [clrev(n) for n in ancestorsof]
247 248 else:
248 249 ownheads = [rev for rev in cl.headrevs() if rev != nullrev]
249 250
250 251 # early exit if we know all the specified remote heads already
251 252 ui.debug("query 1; heads\n")
252 253 roundtrips += 1
253 254 sample = _limitsample(ownheads, initialsamplesize)
254 255 # indices between sample and externalized version must match
255 256 sample = list(sample)
256 257
257 258 with remote.commandexecutor() as e:
258 259 fheads = e.callcommand('heads', {})
259 260 fknown = e.callcommand('known', {
260 261 'nodes': [clnode(r) for r in sample],
261 262 })
262 263
263 264 srvheadhashes, yesno = fheads.result(), fknown.result()
264 265
265 266 if cl.tip() == nullid:
266 267 if srvheadhashes != [nullid]:
267 268 return [nullid], True, srvheadhashes
268 269 return [nullid], False, []
269 270
270 271 # start actual discovery (we note this before the next "if" for
271 272 # compatibility reasons)
272 273 ui.status(_("searching for changes\n"))
273 274
274 275 srvheads = []
275 276 for node in srvheadhashes:
276 277 if node == nullid:
277 278 continue
278 279
279 280 try:
280 281 srvheads.append(clrev(node))
281 282 # Catches unknown and filtered nodes.
282 283 except error.LookupError:
283 284 continue
284 285
285 286 if len(srvheads) == len(srvheadhashes):
286 287 ui.debug("all remote heads known locally\n")
287 288 return srvheadhashes, False, srvheadhashes
288 289
289 290 if len(sample) == len(ownheads) and all(yesno):
290 291 ui.note(_("all local heads known remotely\n"))
291 292 ownheadhashes = [clnode(r) for r in ownheads]
292 293 return ownheadhashes, True, srvheadhashes
293 294
294 295 # full blown discovery
295 296
296 297 disco = partialdiscovery(local, ownheads)
297 298 # treat remote heads (and maybe own heads) as a first implicit sample
298 299 # response
299 300 disco.addcommons(srvheads)
300 301 disco.addinfo(zip(sample, yesno))
301 302
302 303 full = False
303 304 progress = ui.makeprogress(_('searching'), unit=_('queries'))
304 305 while not disco.iscomplete():
305 306
306 307 if full or disco.hasinfo():
307 308 if full:
308 309 ui.note(_("sampling from both directions\n"))
309 310 else:
310 311 ui.debug("taking initial sample\n")
311 312 samplefunc = _takefullsample
312 313 targetsize = fullsamplesize
313 314 else:
314 315 # use even cheaper initial sample
315 316 ui.debug("taking quick initial sample\n")
316 317 samplefunc = _takequicksample
317 318 targetsize = initialsamplesize
318 319 sample = samplefunc(local, ownheads, disco.undecided, targetsize)
319 320
320 321 roundtrips += 1
321 322 progress.update(roundtrips)
322 323 ui.debug("query %i; still undecided: %i, sample size is: %i\n"
323 324 % (roundtrips, len(disco.undecided), len(sample)))
324 325 # indices between sample and externalized version must match
325 326 sample = list(sample)
326 327
327 328 with remote.commandexecutor() as e:
328 329 yesno = e.callcommand('known', {
329 330 'nodes': [clnode(r) for r in sample],
330 331 }).result()
331 332
332 333 full = True
333 334
334 335 disco.addinfo(zip(sample, yesno))
335 336
336 337 result = disco.commonheads()
337 338 elapsed = util.timer() - start
338 339 progress.complete()
339 340 ui.debug("%d total queries in %.4fs\n" % (roundtrips, elapsed))
340 341 msg = ('found %d common and %d unknown server heads,'
341 342 ' %d roundtrips in %.4fs\n')
342 343 missing = set(result) - set(srvheads)
343 344 ui.log('discovery', msg, len(result), len(missing), roundtrips,
344 345 elapsed)
345 346
346 347 if not result and srvheadhashes != [nullid]:
347 348 if abortwhenunrelated:
348 349 raise error.Abort(_("repository is unrelated"))
349 350 else:
350 351 ui.warn(_("warning: repository is unrelated\n"))
351 352 return ({nullid}, True, srvheadhashes,)
352 353
353 354 anyincoming = (srvheadhashes != [nullid])
354 355 result = {clnode(r) for r in result}
355 356 return result, anyincoming, srvheadhashes
@@ -1,2077 +1,2110
1 1 # ui.py - user interface bits for mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@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 from __future__ import absolute_import
9 9
10 10 import collections
11 11 import contextlib
12 12 import errno
13 13 import getpass
14 14 import inspect
15 15 import os
16 16 import re
17 17 import signal
18 18 import socket
19 19 import subprocess
20 20 import sys
21 21 import traceback
22 22
23 23 from .i18n import _
24 24 from .node import hex
25 25
26 26 from . import (
27 27 color,
28 28 config,
29 29 configitems,
30 30 encoding,
31 31 error,
32 32 formatter,
33 33 loggingutil,
34 34 progress,
35 35 pycompat,
36 36 rcutil,
37 37 scmutil,
38 38 util,
39 39 )
40 40 from .utils import (
41 41 dateutil,
42 42 procutil,
43 43 stringutil,
44 44 )
45 45
46 46 urlreq = util.urlreq
47 47
48 48 # for use with str.translate(None, _keepalnum), to keep just alphanumerics
49 49 _keepalnum = ''.join(c for c in map(pycompat.bytechr, range(256))
50 50 if not c.isalnum())
51 51
52 52 # The config knobs that will be altered (if unset) by ui.tweakdefaults.
53 53 tweakrc = b"""
54 54 [ui]
55 55 # The rollback command is dangerous. As a rule, don't use it.
56 56 rollback = False
57 57 # Make `hg status` report copy information
58 58 statuscopies = yes
59 59 # Prefer curses UIs when available. Revert to plain-text with `text`.
60 60 interface = curses
61 61
62 62 [commands]
63 63 # Grep working directory by default.
64 64 grep.all-files = True
65 65 # Make `hg status` emit cwd-relative paths by default.
66 66 status.relative = yes
67 67 # Refuse to perform an `hg update` that would cause a file content merge
68 68 update.check = noconflict
69 69 # Show conflicts information in `hg status`
70 70 status.verbose = True
71 71
72 72 [diff]
73 73 git = 1
74 74 showfunc = 1
75 75 word-diff = 1
76 76 """
77 77
78 78 samplehgrcs = {
79 79 'user':
80 80 b"""# example user config (see 'hg help config' for more info)
81 81 [ui]
82 82 # name and email, e.g.
83 83 # username = Jane Doe <jdoe@example.com>
84 84 username =
85 85
86 86 # We recommend enabling tweakdefaults to get slight improvements to
87 87 # the UI over time. Make sure to set HGPLAIN in the environment when
88 88 # writing scripts!
89 89 # tweakdefaults = True
90 90
91 91 # uncomment to disable color in command output
92 92 # (see 'hg help color' for details)
93 93 # color = never
94 94
95 95 # uncomment to disable command output pagination
96 96 # (see 'hg help pager' for details)
97 97 # paginate = never
98 98
99 99 [extensions]
100 100 # uncomment these lines to enable some popular extensions
101 101 # (see 'hg help extensions' for more info)
102 102 #
103 103 # churn =
104 104 """,
105 105
106 106 'cloned':
107 107 b"""# example repository config (see 'hg help config' for more info)
108 108 [paths]
109 109 default = %s
110 110
111 111 # path aliases to other clones of this repo in URLs or filesystem paths
112 112 # (see 'hg help config.paths' for more info)
113 113 #
114 114 # default:pushurl = ssh://jdoe@example.net/hg/jdoes-fork
115 115 # my-fork = ssh://jdoe@example.net/hg/jdoes-fork
116 116 # my-clone = /home/jdoe/jdoes-clone
117 117
118 118 [ui]
119 119 # name and email (local to this repository, optional), e.g.
120 120 # username = Jane Doe <jdoe@example.com>
121 121 """,
122 122
123 123 'local':
124 124 b"""# example repository config (see 'hg help config' for more info)
125 125 [paths]
126 126 # path aliases to other clones of this repo in URLs or filesystem paths
127 127 # (see 'hg help config.paths' for more info)
128 128 #
129 129 # default = http://example.com/hg/example-repo
130 130 # default:pushurl = ssh://jdoe@example.net/hg/jdoes-fork
131 131 # my-fork = ssh://jdoe@example.net/hg/jdoes-fork
132 132 # my-clone = /home/jdoe/jdoes-clone
133 133
134 134 [ui]
135 135 # name and email (local to this repository, optional), e.g.
136 136 # username = Jane Doe <jdoe@example.com>
137 137 """,
138 138
139 139 'global':
140 140 b"""# example system-wide hg config (see 'hg help config' for more info)
141 141
142 142 [ui]
143 143 # uncomment to disable color in command output
144 144 # (see 'hg help color' for details)
145 145 # color = never
146 146
147 147 # uncomment to disable command output pagination
148 148 # (see 'hg help pager' for details)
149 149 # paginate = never
150 150
151 151 [extensions]
152 152 # uncomment these lines to enable some popular extensions
153 153 # (see 'hg help extensions' for more info)
154 154 #
155 155 # blackbox =
156 156 # churn =
157 157 """,
158 158 }
159 159
160 160 def _maybestrurl(maybebytes):
161 161 return pycompat.rapply(pycompat.strurl, maybebytes)
162 162
163 163 def _maybebytesurl(maybestr):
164 164 return pycompat.rapply(pycompat.bytesurl, maybestr)
165 165
166 166 class httppasswordmgrdbproxy(object):
167 167 """Delays loading urllib2 until it's needed."""
168 168 def __init__(self):
169 169 self._mgr = None
170 170
171 171 def _get_mgr(self):
172 172 if self._mgr is None:
173 173 self._mgr = urlreq.httppasswordmgrwithdefaultrealm()
174 174 return self._mgr
175 175
176 176 def add_password(self, realm, uris, user, passwd):
177 177 return self._get_mgr().add_password(
178 178 _maybestrurl(realm), _maybestrurl(uris),
179 179 _maybestrurl(user), _maybestrurl(passwd))
180 180
181 181 def find_user_password(self, realm, uri):
182 182 mgr = self._get_mgr()
183 183 return _maybebytesurl(mgr.find_user_password(_maybestrurl(realm),
184 184 _maybestrurl(uri)))
185 185
186 186 def _catchterm(*args):
187 187 raise error.SignalInterrupt
188 188
189 189 # unique object used to detect no default value has been provided when
190 190 # retrieving configuration value.
191 191 _unset = object()
192 192
193 193 # _reqexithandlers: callbacks run at the end of a request
194 194 _reqexithandlers = []
195 195
196 196 class ui(object):
197 197 def __init__(self, src=None):
198 198 """Create a fresh new ui object if no src given
199 199
200 200 Use uimod.ui.load() to create a ui which knows global and user configs.
201 201 In most cases, you should use ui.copy() to create a copy of an existing
202 202 ui object.
203 203 """
204 204 # _buffers: used for temporary capture of output
205 205 self._buffers = []
206 206 # 3-tuple describing how each buffer in the stack behaves.
207 207 # Values are (capture stderr, capture subprocesses, apply labels).
208 208 self._bufferstates = []
209 209 # When a buffer is active, defines whether we are expanding labels.
210 210 # This exists to prevent an extra list lookup.
211 211 self._bufferapplylabels = None
212 212 self.quiet = self.verbose = self.debugflag = self.tracebackflag = False
213 213 self._reportuntrusted = True
214 214 self._knownconfig = configitems.coreitems
215 215 self._ocfg = config.config() # overlay
216 216 self._tcfg = config.config() # trusted
217 217 self._ucfg = config.config() # untrusted
218 218 self._trustusers = set()
219 219 self._trustgroups = set()
220 220 self.callhooks = True
221 221 # Insecure server connections requested.
222 222 self.insecureconnections = False
223 223 # Blocked time
224 224 self.logblockedtimes = False
225 225 # color mode: see mercurial/color.py for possible value
226 226 self._colormode = None
227 227 self._terminfoparams = {}
228 228 self._styles = {}
229 229 self._uninterruptible = False
230 230
231 231 if src:
232 232 self._fout = src._fout
233 233 self._ferr = src._ferr
234 234 self._fin = src._fin
235 235 self._fmsg = src._fmsg
236 236 self._fmsgout = src._fmsgout
237 237 self._fmsgerr = src._fmsgerr
238 238 self._finoutredirected = src._finoutredirected
239 239 self._loggers = src._loggers.copy()
240 240 self.pageractive = src.pageractive
241 241 self._disablepager = src._disablepager
242 242 self._tweaked = src._tweaked
243 243
244 244 self._tcfg = src._tcfg.copy()
245 245 self._ucfg = src._ucfg.copy()
246 246 self._ocfg = src._ocfg.copy()
247 247 self._trustusers = src._trustusers.copy()
248 248 self._trustgroups = src._trustgroups.copy()
249 249 self.environ = src.environ
250 250 self.callhooks = src.callhooks
251 251 self.insecureconnections = src.insecureconnections
252 252 self._colormode = src._colormode
253 253 self._terminfoparams = src._terminfoparams.copy()
254 254 self._styles = src._styles.copy()
255 255
256 256 self.fixconfig()
257 257
258 258 self.httppasswordmgrdb = src.httppasswordmgrdb
259 259 self._blockedtimes = src._blockedtimes
260 260 else:
261 261 self._fout = procutil.stdout
262 262 self._ferr = procutil.stderr
263 263 self._fin = procutil.stdin
264 264 self._fmsg = None
265 265 self._fmsgout = self.fout # configurable
266 266 self._fmsgerr = self.ferr # configurable
267 267 self._finoutredirected = False
268 268 self._loggers = {}
269 269 self.pageractive = False
270 270 self._disablepager = False
271 271 self._tweaked = False
272 272
273 273 # shared read-only environment
274 274 self.environ = encoding.environ
275 275
276 276 self.httppasswordmgrdb = httppasswordmgrdbproxy()
277 277 self._blockedtimes = collections.defaultdict(int)
278 278
279 279 allowed = self.configlist('experimental', 'exportableenviron')
280 280 if '*' in allowed:
281 281 self._exportableenviron = self.environ
282 282 else:
283 283 self._exportableenviron = {}
284 284 for k in allowed:
285 285 if k in self.environ:
286 286 self._exportableenviron[k] = self.environ[k]
287 287
288 288 @classmethod
289 289 def load(cls):
290 290 """Create a ui and load global and user configs"""
291 291 u = cls()
292 292 # we always trust global config files and environment variables
293 293 for t, f in rcutil.rccomponents():
294 294 if t == 'path':
295 295 u.readconfig(f, trust=True)
296 296 elif t == 'items':
297 297 sections = set()
298 298 for section, name, value, source in f:
299 299 # do not set u._ocfg
300 300 # XXX clean this up once immutable config object is a thing
301 301 u._tcfg.set(section, name, value, source)
302 302 u._ucfg.set(section, name, value, source)
303 303 sections.add(section)
304 304 for section in sections:
305 305 u.fixconfig(section=section)
306 306 else:
307 307 raise error.ProgrammingError('unknown rctype: %s' % t)
308 308 u._maybetweakdefaults()
309 309 return u
310 310
311 311 def _maybetweakdefaults(self):
312 312 if not self.configbool('ui', 'tweakdefaults'):
313 313 return
314 314 if self._tweaked or self.plain('tweakdefaults'):
315 315 return
316 316
317 317 # Note: it is SUPER IMPORTANT that you set self._tweaked to
318 318 # True *before* any calls to setconfig(), otherwise you'll get
319 319 # infinite recursion between setconfig and this method.
320 320 #
321 321 # TODO: We should extract an inner method in setconfig() to
322 322 # avoid this weirdness.
323 323 self._tweaked = True
324 324 tmpcfg = config.config()
325 325 tmpcfg.parse('<tweakdefaults>', tweakrc)
326 326 for section in tmpcfg:
327 327 for name, value in tmpcfg.items(section):
328 328 if not self.hasconfig(section, name):
329 329 self.setconfig(section, name, value, "<tweakdefaults>")
330 330
331 331 def copy(self):
332 332 return self.__class__(self)
333 333
334 334 def resetstate(self):
335 335 """Clear internal state that shouldn't persist across commands"""
336 336 if self._progbar:
337 337 self._progbar.resetstate() # reset last-print time of progress bar
338 338 self.httppasswordmgrdb = httppasswordmgrdbproxy()
339 339
340 340 @contextlib.contextmanager
341 341 def timeblockedsection(self, key):
342 342 # this is open-coded below - search for timeblockedsection to find them
343 343 starttime = util.timer()
344 344 try:
345 345 yield
346 346 finally:
347 347 self._blockedtimes[key + '_blocked'] += \
348 348 (util.timer() - starttime) * 1000
349 349
350 350 @contextlib.contextmanager
351 351 def uninterruptible(self):
352 352 """Mark an operation as unsafe.
353 353
354 354 Most operations on a repository are safe to interrupt, but a
355 355 few are risky (for example repair.strip). This context manager
356 356 lets you advise Mercurial that something risky is happening so
357 357 that control-C etc can be blocked if desired.
358 358 """
359 359 enabled = self.configbool('experimental', 'nointerrupt')
360 360 if (enabled and
361 361 self.configbool('experimental', 'nointerrupt-interactiveonly')):
362 362 enabled = self.interactive()
363 363 if self._uninterruptible or not enabled:
364 364 # if nointerrupt support is turned off, the process isn't
365 365 # interactive, or we're already in an uninterruptible
366 366 # block, do nothing.
367 367 yield
368 368 return
369 369 def warn():
370 370 self.warn(_("shutting down cleanly\n"))
371 371 self.warn(
372 372 _("press ^C again to terminate immediately (dangerous)\n"))
373 373 return True
374 374 with procutil.uninterruptible(warn):
375 375 try:
376 376 self._uninterruptible = True
377 377 yield
378 378 finally:
379 379 self._uninterruptible = False
380 380
381 381 def formatter(self, topic, opts):
382 382 return formatter.formatter(self, self, topic, opts)
383 383
384 384 def _trusted(self, fp, f):
385 385 st = util.fstat(fp)
386 386 if util.isowner(st):
387 387 return True
388 388
389 389 tusers, tgroups = self._trustusers, self._trustgroups
390 390 if '*' in tusers or '*' in tgroups:
391 391 return True
392 392
393 393 user = util.username(st.st_uid)
394 394 group = util.groupname(st.st_gid)
395 395 if user in tusers or group in tgroups or user == util.username():
396 396 return True
397 397
398 398 if self._reportuntrusted:
399 399 self.warn(_('not trusting file %s from untrusted '
400 400 'user %s, group %s\n') % (f, user, group))
401 401 return False
402 402
403 403 def readconfig(self, filename, root=None, trust=False,
404 404 sections=None, remap=None):
405 405 try:
406 406 fp = open(filename, r'rb')
407 407 except IOError:
408 408 if not sections: # ignore unless we were looking for something
409 409 return
410 410 raise
411 411
412 412 cfg = config.config()
413 413 trusted = sections or trust or self._trusted(fp, filename)
414 414
415 415 try:
416 416 cfg.read(filename, fp, sections=sections, remap=remap)
417 417 fp.close()
418 418 except error.ConfigError as inst:
419 419 if trusted:
420 420 raise
421 421 self.warn(_("ignored: %s\n") % stringutil.forcebytestr(inst))
422 422
423 423 if self.plain():
424 424 for k in ('debug', 'fallbackencoding', 'quiet', 'slash',
425 425 'logtemplate', 'message-output', 'statuscopies', 'style',
426 426 'traceback', 'verbose'):
427 427 if k in cfg['ui']:
428 428 del cfg['ui'][k]
429 429 for k, v in cfg.items('defaults'):
430 430 del cfg['defaults'][k]
431 431 for k, v in cfg.items('commands'):
432 432 del cfg['commands'][k]
433 433 # Don't remove aliases from the configuration if in the exceptionlist
434 434 if self.plain('alias'):
435 435 for k, v in cfg.items('alias'):
436 436 del cfg['alias'][k]
437 437 if self.plain('revsetalias'):
438 438 for k, v in cfg.items('revsetalias'):
439 439 del cfg['revsetalias'][k]
440 440 if self.plain('templatealias'):
441 441 for k, v in cfg.items('templatealias'):
442 442 del cfg['templatealias'][k]
443 443
444 444 if trusted:
445 445 self._tcfg.update(cfg)
446 446 self._tcfg.update(self._ocfg)
447 447 self._ucfg.update(cfg)
448 448 self._ucfg.update(self._ocfg)
449 449
450 450 if root is None:
451 451 root = os.path.expanduser('~')
452 452 self.fixconfig(root=root)
453 453
454 454 def fixconfig(self, root=None, section=None):
455 455 if section in (None, 'paths'):
456 456 # expand vars and ~
457 457 # translate paths relative to root (or home) into absolute paths
458 458 root = root or encoding.getcwd()
459 459 for c in self._tcfg, self._ucfg, self._ocfg:
460 460 for n, p in c.items('paths'):
461 461 # Ignore sub-options.
462 462 if ':' in n:
463 463 continue
464 464 if not p:
465 465 continue
466 466 if '%%' in p:
467 467 s = self.configsource('paths', n) or 'none'
468 468 self.warn(_("(deprecated '%%' in path %s=%s from %s)\n")
469 469 % (n, p, s))
470 470 p = p.replace('%%', '%')
471 471 p = util.expandpath(p)
472 472 if not util.hasscheme(p) and not os.path.isabs(p):
473 473 p = os.path.normpath(os.path.join(root, p))
474 474 c.set("paths", n, p)
475 475
476 476 if section in (None, 'ui'):
477 477 # update ui options
478 478 self._fmsgout, self._fmsgerr = _selectmsgdests(self)
479 479 self.debugflag = self.configbool('ui', 'debug')
480 480 self.verbose = self.debugflag or self.configbool('ui', 'verbose')
481 481 self.quiet = not self.debugflag and self.configbool('ui', 'quiet')
482 482 if self.verbose and self.quiet:
483 483 self.quiet = self.verbose = False
484 484 self._reportuntrusted = self.debugflag or self.configbool("ui",
485 485 "report_untrusted")
486 486 self.tracebackflag = self.configbool('ui', 'traceback')
487 487 self.logblockedtimes = self.configbool('ui', 'logblockedtimes')
488 488
489 489 if section in (None, 'trusted'):
490 490 # update trust information
491 491 self._trustusers.update(self.configlist('trusted', 'users'))
492 492 self._trustgroups.update(self.configlist('trusted', 'groups'))
493 493
494 494 if section in (None, b'devel', b'ui') and self.debugflag:
495 495 tracked = set()
496 496 if self.configbool(b'devel', b'debug.extensions'):
497 497 tracked.add(b'extension')
498 498 if tracked:
499 499 logger = loggingutil.fileobjectlogger(self._ferr, tracked)
500 500 self.setlogger(b'debug', logger)
501 501
502 502 def backupconfig(self, section, item):
503 503 return (self._ocfg.backup(section, item),
504 504 self._tcfg.backup(section, item),
505 505 self._ucfg.backup(section, item),)
506 506 def restoreconfig(self, data):
507 507 self._ocfg.restore(data[0])
508 508 self._tcfg.restore(data[1])
509 509 self._ucfg.restore(data[2])
510 510
511 511 def setconfig(self, section, name, value, source=''):
512 512 for cfg in (self._ocfg, self._tcfg, self._ucfg):
513 513 cfg.set(section, name, value, source)
514 514 self.fixconfig(section=section)
515 515 self._maybetweakdefaults()
516 516
517 517 def _data(self, untrusted):
518 518 return untrusted and self._ucfg or self._tcfg
519 519
520 520 def configsource(self, section, name, untrusted=False):
521 521 return self._data(untrusted).source(section, name)
522 522
523 523 def config(self, section, name, default=_unset, untrusted=False):
524 524 """return the plain string version of a config"""
525 525 value = self._config(section, name, default=default,
526 526 untrusted=untrusted)
527 527 if value is _unset:
528 528 return None
529 529 return value
530 530
531 531 def _config(self, section, name, default=_unset, untrusted=False):
532 532 value = itemdefault = default
533 533 item = self._knownconfig.get(section, {}).get(name)
534 534 alternates = [(section, name)]
535 535
536 536 if item is not None:
537 537 alternates.extend(item.alias)
538 538 if callable(item.default):
539 539 itemdefault = item.default()
540 540 else:
541 541 itemdefault = item.default
542 542 else:
543 543 msg = ("accessing unregistered config item: '%s.%s'")
544 544 msg %= (section, name)
545 545 self.develwarn(msg, 2, 'warn-config-unknown')
546 546
547 547 if default is _unset:
548 548 if item is None:
549 549 value = default
550 550 elif item.default is configitems.dynamicdefault:
551 551 value = None
552 552 msg = "config item requires an explicit default value: '%s.%s'"
553 553 msg %= (section, name)
554 554 self.develwarn(msg, 2, 'warn-config-default')
555 555 else:
556 556 value = itemdefault
557 557 elif (item is not None
558 558 and item.default is not configitems.dynamicdefault
559 559 and default != itemdefault):
560 560 msg = ("specifying a mismatched default value for a registered "
561 561 "config item: '%s.%s' '%s'")
562 562 msg %= (section, name, pycompat.bytestr(default))
563 563 self.develwarn(msg, 2, 'warn-config-default')
564 564
565 565 for s, n in alternates:
566 566 candidate = self._data(untrusted).get(s, n, None)
567 567 if candidate is not None:
568 568 value = candidate
569 569 section = s
570 570 name = n
571 571 break
572 572
573 573 if self.debugflag and not untrusted and self._reportuntrusted:
574 574 for s, n in alternates:
575 575 uvalue = self._ucfg.get(s, n)
576 576 if uvalue is not None and uvalue != value:
577 577 self.debug("ignoring untrusted configuration option "
578 578 "%s.%s = %s\n" % (s, n, uvalue))
579 579 return value
580 580
581 581 def configsuboptions(self, section, name, default=_unset, untrusted=False):
582 582 """Get a config option and all sub-options.
583 583
584 584 Some config options have sub-options that are declared with the
585 585 format "key:opt = value". This method is used to return the main
586 586 option and all its declared sub-options.
587 587
588 588 Returns a 2-tuple of ``(option, sub-options)``, where `sub-options``
589 589 is a dict of defined sub-options where keys and values are strings.
590 590 """
591 591 main = self.config(section, name, default, untrusted=untrusted)
592 592 data = self._data(untrusted)
593 593 sub = {}
594 594 prefix = '%s:' % name
595 595 for k, v in data.items(section):
596 596 if k.startswith(prefix):
597 597 sub[k[len(prefix):]] = v
598 598
599 599 if self.debugflag and not untrusted and self._reportuntrusted:
600 600 for k, v in sub.items():
601 601 uvalue = self._ucfg.get(section, '%s:%s' % (name, k))
602 602 if uvalue is not None and uvalue != v:
603 603 self.debug('ignoring untrusted configuration option '
604 604 '%s:%s.%s = %s\n' % (section, name, k, uvalue))
605 605
606 606 return main, sub
607 607
608 608 def configpath(self, section, name, default=_unset, untrusted=False):
609 609 'get a path config item, expanded relative to repo root or config file'
610 610 v = self.config(section, name, default, untrusted)
611 611 if v is None:
612 612 return None
613 613 if not os.path.isabs(v) or "://" not in v:
614 614 src = self.configsource(section, name, untrusted)
615 615 if ':' in src:
616 616 base = os.path.dirname(src.rsplit(':')[0])
617 617 v = os.path.join(base, os.path.expanduser(v))
618 618 return v
619 619
620 620 def configbool(self, section, name, default=_unset, untrusted=False):
621 621 """parse a configuration element as a boolean
622 622
623 623 >>> u = ui(); s = b'foo'
624 624 >>> u.setconfig(s, b'true', b'yes')
625 625 >>> u.configbool(s, b'true')
626 626 True
627 627 >>> u.setconfig(s, b'false', b'no')
628 628 >>> u.configbool(s, b'false')
629 629 False
630 630 >>> u.configbool(s, b'unknown')
631 631 False
632 632 >>> u.configbool(s, b'unknown', True)
633 633 True
634 634 >>> u.setconfig(s, b'invalid', b'somevalue')
635 635 >>> u.configbool(s, b'invalid')
636 636 Traceback (most recent call last):
637 637 ...
638 638 ConfigError: foo.invalid is not a boolean ('somevalue')
639 639 """
640 640
641 641 v = self._config(section, name, default, untrusted=untrusted)
642 642 if v is None:
643 643 return v
644 644 if v is _unset:
645 645 if default is _unset:
646 646 return False
647 647 return default
648 648 if isinstance(v, bool):
649 649 return v
650 650 b = stringutil.parsebool(v)
651 651 if b is None:
652 652 raise error.ConfigError(_("%s.%s is not a boolean ('%s')")
653 653 % (section, name, v))
654 654 return b
655 655
656 656 def configwith(self, convert, section, name, default=_unset,
657 657 desc=None, untrusted=False):
658 658 """parse a configuration element with a conversion function
659 659
660 660 >>> u = ui(); s = b'foo'
661 661 >>> u.setconfig(s, b'float1', b'42')
662 662 >>> u.configwith(float, s, b'float1')
663 663 42.0
664 664 >>> u.setconfig(s, b'float2', b'-4.25')
665 665 >>> u.configwith(float, s, b'float2')
666 666 -4.25
667 667 >>> u.configwith(float, s, b'unknown', 7)
668 668 7.0
669 669 >>> u.setconfig(s, b'invalid', b'somevalue')
670 670 >>> u.configwith(float, s, b'invalid')
671 671 Traceback (most recent call last):
672 672 ...
673 673 ConfigError: foo.invalid is not a valid float ('somevalue')
674 674 >>> u.configwith(float, s, b'invalid', desc=b'womble')
675 675 Traceback (most recent call last):
676 676 ...
677 677 ConfigError: foo.invalid is not a valid womble ('somevalue')
678 678 """
679 679
680 680 v = self.config(section, name, default, untrusted)
681 681 if v is None:
682 682 return v # do not attempt to convert None
683 683 try:
684 684 return convert(v)
685 685 except (ValueError, error.ParseError):
686 686 if desc is None:
687 687 desc = pycompat.sysbytes(convert.__name__)
688 688 raise error.ConfigError(_("%s.%s is not a valid %s ('%s')")
689 689 % (section, name, desc, v))
690 690
691 691 def configint(self, section, name, default=_unset, untrusted=False):
692 692 """parse a configuration element as an integer
693 693
694 694 >>> u = ui(); s = b'foo'
695 695 >>> u.setconfig(s, b'int1', b'42')
696 696 >>> u.configint(s, b'int1')
697 697 42
698 698 >>> u.setconfig(s, b'int2', b'-42')
699 699 >>> u.configint(s, b'int2')
700 700 -42
701 701 >>> u.configint(s, b'unknown', 7)
702 702 7
703 703 >>> u.setconfig(s, b'invalid', b'somevalue')
704 704 >>> u.configint(s, b'invalid')
705 705 Traceback (most recent call last):
706 706 ...
707 707 ConfigError: foo.invalid is not a valid integer ('somevalue')
708 708 """
709 709
710 710 return self.configwith(int, section, name, default, 'integer',
711 711 untrusted)
712 712
713 713 def configbytes(self, section, name, default=_unset, untrusted=False):
714 714 """parse a configuration element as a quantity in bytes
715 715
716 716 Units can be specified as b (bytes), k or kb (kilobytes), m or
717 717 mb (megabytes), g or gb (gigabytes).
718 718
719 719 >>> u = ui(); s = b'foo'
720 720 >>> u.setconfig(s, b'val1', b'42')
721 721 >>> u.configbytes(s, b'val1')
722 722 42
723 723 >>> u.setconfig(s, b'val2', b'42.5 kb')
724 724 >>> u.configbytes(s, b'val2')
725 725 43520
726 726 >>> u.configbytes(s, b'unknown', b'7 MB')
727 727 7340032
728 728 >>> u.setconfig(s, b'invalid', b'somevalue')
729 729 >>> u.configbytes(s, b'invalid')
730 730 Traceback (most recent call last):
731 731 ...
732 732 ConfigError: foo.invalid is not a byte quantity ('somevalue')
733 733 """
734 734
735 735 value = self._config(section, name, default, untrusted)
736 736 if value is _unset:
737 737 if default is _unset:
738 738 default = 0
739 739 value = default
740 740 if not isinstance(value, bytes):
741 741 return value
742 742 try:
743 743 return util.sizetoint(value)
744 744 except error.ParseError:
745 745 raise error.ConfigError(_("%s.%s is not a byte quantity ('%s')")
746 746 % (section, name, value))
747 747
748 748 def configlist(self, section, name, default=_unset, untrusted=False):
749 749 """parse a configuration element as a list of comma/space separated
750 750 strings
751 751
752 752 >>> u = ui(); s = b'foo'
753 753 >>> u.setconfig(s, b'list1', b'this,is "a small" ,test')
754 754 >>> u.configlist(s, b'list1')
755 755 ['this', 'is', 'a small', 'test']
756 756 >>> u.setconfig(s, b'list2', b'this, is "a small" , test ')
757 757 >>> u.configlist(s, b'list2')
758 758 ['this', 'is', 'a small', 'test']
759 759 """
760 760 # default is not always a list
761 761 v = self.configwith(config.parselist, section, name, default,
762 762 'list', untrusted)
763 763 if isinstance(v, bytes):
764 764 return config.parselist(v)
765 765 elif v is None:
766 766 return []
767 767 return v
768 768
769 769 def configdate(self, section, name, default=_unset, untrusted=False):
770 770 """parse a configuration element as a tuple of ints
771 771
772 772 >>> u = ui(); s = b'foo'
773 773 >>> u.setconfig(s, b'date', b'0 0')
774 774 >>> u.configdate(s, b'date')
775 775 (0, 0)
776 776 """
777 777 if self.config(section, name, default, untrusted):
778 778 return self.configwith(dateutil.parsedate, section, name, default,
779 779 'date', untrusted)
780 780 if default is _unset:
781 781 return None
782 782 return default
783 783
784 784 def hasconfig(self, section, name, untrusted=False):
785 785 return self._data(untrusted).hasitem(section, name)
786 786
787 787 def has_section(self, section, untrusted=False):
788 788 '''tell whether section exists in config.'''
789 789 return section in self._data(untrusted)
790 790
791 791 def configitems(self, section, untrusted=False, ignoresub=False):
792 792 items = self._data(untrusted).items(section)
793 793 if ignoresub:
794 794 items = [i for i in items if ':' not in i[0]]
795 795 if self.debugflag and not untrusted and self._reportuntrusted:
796 796 for k, v in self._ucfg.items(section):
797 797 if self._tcfg.get(section, k) != v:
798 798 self.debug("ignoring untrusted configuration option "
799 799 "%s.%s = %s\n" % (section, k, v))
800 800 return items
801 801
802 802 def walkconfig(self, untrusted=False):
803 803 cfg = self._data(untrusted)
804 804 for section in cfg.sections():
805 805 for name, value in self.configitems(section, untrusted):
806 806 yield section, name, value
807 807
808 808 def plain(self, feature=None):
809 809 '''is plain mode active?
810 810
811 811 Plain mode means that all configuration variables which affect
812 812 the behavior and output of Mercurial should be
813 813 ignored. Additionally, the output should be stable,
814 814 reproducible and suitable for use in scripts or applications.
815 815
816 816 The only way to trigger plain mode is by setting either the
817 817 `HGPLAIN' or `HGPLAINEXCEPT' environment variables.
818 818
819 819 The return value can either be
820 820 - False if HGPLAIN is not set, or feature is in HGPLAINEXCEPT
821 821 - False if feature is disabled by default and not included in HGPLAIN
822 822 - True otherwise
823 823 '''
824 824 if ('HGPLAIN' not in encoding.environ and
825 825 'HGPLAINEXCEPT' not in encoding.environ):
826 826 return False
827 827 exceptions = encoding.environ.get('HGPLAINEXCEPT',
828 828 '').strip().split(',')
829 829 # TODO: add support for HGPLAIN=+feature,-feature syntax
830 830 if '+strictflags' not in encoding.environ.get('HGPLAIN', '').split(','):
831 831 exceptions.append('strictflags')
832 832 if feature and exceptions:
833 833 return feature not in exceptions
834 834 return True
835 835
836 836 def username(self, acceptempty=False):
837 837 """Return default username to be used in commits.
838 838
839 839 Searched in this order: $HGUSER, [ui] section of hgrcs, $EMAIL
840 840 and stop searching if one of these is set.
841 841 If not found and acceptempty is True, returns None.
842 842 If not found and ui.askusername is True, ask the user, else use
843 843 ($LOGNAME or $USER or $LNAME or $USERNAME) + "@full.hostname".
844 844 If no username could be found, raise an Abort error.
845 845 """
846 846 user = encoding.environ.get("HGUSER")
847 847 if user is None:
848 848 user = self.config("ui", "username")
849 849 if user is not None:
850 850 user = os.path.expandvars(user)
851 851 if user is None:
852 852 user = encoding.environ.get("EMAIL")
853 853 if user is None and acceptempty:
854 854 return user
855 855 if user is None and self.configbool("ui", "askusername"):
856 856 user = self.prompt(_("enter a commit username:"), default=None)
857 857 if user is None and not self.interactive():
858 858 try:
859 859 user = '%s@%s' % (procutil.getuser(),
860 860 encoding.strtolocal(socket.getfqdn()))
861 861 self.warn(_("no username found, using '%s' instead\n") % user)
862 862 except KeyError:
863 863 pass
864 864 if not user:
865 865 raise error.Abort(_('no username supplied'),
866 866 hint=_("use 'hg config --edit' "
867 867 'to set your username'))
868 868 if "\n" in user:
869 869 raise error.Abort(_("username %r contains a newline\n")
870 870 % pycompat.bytestr(user))
871 871 return user
872 872
873 873 def shortuser(self, user):
874 874 """Return a short representation of a user name or email address."""
875 875 if not self.verbose:
876 876 user = stringutil.shortuser(user)
877 877 return user
878 878
879 879 def expandpath(self, loc, default=None):
880 880 """Return repository location relative to cwd or from [paths]"""
881 881 try:
882 882 p = self.paths.getpath(loc)
883 883 if p:
884 884 return p.rawloc
885 885 except error.RepoError:
886 886 pass
887 887
888 888 if default:
889 889 try:
890 890 p = self.paths.getpath(default)
891 891 if p:
892 892 return p.rawloc
893 893 except error.RepoError:
894 894 pass
895 895
896 896 return loc
897 897
898 898 @util.propertycache
899 899 def paths(self):
900 900 return paths(self)
901 901
902 902 @property
903 903 def fout(self):
904 904 return self._fout
905 905
906 906 @fout.setter
907 907 def fout(self, f):
908 908 self._fout = f
909 909 self._fmsgout, self._fmsgerr = _selectmsgdests(self)
910 910
911 911 @property
912 912 def ferr(self):
913 913 return self._ferr
914 914
915 915 @ferr.setter
916 916 def ferr(self, f):
917 917 self._ferr = f
918 918 self._fmsgout, self._fmsgerr = _selectmsgdests(self)
919 919
920 920 @property
921 921 def fin(self):
922 922 return self._fin
923 923
924 924 @fin.setter
925 925 def fin(self, f):
926 926 self._fin = f
927 927
928 928 @property
929 929 def fmsg(self):
930 930 """Stream dedicated for status/error messages; may be None if
931 931 fout/ferr are used"""
932 932 return self._fmsg
933 933
934 934 @fmsg.setter
935 935 def fmsg(self, f):
936 936 self._fmsg = f
937 937 self._fmsgout, self._fmsgerr = _selectmsgdests(self)
938 938
939 939 def pushbuffer(self, error=False, subproc=False, labeled=False):
940 940 """install a buffer to capture standard output of the ui object
941 941
942 942 If error is True, the error output will be captured too.
943 943
944 944 If subproc is True, output from subprocesses (typically hooks) will be
945 945 captured too.
946 946
947 947 If labeled is True, any labels associated with buffered
948 948 output will be handled. By default, this has no effect
949 949 on the output returned, but extensions and GUI tools may
950 950 handle this argument and returned styled output. If output
951 951 is being buffered so it can be captured and parsed or
952 952 processed, labeled should not be set to True.
953 953 """
954 954 self._buffers.append([])
955 955 self._bufferstates.append((error, subproc, labeled))
956 956 self._bufferapplylabels = labeled
957 957
958 958 def popbuffer(self):
959 959 '''pop the last buffer and return the buffered output'''
960 960 self._bufferstates.pop()
961 961 if self._bufferstates:
962 962 self._bufferapplylabels = self._bufferstates[-1][2]
963 963 else:
964 964 self._bufferapplylabels = None
965 965
966 966 return "".join(self._buffers.pop())
967 967
968 968 def _isbuffered(self, dest):
969 969 if dest is self._fout:
970 970 return bool(self._buffers)
971 971 if dest is self._ferr:
972 972 return bool(self._bufferstates and self._bufferstates[-1][0])
973 973 return False
974 974
975 975 def canwritewithoutlabels(self):
976 976 '''check if write skips the label'''
977 977 if self._buffers and not self._bufferapplylabels:
978 978 return True
979 979 return self._colormode is None
980 980
981 981 def canbatchlabeledwrites(self):
982 982 '''check if write calls with labels are batchable'''
983 983 # Windows color printing is special, see ``write``.
984 984 return self._colormode != 'win32'
985 985
986 986 def write(self, *args, **opts):
987 987 '''write args to output
988 988
989 989 By default, this method simply writes to the buffer or stdout.
990 990 Color mode can be set on the UI class to have the output decorated
991 991 with color modifier before being written to stdout.
992 992
993 993 The color used is controlled by an optional keyword argument, "label".
994 994 This should be a string containing label names separated by space.
995 995 Label names take the form of "topic.type". For example, ui.debug()
996 996 issues a label of "ui.debug".
997 997
998 998 When labeling output for a specific command, a label of
999 999 "cmdname.type" is recommended. For example, status issues
1000 1000 a label of "status.modified" for modified files.
1001 1001 '''
1002 self._write(self._fout, *args, **opts)
1002 dest = self._fout
1003
1004 # inlined _write() for speed
1005 if self._buffers:
1006 label = opts.get(r'label', '')
1007 if label and self._bufferapplylabels:
1008 self._buffers[-1].extend(self.label(a, label) for a in args)
1009 else:
1010 self._buffers[-1].extend(args)
1011 return
1012
1013 # inliend _writenobuf() for speed
1014 self._progclear()
1015 msg = b''.join(args)
1016
1017 # opencode timeblockedsection because this is a critical path
1018 starttime = util.timer()
1019 try:
1020 if self._colormode == 'win32':
1021 # windows color printing is its own can of crab, defer to
1022 # the color module and that is it.
1023 color.win32print(self, dest.write, msg, **opts)
1024 else:
1025 if self._colormode is not None:
1026 label = opts.get(r'label', '')
1027 msg = self.label(msg, label)
1028 dest.write(msg)
1029 except IOError as err:
1030 raise error.StdioError(err)
1031 finally:
1032 self._blockedtimes['stdio_blocked'] += \
1033 (util.timer() - starttime) * 1000
1003 1034
1004 1035 def write_err(self, *args, **opts):
1005 1036 self._write(self._ferr, *args, **opts)
1006 1037
1007 1038 def _write(self, dest, *args, **opts):
1039 # update write() as well if you touch this code
1008 1040 if self._isbuffered(dest):
1009 if self._bufferapplylabels:
1010 label = opts.get(r'label', '')
1041 label = opts.get(r'label', '')
1042 if label and self._bufferapplylabels:
1011 1043 self._buffers[-1].extend(self.label(a, label) for a in args)
1012 1044 else:
1013 1045 self._buffers[-1].extend(args)
1014 1046 else:
1015 1047 self._writenobuf(dest, *args, **opts)
1016 1048
1017 1049 def _writenobuf(self, dest, *args, **opts):
1050 # update write() as well if you touch this code
1018 1051 self._progclear()
1019 1052 msg = b''.join(args)
1020 1053
1021 1054 # opencode timeblockedsection because this is a critical path
1022 1055 starttime = util.timer()
1023 1056 try:
1024 1057 if dest is self._ferr and not getattr(self._fout, 'closed', False):
1025 1058 self._fout.flush()
1026 1059 if getattr(dest, 'structured', False):
1027 1060 # channel for machine-readable output with metadata, where
1028 1061 # no extra colorization is necessary.
1029 1062 dest.write(msg, **opts)
1030 1063 elif self._colormode == 'win32':
1031 1064 # windows color printing is its own can of crab, defer to
1032 1065 # the color module and that is it.
1033 1066 color.win32print(self, dest.write, msg, **opts)
1034 1067 else:
1035 1068 if self._colormode is not None:
1036 1069 label = opts.get(r'label', '')
1037 1070 msg = self.label(msg, label)
1038 1071 dest.write(msg)
1039 1072 # stderr may be buffered under win32 when redirected to files,
1040 1073 # including stdout.
1041 1074 if dest is self._ferr and not getattr(self._ferr, 'closed', False):
1042 1075 dest.flush()
1043 1076 except IOError as err:
1044 1077 if (dest is self._ferr
1045 1078 and err.errno in (errno.EPIPE, errno.EIO, errno.EBADF)):
1046 1079 # no way to report the error, so ignore it
1047 1080 return
1048 1081 raise error.StdioError(err)
1049 1082 finally:
1050 1083 self._blockedtimes['stdio_blocked'] += \
1051 1084 (util.timer() - starttime) * 1000
1052 1085
1053 1086 def _writemsg(self, dest, *args, **opts):
1054 1087 _writemsgwith(self._write, dest, *args, **opts)
1055 1088
1056 1089 def _writemsgnobuf(self, dest, *args, **opts):
1057 1090 _writemsgwith(self._writenobuf, dest, *args, **opts)
1058 1091
1059 1092 def flush(self):
1060 1093 # opencode timeblockedsection because this is a critical path
1061 1094 starttime = util.timer()
1062 1095 try:
1063 1096 try:
1064 1097 self._fout.flush()
1065 1098 except IOError as err:
1066 1099 if err.errno not in (errno.EPIPE, errno.EIO, errno.EBADF):
1067 1100 raise error.StdioError(err)
1068 1101 finally:
1069 1102 try:
1070 1103 self._ferr.flush()
1071 1104 except IOError as err:
1072 1105 if err.errno not in (errno.EPIPE, errno.EIO, errno.EBADF):
1073 1106 raise error.StdioError(err)
1074 1107 finally:
1075 1108 self._blockedtimes['stdio_blocked'] += \
1076 1109 (util.timer() - starttime) * 1000
1077 1110
1078 1111 def _isatty(self, fh):
1079 1112 if self.configbool('ui', 'nontty'):
1080 1113 return False
1081 1114 return procutil.isatty(fh)
1082 1115
1083 1116 def protectfinout(self):
1084 1117 """Duplicate ui streams and redirect original if they are stdio
1085 1118
1086 1119 Returns (fin, fout) which point to the original ui fds, but may be
1087 1120 copy of them. The returned streams can be considered "owned" in that
1088 1121 print(), exec(), etc. never reach to them.
1089 1122 """
1090 1123 if self._finoutredirected:
1091 1124 # if already redirected, protectstdio() would just create another
1092 1125 # nullfd pair, which is equivalent to returning self._fin/_fout.
1093 1126 return self._fin, self._fout
1094 1127 fin, fout = procutil.protectstdio(self._fin, self._fout)
1095 1128 self._finoutredirected = (fin, fout) != (self._fin, self._fout)
1096 1129 return fin, fout
1097 1130
1098 1131 def restorefinout(self, fin, fout):
1099 1132 """Restore ui streams from possibly duplicated (fin, fout)"""
1100 1133 if (fin, fout) == (self._fin, self._fout):
1101 1134 return
1102 1135 procutil.restorestdio(self._fin, self._fout, fin, fout)
1103 1136 # protectfinout() won't create more than one duplicated streams,
1104 1137 # so we can just turn the redirection flag off.
1105 1138 self._finoutredirected = False
1106 1139
1107 1140 @contextlib.contextmanager
1108 1141 def protectedfinout(self):
1109 1142 """Run code block with protected standard streams"""
1110 1143 fin, fout = self.protectfinout()
1111 1144 try:
1112 1145 yield fin, fout
1113 1146 finally:
1114 1147 self.restorefinout(fin, fout)
1115 1148
1116 1149 def disablepager(self):
1117 1150 self._disablepager = True
1118 1151
1119 1152 def pager(self, command):
1120 1153 """Start a pager for subsequent command output.
1121 1154
1122 1155 Commands which produce a long stream of output should call
1123 1156 this function to activate the user's preferred pagination
1124 1157 mechanism (which may be no pager). Calling this function
1125 1158 precludes any future use of interactive functionality, such as
1126 1159 prompting the user or activating curses.
1127 1160
1128 1161 Args:
1129 1162 command: The full, non-aliased name of the command. That is, "log"
1130 1163 not "history, "summary" not "summ", etc.
1131 1164 """
1132 1165 if (self._disablepager
1133 1166 or self.pageractive):
1134 1167 # how pager should do is already determined
1135 1168 return
1136 1169
1137 1170 if not command.startswith('internal-always-') and (
1138 1171 # explicit --pager=on (= 'internal-always-' prefix) should
1139 1172 # take precedence over disabling factors below
1140 1173 command in self.configlist('pager', 'ignore')
1141 1174 or not self.configbool('ui', 'paginate')
1142 1175 or not self.configbool('pager', 'attend-' + command, True)
1143 1176 or encoding.environ.get('TERM') == 'dumb'
1144 1177 # TODO: if we want to allow HGPLAINEXCEPT=pager,
1145 1178 # formatted() will need some adjustment.
1146 1179 or not self.formatted()
1147 1180 or self.plain()
1148 1181 or self._buffers
1149 1182 # TODO: expose debugger-enabled on the UI object
1150 1183 or '--debugger' in pycompat.sysargv):
1151 1184 # We only want to paginate if the ui appears to be
1152 1185 # interactive, the user didn't say HGPLAIN or
1153 1186 # HGPLAINEXCEPT=pager, and the user didn't specify --debug.
1154 1187 return
1155 1188
1156 1189 pagercmd = self.config('pager', 'pager', rcutil.fallbackpager)
1157 1190 if not pagercmd:
1158 1191 return
1159 1192
1160 1193 pagerenv = {}
1161 1194 for name, value in rcutil.defaultpagerenv().items():
1162 1195 if name not in encoding.environ:
1163 1196 pagerenv[name] = value
1164 1197
1165 1198 self.debug('starting pager for command %s\n' %
1166 1199 stringutil.pprint(command))
1167 1200 self.flush()
1168 1201
1169 1202 wasformatted = self.formatted()
1170 1203 if util.safehasattr(signal, "SIGPIPE"):
1171 1204 signal.signal(signal.SIGPIPE, _catchterm)
1172 1205 if self._runpager(pagercmd, pagerenv):
1173 1206 self.pageractive = True
1174 1207 # Preserve the formatted-ness of the UI. This is important
1175 1208 # because we mess with stdout, which might confuse
1176 1209 # auto-detection of things being formatted.
1177 1210 self.setconfig('ui', 'formatted', wasformatted, 'pager')
1178 1211 self.setconfig('ui', 'interactive', False, 'pager')
1179 1212
1180 1213 # If pagermode differs from color.mode, reconfigure color now that
1181 1214 # pageractive is set.
1182 1215 cm = self._colormode
1183 1216 if cm != self.config('color', 'pagermode', cm):
1184 1217 color.setup(self)
1185 1218 else:
1186 1219 # If the pager can't be spawned in dispatch when --pager=on is
1187 1220 # given, don't try again when the command runs, to avoid a duplicate
1188 1221 # warning about a missing pager command.
1189 1222 self.disablepager()
1190 1223
1191 1224 def _runpager(self, command, env=None):
1192 1225 """Actually start the pager and set up file descriptors.
1193 1226
1194 1227 This is separate in part so that extensions (like chg) can
1195 1228 override how a pager is invoked.
1196 1229 """
1197 1230 if command == 'cat':
1198 1231 # Save ourselves some work.
1199 1232 return False
1200 1233 # If the command doesn't contain any of these characters, we
1201 1234 # assume it's a binary and exec it directly. This means for
1202 1235 # simple pager command configurations, we can degrade
1203 1236 # gracefully and tell the user about their broken pager.
1204 1237 shell = any(c in command for c in "|&;<>()$`\\\"' \t\n*?[#~=%")
1205 1238
1206 1239 if pycompat.iswindows and not shell:
1207 1240 # Window's built-in `more` cannot be invoked with shell=False, but
1208 1241 # its `more.com` can. Hide this implementation detail from the
1209 1242 # user so we can also get sane bad PAGER behavior. MSYS has
1210 1243 # `more.exe`, so do a cmd.exe style resolution of the executable to
1211 1244 # determine which one to use.
1212 1245 fullcmd = procutil.findexe(command)
1213 1246 if not fullcmd:
1214 1247 self.warn(_("missing pager command '%s', skipping pager\n")
1215 1248 % command)
1216 1249 return False
1217 1250
1218 1251 command = fullcmd
1219 1252
1220 1253 try:
1221 1254 pager = subprocess.Popen(
1222 1255 procutil.tonativestr(command), shell=shell, bufsize=-1,
1223 1256 close_fds=procutil.closefds, stdin=subprocess.PIPE,
1224 1257 stdout=procutil.stdout, stderr=procutil.stderr,
1225 1258 env=procutil.tonativeenv(procutil.shellenviron(env)))
1226 1259 except OSError as e:
1227 1260 if e.errno == errno.ENOENT and not shell:
1228 1261 self.warn(_("missing pager command '%s', skipping pager\n")
1229 1262 % command)
1230 1263 return False
1231 1264 raise
1232 1265
1233 1266 # back up original file descriptors
1234 1267 stdoutfd = os.dup(procutil.stdout.fileno())
1235 1268 stderrfd = os.dup(procutil.stderr.fileno())
1236 1269
1237 1270 os.dup2(pager.stdin.fileno(), procutil.stdout.fileno())
1238 1271 if self._isatty(procutil.stderr):
1239 1272 os.dup2(pager.stdin.fileno(), procutil.stderr.fileno())
1240 1273
1241 1274 @self.atexit
1242 1275 def killpager():
1243 1276 if util.safehasattr(signal, "SIGINT"):
1244 1277 signal.signal(signal.SIGINT, signal.SIG_IGN)
1245 1278 # restore original fds, closing pager.stdin copies in the process
1246 1279 os.dup2(stdoutfd, procutil.stdout.fileno())
1247 1280 os.dup2(stderrfd, procutil.stderr.fileno())
1248 1281 pager.stdin.close()
1249 1282 pager.wait()
1250 1283
1251 1284 return True
1252 1285
1253 1286 @property
1254 1287 def _exithandlers(self):
1255 1288 return _reqexithandlers
1256 1289
1257 1290 def atexit(self, func, *args, **kwargs):
1258 1291 '''register a function to run after dispatching a request
1259 1292
1260 1293 Handlers do not stay registered across request boundaries.'''
1261 1294 self._exithandlers.append((func, args, kwargs))
1262 1295 return func
1263 1296
1264 1297 def interface(self, feature):
1265 1298 """what interface to use for interactive console features?
1266 1299
1267 1300 The interface is controlled by the value of `ui.interface` but also by
1268 1301 the value of feature-specific configuration. For example:
1269 1302
1270 1303 ui.interface.histedit = text
1271 1304 ui.interface.chunkselector = curses
1272 1305
1273 1306 Here the features are "histedit" and "chunkselector".
1274 1307
1275 1308 The configuration above means that the default interfaces for commands
1276 1309 is curses, the interface for histedit is text and the interface for
1277 1310 selecting chunk is crecord (the best curses interface available).
1278 1311
1279 1312 Consider the following example:
1280 1313 ui.interface = curses
1281 1314 ui.interface.histedit = text
1282 1315
1283 1316 Then histedit will use the text interface and chunkselector will use
1284 1317 the default curses interface (crecord at the moment).
1285 1318 """
1286 1319 alldefaults = frozenset(["text", "curses"])
1287 1320
1288 1321 featureinterfaces = {
1289 1322 "chunkselector": [
1290 1323 "text",
1291 1324 "curses",
1292 1325 ],
1293 1326 "histedit": [
1294 1327 "text",
1295 1328 "curses",
1296 1329 ],
1297 1330 }
1298 1331
1299 1332 # Feature-specific interface
1300 1333 if feature not in featureinterfaces.keys():
1301 1334 # Programming error, not user error
1302 1335 raise ValueError("Unknown feature requested %s" % feature)
1303 1336
1304 1337 availableinterfaces = frozenset(featureinterfaces[feature])
1305 1338 if alldefaults > availableinterfaces:
1306 1339 # Programming error, not user error. We need a use case to
1307 1340 # define the right thing to do here.
1308 1341 raise ValueError(
1309 1342 "Feature %s does not handle all default interfaces" %
1310 1343 feature)
1311 1344
1312 1345 if self.plain() or encoding.environ.get('TERM') == 'dumb':
1313 1346 return "text"
1314 1347
1315 1348 # Default interface for all the features
1316 1349 defaultinterface = "text"
1317 1350 i = self.config("ui", "interface")
1318 1351 if i in alldefaults:
1319 1352 defaultinterface = i
1320 1353
1321 1354 choseninterface = defaultinterface
1322 1355 f = self.config("ui", "interface.%s" % feature)
1323 1356 if f in availableinterfaces:
1324 1357 choseninterface = f
1325 1358
1326 1359 if i is not None and defaultinterface != i:
1327 1360 if f is not None:
1328 1361 self.warn(_("invalid value for ui.interface: %s\n") %
1329 1362 (i,))
1330 1363 else:
1331 1364 self.warn(_("invalid value for ui.interface: %s (using %s)\n") %
1332 1365 (i, choseninterface))
1333 1366 if f is not None and choseninterface != f:
1334 1367 self.warn(_("invalid value for ui.interface.%s: %s (using %s)\n") %
1335 1368 (feature, f, choseninterface))
1336 1369
1337 1370 return choseninterface
1338 1371
1339 1372 def interactive(self):
1340 1373 '''is interactive input allowed?
1341 1374
1342 1375 An interactive session is a session where input can be reasonably read
1343 1376 from `sys.stdin'. If this function returns false, any attempt to read
1344 1377 from stdin should fail with an error, unless a sensible default has been
1345 1378 specified.
1346 1379
1347 1380 Interactiveness is triggered by the value of the `ui.interactive'
1348 1381 configuration variable or - if it is unset - when `sys.stdin' points
1349 1382 to a terminal device.
1350 1383
1351 1384 This function refers to input only; for output, see `ui.formatted()'.
1352 1385 '''
1353 1386 i = self.configbool("ui", "interactive")
1354 1387 if i is None:
1355 1388 # some environments replace stdin without implementing isatty
1356 1389 # usually those are non-interactive
1357 1390 return self._isatty(self._fin)
1358 1391
1359 1392 return i
1360 1393
1361 1394 def termwidth(self):
1362 1395 '''how wide is the terminal in columns?
1363 1396 '''
1364 1397 if 'COLUMNS' in encoding.environ:
1365 1398 try:
1366 1399 return int(encoding.environ['COLUMNS'])
1367 1400 except ValueError:
1368 1401 pass
1369 1402 return scmutil.termsize(self)[0]
1370 1403
1371 1404 def formatted(self):
1372 1405 '''should formatted output be used?
1373 1406
1374 1407 It is often desirable to format the output to suite the output medium.
1375 1408 Examples of this are truncating long lines or colorizing messages.
1376 1409 However, this is not often not desirable when piping output into other
1377 1410 utilities, e.g. `grep'.
1378 1411
1379 1412 Formatted output is triggered by the value of the `ui.formatted'
1380 1413 configuration variable or - if it is unset - when `sys.stdout' points
1381 1414 to a terminal device. Please note that `ui.formatted' should be
1382 1415 considered an implementation detail; it is not intended for use outside
1383 1416 Mercurial or its extensions.
1384 1417
1385 1418 This function refers to output only; for input, see `ui.interactive()'.
1386 1419 This function always returns false when in plain mode, see `ui.plain()'.
1387 1420 '''
1388 1421 if self.plain():
1389 1422 return False
1390 1423
1391 1424 i = self.configbool("ui", "formatted")
1392 1425 if i is None:
1393 1426 # some environments replace stdout without implementing isatty
1394 1427 # usually those are non-interactive
1395 1428 return self._isatty(self._fout)
1396 1429
1397 1430 return i
1398 1431
1399 1432 def _readline(self):
1400 1433 # Replacing stdin/stdout temporarily is a hard problem on Python 3
1401 1434 # because they have to be text streams with *no buffering*. Instead,
1402 1435 # we use rawinput() only if call_readline() will be invoked by
1403 1436 # PyOS_Readline(), so no I/O will be made at Python layer.
1404 1437 usereadline = (self._isatty(self._fin) and self._isatty(self._fout)
1405 1438 and procutil.isstdin(self._fin)
1406 1439 and procutil.isstdout(self._fout))
1407 1440 if usereadline:
1408 1441 try:
1409 1442 # magically add command line editing support, where
1410 1443 # available
1411 1444 import readline
1412 1445 # force demandimport to really load the module
1413 1446 readline.read_history_file
1414 1447 # windows sometimes raises something other than ImportError
1415 1448 except Exception:
1416 1449 usereadline = False
1417 1450
1418 1451 # prompt ' ' must exist; otherwise readline may delete entire line
1419 1452 # - http://bugs.python.org/issue12833
1420 1453 with self.timeblockedsection('stdio'):
1421 1454 if usereadline:
1422 1455 line = encoding.strtolocal(pycompat.rawinput(r' '))
1423 1456 # When stdin is in binary mode on Windows, it can cause
1424 1457 # raw_input() to emit an extra trailing carriage return
1425 1458 if pycompat.oslinesep == b'\r\n' and line.endswith(b'\r'):
1426 1459 line = line[:-1]
1427 1460 else:
1428 1461 self._fout.write(b' ')
1429 1462 self._fout.flush()
1430 1463 line = self._fin.readline()
1431 1464 if not line:
1432 1465 raise EOFError
1433 1466 line = line.rstrip(pycompat.oslinesep)
1434 1467
1435 1468 return line
1436 1469
1437 1470 def prompt(self, msg, default="y"):
1438 1471 """Prompt user with msg, read response.
1439 1472 If ui is not interactive, the default is returned.
1440 1473 """
1441 1474 return self._prompt(msg, default=default)
1442 1475
1443 1476 def _prompt(self, msg, **opts):
1444 1477 default = opts[r'default']
1445 1478 if not self.interactive():
1446 1479 self._writemsg(self._fmsgout, msg, ' ', type='prompt', **opts)
1447 1480 self._writemsg(self._fmsgout, default or '', "\n",
1448 1481 type='promptecho')
1449 1482 return default
1450 1483 self._writemsgnobuf(self._fmsgout, msg, type='prompt', **opts)
1451 1484 self.flush()
1452 1485 try:
1453 1486 r = self._readline()
1454 1487 if not r:
1455 1488 r = default
1456 1489 if self.configbool('ui', 'promptecho'):
1457 1490 self._writemsg(self._fmsgout, r, "\n", type='promptecho')
1458 1491 return r
1459 1492 except EOFError:
1460 1493 raise error.ResponseExpected()
1461 1494
1462 1495 @staticmethod
1463 1496 def extractchoices(prompt):
1464 1497 """Extract prompt message and list of choices from specified prompt.
1465 1498
1466 1499 This returns tuple "(message, choices)", and "choices" is the
1467 1500 list of tuple "(response character, text without &)".
1468 1501
1469 1502 >>> ui.extractchoices(b"awake? $$ &Yes $$ &No")
1470 1503 ('awake? ', [('y', 'Yes'), ('n', 'No')])
1471 1504 >>> ui.extractchoices(b"line\\nbreak? $$ &Yes $$ &No")
1472 1505 ('line\\nbreak? ', [('y', 'Yes'), ('n', 'No')])
1473 1506 >>> ui.extractchoices(b"want lots of $$money$$?$$Ye&s$$N&o")
1474 1507 ('want lots of $$money$$?', [('s', 'Yes'), ('o', 'No')])
1475 1508 """
1476 1509
1477 1510 # Sadly, the prompt string may have been built with a filename
1478 1511 # containing "$$" so let's try to find the first valid-looking
1479 1512 # prompt to start parsing. Sadly, we also can't rely on
1480 1513 # choices containing spaces, ASCII, or basically anything
1481 1514 # except an ampersand followed by a character.
1482 1515 m = re.match(br'(?s)(.+?)\$\$([^\$]*&[^ \$].*)', prompt)
1483 1516 msg = m.group(1)
1484 1517 choices = [p.strip(' ') for p in m.group(2).split('$$')]
1485 1518 def choicetuple(s):
1486 1519 ampidx = s.index('&')
1487 1520 return s[ampidx + 1:ampidx + 2].lower(), s.replace('&', '', 1)
1488 1521 return (msg, [choicetuple(s) for s in choices])
1489 1522
1490 1523 def promptchoice(self, prompt, default=0):
1491 1524 """Prompt user with a message, read response, and ensure it matches
1492 1525 one of the provided choices. The prompt is formatted as follows:
1493 1526
1494 1527 "would you like fries with that (Yn)? $$ &Yes $$ &No"
1495 1528
1496 1529 The index of the choice is returned. Responses are case
1497 1530 insensitive. If ui is not interactive, the default is
1498 1531 returned.
1499 1532 """
1500 1533
1501 1534 msg, choices = self.extractchoices(prompt)
1502 1535 resps = [r for r, t in choices]
1503 1536 while True:
1504 1537 r = self._prompt(msg, default=resps[default], choices=choices)
1505 1538 if r.lower() in resps:
1506 1539 return resps.index(r.lower())
1507 1540 # TODO: shouldn't it be a warning?
1508 1541 self._writemsg(self._fmsgout, _("unrecognized response\n"))
1509 1542
1510 1543 def getpass(self, prompt=None, default=None):
1511 1544 if not self.interactive():
1512 1545 return default
1513 1546 try:
1514 1547 self._writemsg(self._fmsgerr, prompt or _('password: '),
1515 1548 type='prompt', password=True)
1516 1549 # disable getpass() only if explicitly specified. it's still valid
1517 1550 # to interact with tty even if fin is not a tty.
1518 1551 with self.timeblockedsection('stdio'):
1519 1552 if self.configbool('ui', 'nontty'):
1520 1553 l = self._fin.readline()
1521 1554 if not l:
1522 1555 raise EOFError
1523 1556 return l.rstrip('\n')
1524 1557 else:
1525 1558 return getpass.getpass('')
1526 1559 except EOFError:
1527 1560 raise error.ResponseExpected()
1528 1561
1529 1562 def status(self, *msg, **opts):
1530 1563 '''write status message to output (if ui.quiet is False)
1531 1564
1532 1565 This adds an output label of "ui.status".
1533 1566 '''
1534 1567 if not self.quiet:
1535 1568 self._writemsg(self._fmsgout, type='status', *msg, **opts)
1536 1569
1537 1570 def warn(self, *msg, **opts):
1538 1571 '''write warning message to output (stderr)
1539 1572
1540 1573 This adds an output label of "ui.warning".
1541 1574 '''
1542 1575 self._writemsg(self._fmsgerr, type='warning', *msg, **opts)
1543 1576
1544 1577 def error(self, *msg, **opts):
1545 1578 '''write error message to output (stderr)
1546 1579
1547 1580 This adds an output label of "ui.error".
1548 1581 '''
1549 1582 self._writemsg(self._fmsgerr, type='error', *msg, **opts)
1550 1583
1551 1584 def note(self, *msg, **opts):
1552 1585 '''write note to output (if ui.verbose is True)
1553 1586
1554 1587 This adds an output label of "ui.note".
1555 1588 '''
1556 1589 if self.verbose:
1557 1590 self._writemsg(self._fmsgout, type='note', *msg, **opts)
1558 1591
1559 1592 def debug(self, *msg, **opts):
1560 1593 '''write debug message to output (if ui.debugflag is True)
1561 1594
1562 1595 This adds an output label of "ui.debug".
1563 1596 '''
1564 1597 if self.debugflag:
1565 1598 self._writemsg(self._fmsgout, type='debug', *msg, **opts)
1566 1599 self.log(b'debug', b'%s', b''.join(msg))
1567 1600
1568 1601 def edit(self, text, user, extra=None, editform=None, pending=None,
1569 1602 repopath=None, action=None):
1570 1603 if action is None:
1571 1604 self.develwarn('action is None but will soon be a required '
1572 1605 'parameter to ui.edit()')
1573 1606 extra_defaults = {
1574 1607 'prefix': 'editor',
1575 1608 'suffix': '.txt',
1576 1609 }
1577 1610 if extra is not None:
1578 1611 if extra.get('suffix') is not None:
1579 1612 self.develwarn('extra.suffix is not None but will soon be '
1580 1613 'ignored by ui.edit()')
1581 1614 extra_defaults.update(extra)
1582 1615 extra = extra_defaults
1583 1616
1584 1617 if action == 'diff':
1585 1618 suffix = '.diff'
1586 1619 elif action:
1587 1620 suffix = '.%s.hg.txt' % action
1588 1621 else:
1589 1622 suffix = extra['suffix']
1590 1623
1591 1624 rdir = None
1592 1625 if self.configbool('experimental', 'editortmpinhg'):
1593 1626 rdir = repopath
1594 1627 (fd, name) = pycompat.mkstemp(prefix='hg-' + extra['prefix'] + '-',
1595 1628 suffix=suffix,
1596 1629 dir=rdir)
1597 1630 try:
1598 1631 f = os.fdopen(fd, r'wb')
1599 1632 f.write(util.tonativeeol(text))
1600 1633 f.close()
1601 1634
1602 1635 environ = {'HGUSER': user}
1603 1636 if 'transplant_source' in extra:
1604 1637 environ.update({'HGREVISION': hex(extra['transplant_source'])})
1605 1638 for label in ('intermediate-source', 'source', 'rebase_source'):
1606 1639 if label in extra:
1607 1640 environ.update({'HGREVISION': extra[label]})
1608 1641 break
1609 1642 if editform:
1610 1643 environ.update({'HGEDITFORM': editform})
1611 1644 if pending:
1612 1645 environ.update({'HG_PENDING': pending})
1613 1646
1614 1647 editor = self.geteditor()
1615 1648
1616 1649 self.system("%s \"%s\"" % (editor, name),
1617 1650 environ=environ,
1618 1651 onerr=error.Abort, errprefix=_("edit failed"),
1619 1652 blockedtag='editor')
1620 1653
1621 1654 f = open(name, r'rb')
1622 1655 t = util.fromnativeeol(f.read())
1623 1656 f.close()
1624 1657 finally:
1625 1658 os.unlink(name)
1626 1659
1627 1660 return t
1628 1661
1629 1662 def system(self, cmd, environ=None, cwd=None, onerr=None, errprefix=None,
1630 1663 blockedtag=None):
1631 1664 '''execute shell command with appropriate output stream. command
1632 1665 output will be redirected if fout is not stdout.
1633 1666
1634 1667 if command fails and onerr is None, return status, else raise onerr
1635 1668 object as exception.
1636 1669 '''
1637 1670 if blockedtag is None:
1638 1671 # Long cmds tend to be because of an absolute path on cmd. Keep
1639 1672 # the tail end instead
1640 1673 cmdsuffix = cmd.translate(None, _keepalnum)[-85:]
1641 1674 blockedtag = 'unknown_system_' + cmdsuffix
1642 1675 out = self._fout
1643 1676 if any(s[1] for s in self._bufferstates):
1644 1677 out = self
1645 1678 with self.timeblockedsection(blockedtag):
1646 1679 rc = self._runsystem(cmd, environ=environ, cwd=cwd, out=out)
1647 1680 if rc and onerr:
1648 1681 errmsg = '%s %s' % (os.path.basename(cmd.split(None, 1)[0]),
1649 1682 procutil.explainexit(rc))
1650 1683 if errprefix:
1651 1684 errmsg = '%s: %s' % (errprefix, errmsg)
1652 1685 raise onerr(errmsg)
1653 1686 return rc
1654 1687
1655 1688 def _runsystem(self, cmd, environ, cwd, out):
1656 1689 """actually execute the given shell command (can be overridden by
1657 1690 extensions like chg)"""
1658 1691 return procutil.system(cmd, environ=environ, cwd=cwd, out=out)
1659 1692
1660 1693 def traceback(self, exc=None, force=False):
1661 1694 '''print exception traceback if traceback printing enabled or forced.
1662 1695 only to call in exception handler. returns true if traceback
1663 1696 printed.'''
1664 1697 if self.tracebackflag or force:
1665 1698 if exc is None:
1666 1699 exc = sys.exc_info()
1667 1700 cause = getattr(exc[1], 'cause', None)
1668 1701
1669 1702 if cause is not None:
1670 1703 causetb = traceback.format_tb(cause[2])
1671 1704 exctb = traceback.format_tb(exc[2])
1672 1705 exconly = traceback.format_exception_only(cause[0], cause[1])
1673 1706
1674 1707 # exclude frame where 'exc' was chained and rethrown from exctb
1675 1708 self.write_err('Traceback (most recent call last):\n',
1676 1709 ''.join(exctb[:-1]),
1677 1710 ''.join(causetb),
1678 1711 ''.join(exconly))
1679 1712 else:
1680 1713 output = traceback.format_exception(exc[0], exc[1], exc[2])
1681 1714 self.write_err(encoding.strtolocal(r''.join(output)))
1682 1715 return self.tracebackflag or force
1683 1716
1684 1717 def geteditor(self):
1685 1718 '''return editor to use'''
1686 1719 if pycompat.sysplatform == 'plan9':
1687 1720 # vi is the MIPS instruction simulator on Plan 9. We
1688 1721 # instead default to E to plumb commit messages to
1689 1722 # avoid confusion.
1690 1723 editor = 'E'
1691 1724 else:
1692 1725 editor = 'vi'
1693 1726 return (encoding.environ.get("HGEDITOR") or
1694 1727 self.config("ui", "editor", editor))
1695 1728
1696 1729 @util.propertycache
1697 1730 def _progbar(self):
1698 1731 """setup the progbar singleton to the ui object"""
1699 1732 if (self.quiet or self.debugflag
1700 1733 or self.configbool('progress', 'disable')
1701 1734 or not progress.shouldprint(self)):
1702 1735 return None
1703 1736 return getprogbar(self)
1704 1737
1705 1738 def _progclear(self):
1706 1739 """clear progress bar output if any. use it before any output"""
1707 1740 if not haveprogbar(): # nothing loaded yet
1708 1741 return
1709 1742 if self._progbar is not None and self._progbar.printed:
1710 1743 self._progbar.clear()
1711 1744
1712 1745 def progress(self, topic, pos, item="", unit="", total=None):
1713 1746 '''show a progress message
1714 1747
1715 1748 By default a textual progress bar will be displayed if an operation
1716 1749 takes too long. 'topic' is the current operation, 'item' is a
1717 1750 non-numeric marker of the current position (i.e. the currently
1718 1751 in-process file), 'pos' is the current numeric position (i.e.
1719 1752 revision, bytes, etc.), unit is a corresponding unit label,
1720 1753 and total is the highest expected pos.
1721 1754
1722 1755 Multiple nested topics may be active at a time.
1723 1756
1724 1757 All topics should be marked closed by setting pos to None at
1725 1758 termination.
1726 1759 '''
1727 1760 self.deprecwarn("use ui.makeprogress() instead of ui.progress()",
1728 1761 "5.1")
1729 1762 progress = self.makeprogress(topic, unit, total)
1730 1763 if pos is not None:
1731 1764 progress.update(pos, item=item)
1732 1765 else:
1733 1766 progress.complete()
1734 1767
1735 1768 def makeprogress(self, topic, unit="", total=None):
1736 1769 """Create a progress helper for the specified topic"""
1737 1770 if getattr(self._fmsgerr, 'structured', False):
1738 1771 # channel for machine-readable output with metadata, just send
1739 1772 # raw information
1740 1773 # TODO: consider porting some useful information (e.g. estimated
1741 1774 # time) from progbar. we might want to support update delay to
1742 1775 # reduce the cost of transferring progress messages.
1743 1776 def updatebar(topic, pos, item, unit, total):
1744 1777 self._fmsgerr.write(None, type=b'progress', topic=topic,
1745 1778 pos=pos, item=item, unit=unit, total=total)
1746 1779 elif self._progbar is not None:
1747 1780 updatebar = self._progbar.progress
1748 1781 else:
1749 1782 def updatebar(topic, pos, item, unit, total):
1750 1783 pass
1751 1784 return scmutil.progress(self, updatebar, topic, unit, total)
1752 1785
1753 1786 def getlogger(self, name):
1754 1787 """Returns a logger of the given name; or None if not registered"""
1755 1788 return self._loggers.get(name)
1756 1789
1757 1790 def setlogger(self, name, logger):
1758 1791 """Install logger which can be identified later by the given name
1759 1792
1760 1793 More than one loggers can be registered. Use extension or module
1761 1794 name to uniquely identify the logger instance.
1762 1795 """
1763 1796 self._loggers[name] = logger
1764 1797
1765 1798 def log(self, event, msgfmt, *msgargs, **opts):
1766 1799 '''hook for logging facility extensions
1767 1800
1768 1801 event should be a readily-identifiable subsystem, which will
1769 1802 allow filtering.
1770 1803
1771 1804 msgfmt should be a newline-terminated format string to log, and
1772 1805 *msgargs are %-formatted into it.
1773 1806
1774 1807 **opts currently has no defined meanings.
1775 1808 '''
1776 1809 if not self._loggers:
1777 1810 return
1778 1811 activeloggers = [l for l in self._loggers.itervalues()
1779 1812 if l.tracked(event)]
1780 1813 if not activeloggers:
1781 1814 return
1782 1815 msg = msgfmt % msgargs
1783 1816 opts = pycompat.byteskwargs(opts)
1784 1817 # guard against recursion from e.g. ui.debug()
1785 1818 registeredloggers = self._loggers
1786 1819 self._loggers = {}
1787 1820 try:
1788 1821 for logger in activeloggers:
1789 1822 logger.log(self, event, msg, opts)
1790 1823 finally:
1791 1824 self._loggers = registeredloggers
1792 1825
1793 1826 def label(self, msg, label):
1794 1827 '''style msg based on supplied label
1795 1828
1796 1829 If some color mode is enabled, this will add the necessary control
1797 1830 characters to apply such color. In addition, 'debug' color mode adds
1798 1831 markup showing which label affects a piece of text.
1799 1832
1800 1833 ui.write(s, 'label') is equivalent to
1801 1834 ui.write(ui.label(s, 'label')).
1802 1835 '''
1803 1836 if self._colormode is not None:
1804 1837 return color.colorlabel(self, msg, label)
1805 1838 return msg
1806 1839
1807 1840 def develwarn(self, msg, stacklevel=1, config=None):
1808 1841 """issue a developer warning message
1809 1842
1810 1843 Use 'stacklevel' to report the offender some layers further up in the
1811 1844 stack.
1812 1845 """
1813 1846 if not self.configbool('devel', 'all-warnings'):
1814 1847 if config is None or not self.configbool('devel', config):
1815 1848 return
1816 1849 msg = 'devel-warn: ' + msg
1817 1850 stacklevel += 1 # get in develwarn
1818 1851 if self.tracebackflag:
1819 1852 util.debugstacktrace(msg, stacklevel, self._ferr, self._fout)
1820 1853 self.log('develwarn', '%s at:\n%s' %
1821 1854 (msg, ''.join(util.getstackframes(stacklevel))))
1822 1855 else:
1823 1856 curframe = inspect.currentframe()
1824 1857 calframe = inspect.getouterframes(curframe, 2)
1825 1858 fname, lineno, fmsg = calframe[stacklevel][1:4]
1826 1859 fname, fmsg = pycompat.sysbytes(fname), pycompat.sysbytes(fmsg)
1827 1860 self.write_err('%s at: %s:%d (%s)\n'
1828 1861 % (msg, fname, lineno, fmsg))
1829 1862 self.log('develwarn', '%s at: %s:%d (%s)\n',
1830 1863 msg, fname, lineno, fmsg)
1831 1864 curframe = calframe = None # avoid cycles
1832 1865
1833 1866 def deprecwarn(self, msg, version, stacklevel=2):
1834 1867 """issue a deprecation warning
1835 1868
1836 1869 - msg: message explaining what is deprecated and how to upgrade,
1837 1870 - version: last version where the API will be supported,
1838 1871 """
1839 1872 if not (self.configbool('devel', 'all-warnings')
1840 1873 or self.configbool('devel', 'deprec-warn')):
1841 1874 return
1842 1875 msg += ("\n(compatibility will be dropped after Mercurial-%s,"
1843 1876 " update your code.)") % version
1844 1877 self.develwarn(msg, stacklevel=stacklevel, config='deprec-warn')
1845 1878
1846 1879 def exportableenviron(self):
1847 1880 """The environment variables that are safe to export, e.g. through
1848 1881 hgweb.
1849 1882 """
1850 1883 return self._exportableenviron
1851 1884
1852 1885 @contextlib.contextmanager
1853 1886 def configoverride(self, overrides, source=""):
1854 1887 """Context manager for temporary config overrides
1855 1888 `overrides` must be a dict of the following structure:
1856 1889 {(section, name) : value}"""
1857 1890 backups = {}
1858 1891 try:
1859 1892 for (section, name), value in overrides.items():
1860 1893 backups[(section, name)] = self.backupconfig(section, name)
1861 1894 self.setconfig(section, name, value, source)
1862 1895 yield
1863 1896 finally:
1864 1897 for __, backup in backups.items():
1865 1898 self.restoreconfig(backup)
1866 1899 # just restoring ui.quiet config to the previous value is not enough
1867 1900 # as it does not update ui.quiet class member
1868 1901 if ('ui', 'quiet') in overrides:
1869 1902 self.fixconfig(section='ui')
1870 1903
1871 1904 class paths(dict):
1872 1905 """Represents a collection of paths and their configs.
1873 1906
1874 1907 Data is initially derived from ui instances and the config files they have
1875 1908 loaded.
1876 1909 """
1877 1910 def __init__(self, ui):
1878 1911 dict.__init__(self)
1879 1912
1880 1913 for name, loc in ui.configitems('paths', ignoresub=True):
1881 1914 # No location is the same as not existing.
1882 1915 if not loc:
1883 1916 continue
1884 1917 loc, sub = ui.configsuboptions('paths', name)
1885 1918 self[name] = path(ui, name, rawloc=loc, suboptions=sub)
1886 1919
1887 1920 def getpath(self, name, default=None):
1888 1921 """Return a ``path`` from a string, falling back to default.
1889 1922
1890 1923 ``name`` can be a named path or locations. Locations are filesystem
1891 1924 paths or URIs.
1892 1925
1893 1926 Returns None if ``name`` is not a registered path, a URI, or a local
1894 1927 path to a repo.
1895 1928 """
1896 1929 # Only fall back to default if no path was requested.
1897 1930 if name is None:
1898 1931 if not default:
1899 1932 default = ()
1900 1933 elif not isinstance(default, (tuple, list)):
1901 1934 default = (default,)
1902 1935 for k in default:
1903 1936 try:
1904 1937 return self[k]
1905 1938 except KeyError:
1906 1939 continue
1907 1940 return None
1908 1941
1909 1942 # Most likely empty string.
1910 1943 # This may need to raise in the future.
1911 1944 if not name:
1912 1945 return None
1913 1946
1914 1947 try:
1915 1948 return self[name]
1916 1949 except KeyError:
1917 1950 # Try to resolve as a local path or URI.
1918 1951 try:
1919 1952 # We don't pass sub-options in, so no need to pass ui instance.
1920 1953 return path(None, None, rawloc=name)
1921 1954 except ValueError:
1922 1955 raise error.RepoError(_('repository %s does not exist') %
1923 1956 name)
1924 1957
1925 1958 _pathsuboptions = {}
1926 1959
1927 1960 def pathsuboption(option, attr):
1928 1961 """Decorator used to declare a path sub-option.
1929 1962
1930 1963 Arguments are the sub-option name and the attribute it should set on
1931 1964 ``path`` instances.
1932 1965
1933 1966 The decorated function will receive as arguments a ``ui`` instance,
1934 1967 ``path`` instance, and the string value of this option from the config.
1935 1968 The function should return the value that will be set on the ``path``
1936 1969 instance.
1937 1970
1938 1971 This decorator can be used to perform additional verification of
1939 1972 sub-options and to change the type of sub-options.
1940 1973 """
1941 1974 def register(func):
1942 1975 _pathsuboptions[option] = (attr, func)
1943 1976 return func
1944 1977 return register
1945 1978
1946 1979 @pathsuboption('pushurl', 'pushloc')
1947 1980 def pushurlpathoption(ui, path, value):
1948 1981 u = util.url(value)
1949 1982 # Actually require a URL.
1950 1983 if not u.scheme:
1951 1984 ui.warn(_('(paths.%s:pushurl not a URL; ignoring)\n') % path.name)
1952 1985 return None
1953 1986
1954 1987 # Don't support the #foo syntax in the push URL to declare branch to
1955 1988 # push.
1956 1989 if u.fragment:
1957 1990 ui.warn(_('("#fragment" in paths.%s:pushurl not supported; '
1958 1991 'ignoring)\n') % path.name)
1959 1992 u.fragment = None
1960 1993
1961 1994 return bytes(u)
1962 1995
1963 1996 @pathsuboption('pushrev', 'pushrev')
1964 1997 def pushrevpathoption(ui, path, value):
1965 1998 return value
1966 1999
1967 2000 class path(object):
1968 2001 """Represents an individual path and its configuration."""
1969 2002
1970 2003 def __init__(self, ui, name, rawloc=None, suboptions=None):
1971 2004 """Construct a path from its config options.
1972 2005
1973 2006 ``ui`` is the ``ui`` instance the path is coming from.
1974 2007 ``name`` is the symbolic name of the path.
1975 2008 ``rawloc`` is the raw location, as defined in the config.
1976 2009 ``pushloc`` is the raw locations pushes should be made to.
1977 2010
1978 2011 If ``name`` is not defined, we require that the location be a) a local
1979 2012 filesystem path with a .hg directory or b) a URL. If not,
1980 2013 ``ValueError`` is raised.
1981 2014 """
1982 2015 if not rawloc:
1983 2016 raise ValueError('rawloc must be defined')
1984 2017
1985 2018 # Locations may define branches via syntax <base>#<branch>.
1986 2019 u = util.url(rawloc)
1987 2020 branch = None
1988 2021 if u.fragment:
1989 2022 branch = u.fragment
1990 2023 u.fragment = None
1991 2024
1992 2025 self.url = u
1993 2026 self.branch = branch
1994 2027
1995 2028 self.name = name
1996 2029 self.rawloc = rawloc
1997 2030 self.loc = '%s' % u
1998 2031
1999 2032 # When given a raw location but not a symbolic name, validate the
2000 2033 # location is valid.
2001 2034 if not name and not u.scheme and not self._isvalidlocalpath(self.loc):
2002 2035 raise ValueError('location is not a URL or path to a local '
2003 2036 'repo: %s' % rawloc)
2004 2037
2005 2038 suboptions = suboptions or {}
2006 2039
2007 2040 # Now process the sub-options. If a sub-option is registered, its
2008 2041 # attribute will always be present. The value will be None if there
2009 2042 # was no valid sub-option.
2010 2043 for suboption, (attr, func) in _pathsuboptions.iteritems():
2011 2044 if suboption not in suboptions:
2012 2045 setattr(self, attr, None)
2013 2046 continue
2014 2047
2015 2048 value = func(ui, self, suboptions[suboption])
2016 2049 setattr(self, attr, value)
2017 2050
2018 2051 def _isvalidlocalpath(self, path):
2019 2052 """Returns True if the given path is a potentially valid repository.
2020 2053 This is its own function so that extensions can change the definition of
2021 2054 'valid' in this case (like when pulling from a git repo into a hg
2022 2055 one)."""
2023 2056 return os.path.isdir(os.path.join(path, '.hg'))
2024 2057
2025 2058 @property
2026 2059 def suboptions(self):
2027 2060 """Return sub-options and their values for this path.
2028 2061
2029 2062 This is intended to be used for presentation purposes.
2030 2063 """
2031 2064 d = {}
2032 2065 for subopt, (attr, _func) in _pathsuboptions.iteritems():
2033 2066 value = getattr(self, attr)
2034 2067 if value is not None:
2035 2068 d[subopt] = value
2036 2069 return d
2037 2070
2038 2071 # we instantiate one globally shared progress bar to avoid
2039 2072 # competing progress bars when multiple UI objects get created
2040 2073 _progresssingleton = None
2041 2074
2042 2075 def getprogbar(ui):
2043 2076 global _progresssingleton
2044 2077 if _progresssingleton is None:
2045 2078 # passing 'ui' object to the singleton is fishy,
2046 2079 # this is how the extension used to work but feel free to rework it.
2047 2080 _progresssingleton = progress.progbar(ui)
2048 2081 return _progresssingleton
2049 2082
2050 2083 def haveprogbar():
2051 2084 return _progresssingleton is not None
2052 2085
2053 2086 def _selectmsgdests(ui):
2054 2087 name = ui.config(b'ui', b'message-output')
2055 2088 if name == b'channel':
2056 2089 if ui.fmsg:
2057 2090 return ui.fmsg, ui.fmsg
2058 2091 else:
2059 2092 # fall back to ferr if channel isn't ready so that status/error
2060 2093 # messages can be printed
2061 2094 return ui.ferr, ui.ferr
2062 2095 if name == b'stdio':
2063 2096 return ui.fout, ui.ferr
2064 2097 if name == b'stderr':
2065 2098 return ui.ferr, ui.ferr
2066 2099 raise error.Abort(b'invalid ui.message-output destination: %s' % name)
2067 2100
2068 2101 def _writemsgwith(write, dest, *args, **opts):
2069 2102 """Write ui message with the given ui._write*() function
2070 2103
2071 2104 The specified message type is translated to 'ui.<type>' label if the dest
2072 2105 isn't a structured channel, so that the message will be colorized.
2073 2106 """
2074 2107 # TODO: maybe change 'type' to a mandatory option
2075 2108 if r'type' in opts and not getattr(dest, 'structured', False):
2076 2109 opts[r'label'] = opts.get(r'label', '') + ' ui.%s' % opts.pop(r'type')
2077 2110 write(dest, *args, **opts)
@@ -1,30 +1,37
1 1 // Copyright 2018 Georges Racinet <gracinet@anybox.fr>
2 2 //
3 3 // This software may be used and distributed according to the terms of the
4 4 // GNU General Public License version 2 or any later version.
5 5 mod ancestors;
6 6 pub mod dagops;
7 7 pub use ancestors::{AncestorsIterator, LazyAncestors, MissingAncestors};
8 8 #[cfg(test)]
9 9 pub mod testing;
10 10
11 11 /// Mercurial revision numbers
12 12 ///
13 13 /// As noted in revlog.c, revision numbers are actually encoded in
14 14 /// 4 bytes, and are liberally converted to ints, whence the i32
15 15 pub type Revision = i32;
16 16
17 17 pub const NULL_REVISION: Revision = -1;
18 18
19 /// Same as `mercurial.node.wdirrev`
20 ///
21 /// This is also equal to `i32::max_value()`, but it's better to spell
22 /// it out explicitely, same as in `mercurial.node`
23 pub const WORKING_DIRECTORY_REVISION: Revision = 0x7fffffff;
24
19 25 /// The simplest expression of what we need of Mercurial DAGs.
20 26 pub trait Graph {
21 27 /// Return the two parents of the given `Revision`.
22 28 ///
23 29 /// Each of the parents can be independently `NULL_REVISION`
24 30 fn parents(&self, Revision) -> Result<[Revision; 2], GraphError>;
25 31 }
26 32
27 33 #[derive(Clone, Debug, PartialEq)]
28 34 pub enum GraphError {
29 35 ParentOutOfRange(Revision),
36 WorkingDirectoryUnsupported,
30 37 }
@@ -1,130 +1,133
1 1 // cindex.rs
2 2 //
3 3 // Copyright 2018 Georges Racinet <gracinet@anybox.fr>
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 //! Bindings to use the Index defined by the parsers C extension
9 9 //!
10 10 //! Ideally, we should use an Index entirely implemented in Rust,
11 11 //! but this will take some time to get there.
12 12 #[cfg(feature = "python27")]
13 13 extern crate python27_sys as python_sys;
14 14 #[cfg(feature = "python3")]
15 15 extern crate python3_sys as python_sys;
16 16
17 17 use self::python_sys::PyCapsule_Import;
18 18 use cpython::{PyClone, PyErr, PyObject, PyResult, Python};
19 use hg::{Graph, GraphError, Revision};
19 use hg::{Graph, GraphError, Revision, WORKING_DIRECTORY_REVISION};
20 20 use libc::c_int;
21 21 use std::ffi::CStr;
22 22 use std::mem::transmute;
23 23
24 24 type IndexParentsFn = unsafe extern "C" fn(
25 25 index: *mut python_sys::PyObject,
26 26 rev: c_int,
27 27 ps: *mut [c_int; 2],
28 28 ) -> c_int;
29 29
30 30 /// A `Graph` backed up by objects and functions from revlog.c
31 31 ///
32 32 /// This implementation of the `Graph` trait, relies on (pointers to)
33 33 /// - the C index object (`index` member)
34 34 /// - the `index_get_parents()` function (`parents` member)
35 35 ///
36 36 /// # Safety
37 37 ///
38 38 /// The C index itself is mutable, and this Rust exposition is **not
39 39 /// protected by the GIL**, meaning that this construct isn't safe with respect
40 40 /// to Python threads.
41 41 ///
42 42 /// All callers of this `Index` must acquire the GIL and must not release it
43 43 /// while working.
44 44 ///
45 45 /// # TODO find a solution to make it GIL safe again.
46 46 ///
47 47 /// This is non trivial, and can wait until we have a clearer picture with
48 48 /// more Rust Mercurial constructs.
49 49 ///
50 50 /// One possibility would be to a `GILProtectedIndex` wrapper enclosing
51 51 /// a `Python<'p>` marker and have it be the one implementing the
52 52 /// `Graph` trait, but this would mean the `Graph` implementor would become
53 53 /// likely to change between subsequent method invocations of the `hg-core`
54 54 /// objects (a serious change of the `hg-core` API):
55 55 /// either exposing ways to mutate the `Graph`, or making it a non persistent
56 56 /// parameter in the relevant methods that need one.
57 57 ///
58 58 /// Another possibility would be to introduce an abstract lock handle into
59 59 /// the core API, that would be tied to `GILGuard` / `Python<'p>`
60 60 /// in the case of the `cpython` crate bindings yet could leave room for other
61 61 /// mechanisms in other contexts.
62 62 pub struct Index {
63 63 index: PyObject,
64 64 parents: IndexParentsFn,
65 65 }
66 66
67 67 impl Index {
68 68 pub fn new(py: Python, index: PyObject) -> PyResult<Self> {
69 69 Ok(Index {
70 70 index: index,
71 71 parents: decapsule_parents_fn(py)?,
72 72 })
73 73 }
74 74 }
75 75
76 76 impl Clone for Index {
77 77 fn clone(&self) -> Self {
78 78 let guard = Python::acquire_gil();
79 79 Index {
80 80 index: self.index.clone_ref(guard.python()),
81 81 parents: self.parents.clone(),
82 82 }
83 83 }
84 84 }
85 85
86 86 impl Graph for Index {
87 87 /// wrap a call to the C extern parents function
88 88 fn parents(&self, rev: Revision) -> Result<[Revision; 2], GraphError> {
89 if rev == WORKING_DIRECTORY_REVISION {
90 return Err(GraphError::WorkingDirectoryUnsupported);
91 }
89 92 let mut res: [c_int; 2] = [0; 2];
90 93 let code = unsafe {
91 94 (self.parents)(
92 95 self.index.as_ptr(),
93 96 rev as c_int,
94 97 &mut res as *mut [c_int; 2],
95 98 )
96 99 };
97 100 match code {
98 101 0 => Ok(res),
99 102 _ => Err(GraphError::ParentOutOfRange(rev)),
100 103 }
101 104 }
102 105 }
103 106
104 107 /// Return the `index_get_parents` function of the parsers C Extension module.
105 108 ///
106 109 /// A pointer to the function is stored in the `parsers` module as a
107 110 /// standard [Python capsule](https://docs.python.org/2/c-api/capsule.html).
108 111 ///
109 112 /// This function retrieves the capsule and casts the function pointer
110 113 ///
111 114 /// Casting function pointers is one of the rare cases of
112 115 /// legitimate use cases of `mem::transmute()` (see
113 116 /// https://doc.rust-lang.org/std/mem/fn.transmute.html of
114 117 /// `mem::transmute()`.
115 118 /// It is inappropriate for architectures where
116 119 /// function and data pointer sizes differ (so-called "Harvard
117 120 /// architectures"), but these are nowadays mostly DSPs
118 121 /// and microcontrollers, hence out of our scope.
119 122 fn decapsule_parents_fn(py: Python) -> PyResult<IndexParentsFn> {
120 123 unsafe {
121 124 let caps_name = CStr::from_bytes_with_nul_unchecked(
122 125 b"mercurial.cext.parsers.index_get_parents_CAPI\0",
123 126 );
124 127 let from_caps = PyCapsule_Import(caps_name.as_ptr(), 0);
125 128 if from_caps.is_null() {
126 129 return Err(PyErr::fetch(py));
127 130 }
128 131 Ok(transmute(from_caps))
129 132 }
130 133 }
@@ -1,27 +1,38
1 1 // ancestors.rs
2 2 //
3 3 // Copyright 2018 Georges Racinet <gracinet@anybox.fr>
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 //! Bindings for Rust errors
9 9 //!
10 10 //! [`GraphError`] exposes `hg::GraphError` as a subclass of `ValueError`
11 //! but some variants of `hg::GraphError` can be converted directly to other
12 //! existing Python exceptions if appropriate.
11 13 //!
12 14 //! [`GraphError`]: struct.GraphError.html
13 15 use cpython::exc::ValueError;
14 16 use cpython::{PyErr, Python};
15 17 use hg;
16 18
17 19 py_exception!(rustext, GraphError, ValueError);
18 20
19 21 impl GraphError {
20 22 pub fn pynew(py: Python, inner: hg::GraphError) -> PyErr {
21 23 match inner {
22 24 hg::GraphError::ParentOutOfRange(r) => {
23 25 GraphError::new(py, ("ParentOutOfRange", r))
24 26 }
27 hg::GraphError::WorkingDirectoryUnsupported => {
28 match py
29 .import("mercurial.error")
30 .and_then(|m| m.get(py, "WdirUnsupported"))
31 {
32 Err(e) => e,
33 Ok(cls) => PyErr::from_instance(py, cls),
34 }
35 }
25 36 }
26 37 }
27 38 }
@@ -1,1290 +1,1293
1 1 #testcases sshv1 sshv2
2 2
3 3 #if sshv2
4 4 $ cat >> $HGRCPATH << EOF
5 5 > [experimental]
6 6 > sshpeer.advertise-v2 = true
7 7 > sshserver.support-v2 = true
8 8 > EOF
9 9 #endif
10 10
11 11 Prepare repo a:
12 12
13 13 $ hg init a
14 14 $ cd a
15 15 $ echo a > a
16 16 $ hg add a
17 17 $ hg commit -m test
18 18 $ echo first line > b
19 19 $ hg add b
20 20
21 21 Create a non-inlined filelog:
22 22
23 23 $ "$PYTHON" -c 'open("data1", "wb").write(b"".join(b"%d\n" % x for x in range(10000)))'
24 24 $ for j in 0 1 2 3 4 5 6 7 8 9; do
25 25 > cat data1 >> b
26 26 > hg commit -m test
27 27 > done
28 28
29 29 List files in store/data (should show a 'b.d'):
30 30
31 31 #if reporevlogstore
32 32 $ for i in .hg/store/data/*; do
33 33 > echo $i
34 34 > done
35 35 .hg/store/data/a.i
36 36 .hg/store/data/b.d
37 37 .hg/store/data/b.i
38 38 #endif
39 39
40 40 Trigger branchcache creation:
41 41
42 42 $ hg branches
43 43 default 10:a7949464abda
44 44 $ ls .hg/cache
45 45 branch2-served
46 46 manifestfulltextcache (reporevlogstore !)
47 47 rbc-names-v1
48 48 rbc-revs-v1
49 49
50 50 Default operation:
51 51
52 52 $ hg clone . ../b
53 53 updating to branch default
54 54 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
55 55 $ cd ../b
56 56
57 57 Ensure branchcache got copied over:
58 58
59 59 $ ls .hg/cache
60 60 branch2-served
61 61 rbc-names-v1
62 62 rbc-revs-v1
63 63
64 64 $ cat a
65 65 a
66 66 $ hg verify
67 67 checking changesets
68 68 checking manifests
69 69 crosschecking files in changesets and manifests
70 70 checking files
71 71 checked 11 changesets with 11 changes to 2 files
72 72
73 73 Invalid dest '' must abort:
74 74
75 75 $ hg clone . ''
76 76 abort: empty destination path is not valid
77 77 [255]
78 78
79 79 No update, with debug option:
80 80
81 81 #if hardlink
82 82 $ hg --debug clone -U . ../c --config progress.debug=true
83 83 linking: 1 files
84 84 linking: 2 files
85 85 linking: 3 files
86 86 linking: 4 files
87 87 linking: 5 files
88 88 linking: 6 files
89 89 linking: 7 files
90 90 linking: 8 files
91 91 linked 8 files (reporevlogstore !)
92 92 linking: 9 files (reposimplestore !)
93 93 linking: 10 files (reposimplestore !)
94 94 linking: 11 files (reposimplestore !)
95 95 linking: 12 files (reposimplestore !)
96 96 linking: 13 files (reposimplestore !)
97 97 linking: 14 files (reposimplestore !)
98 98 linking: 15 files (reposimplestore !)
99 99 linking: 16 files (reposimplestore !)
100 100 linking: 17 files (reposimplestore !)
101 101 linking: 18 files (reposimplestore !)
102 102 linked 18 files (reposimplestore !)
103 103 #else
104 104 $ hg --debug clone -U . ../c --config progress.debug=true
105 105 linking: 1 files
106 106 copying: 2 files
107 107 copying: 3 files
108 108 copying: 4 files
109 109 copying: 5 files
110 110 copying: 6 files
111 111 copying: 7 files
112 112 copying: 8 files
113 113 copied 8 files (reporevlogstore !)
114 114 copying: 9 files (reposimplestore !)
115 115 copying: 10 files (reposimplestore !)
116 116 copying: 11 files (reposimplestore !)
117 117 copying: 12 files (reposimplestore !)
118 118 copying: 13 files (reposimplestore !)
119 119 copying: 14 files (reposimplestore !)
120 120 copying: 15 files (reposimplestore !)
121 121 copying: 16 files (reposimplestore !)
122 122 copying: 17 files (reposimplestore !)
123 123 copying: 18 files (reposimplestore !)
124 124 copied 18 files (reposimplestore !)
125 125 #endif
126 126 $ cd ../c
127 127
128 128 Ensure branchcache got copied over:
129 129
130 130 $ ls .hg/cache
131 131 branch2-served
132 132 rbc-names-v1
133 133 rbc-revs-v1
134 134
135 135 $ cat a 2>/dev/null || echo "a not present"
136 136 a not present
137 137 $ hg verify
138 138 checking changesets
139 139 checking manifests
140 140 crosschecking files in changesets and manifests
141 141 checking files
142 142 checked 11 changesets with 11 changes to 2 files
143 143
144 144 Default destination:
145 145
146 146 $ mkdir ../d
147 147 $ cd ../d
148 148 $ hg clone ../a
149 149 destination directory: a
150 150 updating to branch default
151 151 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
152 152 $ cd a
153 153 $ hg cat a
154 154 a
155 155 $ cd ../..
156 156
157 157 Check that we drop the 'file:' from the path before writing the .hgrc:
158 158
159 159 $ hg clone file:a e
160 160 updating to branch default
161 161 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
162 162 $ grep 'file:' e/.hg/hgrc
163 163 [1]
164 164
165 165 Check that path aliases are expanded:
166 166
167 167 $ hg clone -q -U --config 'paths.foobar=a#0' foobar f
168 168 $ hg -R f showconfig paths.default
169 169 $TESTTMP/a#0
170 170
171 171 Use --pull:
172 172
173 173 $ hg clone --pull a g
174 174 requesting all changes
175 175 adding changesets
176 176 adding manifests
177 177 adding file changes
178 178 added 11 changesets with 11 changes to 2 files
179 179 new changesets acb14030fe0a:a7949464abda
180 180 updating to branch default
181 181 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
182 182 $ hg -R g verify
183 183 checking changesets
184 184 checking manifests
185 185 crosschecking files in changesets and manifests
186 186 checking files
187 187 checked 11 changesets with 11 changes to 2 files
188 188
189 189 Invalid dest '' with --pull must abort (issue2528):
190 190
191 191 $ hg clone --pull a ''
192 192 abort: empty destination path is not valid
193 193 [255]
194 194
195 195 Clone to '.':
196 196
197 197 $ mkdir h
198 198 $ cd h
199 199 $ hg clone ../a .
200 200 updating to branch default
201 201 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
202 202 $ cd ..
203 203
204 204
205 205 *** Tests for option -u ***
206 206
207 207 Adding some more history to repo a:
208 208
209 209 $ cd a
210 210 $ hg tag ref1
211 211 $ echo the quick brown fox >a
212 212 $ hg ci -m "hacked default"
213 213 $ hg up ref1
214 214 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
215 215 $ hg branch stable
216 216 marked working directory as branch stable
217 217 (branches are permanent and global, did you want a bookmark?)
218 218 $ echo some text >a
219 219 $ hg ci -m "starting branch stable"
220 220 $ hg tag ref2
221 221 $ echo some more text >a
222 222 $ hg ci -m "another change for branch stable"
223 223 $ hg up ref2
224 224 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
225 225 $ hg parents
226 226 changeset: 13:e8ece76546a6
227 227 branch: stable
228 228 tag: ref2
229 229 parent: 10:a7949464abda
230 230 user: test
231 231 date: Thu Jan 01 00:00:00 1970 +0000
232 232 summary: starting branch stable
233 233
234 234
235 235 Repo a has two heads:
236 236
237 237 $ hg heads
238 238 changeset: 15:0aae7cf88f0d
239 239 branch: stable
240 240 tag: tip
241 241 user: test
242 242 date: Thu Jan 01 00:00:00 1970 +0000
243 243 summary: another change for branch stable
244 244
245 245 changeset: 12:f21241060d6a
246 246 user: test
247 247 date: Thu Jan 01 00:00:00 1970 +0000
248 248 summary: hacked default
249 249
250 250
251 251 $ cd ..
252 252
253 253
254 254 Testing --noupdate with --updaterev (must abort):
255 255
256 256 $ hg clone --noupdate --updaterev 1 a ua
257 257 abort: cannot specify both --noupdate and --updaterev
258 258 [255]
259 259
260 260
261 261 Testing clone -u:
262 262
263 263 $ hg clone -u . a ua
264 264 updating to branch stable
265 265 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
266 266
267 267 Repo ua has both heads:
268 268
269 269 $ hg -R ua heads
270 270 changeset: 15:0aae7cf88f0d
271 271 branch: stable
272 272 tag: tip
273 273 user: test
274 274 date: Thu Jan 01 00:00:00 1970 +0000
275 275 summary: another change for branch stable
276 276
277 277 changeset: 12:f21241060d6a
278 278 user: test
279 279 date: Thu Jan 01 00:00:00 1970 +0000
280 280 summary: hacked default
281 281
282 282
283 283 Same revision checked out in repo a and ua:
284 284
285 285 $ hg -R a parents --template "{node|short}\n"
286 286 e8ece76546a6
287 287 $ hg -R ua parents --template "{node|short}\n"
288 288 e8ece76546a6
289 289
290 290 $ rm -r ua
291 291
292 292
293 293 Testing clone --pull -u:
294 294
295 295 $ hg clone --pull -u . a ua
296 296 requesting all changes
297 297 adding changesets
298 298 adding manifests
299 299 adding file changes
300 300 added 16 changesets with 16 changes to 3 files (+1 heads)
301 301 new changesets acb14030fe0a:0aae7cf88f0d
302 302 updating to branch stable
303 303 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
304 304
305 305 Repo ua has both heads:
306 306
307 307 $ hg -R ua heads
308 308 changeset: 15:0aae7cf88f0d
309 309 branch: stable
310 310 tag: tip
311 311 user: test
312 312 date: Thu Jan 01 00:00:00 1970 +0000
313 313 summary: another change for branch stable
314 314
315 315 changeset: 12:f21241060d6a
316 316 user: test
317 317 date: Thu Jan 01 00:00:00 1970 +0000
318 318 summary: hacked default
319 319
320 320
321 321 Same revision checked out in repo a and ua:
322 322
323 323 $ hg -R a parents --template "{node|short}\n"
324 324 e8ece76546a6
325 325 $ hg -R ua parents --template "{node|short}\n"
326 326 e8ece76546a6
327 327
328 328 $ rm -r ua
329 329
330 330
331 331 Testing clone -u <branch>:
332 332
333 333 $ hg clone -u stable a ua
334 334 updating to branch stable
335 335 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
336 336
337 337 Repo ua has both heads:
338 338
339 339 $ hg -R ua heads
340 340 changeset: 15:0aae7cf88f0d
341 341 branch: stable
342 342 tag: tip
343 343 user: test
344 344 date: Thu Jan 01 00:00:00 1970 +0000
345 345 summary: another change for branch stable
346 346
347 347 changeset: 12:f21241060d6a
348 348 user: test
349 349 date: Thu Jan 01 00:00:00 1970 +0000
350 350 summary: hacked default
351 351
352 352
353 353 Branch 'stable' is checked out:
354 354
355 355 $ hg -R ua parents
356 356 changeset: 15:0aae7cf88f0d
357 357 branch: stable
358 358 tag: tip
359 359 user: test
360 360 date: Thu Jan 01 00:00:00 1970 +0000
361 361 summary: another change for branch stable
362 362
363 363
364 364 $ rm -r ua
365 365
366 366
367 367 Testing default checkout:
368 368
369 369 $ hg clone a ua
370 370 updating to branch default
371 371 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
372 372
373 373 Repo ua has both heads:
374 374
375 375 $ hg -R ua heads
376 376 changeset: 15:0aae7cf88f0d
377 377 branch: stable
378 378 tag: tip
379 379 user: test
380 380 date: Thu Jan 01 00:00:00 1970 +0000
381 381 summary: another change for branch stable
382 382
383 383 changeset: 12:f21241060d6a
384 384 user: test
385 385 date: Thu Jan 01 00:00:00 1970 +0000
386 386 summary: hacked default
387 387
388 388
389 389 Branch 'default' is checked out:
390 390
391 391 $ hg -R ua parents
392 392 changeset: 12:f21241060d6a
393 393 user: test
394 394 date: Thu Jan 01 00:00:00 1970 +0000
395 395 summary: hacked default
396 396
397 397 Test clone with a branch named "@" (issue3677)
398 398
399 399 $ hg -R ua branch @
400 400 marked working directory as branch @
401 401 $ hg -R ua commit -m 'created branch @'
402 402 $ hg clone ua atbranch
403 403 updating to branch default
404 404 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
405 405 $ hg -R atbranch heads
406 406 changeset: 16:798b6d97153e
407 407 branch: @
408 408 tag: tip
409 409 parent: 12:f21241060d6a
410 410 user: test
411 411 date: Thu Jan 01 00:00:00 1970 +0000
412 412 summary: created branch @
413 413
414 414 changeset: 15:0aae7cf88f0d
415 415 branch: stable
416 416 user: test
417 417 date: Thu Jan 01 00:00:00 1970 +0000
418 418 summary: another change for branch stable
419 419
420 420 changeset: 12:f21241060d6a
421 421 user: test
422 422 date: Thu Jan 01 00:00:00 1970 +0000
423 423 summary: hacked default
424 424
425 425 $ hg -R atbranch parents
426 426 changeset: 12:f21241060d6a
427 427 user: test
428 428 date: Thu Jan 01 00:00:00 1970 +0000
429 429 summary: hacked default
430 430
431 431
432 432 $ rm -r ua atbranch
433 433
434 434
435 435 Testing #<branch>:
436 436
437 437 $ hg clone -u . a#stable ua
438 438 adding changesets
439 439 adding manifests
440 440 adding file changes
441 441 added 14 changesets with 14 changes to 3 files
442 442 new changesets acb14030fe0a:0aae7cf88f0d
443 443 updating to branch stable
444 444 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
445 445
446 446 Repo ua has branch 'stable' and 'default' (was changed in fd511e9eeea6):
447 447
448 448 $ hg -R ua heads
449 449 changeset: 13:0aae7cf88f0d
450 450 branch: stable
451 451 tag: tip
452 452 user: test
453 453 date: Thu Jan 01 00:00:00 1970 +0000
454 454 summary: another change for branch stable
455 455
456 456 changeset: 10:a7949464abda
457 457 user: test
458 458 date: Thu Jan 01 00:00:00 1970 +0000
459 459 summary: test
460 460
461 461
462 462 Same revision checked out in repo a and ua:
463 463
464 464 $ hg -R a parents --template "{node|short}\n"
465 465 e8ece76546a6
466 466 $ hg -R ua parents --template "{node|short}\n"
467 467 e8ece76546a6
468 468
469 469 $ rm -r ua
470 470
471 471
472 472 Testing -u -r <branch>:
473 473
474 474 $ hg clone -u . -r stable a ua
475 475 adding changesets
476 476 adding manifests
477 477 adding file changes
478 478 added 14 changesets with 14 changes to 3 files
479 479 new changesets acb14030fe0a:0aae7cf88f0d
480 480 updating to branch stable
481 481 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
482 482
483 483 Repo ua has branch 'stable' and 'default' (was changed in fd511e9eeea6):
484 484
485 485 $ hg -R ua heads
486 486 changeset: 13:0aae7cf88f0d
487 487 branch: stable
488 488 tag: tip
489 489 user: test
490 490 date: Thu Jan 01 00:00:00 1970 +0000
491 491 summary: another change for branch stable
492 492
493 493 changeset: 10:a7949464abda
494 494 user: test
495 495 date: Thu Jan 01 00:00:00 1970 +0000
496 496 summary: test
497 497
498 498
499 499 Same revision checked out in repo a and ua:
500 500
501 501 $ hg -R a parents --template "{node|short}\n"
502 502 e8ece76546a6
503 503 $ hg -R ua parents --template "{node|short}\n"
504 504 e8ece76546a6
505 505
506 506 $ rm -r ua
507 507
508 508
509 509 Testing -r <branch>:
510 510
511 511 $ hg clone -r stable a ua
512 512 adding changesets
513 513 adding manifests
514 514 adding file changes
515 515 added 14 changesets with 14 changes to 3 files
516 516 new changesets acb14030fe0a:0aae7cf88f0d
517 517 updating to branch stable
518 518 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
519 519
520 520 Repo ua has branch 'stable' and 'default' (was changed in fd511e9eeea6):
521 521
522 522 $ hg -R ua heads
523 523 changeset: 13:0aae7cf88f0d
524 524 branch: stable
525 525 tag: tip
526 526 user: test
527 527 date: Thu Jan 01 00:00:00 1970 +0000
528 528 summary: another change for branch stable
529 529
530 530 changeset: 10:a7949464abda
531 531 user: test
532 532 date: Thu Jan 01 00:00:00 1970 +0000
533 533 summary: test
534 534
535 535
536 536 Branch 'stable' is checked out:
537 537
538 538 $ hg -R ua parents
539 539 changeset: 13:0aae7cf88f0d
540 540 branch: stable
541 541 tag: tip
542 542 user: test
543 543 date: Thu Jan 01 00:00:00 1970 +0000
544 544 summary: another change for branch stable
545 545
546 546
547 547 $ rm -r ua
548 548
549 549
550 550 Issue2267: Error in 1.6 hg.py: TypeError: 'NoneType' object is not
551 551 iterable in addbranchrevs()
552 552
553 553 $ cat <<EOF > simpleclone.py
554 554 > from mercurial import hg, ui as uimod
555 555 > myui = uimod.ui.load()
556 556 > repo = hg.repository(myui, b'a')
557 557 > hg.clone(myui, {}, repo, dest=b"ua")
558 558 > EOF
559 559
560 560 $ "$PYTHON" simpleclone.py
561 561 updating to branch default
562 562 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
563 563
564 564 $ rm -r ua
565 565
566 566 $ cat <<EOF > branchclone.py
567 567 > from mercurial import extensions, hg, ui as uimod
568 568 > myui = uimod.ui.load()
569 569 > extensions.loadall(myui)
570 570 > extensions.populateui(myui)
571 571 > repo = hg.repository(myui, b'a')
572 572 > hg.clone(myui, {}, repo, dest=b"ua", branch=[b"stable",])
573 573 > EOF
574 574
575 575 $ "$PYTHON" branchclone.py
576 576 adding changesets
577 577 adding manifests
578 578 adding file changes
579 579 added 14 changesets with 14 changes to 3 files
580 580 new changesets acb14030fe0a:0aae7cf88f0d
581 581 updating to branch stable
582 582 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
583 583 $ rm -r ua
584 584
585 585
586 586 Test clone with special '@' bookmark:
587 587 $ cd a
588 588 $ hg bookmark -r a7949464abda @ # branch point of stable from default
589 589 $ hg clone . ../i
590 590 updating to bookmark @
591 591 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
592 592 $ hg id -i ../i
593 593 a7949464abda
594 594 $ rm -r ../i
595 595
596 596 $ hg bookmark -f -r stable @
597 597 $ hg bookmarks
598 598 @ 15:0aae7cf88f0d
599 599 $ hg clone . ../i
600 600 updating to bookmark @ on branch stable
601 601 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
602 602 $ hg id -i ../i
603 603 0aae7cf88f0d
604 604 $ cd "$TESTTMP"
605 605
606 606
607 607 Testing failures:
608 608
609 609 $ mkdir fail
610 610 $ cd fail
611 611
612 612 No local source
613 613
614 614 $ hg clone a b
615 615 abort: repository a not found!
616 616 [255]
617 617
618 618 No remote source
619 619
620 620 #if windows
621 621 $ hg clone http://$LOCALIP:3121/a b
622 622 abort: error: * (glob)
623 623 [255]
624 624 #else
625 625 $ hg clone http://$LOCALIP:3121/a b
626 626 abort: error: *refused* (glob)
627 627 [255]
628 628 #endif
629 629 $ rm -rf b # work around bug with http clone
630 630
631 631
632 632 #if unix-permissions no-root
633 633
634 634 Inaccessible source
635 635
636 636 $ mkdir a
637 637 $ chmod 000 a
638 638 $ hg clone a b
639 639 abort: Permission denied: *$TESTTMP/fail/a/.hg* (glob)
640 640 [255]
641 641
642 642 Inaccessible destination
643 643
644 644 $ hg init b
645 645 $ cd b
646 646 $ hg clone . ../a
647 647 abort: Permission denied: *../a* (glob)
648 648 [255]
649 649 $ cd ..
650 650 $ chmod 700 a
651 651 $ rm -r a b
652 652
653 653 #endif
654 654
655 655
656 656 #if fifo
657 657
658 658 Source of wrong type
659 659
660 660 $ mkfifo a
661 661 $ hg clone a b
662 662 abort: $ENOTDIR$: *$TESTTMP/fail/a/.hg* (glob)
663 663 [255]
664 664 $ rm a
665 665
666 666 #endif
667 667
668 668 Default destination, same directory
669 669
670 670 $ hg init q
671 671 $ hg clone q
672 672 destination directory: q
673 673 abort: destination 'q' is not empty
674 674 [255]
675 675
676 676 destination directory not empty
677 677
678 678 $ mkdir a
679 679 $ echo stuff > a/a
680 680 $ hg clone q a
681 681 abort: destination 'a' is not empty
682 682 [255]
683 683
684 684
685 685 #if unix-permissions no-root
686 686
687 687 leave existing directory in place after clone failure
688 688
689 689 $ hg init c
690 690 $ cd c
691 691 $ echo c > c
692 692 $ hg commit -A -m test
693 693 adding c
694 694 $ chmod -rx .hg/store/data
695 695 $ cd ..
696 696 $ mkdir d
697 697 $ hg clone c d 2> err
698 698 [255]
699 699 $ test -d d
700 700 $ test -d d/.hg
701 701 [1]
702 702
703 703 re-enable perm to allow deletion
704 704
705 705 $ chmod +rx c/.hg/store/data
706 706
707 707 #endif
708 708
709 709 $ cd ..
710 710
711 711 Test clone from the repository in (emulated) revlog format 0 (issue4203):
712 712
713 713 $ mkdir issue4203
714 714 $ mkdir -p src/.hg
715 715 $ echo foo > src/foo
716 716 $ hg -R src add src/foo
717 717 $ hg -R src commit -m '#0'
718 718 $ hg -R src log -q
719 719 0:e1bab28bca43
720 $ hg -R src debugrevlog -c | egrep 'format|flags'
721 format : 0
722 flags : (none)
720 723 $ hg clone -U -q src dst
721 724 $ hg -R dst log -q
722 725 0:e1bab28bca43
723 726
724 727 Create repositories to test auto sharing functionality
725 728
726 729 $ cat >> $HGRCPATH << EOF
727 730 > [extensions]
728 731 > share=
729 732 > EOF
730 733
731 734 $ hg init empty
732 735 $ hg init source1a
733 736 $ cd source1a
734 737 $ echo initial1 > foo
735 738 $ hg -q commit -A -m initial
736 739 $ echo second > foo
737 740 $ hg commit -m second
738 741 $ cd ..
739 742
740 743 $ hg init filteredrev0
741 744 $ cd filteredrev0
742 745 $ cat >> .hg/hgrc << EOF
743 746 > [experimental]
744 747 > evolution.createmarkers=True
745 748 > EOF
746 749 $ echo initial1 > foo
747 750 $ hg -q commit -A -m initial0
748 751 $ hg -q up -r null
749 752 $ echo initial2 > foo
750 753 $ hg -q commit -A -m initial1
751 754 $ hg debugobsolete c05d5c47a5cf81401869999f3d05f7d699d2b29a e082c1832e09a7d1e78b7fd49a592d372de854c8
752 755 obsoleted 1 changesets
753 756 $ cd ..
754 757
755 758 $ hg -q clone --pull source1a source1b
756 759 $ cd source1a
757 760 $ hg bookmark bookA
758 761 $ echo 1a > foo
759 762 $ hg commit -m 1a
760 763 $ cd ../source1b
761 764 $ hg -q up -r 0
762 765 $ echo head1 > foo
763 766 $ hg commit -m head1
764 767 created new head
765 768 $ hg bookmark head1
766 769 $ hg -q up -r 0
767 770 $ echo head2 > foo
768 771 $ hg commit -m head2
769 772 created new head
770 773 $ hg bookmark head2
771 774 $ hg -q up -r 0
772 775 $ hg branch branch1
773 776 marked working directory as branch branch1
774 777 (branches are permanent and global, did you want a bookmark?)
775 778 $ echo branch1 > foo
776 779 $ hg commit -m branch1
777 780 $ hg -q up -r 0
778 781 $ hg branch branch2
779 782 marked working directory as branch branch2
780 783 $ echo branch2 > foo
781 784 $ hg commit -m branch2
782 785 $ cd ..
783 786 $ hg init source2
784 787 $ cd source2
785 788 $ echo initial2 > foo
786 789 $ hg -q commit -A -m initial2
787 790 $ echo second > foo
788 791 $ hg commit -m second
789 792 $ cd ..
790 793
791 794 Clone with auto share from an empty repo should not result in share
792 795
793 796 $ mkdir share
794 797 $ hg --config share.pool=share clone empty share-empty
795 798 (not using pooled storage: remote appears to be empty)
796 799 updating to branch default
797 800 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
798 801 $ ls share
799 802 $ test -d share-empty/.hg/store
800 803 $ test -f share-empty/.hg/sharedpath
801 804 [1]
802 805
803 806 Clone with auto share from a repo with filtered revision 0 should not result in share
804 807
805 808 $ hg --config share.pool=share clone filteredrev0 share-filtered
806 809 (not using pooled storage: unable to resolve identity of remote)
807 810 requesting all changes
808 811 adding changesets
809 812 adding manifests
810 813 adding file changes
811 814 added 1 changesets with 1 changes to 1 files
812 815 new changesets e082c1832e09
813 816 updating to branch default
814 817 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
815 818
816 819 Clone from repo with content should result in shared store being created
817 820
818 821 $ hg --config share.pool=share clone source1a share-dest1a
819 822 (sharing from new pooled repository b5f04eac9d8f7a6a9fcb070243cccea7dc5ea0c1)
820 823 requesting all changes
821 824 adding changesets
822 825 adding manifests
823 826 adding file changes
824 827 added 3 changesets with 3 changes to 1 files
825 828 new changesets b5f04eac9d8f:e5bfe23c0b47
826 829 searching for changes
827 830 no changes found
828 831 adding remote bookmark bookA
829 832 updating working directory
830 833 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
831 834
832 835 The shared repo should have been created
833 836
834 837 $ ls share
835 838 b5f04eac9d8f7a6a9fcb070243cccea7dc5ea0c1
836 839
837 840 The destination should point to it
838 841
839 842 $ cat share-dest1a/.hg/sharedpath; echo
840 843 $TESTTMP/share/b5f04eac9d8f7a6a9fcb070243cccea7dc5ea0c1/.hg
841 844
842 845 The destination should have bookmarks
843 846
844 847 $ hg -R share-dest1a bookmarks
845 848 bookA 2:e5bfe23c0b47
846 849
847 850 The default path should be the remote, not the share
848 851
849 852 $ hg -R share-dest1a config paths.default
850 853 $TESTTMP/source1a
851 854
852 855 Clone with existing share dir should result in pull + share
853 856
854 857 $ hg --config share.pool=share clone source1b share-dest1b
855 858 (sharing from existing pooled repository b5f04eac9d8f7a6a9fcb070243cccea7dc5ea0c1)
856 859 searching for changes
857 860 adding changesets
858 861 adding manifests
859 862 adding file changes
860 863 added 4 changesets with 4 changes to 1 files (+4 heads)
861 864 adding remote bookmark head1
862 865 adding remote bookmark head2
863 866 new changesets 4a8dc1ab4c13:6bacf4683960
864 867 updating working directory
865 868 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
866 869
867 870 $ ls share
868 871 b5f04eac9d8f7a6a9fcb070243cccea7dc5ea0c1
869 872
870 873 $ cat share-dest1b/.hg/sharedpath; echo
871 874 $TESTTMP/share/b5f04eac9d8f7a6a9fcb070243cccea7dc5ea0c1/.hg
872 875
873 876 We only get bookmarks from the remote, not everything in the share
874 877
875 878 $ hg -R share-dest1b bookmarks
876 879 head1 3:4a8dc1ab4c13
877 880 head2 4:99f71071f117
878 881
879 882 Default path should be source, not share.
880 883
881 884 $ hg -R share-dest1b config paths.default
882 885 $TESTTMP/source1b
883 886
884 887 Checked out revision should be head of default branch
885 888
886 889 $ hg -R share-dest1b log -r .
887 890 changeset: 4:99f71071f117
888 891 bookmark: head2
889 892 parent: 0:b5f04eac9d8f
890 893 user: test
891 894 date: Thu Jan 01 00:00:00 1970 +0000
892 895 summary: head2
893 896
894 897
895 898 Clone from unrelated repo should result in new share
896 899
897 900 $ hg --config share.pool=share clone source2 share-dest2
898 901 (sharing from new pooled repository 22aeff664783fd44c6d9b435618173c118c3448e)
899 902 requesting all changes
900 903 adding changesets
901 904 adding manifests
902 905 adding file changes
903 906 added 2 changesets with 2 changes to 1 files
904 907 new changesets 22aeff664783:63cf6c3dba4a
905 908 searching for changes
906 909 no changes found
907 910 updating working directory
908 911 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
909 912
910 913 $ ls share
911 914 22aeff664783fd44c6d9b435618173c118c3448e
912 915 b5f04eac9d8f7a6a9fcb070243cccea7dc5ea0c1
913 916
914 917 remote naming mode works as advertised
915 918
916 919 $ hg --config share.pool=shareremote --config share.poolnaming=remote clone source1a share-remote1a
917 920 (sharing from new pooled repository 195bb1fcdb595c14a6c13e0269129ed78f6debde)
918 921 requesting all changes
919 922 adding changesets
920 923 adding manifests
921 924 adding file changes
922 925 added 3 changesets with 3 changes to 1 files
923 926 new changesets b5f04eac9d8f:e5bfe23c0b47
924 927 searching for changes
925 928 no changes found
926 929 adding remote bookmark bookA
927 930 updating working directory
928 931 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
929 932
930 933 $ ls shareremote
931 934 195bb1fcdb595c14a6c13e0269129ed78f6debde
932 935
933 936 $ hg --config share.pool=shareremote --config share.poolnaming=remote clone source1b share-remote1b
934 937 (sharing from new pooled repository c0d4f83847ca2a873741feb7048a45085fd47c46)
935 938 requesting all changes
936 939 adding changesets
937 940 adding manifests
938 941 adding file changes
939 942 added 6 changesets with 6 changes to 1 files (+4 heads)
940 943 new changesets b5f04eac9d8f:6bacf4683960
941 944 searching for changes
942 945 no changes found
943 946 adding remote bookmark head1
944 947 adding remote bookmark head2
945 948 updating working directory
946 949 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
947 950
948 951 $ ls shareremote
949 952 195bb1fcdb595c14a6c13e0269129ed78f6debde
950 953 c0d4f83847ca2a873741feb7048a45085fd47c46
951 954
952 955 request to clone a single revision is respected in sharing mode
953 956
954 957 $ hg --config share.pool=sharerevs clone -r 4a8dc1ab4c13 source1b share-1arev
955 958 (sharing from new pooled repository b5f04eac9d8f7a6a9fcb070243cccea7dc5ea0c1)
956 959 adding changesets
957 960 adding manifests
958 961 adding file changes
959 962 added 2 changesets with 2 changes to 1 files
960 963 new changesets b5f04eac9d8f:4a8dc1ab4c13
961 964 no changes found
962 965 adding remote bookmark head1
963 966 updating working directory
964 967 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
965 968
966 969 $ hg -R share-1arev log -G
967 970 @ changeset: 1:4a8dc1ab4c13
968 971 | bookmark: head1
969 972 | tag: tip
970 973 | user: test
971 974 | date: Thu Jan 01 00:00:00 1970 +0000
972 975 | summary: head1
973 976 |
974 977 o changeset: 0:b5f04eac9d8f
975 978 user: test
976 979 date: Thu Jan 01 00:00:00 1970 +0000
977 980 summary: initial
978 981
979 982
980 983 making another clone should only pull down requested rev
981 984
982 985 $ hg --config share.pool=sharerevs clone -r 99f71071f117 source1b share-1brev
983 986 (sharing from existing pooled repository b5f04eac9d8f7a6a9fcb070243cccea7dc5ea0c1)
984 987 searching for changes
985 988 adding changesets
986 989 adding manifests
987 990 adding file changes
988 991 added 1 changesets with 1 changes to 1 files (+1 heads)
989 992 adding remote bookmark head1
990 993 adding remote bookmark head2
991 994 new changesets 99f71071f117
992 995 updating working directory
993 996 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
994 997
995 998 $ hg -R share-1brev log -G
996 999 @ changeset: 2:99f71071f117
997 1000 | bookmark: head2
998 1001 | tag: tip
999 1002 | parent: 0:b5f04eac9d8f
1000 1003 | user: test
1001 1004 | date: Thu Jan 01 00:00:00 1970 +0000
1002 1005 | summary: head2
1003 1006 |
1004 1007 | o changeset: 1:4a8dc1ab4c13
1005 1008 |/ bookmark: head1
1006 1009 | user: test
1007 1010 | date: Thu Jan 01 00:00:00 1970 +0000
1008 1011 | summary: head1
1009 1012 |
1010 1013 o changeset: 0:b5f04eac9d8f
1011 1014 user: test
1012 1015 date: Thu Jan 01 00:00:00 1970 +0000
1013 1016 summary: initial
1014 1017
1015 1018
1016 1019 Request to clone a single branch is respected in sharing mode
1017 1020
1018 1021 $ hg --config share.pool=sharebranch clone -b branch1 source1b share-1bbranch1
1019 1022 (sharing from new pooled repository b5f04eac9d8f7a6a9fcb070243cccea7dc5ea0c1)
1020 1023 adding changesets
1021 1024 adding manifests
1022 1025 adding file changes
1023 1026 added 2 changesets with 2 changes to 1 files
1024 1027 new changesets b5f04eac9d8f:5f92a6c1a1b1
1025 1028 no changes found
1026 1029 updating working directory
1027 1030 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1028 1031
1029 1032 $ hg -R share-1bbranch1 log -G
1030 1033 o changeset: 1:5f92a6c1a1b1
1031 1034 | branch: branch1
1032 1035 | tag: tip
1033 1036 | user: test
1034 1037 | date: Thu Jan 01 00:00:00 1970 +0000
1035 1038 | summary: branch1
1036 1039 |
1037 1040 @ changeset: 0:b5f04eac9d8f
1038 1041 user: test
1039 1042 date: Thu Jan 01 00:00:00 1970 +0000
1040 1043 summary: initial
1041 1044
1042 1045
1043 1046 $ hg --config share.pool=sharebranch clone -b branch2 source1b share-1bbranch2
1044 1047 (sharing from existing pooled repository b5f04eac9d8f7a6a9fcb070243cccea7dc5ea0c1)
1045 1048 searching for changes
1046 1049 adding changesets
1047 1050 adding manifests
1048 1051 adding file changes
1049 1052 added 1 changesets with 1 changes to 1 files (+1 heads)
1050 1053 new changesets 6bacf4683960
1051 1054 updating working directory
1052 1055 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1053 1056
1054 1057 $ hg -R share-1bbranch2 log -G
1055 1058 o changeset: 2:6bacf4683960
1056 1059 | branch: branch2
1057 1060 | tag: tip
1058 1061 | parent: 0:b5f04eac9d8f
1059 1062 | user: test
1060 1063 | date: Thu Jan 01 00:00:00 1970 +0000
1061 1064 | summary: branch2
1062 1065 |
1063 1066 | o changeset: 1:5f92a6c1a1b1
1064 1067 |/ branch: branch1
1065 1068 | user: test
1066 1069 | date: Thu Jan 01 00:00:00 1970 +0000
1067 1070 | summary: branch1
1068 1071 |
1069 1072 @ changeset: 0:b5f04eac9d8f
1070 1073 user: test
1071 1074 date: Thu Jan 01 00:00:00 1970 +0000
1072 1075 summary: initial
1073 1076
1074 1077
1075 1078 -U is respected in share clone mode
1076 1079
1077 1080 $ hg --config share.pool=share clone -U source1a share-1anowc
1078 1081 (sharing from existing pooled repository b5f04eac9d8f7a6a9fcb070243cccea7dc5ea0c1)
1079 1082 searching for changes
1080 1083 no changes found
1081 1084 adding remote bookmark bookA
1082 1085
1083 1086 $ ls share-1anowc
1084 1087
1085 1088 Test that auto sharing doesn't cause failure of "hg clone local remote"
1086 1089
1087 1090 $ cd $TESTTMP
1088 1091 $ hg -R a id -r 0
1089 1092 acb14030fe0a
1090 1093 $ hg id -R remote -r 0
1091 1094 abort: repository remote not found!
1092 1095 [255]
1093 1096 $ hg --config share.pool=share -q clone -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" a ssh://user@dummy/remote
1094 1097 $ hg -R remote id -r 0
1095 1098 acb14030fe0a
1096 1099
1097 1100 Cloning into pooled storage doesn't race (issue5104)
1098 1101
1099 1102 $ HGPOSTLOCKDELAY=2.0 hg --config share.pool=racepool --config extensions.lockdelay=$TESTDIR/lockdelay.py clone source1a share-destrace1 > race1.log 2>&1 &
1100 1103 $ HGPRELOCKDELAY=1.0 hg --config share.pool=racepool --config extensions.lockdelay=$TESTDIR/lockdelay.py clone source1a share-destrace2 > race2.log 2>&1
1101 1104 $ wait
1102 1105
1103 1106 $ hg -R share-destrace1 log -r tip
1104 1107 changeset: 2:e5bfe23c0b47
1105 1108 bookmark: bookA
1106 1109 tag: tip
1107 1110 user: test
1108 1111 date: Thu Jan 01 00:00:00 1970 +0000
1109 1112 summary: 1a
1110 1113
1111 1114
1112 1115 $ hg -R share-destrace2 log -r tip
1113 1116 changeset: 2:e5bfe23c0b47
1114 1117 bookmark: bookA
1115 1118 tag: tip
1116 1119 user: test
1117 1120 date: Thu Jan 01 00:00:00 1970 +0000
1118 1121 summary: 1a
1119 1122
1120 1123 One repo should be new, the other should be shared from the pool. We
1121 1124 don't care which is which, so we just make sure we always print the
1122 1125 one containing "new pooled" first, then one one containing "existing
1123 1126 pooled".
1124 1127
1125 1128 $ (grep 'new pooled' race1.log > /dev/null && cat race1.log || cat race2.log) | grep -v lock
1126 1129 (sharing from new pooled repository b5f04eac9d8f7a6a9fcb070243cccea7dc5ea0c1)
1127 1130 requesting all changes
1128 1131 adding changesets
1129 1132 adding manifests
1130 1133 adding file changes
1131 1134 added 3 changesets with 3 changes to 1 files
1132 1135 new changesets b5f04eac9d8f:e5bfe23c0b47
1133 1136 searching for changes
1134 1137 no changes found
1135 1138 adding remote bookmark bookA
1136 1139 updating working directory
1137 1140 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1138 1141
1139 1142 $ (grep 'existing pooled' race1.log > /dev/null && cat race1.log || cat race2.log) | grep -v lock
1140 1143 (sharing from existing pooled repository b5f04eac9d8f7a6a9fcb070243cccea7dc5ea0c1)
1141 1144 searching for changes
1142 1145 no changes found
1143 1146 adding remote bookmark bookA
1144 1147 updating working directory
1145 1148 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1146 1149
1147 1150 SEC: check for unsafe ssh url
1148 1151
1149 1152 $ cat >> $HGRCPATH << EOF
1150 1153 > [ui]
1151 1154 > ssh = sh -c "read l; read l; read l"
1152 1155 > EOF
1153 1156
1154 1157 $ hg clone 'ssh://-oProxyCommand=touch${IFS}owned/path'
1155 1158 abort: potentially unsafe url: 'ssh://-oProxyCommand=touch${IFS}owned/path'
1156 1159 [255]
1157 1160 $ hg clone 'ssh://%2DoProxyCommand=touch${IFS}owned/path'
1158 1161 abort: potentially unsafe url: 'ssh://-oProxyCommand=touch${IFS}owned/path'
1159 1162 [255]
1160 1163 $ hg clone 'ssh://fakehost|touch%20owned/path'
1161 1164 abort: no suitable response from remote hg!
1162 1165 [255]
1163 1166 $ hg clone 'ssh://fakehost%7Ctouch%20owned/path'
1164 1167 abort: no suitable response from remote hg!
1165 1168 [255]
1166 1169
1167 1170 $ hg clone 'ssh://-oProxyCommand=touch owned%20foo@example.com/nonexistent/path'
1168 1171 abort: potentially unsafe url: 'ssh://-oProxyCommand=touch owned foo@example.com/nonexistent/path'
1169 1172 [255]
1170 1173
1171 1174 #if windows
1172 1175 $ hg clone "ssh://%26touch%20owned%20/" --debug
1173 1176 running sh -c "read l; read l; read l" "&touch owned " "hg -R . serve --stdio"
1174 1177 sending upgrade request: * proto=exp-ssh-v2-0003 (glob) (sshv2 !)
1175 1178 sending hello command
1176 1179 sending between command
1177 1180 abort: no suitable response from remote hg!
1178 1181 [255]
1179 1182 $ hg clone "ssh://example.com:%26touch%20owned%20/" --debug
1180 1183 running sh -c "read l; read l; read l" -p "&touch owned " example.com "hg -R . serve --stdio"
1181 1184 sending upgrade request: * proto=exp-ssh-v2-0003 (glob) (sshv2 !)
1182 1185 sending hello command
1183 1186 sending between command
1184 1187 abort: no suitable response from remote hg!
1185 1188 [255]
1186 1189 #else
1187 1190 $ hg clone "ssh://%3btouch%20owned%20/" --debug
1188 1191 running sh -c "read l; read l; read l" ';touch owned ' 'hg -R . serve --stdio'
1189 1192 sending upgrade request: * proto=exp-ssh-v2-0003 (glob) (sshv2 !)
1190 1193 sending hello command
1191 1194 sending between command
1192 1195 abort: no suitable response from remote hg!
1193 1196 [255]
1194 1197 $ hg clone "ssh://example.com:%3btouch%20owned%20/" --debug
1195 1198 running sh -c "read l; read l; read l" -p ';touch owned ' example.com 'hg -R . serve --stdio'
1196 1199 sending upgrade request: * proto=exp-ssh-v2-0003 (glob) (sshv2 !)
1197 1200 sending hello command
1198 1201 sending between command
1199 1202 abort: no suitable response from remote hg!
1200 1203 [255]
1201 1204 #endif
1202 1205
1203 1206 $ hg clone "ssh://v-alid.example.com/" --debug
1204 1207 running sh -c "read l; read l; read l" v-alid\.example\.com ['"]hg -R \. serve --stdio['"] (re)
1205 1208 sending upgrade request: * proto=exp-ssh-v2-0003 (glob) (sshv2 !)
1206 1209 sending hello command
1207 1210 sending between command
1208 1211 abort: no suitable response from remote hg!
1209 1212 [255]
1210 1213
1211 1214 We should not have created a file named owned - if it exists, the
1212 1215 attack succeeded.
1213 1216 $ if test -f owned; then echo 'you got owned'; fi
1214 1217
1215 1218 Cloning without fsmonitor enabled does not print a warning for small repos
1216 1219
1217 1220 $ hg clone a fsmonitor-default
1218 1221 updating to bookmark @ on branch stable
1219 1222 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
1220 1223
1221 1224 Lower the warning threshold to simulate a large repo
1222 1225
1223 1226 $ cat >> $HGRCPATH << EOF
1224 1227 > [fsmonitor]
1225 1228 > warn_update_file_count = 2
1226 1229 > EOF
1227 1230
1228 1231 We should see a warning about no fsmonitor on supported platforms
1229 1232
1230 1233 #if linuxormacos no-fsmonitor
1231 1234 $ hg clone a nofsmonitor
1232 1235 updating to bookmark @ on branch stable
1233 1236 (warning: large working directory being used without fsmonitor enabled; enable fsmonitor to improve performance; see "hg help -e fsmonitor")
1234 1237 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
1235 1238 #else
1236 1239 $ hg clone a nofsmonitor
1237 1240 updating to bookmark @ on branch stable
1238 1241 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
1239 1242 #endif
1240 1243
1241 1244 We should not see warning about fsmonitor when it is enabled
1242 1245
1243 1246 #if fsmonitor
1244 1247 $ hg clone a fsmonitor-enabled
1245 1248 updating to bookmark @ on branch stable
1246 1249 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
1247 1250 #endif
1248 1251
1249 1252 We can disable the fsmonitor warning
1250 1253
1251 1254 $ hg --config fsmonitor.warn_when_unused=false clone a fsmonitor-disable-warning
1252 1255 updating to bookmark @ on branch stable
1253 1256 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
1254 1257
1255 1258 Loaded fsmonitor but disabled in config should still print warning
1256 1259
1257 1260 #if linuxormacos fsmonitor
1258 1261 $ hg --config fsmonitor.mode=off clone a fsmonitor-mode-off
1259 1262 updating to bookmark @ on branch stable
1260 1263 (warning: large working directory being used without fsmonitor enabled; enable fsmonitor to improve performance; see "hg help -e fsmonitor") (fsmonitor !)
1261 1264 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
1262 1265 #endif
1263 1266
1264 1267 Warning not printed if working directory isn't empty
1265 1268
1266 1269 $ hg -q clone a fsmonitor-update
1267 1270 (warning: large working directory being used without fsmonitor enabled; enable fsmonitor to improve performance; see "hg help -e fsmonitor") (?)
1268 1271 $ cd fsmonitor-update
1269 1272 $ hg up acb14030fe0a
1270 1273 1 files updated, 0 files merged, 2 files removed, 0 files unresolved
1271 1274 (leaving bookmark @)
1272 1275 $ hg up cf0fe1914066
1273 1276 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1274 1277
1275 1278 `hg update` from null revision also prints
1276 1279
1277 1280 $ hg up null
1278 1281 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
1279 1282
1280 1283 #if linuxormacos no-fsmonitor
1281 1284 $ hg up cf0fe1914066
1282 1285 (warning: large working directory being used without fsmonitor enabled; enable fsmonitor to improve performance; see "hg help -e fsmonitor")
1283 1286 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
1284 1287 #else
1285 1288 $ hg up cf0fe1914066
1286 1289 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
1287 1290 #endif
1288 1291
1289 1292 $ cd ..
1290 1293
@@ -1,159 +1,170
1 1 from __future__ import absolute_import
2 2 import sys
3 3 import unittest
4 4
5 from mercurial import (
6 error,
7 node,
8 )
9
5 10 try:
6 11 from mercurial import rustext
7 12 rustext.__name__ # trigger immediate actual import
8 13 except ImportError:
9 14 rustext = None
10 15 else:
11 16 # this would fail already without appropriate ancestor.__package__
12 17 from mercurial.rustext.ancestor import (
13 18 AncestorsIterator,
14 19 LazyAncestors,
15 20 MissingAncestors,
16 21 )
17 22
18 23 try:
19 24 from mercurial.cext import parsers as cparsers
20 25 except ImportError:
21 26 cparsers = None
22 27
23 28 # picked from test-parse-index2, copied rather than imported
24 29 # so that it stays stable even if test-parse-index2 changes or disappears.
25 30 data_non_inlined = (
26 31 b'\x00\x00\x00\x01\x00\x00\x00\x00\x00\x01D\x19'
27 32 b'\x00\x07e\x12\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff'
28 33 b'\xff\xff\xff\xff\xd1\xf4\xbb\xb0\xbe\xfc\x13\xbd\x8c\xd3\x9d'
29 34 b'\x0f\xcd\xd9;\x8c\x07\x8cJ/\x00\x00\x00\x00\x00\x00\x00\x00\x00'
30 35 b'\x00\x00\x00\x00\x00\x00\x01D\x19\x00\x00\x00\x00\x00\xdf\x00'
31 36 b'\x00\x01q\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\xff'
32 37 b'\xff\xff\xff\xc1\x12\xb9\x04\x96\xa4Z1t\x91\xdfsJ\x90\xf0\x9bh'
33 38 b'\x07l&\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
34 39 b'\x00\x01D\xf8\x00\x00\x00\x00\x01\x1b\x00\x00\x01\xb8\x00\x00'
35 40 b'\x00\x01\x00\x00\x00\x02\x00\x00\x00\x01\xff\xff\xff\xff\x02\n'
36 41 b'\x0e\xc6&\xa1\x92\xae6\x0b\x02i\xfe-\xe5\xbao\x05\xd1\xe7\x00'
37 42 b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01F'
38 43 b'\x13\x00\x00\x00\x00\x01\xec\x00\x00\x03\x06\x00\x00\x00\x01'
39 44 b'\x00\x00\x00\x03\x00\x00\x00\x02\xff\xff\xff\xff\x12\xcb\xeby1'
40 45 b'\xb6\r\x98B\xcb\x07\xbd`\x8f\x92\xd9\xc4\x84\xbdK\x00\x00\x00'
41 46 b'\x00\x00\x00\x00\x00\x00\x00\x00\x00'
42 47 )
43 48
44 49
45 50 @unittest.skipIf(rustext is None or cparsers is None,
46 51 "rustext or the C Extension parsers module "
47 52 "ancestor relies on is not available")
48 53 class rustancestorstest(unittest.TestCase):
49 54 """Test the correctness of binding to Rust code.
50 55
51 56 This test is merely for the binding to Rust itself: extraction of
52 57 Python variable, giving back the results etc.
53 58
54 59 It is not meant to test the algorithmic correctness of the operations
55 60 on ancestors it provides. Hence the very simple embedded index data is
56 61 good enough.
57 62
58 63 Algorithmic correctness is asserted by the Rust unit tests.
59 64 """
60 65
61 66 def parseindex(self):
62 67 return cparsers.parse_index2(data_non_inlined, False)[0]
63 68
64 69 def testiteratorrevlist(self):
65 70 idx = self.parseindex()
66 71 # checking test assumption about the index binary data:
67 72 self.assertEqual({i: (r[5], r[6]) for i, r in enumerate(idx)},
68 73 {0: (-1, -1),
69 74 1: (0, -1),
70 75 2: (1, -1),
71 76 3: (2, -1)})
72 77 ait = AncestorsIterator(idx, [3], 0, True)
73 78 self.assertEqual([r for r in ait], [3, 2, 1, 0])
74 79
75 80 ait = AncestorsIterator(idx, [3], 0, False)
76 81 self.assertEqual([r for r in ait], [2, 1, 0])
77 82
78 83 def testlazyancestors(self):
79 84 idx = self.parseindex()
80 85 start_count = sys.getrefcount(idx) # should be 2 (see Python doc)
81 86 self.assertEqual({i: (r[5], r[6]) for i, r in enumerate(idx)},
82 87 {0: (-1, -1),
83 88 1: (0, -1),
84 89 2: (1, -1),
85 90 3: (2, -1)})
86 91 lazy = LazyAncestors(idx, [3], 0, True)
87 92 # we have two more references to the index:
88 93 # - in its inner iterator for __contains__ and __bool__
89 94 # - in the LazyAncestors instance itself (to spawn new iterators)
90 95 self.assertEqual(sys.getrefcount(idx), start_count + 2)
91 96
92 97 self.assertTrue(2 in lazy)
93 98 self.assertTrue(bool(lazy))
94 99 self.assertEqual(list(lazy), [3, 2, 1, 0])
95 100 # a second time to validate that we spawn new iterators
96 101 self.assertEqual(list(lazy), [3, 2, 1, 0])
97 102
98 103 # now let's watch the refcounts closer
99 104 ait = iter(lazy)
100 105 self.assertEqual(sys.getrefcount(idx), start_count + 3)
101 106 del ait
102 107 self.assertEqual(sys.getrefcount(idx), start_count + 2)
103 108 del lazy
104 109 self.assertEqual(sys.getrefcount(idx), start_count)
105 110
106 111 # let's check bool for an empty one
107 112 self.assertFalse(LazyAncestors(idx, [0], 0, False))
108 113
109 114 def testmissingancestors(self):
110 115 idx = self.parseindex()
111 116 missanc = MissingAncestors(idx, [1])
112 117 self.assertTrue(missanc.hasbases())
113 118 self.assertEqual(missanc.missingancestors([3]), [2, 3])
114 119 missanc.addbases({2})
115 120 self.assertEqual(missanc.bases(), {1, 2})
116 121 self.assertEqual(missanc.missingancestors([3]), [3])
117 122 self.assertEqual(missanc.basesheads(), {2})
118 123
119 124 def testmissingancestorsremove(self):
120 125 idx = self.parseindex()
121 126 missanc = MissingAncestors(idx, [1])
122 127 revs = {0, 1, 2, 3}
123 128 missanc.removeancestorsfrom(revs)
124 129 self.assertEqual(revs, {2, 3})
125 130
126 131 def testrefcount(self):
127 132 idx = self.parseindex()
128 133 start_count = sys.getrefcount(idx)
129 134
130 135 # refcount increases upon iterator init...
131 136 ait = AncestorsIterator(idx, [3], 0, True)
132 137 self.assertEqual(sys.getrefcount(idx), start_count + 1)
133 138 self.assertEqual(next(ait), 3)
134 139
135 140 # and decreases once the iterator is removed
136 141 del ait
137 142 self.assertEqual(sys.getrefcount(idx), start_count)
138 143
139 144 # and removing ref to the index after iterator init is no issue
140 145 ait = AncestorsIterator(idx, [3], 0, True)
141 146 del idx
142 147 self.assertEqual(list(ait), [3, 2, 1, 0])
143 148
144 149 def testgrapherror(self):
145 150 data = (data_non_inlined[:64 + 27] +
146 151 b'\xf2' +
147 152 data_non_inlined[64 + 28:])
148 153 idx = cparsers.parse_index2(data, False)[0]
149 154 with self.assertRaises(rustext.GraphError) as arc:
150 155 AncestorsIterator(idx, [1], -1, False)
151 156 exc = arc.exception
152 157 self.assertIsInstance(exc, ValueError)
153 158 # rust-cpython issues appropriate str instances for Python 2 and 3
154 159 self.assertEqual(exc.args, ('ParentOutOfRange', 1))
155 160
161 def testwdirunsupported(self):
162 # trying to access ancestors of the working directory raises
163 # WdirUnsupported directly
164 idx = self.parseindex()
165 with self.assertRaises(error.WdirUnsupported):
166 list(AncestorsIterator(idx, [node.wdirrev], -1, False))
156 167
157 168 if __name__ == '__main__':
158 169 import silenttestrunner
159 170 silenttestrunner.main(__name__)
General Comments 0
You need to be logged in to leave comments. Login now