##// END OF EJS Templates
dirstate: Removed unused instances of `DirsMultiset`...
Simon Sapin -
r48271:eb416759 default
parent child Browse files
Show More
@@ -1,3971 +1,3980
1 1 # perf.py - performance test routines
2 2 '''helper extension to measure performance
3 3
4 4 Configurations
5 5 ==============
6 6
7 7 ``perf``
8 8 --------
9 9
10 10 ``all-timing``
11 11 When set, additional statistics will be reported for each benchmark: best,
12 12 worst, median average. If not set only the best timing is reported
13 13 (default: off).
14 14
15 15 ``presleep``
16 16 number of second to wait before any group of runs (default: 1)
17 17
18 18 ``pre-run``
19 19 number of run to perform before starting measurement.
20 20
21 21 ``profile-benchmark``
22 22 Enable profiling for the benchmarked section.
23 23 (The first iteration is benchmarked)
24 24
25 25 ``run-limits``
26 26 Control the number of runs each benchmark will perform. The option value
27 27 should be a list of `<time>-<numberofrun>` pairs. After each run the
28 28 conditions are considered in order with the following logic:
29 29
30 30 If benchmark has been running for <time> seconds, and we have performed
31 31 <numberofrun> iterations, stop the benchmark,
32 32
33 33 The default value is: `3.0-100, 10.0-3`
34 34
35 35 ``stub``
36 36 When set, benchmarks will only be run once, useful for testing
37 37 (default: off)
38 38 '''
39 39
40 40 # "historical portability" policy of perf.py:
41 41 #
42 42 # We have to do:
43 43 # - make perf.py "loadable" with as wide Mercurial version as possible
44 44 # This doesn't mean that perf commands work correctly with that Mercurial.
45 45 # BTW, perf.py itself has been available since 1.1 (or eb240755386d).
46 46 # - make historical perf command work correctly with as wide Mercurial
47 47 # version as possible
48 48 #
49 49 # We have to do, if possible with reasonable cost:
50 50 # - make recent perf command for historical feature work correctly
51 51 # with early Mercurial
52 52 #
53 53 # We don't have to do:
54 54 # - make perf command for recent feature work correctly with early
55 55 # Mercurial
56 56
57 57 from __future__ import absolute_import
58 58 import contextlib
59 59 import functools
60 60 import gc
61 61 import os
62 62 import random
63 63 import shutil
64 64 import struct
65 65 import sys
66 66 import tempfile
67 67 import threading
68 68 import time
69 69
70 70 import mercurial.revlog
71 71 from mercurial import (
72 72 changegroup,
73 73 cmdutil,
74 74 commands,
75 75 copies,
76 76 error,
77 77 extensions,
78 78 hg,
79 79 mdiff,
80 80 merge,
81 81 util,
82 82 )
83 83
84 84 # for "historical portability":
85 85 # try to import modules separately (in dict order), and ignore
86 86 # failure, because these aren't available with early Mercurial
87 87 try:
88 88 from mercurial import branchmap # since 2.5 (or bcee63733aad)
89 89 except ImportError:
90 90 pass
91 91 try:
92 92 from mercurial import obsolete # since 2.3 (or ad0d6c2b3279)
93 93 except ImportError:
94 94 pass
95 95 try:
96 96 from mercurial import registrar # since 3.7 (or 37d50250b696)
97 97
98 98 dir(registrar) # forcibly load it
99 99 except ImportError:
100 100 registrar = None
101 101 try:
102 102 from mercurial import repoview # since 2.5 (or 3a6ddacb7198)
103 103 except ImportError:
104 104 pass
105 105 try:
106 106 from mercurial.utils import repoviewutil # since 5.0
107 107 except ImportError:
108 108 repoviewutil = None
109 109 try:
110 110 from mercurial import scmutil # since 1.9 (or 8b252e826c68)
111 111 except ImportError:
112 112 pass
113 113 try:
114 114 from mercurial import setdiscovery # since 1.9 (or cb98fed52495)
115 115 except ImportError:
116 116 pass
117 117
118 118 try:
119 119 from mercurial import profiling
120 120 except ImportError:
121 121 profiling = None
122 122
123 123 try:
124 124 from mercurial.revlogutils import constants as revlog_constants
125 125
126 126 perf_rl_kind = (revlog_constants.KIND_OTHER, b'created-by-perf')
127 127
128 128 def revlog(opener, *args, **kwargs):
129 129 return mercurial.revlog.revlog(opener, perf_rl_kind, *args, **kwargs)
130 130
131 131
132 132 except (ImportError, AttributeError):
133 133 perf_rl_kind = None
134 134
135 135 def revlog(opener, *args, **kwargs):
136 136 return mercurial.revlog.revlog(opener, *args, **kwargs)
137 137
138 138
139 139 def identity(a):
140 140 return a
141 141
142 142
143 143 try:
144 144 from mercurial import pycompat
145 145
146 146 getargspec = pycompat.getargspec # added to module after 4.5
147 147 _byteskwargs = pycompat.byteskwargs # since 4.1 (or fbc3f73dc802)
148 148 _sysstr = pycompat.sysstr # since 4.0 (or 2219f4f82ede)
149 149 _bytestr = pycompat.bytestr # since 4.2 (or b70407bd84d5)
150 150 _xrange = pycompat.xrange # since 4.8 (or 7eba8f83129b)
151 151 fsencode = pycompat.fsencode # since 3.9 (or f4a5e0e86a7e)
152 152 if pycompat.ispy3:
153 153 _maxint = sys.maxsize # per py3 docs for replacing maxint
154 154 else:
155 155 _maxint = sys.maxint
156 156 except (NameError, ImportError, AttributeError):
157 157 import inspect
158 158
159 159 getargspec = inspect.getargspec
160 160 _byteskwargs = identity
161 161 _bytestr = str
162 162 fsencode = identity # no py3 support
163 163 _maxint = sys.maxint # no py3 support
164 164 _sysstr = lambda x: x # no py3 support
165 165 _xrange = xrange
166 166
167 167 try:
168 168 # 4.7+
169 169 queue = pycompat.queue.Queue
170 170 except (NameError, AttributeError, ImportError):
171 171 # <4.7.
172 172 try:
173 173 queue = pycompat.queue
174 174 except (NameError, AttributeError, ImportError):
175 175 import Queue as queue
176 176
177 177 try:
178 178 from mercurial import logcmdutil
179 179
180 180 makelogtemplater = logcmdutil.maketemplater
181 181 except (AttributeError, ImportError):
182 182 try:
183 183 makelogtemplater = cmdutil.makelogtemplater
184 184 except (AttributeError, ImportError):
185 185 makelogtemplater = None
186 186
187 187 # for "historical portability":
188 188 # define util.safehasattr forcibly, because util.safehasattr has been
189 189 # available since 1.9.3 (or 94b200a11cf7)
190 190 _undefined = object()
191 191
192 192
193 193 def safehasattr(thing, attr):
194 194 return getattr(thing, _sysstr(attr), _undefined) is not _undefined
195 195
196 196
197 197 setattr(util, 'safehasattr', safehasattr)
198 198
199 199 # for "historical portability":
200 200 # define util.timer forcibly, because util.timer has been available
201 201 # since ae5d60bb70c9
202 202 if safehasattr(time, 'perf_counter'):
203 203 util.timer = time.perf_counter
204 204 elif os.name == b'nt':
205 205 util.timer = time.clock
206 206 else:
207 207 util.timer = time.time
208 208
209 209 # for "historical portability":
210 210 # use locally defined empty option list, if formatteropts isn't
211 211 # available, because commands.formatteropts has been available since
212 212 # 3.2 (or 7a7eed5176a4), even though formatting itself has been
213 213 # available since 2.2 (or ae5f92e154d3)
214 214 formatteropts = getattr(
215 215 cmdutil, "formatteropts", getattr(commands, "formatteropts", [])
216 216 )
217 217
218 218 # for "historical portability":
219 219 # use locally defined option list, if debugrevlogopts isn't available,
220 220 # because commands.debugrevlogopts has been available since 3.7 (or
221 221 # 5606f7d0d063), even though cmdutil.openrevlog() has been available
222 222 # since 1.9 (or a79fea6b3e77).
223 223 revlogopts = getattr(
224 224 cmdutil,
225 225 "debugrevlogopts",
226 226 getattr(
227 227 commands,
228 228 "debugrevlogopts",
229 229 [
230 230 (b'c', b'changelog', False, b'open changelog'),
231 231 (b'm', b'manifest', False, b'open manifest'),
232 232 (b'', b'dir', False, b'open directory manifest'),
233 233 ],
234 234 ),
235 235 )
236 236
237 237 cmdtable = {}
238 238
239 239 # for "historical portability":
240 240 # define parsealiases locally, because cmdutil.parsealiases has been
241 241 # available since 1.5 (or 6252852b4332)
242 242 def parsealiases(cmd):
243 243 return cmd.split(b"|")
244 244
245 245
246 246 if safehasattr(registrar, 'command'):
247 247 command = registrar.command(cmdtable)
248 248 elif safehasattr(cmdutil, 'command'):
249 249 command = cmdutil.command(cmdtable)
250 250 if 'norepo' not in getargspec(command).args:
251 251 # for "historical portability":
252 252 # wrap original cmdutil.command, because "norepo" option has
253 253 # been available since 3.1 (or 75a96326cecb)
254 254 _command = command
255 255
256 256 def command(name, options=(), synopsis=None, norepo=False):
257 257 if norepo:
258 258 commands.norepo += b' %s' % b' '.join(parsealiases(name))
259 259 return _command(name, list(options), synopsis)
260 260
261 261
262 262 else:
263 263 # for "historical portability":
264 264 # define "@command" annotation locally, because cmdutil.command
265 265 # has been available since 1.9 (or 2daa5179e73f)
266 266 def command(name, options=(), synopsis=None, norepo=False):
267 267 def decorator(func):
268 268 if synopsis:
269 269 cmdtable[name] = func, list(options), synopsis
270 270 else:
271 271 cmdtable[name] = func, list(options)
272 272 if norepo:
273 273 commands.norepo += b' %s' % b' '.join(parsealiases(name))
274 274 return func
275 275
276 276 return decorator
277 277
278 278
279 279 try:
280 280 import mercurial.registrar
281 281 import mercurial.configitems
282 282
283 283 configtable = {}
284 284 configitem = mercurial.registrar.configitem(configtable)
285 285 configitem(
286 286 b'perf',
287 287 b'presleep',
288 288 default=mercurial.configitems.dynamicdefault,
289 289 experimental=True,
290 290 )
291 291 configitem(
292 292 b'perf',
293 293 b'stub',
294 294 default=mercurial.configitems.dynamicdefault,
295 295 experimental=True,
296 296 )
297 297 configitem(
298 298 b'perf',
299 299 b'parentscount',
300 300 default=mercurial.configitems.dynamicdefault,
301 301 experimental=True,
302 302 )
303 303 configitem(
304 304 b'perf',
305 305 b'all-timing',
306 306 default=mercurial.configitems.dynamicdefault,
307 307 experimental=True,
308 308 )
309 309 configitem(
310 310 b'perf',
311 311 b'pre-run',
312 312 default=mercurial.configitems.dynamicdefault,
313 313 )
314 314 configitem(
315 315 b'perf',
316 316 b'profile-benchmark',
317 317 default=mercurial.configitems.dynamicdefault,
318 318 )
319 319 configitem(
320 320 b'perf',
321 321 b'run-limits',
322 322 default=mercurial.configitems.dynamicdefault,
323 323 experimental=True,
324 324 )
325 325 except (ImportError, AttributeError):
326 326 pass
327 327 except TypeError:
328 328 # compatibility fix for a11fd395e83f
329 329 # hg version: 5.2
330 330 configitem(
331 331 b'perf',
332 332 b'presleep',
333 333 default=mercurial.configitems.dynamicdefault,
334 334 )
335 335 configitem(
336 336 b'perf',
337 337 b'stub',
338 338 default=mercurial.configitems.dynamicdefault,
339 339 )
340 340 configitem(
341 341 b'perf',
342 342 b'parentscount',
343 343 default=mercurial.configitems.dynamicdefault,
344 344 )
345 345 configitem(
346 346 b'perf',
347 347 b'all-timing',
348 348 default=mercurial.configitems.dynamicdefault,
349 349 )
350 350 configitem(
351 351 b'perf',
352 352 b'pre-run',
353 353 default=mercurial.configitems.dynamicdefault,
354 354 )
355 355 configitem(
356 356 b'perf',
357 357 b'profile-benchmark',
358 358 default=mercurial.configitems.dynamicdefault,
359 359 )
360 360 configitem(
361 361 b'perf',
362 362 b'run-limits',
363 363 default=mercurial.configitems.dynamicdefault,
364 364 )
365 365
366 366
367 367 def getlen(ui):
368 368 if ui.configbool(b"perf", b"stub", False):
369 369 return lambda x: 1
370 370 return len
371 371
372 372
373 373 class noop(object):
374 374 """dummy context manager"""
375 375
376 376 def __enter__(self):
377 377 pass
378 378
379 379 def __exit__(self, *args):
380 380 pass
381 381
382 382
383 383 NOOPCTX = noop()
384 384
385 385
386 386 def gettimer(ui, opts=None):
387 387 """return a timer function and formatter: (timer, formatter)
388 388
389 389 This function exists to gather the creation of formatter in a single
390 390 place instead of duplicating it in all performance commands."""
391 391
392 392 # enforce an idle period before execution to counteract power management
393 393 # experimental config: perf.presleep
394 394 time.sleep(getint(ui, b"perf", b"presleep", 1))
395 395
396 396 if opts is None:
397 397 opts = {}
398 398 # redirect all to stderr unless buffer api is in use
399 399 if not ui._buffers:
400 400 ui = ui.copy()
401 401 uifout = safeattrsetter(ui, b'fout', ignoremissing=True)
402 402 if uifout:
403 403 # for "historical portability":
404 404 # ui.fout/ferr have been available since 1.9 (or 4e1ccd4c2b6d)
405 405 uifout.set(ui.ferr)
406 406
407 407 # get a formatter
408 408 uiformatter = getattr(ui, 'formatter', None)
409 409 if uiformatter:
410 410 fm = uiformatter(b'perf', opts)
411 411 else:
412 412 # for "historical portability":
413 413 # define formatter locally, because ui.formatter has been
414 414 # available since 2.2 (or ae5f92e154d3)
415 415 from mercurial import node
416 416
417 417 class defaultformatter(object):
418 418 """Minimized composition of baseformatter and plainformatter"""
419 419
420 420 def __init__(self, ui, topic, opts):
421 421 self._ui = ui
422 422 if ui.debugflag:
423 423 self.hexfunc = node.hex
424 424 else:
425 425 self.hexfunc = node.short
426 426
427 427 def __nonzero__(self):
428 428 return False
429 429
430 430 __bool__ = __nonzero__
431 431
432 432 def startitem(self):
433 433 pass
434 434
435 435 def data(self, **data):
436 436 pass
437 437
438 438 def write(self, fields, deftext, *fielddata, **opts):
439 439 self._ui.write(deftext % fielddata, **opts)
440 440
441 441 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
442 442 if cond:
443 443 self._ui.write(deftext % fielddata, **opts)
444 444
445 445 def plain(self, text, **opts):
446 446 self._ui.write(text, **opts)
447 447
448 448 def end(self):
449 449 pass
450 450
451 451 fm = defaultformatter(ui, b'perf', opts)
452 452
453 453 # stub function, runs code only once instead of in a loop
454 454 # experimental config: perf.stub
455 455 if ui.configbool(b"perf", b"stub", False):
456 456 return functools.partial(stub_timer, fm), fm
457 457
458 458 # experimental config: perf.all-timing
459 459 displayall = ui.configbool(b"perf", b"all-timing", False)
460 460
461 461 # experimental config: perf.run-limits
462 462 limitspec = ui.configlist(b"perf", b"run-limits", [])
463 463 limits = []
464 464 for item in limitspec:
465 465 parts = item.split(b'-', 1)
466 466 if len(parts) < 2:
467 467 ui.warn((b'malformatted run limit entry, missing "-": %s\n' % item))
468 468 continue
469 469 try:
470 470 time_limit = float(_sysstr(parts[0]))
471 471 except ValueError as e:
472 472 ui.warn(
473 473 (
474 474 b'malformatted run limit entry, %s: %s\n'
475 475 % (_bytestr(e), item)
476 476 )
477 477 )
478 478 continue
479 479 try:
480 480 run_limit = int(_sysstr(parts[1]))
481 481 except ValueError as e:
482 482 ui.warn(
483 483 (
484 484 b'malformatted run limit entry, %s: %s\n'
485 485 % (_bytestr(e), item)
486 486 )
487 487 )
488 488 continue
489 489 limits.append((time_limit, run_limit))
490 490 if not limits:
491 491 limits = DEFAULTLIMITS
492 492
493 493 profiler = None
494 494 if profiling is not None:
495 495 if ui.configbool(b"perf", b"profile-benchmark", False):
496 496 profiler = profiling.profile(ui)
497 497
498 498 prerun = getint(ui, b"perf", b"pre-run", 0)
499 499 t = functools.partial(
500 500 _timer,
501 501 fm,
502 502 displayall=displayall,
503 503 limits=limits,
504 504 prerun=prerun,
505 505 profiler=profiler,
506 506 )
507 507 return t, fm
508 508
509 509
510 510 def stub_timer(fm, func, setup=None, title=None):
511 511 if setup is not None:
512 512 setup()
513 513 func()
514 514
515 515
516 516 @contextlib.contextmanager
517 517 def timeone():
518 518 r = []
519 519 ostart = os.times()
520 520 cstart = util.timer()
521 521 yield r
522 522 cstop = util.timer()
523 523 ostop = os.times()
524 524 a, b = ostart, ostop
525 525 r.append((cstop - cstart, b[0] - a[0], b[1] - a[1]))
526 526
527 527
528 528 # list of stop condition (elapsed time, minimal run count)
529 529 DEFAULTLIMITS = (
530 530 (3.0, 100),
531 531 (10.0, 3),
532 532 )
533 533
534 534
535 535 def _timer(
536 536 fm,
537 537 func,
538 538 setup=None,
539 539 title=None,
540 540 displayall=False,
541 541 limits=DEFAULTLIMITS,
542 542 prerun=0,
543 543 profiler=None,
544 544 ):
545 545 gc.collect()
546 546 results = []
547 547 begin = util.timer()
548 548 count = 0
549 549 if profiler is None:
550 550 profiler = NOOPCTX
551 551 for i in range(prerun):
552 552 if setup is not None:
553 553 setup()
554 554 func()
555 555 keepgoing = True
556 556 while keepgoing:
557 557 if setup is not None:
558 558 setup()
559 559 with profiler:
560 560 with timeone() as item:
561 561 r = func()
562 562 profiler = NOOPCTX
563 563 count += 1
564 564 results.append(item[0])
565 565 cstop = util.timer()
566 566 # Look for a stop condition.
567 567 elapsed = cstop - begin
568 568 for t, mincount in limits:
569 569 if elapsed >= t and count >= mincount:
570 570 keepgoing = False
571 571 break
572 572
573 573 formatone(fm, results, title=title, result=r, displayall=displayall)
574 574
575 575
576 576 def formatone(fm, timings, title=None, result=None, displayall=False):
577 577
578 578 count = len(timings)
579 579
580 580 fm.startitem()
581 581
582 582 if title:
583 583 fm.write(b'title', b'! %s\n', title)
584 584 if result:
585 585 fm.write(b'result', b'! result: %s\n', result)
586 586
587 587 def display(role, entry):
588 588 prefix = b''
589 589 if role != b'best':
590 590 prefix = b'%s.' % role
591 591 fm.plain(b'!')
592 592 fm.write(prefix + b'wall', b' wall %f', entry[0])
593 593 fm.write(prefix + b'comb', b' comb %f', entry[1] + entry[2])
594 594 fm.write(prefix + b'user', b' user %f', entry[1])
595 595 fm.write(prefix + b'sys', b' sys %f', entry[2])
596 596 fm.write(prefix + b'count', b' (%s of %%d)' % role, count)
597 597 fm.plain(b'\n')
598 598
599 599 timings.sort()
600 600 min_val = timings[0]
601 601 display(b'best', min_val)
602 602 if displayall:
603 603 max_val = timings[-1]
604 604 display(b'max', max_val)
605 605 avg = tuple([sum(x) / count for x in zip(*timings)])
606 606 display(b'avg', avg)
607 607 median = timings[len(timings) // 2]
608 608 display(b'median', median)
609 609
610 610
611 611 # utilities for historical portability
612 612
613 613
614 614 def getint(ui, section, name, default):
615 615 # for "historical portability":
616 616 # ui.configint has been available since 1.9 (or fa2b596db182)
617 617 v = ui.config(section, name, None)
618 618 if v is None:
619 619 return default
620 620 try:
621 621 return int(v)
622 622 except ValueError:
623 623 raise error.ConfigError(
624 624 b"%s.%s is not an integer ('%s')" % (section, name, v)
625 625 )
626 626
627 627
628 628 def safeattrsetter(obj, name, ignoremissing=False):
629 629 """Ensure that 'obj' has 'name' attribute before subsequent setattr
630 630
631 631 This function is aborted, if 'obj' doesn't have 'name' attribute
632 632 at runtime. This avoids overlooking removal of an attribute, which
633 633 breaks assumption of performance measurement, in the future.
634 634
635 635 This function returns the object to (1) assign a new value, and
636 636 (2) restore an original value to the attribute.
637 637
638 638 If 'ignoremissing' is true, missing 'name' attribute doesn't cause
639 639 abortion, and this function returns None. This is useful to
640 640 examine an attribute, which isn't ensured in all Mercurial
641 641 versions.
642 642 """
643 643 if not util.safehasattr(obj, name):
644 644 if ignoremissing:
645 645 return None
646 646 raise error.Abort(
647 647 (
648 648 b"missing attribute %s of %s might break assumption"
649 649 b" of performance measurement"
650 650 )
651 651 % (name, obj)
652 652 )
653 653
654 654 origvalue = getattr(obj, _sysstr(name))
655 655
656 656 class attrutil(object):
657 657 def set(self, newvalue):
658 658 setattr(obj, _sysstr(name), newvalue)
659 659
660 660 def restore(self):
661 661 setattr(obj, _sysstr(name), origvalue)
662 662
663 663 return attrutil()
664 664
665 665
666 666 # utilities to examine each internal API changes
667 667
668 668
669 669 def getbranchmapsubsettable():
670 670 # for "historical portability":
671 671 # subsettable is defined in:
672 672 # - branchmap since 2.9 (or 175c6fd8cacc)
673 673 # - repoview since 2.5 (or 59a9f18d4587)
674 674 # - repoviewutil since 5.0
675 675 for mod in (branchmap, repoview, repoviewutil):
676 676 subsettable = getattr(mod, 'subsettable', None)
677 677 if subsettable:
678 678 return subsettable
679 679
680 680 # bisecting in bcee63733aad::59a9f18d4587 can reach here (both
681 681 # branchmap and repoview modules exist, but subsettable attribute
682 682 # doesn't)
683 683 raise error.Abort(
684 684 b"perfbranchmap not available with this Mercurial",
685 685 hint=b"use 2.5 or later",
686 686 )
687 687
688 688
689 689 def getsvfs(repo):
690 690 """Return appropriate object to access files under .hg/store"""
691 691 # for "historical portability":
692 692 # repo.svfs has been available since 2.3 (or 7034365089bf)
693 693 svfs = getattr(repo, 'svfs', None)
694 694 if svfs:
695 695 return svfs
696 696 else:
697 697 return getattr(repo, 'sopener')
698 698
699 699
700 700 def getvfs(repo):
701 701 """Return appropriate object to access files under .hg"""
702 702 # for "historical portability":
703 703 # repo.vfs has been available since 2.3 (or 7034365089bf)
704 704 vfs = getattr(repo, 'vfs', None)
705 705 if vfs:
706 706 return vfs
707 707 else:
708 708 return getattr(repo, 'opener')
709 709
710 710
711 711 def repocleartagscachefunc(repo):
712 712 """Return the function to clear tags cache according to repo internal API"""
713 713 if util.safehasattr(repo, b'_tagscache'): # since 2.0 (or 9dca7653b525)
714 714 # in this case, setattr(repo, '_tagscache', None) or so isn't
715 715 # correct way to clear tags cache, because existing code paths
716 716 # expect _tagscache to be a structured object.
717 717 def clearcache():
718 718 # _tagscache has been filteredpropertycache since 2.5 (or
719 719 # 98c867ac1330), and delattr() can't work in such case
720 720 if '_tagscache' in vars(repo):
721 721 del repo.__dict__['_tagscache']
722 722
723 723 return clearcache
724 724
725 725 repotags = safeattrsetter(repo, b'_tags', ignoremissing=True)
726 726 if repotags: # since 1.4 (or 5614a628d173)
727 727 return lambda: repotags.set(None)
728 728
729 729 repotagscache = safeattrsetter(repo, b'tagscache', ignoremissing=True)
730 730 if repotagscache: # since 0.6 (or d7df759d0e97)
731 731 return lambda: repotagscache.set(None)
732 732
733 733 # Mercurial earlier than 0.6 (or d7df759d0e97) logically reaches
734 734 # this point, but it isn't so problematic, because:
735 735 # - repo.tags of such Mercurial isn't "callable", and repo.tags()
736 736 # in perftags() causes failure soon
737 737 # - perf.py itself has been available since 1.1 (or eb240755386d)
738 738 raise error.Abort(b"tags API of this hg command is unknown")
739 739
740 740
741 741 # utilities to clear cache
742 742
743 743
744 744 def clearfilecache(obj, attrname):
745 745 unfiltered = getattr(obj, 'unfiltered', None)
746 746 if unfiltered is not None:
747 747 obj = obj.unfiltered()
748 748 if attrname in vars(obj):
749 749 delattr(obj, attrname)
750 750 obj._filecache.pop(attrname, None)
751 751
752 752
753 753 def clearchangelog(repo):
754 754 if repo is not repo.unfiltered():
755 755 object.__setattr__(repo, '_clcachekey', None)
756 756 object.__setattr__(repo, '_clcache', None)
757 757 clearfilecache(repo.unfiltered(), 'changelog')
758 758
759 759
760 760 # perf commands
761 761
762 762
763 763 @command(b'perf::walk|perfwalk', formatteropts)
764 764 def perfwalk(ui, repo, *pats, **opts):
765 765 opts = _byteskwargs(opts)
766 766 timer, fm = gettimer(ui, opts)
767 767 m = scmutil.match(repo[None], pats, {})
768 768 timer(
769 769 lambda: len(
770 770 list(
771 771 repo.dirstate.walk(m, subrepos=[], unknown=True, ignored=False)
772 772 )
773 773 )
774 774 )
775 775 fm.end()
776 776
777 777
778 778 @command(b'perf::annotate|perfannotate', formatteropts)
779 779 def perfannotate(ui, repo, f, **opts):
780 780 opts = _byteskwargs(opts)
781 781 timer, fm = gettimer(ui, opts)
782 782 fc = repo[b'.'][f]
783 783 timer(lambda: len(fc.annotate(True)))
784 784 fm.end()
785 785
786 786
787 787 @command(
788 788 b'perf::status|perfstatus',
789 789 [
790 790 (b'u', b'unknown', False, b'ask status to look for unknown files'),
791 791 (b'', b'dirstate', False, b'benchmark the internal dirstate call'),
792 792 ]
793 793 + formatteropts,
794 794 )
795 795 def perfstatus(ui, repo, **opts):
796 796 """benchmark the performance of a single status call
797 797
798 798 The repository data are preserved between each call.
799 799
800 800 By default, only the status of the tracked file are requested. If
801 801 `--unknown` is passed, the "unknown" files are also tracked.
802 802 """
803 803 opts = _byteskwargs(opts)
804 804 # m = match.always(repo.root, repo.getcwd())
805 805 # timer(lambda: sum(map(len, repo.dirstate.status(m, [], False, False,
806 806 # False))))
807 807 timer, fm = gettimer(ui, opts)
808 808 if opts[b'dirstate']:
809 809 dirstate = repo.dirstate
810 810 m = scmutil.matchall(repo)
811 811 unknown = opts[b'unknown']
812 812
813 813 def status_dirstate():
814 814 s = dirstate.status(
815 815 m, subrepos=[], ignored=False, clean=False, unknown=unknown
816 816 )
817 817 sum(map(bool, s))
818 818
819 819 timer(status_dirstate)
820 820 else:
821 821 timer(lambda: sum(map(len, repo.status(unknown=opts[b'unknown']))))
822 822 fm.end()
823 823
824 824
825 825 @command(b'perf::addremove|perfaddremove', formatteropts)
826 826 def perfaddremove(ui, repo, **opts):
827 827 opts = _byteskwargs(opts)
828 828 timer, fm = gettimer(ui, opts)
829 829 try:
830 830 oldquiet = repo.ui.quiet
831 831 repo.ui.quiet = True
832 832 matcher = scmutil.match(repo[None])
833 833 opts[b'dry_run'] = True
834 834 if 'uipathfn' in getargspec(scmutil.addremove).args:
835 835 uipathfn = scmutil.getuipathfn(repo)
836 836 timer(lambda: scmutil.addremove(repo, matcher, b"", uipathfn, opts))
837 837 else:
838 838 timer(lambda: scmutil.addremove(repo, matcher, b"", opts))
839 839 finally:
840 840 repo.ui.quiet = oldquiet
841 841 fm.end()
842 842
843 843
844 844 def clearcaches(cl):
845 845 # behave somewhat consistently across internal API changes
846 846 if util.safehasattr(cl, b'clearcaches'):
847 847 cl.clearcaches()
848 848 elif util.safehasattr(cl, b'_nodecache'):
849 849 # <= hg-5.2
850 850 from mercurial.node import nullid, nullrev
851 851
852 852 cl._nodecache = {nullid: nullrev}
853 853 cl._nodepos = None
854 854
855 855
856 856 @command(b'perf::heads|perfheads', formatteropts)
857 857 def perfheads(ui, repo, **opts):
858 858 """benchmark the computation of a changelog heads"""
859 859 opts = _byteskwargs(opts)
860 860 timer, fm = gettimer(ui, opts)
861 861 cl = repo.changelog
862 862
863 863 def s():
864 864 clearcaches(cl)
865 865
866 866 def d():
867 867 len(cl.headrevs())
868 868
869 869 timer(d, setup=s)
870 870 fm.end()
871 871
872 872
873 873 @command(
874 874 b'perf::tags|perftags',
875 875 formatteropts
876 876 + [
877 877 (b'', b'clear-revlogs', False, b'refresh changelog and manifest'),
878 878 ],
879 879 )
880 880 def perftags(ui, repo, **opts):
881 881 opts = _byteskwargs(opts)
882 882 timer, fm = gettimer(ui, opts)
883 883 repocleartagscache = repocleartagscachefunc(repo)
884 884 clearrevlogs = opts[b'clear_revlogs']
885 885
886 886 def s():
887 887 if clearrevlogs:
888 888 clearchangelog(repo)
889 889 clearfilecache(repo.unfiltered(), 'manifest')
890 890 repocleartagscache()
891 891
892 892 def t():
893 893 return len(repo.tags())
894 894
895 895 timer(t, setup=s)
896 896 fm.end()
897 897
898 898
899 899 @command(b'perf::ancestors|perfancestors', formatteropts)
900 900 def perfancestors(ui, repo, **opts):
901 901 opts = _byteskwargs(opts)
902 902 timer, fm = gettimer(ui, opts)
903 903 heads = repo.changelog.headrevs()
904 904
905 905 def d():
906 906 for a in repo.changelog.ancestors(heads):
907 907 pass
908 908
909 909 timer(d)
910 910 fm.end()
911 911
912 912
913 913 @command(b'perf::ancestorset|perfancestorset', formatteropts)
914 914 def perfancestorset(ui, repo, revset, **opts):
915 915 opts = _byteskwargs(opts)
916 916 timer, fm = gettimer(ui, opts)
917 917 revs = repo.revs(revset)
918 918 heads = repo.changelog.headrevs()
919 919
920 920 def d():
921 921 s = repo.changelog.ancestors(heads)
922 922 for rev in revs:
923 923 rev in s
924 924
925 925 timer(d)
926 926 fm.end()
927 927
928 928
929 929 @command(b'perf::discovery|perfdiscovery', formatteropts, b'PATH')
930 930 def perfdiscovery(ui, repo, path, **opts):
931 931 """benchmark discovery between local repo and the peer at given path"""
932 932 repos = [repo, None]
933 933 timer, fm = gettimer(ui, opts)
934 934
935 935 try:
936 936 from mercurial.utils.urlutil import get_unique_pull_path
937 937
938 938 path = get_unique_pull_path(b'perfdiscovery', repo, ui, path)[0]
939 939 except ImportError:
940 940 path = ui.expandpath(path)
941 941
942 942 def s():
943 943 repos[1] = hg.peer(ui, opts, path)
944 944
945 945 def d():
946 946 setdiscovery.findcommonheads(ui, *repos)
947 947
948 948 timer(d, setup=s)
949 949 fm.end()
950 950
951 951
952 952 @command(
953 953 b'perf::bookmarks|perfbookmarks',
954 954 formatteropts
955 955 + [
956 956 (b'', b'clear-revlogs', False, b'refresh changelog and manifest'),
957 957 ],
958 958 )
959 959 def perfbookmarks(ui, repo, **opts):
960 960 """benchmark parsing bookmarks from disk to memory"""
961 961 opts = _byteskwargs(opts)
962 962 timer, fm = gettimer(ui, opts)
963 963
964 964 clearrevlogs = opts[b'clear_revlogs']
965 965
966 966 def s():
967 967 if clearrevlogs:
968 968 clearchangelog(repo)
969 969 clearfilecache(repo, b'_bookmarks')
970 970
971 971 def d():
972 972 repo._bookmarks
973 973
974 974 timer(d, setup=s)
975 975 fm.end()
976 976
977 977
978 978 @command(b'perf::bundleread|perfbundleread', formatteropts, b'BUNDLE')
979 979 def perfbundleread(ui, repo, bundlepath, **opts):
980 980 """Benchmark reading of bundle files.
981 981
982 982 This command is meant to isolate the I/O part of bundle reading as
983 983 much as possible.
984 984 """
985 985 from mercurial import (
986 986 bundle2,
987 987 exchange,
988 988 streamclone,
989 989 )
990 990
991 991 opts = _byteskwargs(opts)
992 992
993 993 def makebench(fn):
994 994 def run():
995 995 with open(bundlepath, b'rb') as fh:
996 996 bundle = exchange.readbundle(ui, fh, bundlepath)
997 997 fn(bundle)
998 998
999 999 return run
1000 1000
1001 1001 def makereadnbytes(size):
1002 1002 def run():
1003 1003 with open(bundlepath, b'rb') as fh:
1004 1004 bundle = exchange.readbundle(ui, fh, bundlepath)
1005 1005 while bundle.read(size):
1006 1006 pass
1007 1007
1008 1008 return run
1009 1009
1010 1010 def makestdioread(size):
1011 1011 def run():
1012 1012 with open(bundlepath, b'rb') as fh:
1013 1013 while fh.read(size):
1014 1014 pass
1015 1015
1016 1016 return run
1017 1017
1018 1018 # bundle1
1019 1019
1020 1020 def deltaiter(bundle):
1021 1021 for delta in bundle.deltaiter():
1022 1022 pass
1023 1023
1024 1024 def iterchunks(bundle):
1025 1025 for chunk in bundle.getchunks():
1026 1026 pass
1027 1027
1028 1028 # bundle2
1029 1029
1030 1030 def forwardchunks(bundle):
1031 1031 for chunk in bundle._forwardchunks():
1032 1032 pass
1033 1033
1034 1034 def iterparts(bundle):
1035 1035 for part in bundle.iterparts():
1036 1036 pass
1037 1037
1038 1038 def iterpartsseekable(bundle):
1039 1039 for part in bundle.iterparts(seekable=True):
1040 1040 pass
1041 1041
1042 1042 def seek(bundle):
1043 1043 for part in bundle.iterparts(seekable=True):
1044 1044 part.seek(0, os.SEEK_END)
1045 1045
1046 1046 def makepartreadnbytes(size):
1047 1047 def run():
1048 1048 with open(bundlepath, b'rb') as fh:
1049 1049 bundle = exchange.readbundle(ui, fh, bundlepath)
1050 1050 for part in bundle.iterparts():
1051 1051 while part.read(size):
1052 1052 pass
1053 1053
1054 1054 return run
1055 1055
1056 1056 benches = [
1057 1057 (makestdioread(8192), b'read(8k)'),
1058 1058 (makestdioread(16384), b'read(16k)'),
1059 1059 (makestdioread(32768), b'read(32k)'),
1060 1060 (makestdioread(131072), b'read(128k)'),
1061 1061 ]
1062 1062
1063 1063 with open(bundlepath, b'rb') as fh:
1064 1064 bundle = exchange.readbundle(ui, fh, bundlepath)
1065 1065
1066 1066 if isinstance(bundle, changegroup.cg1unpacker):
1067 1067 benches.extend(
1068 1068 [
1069 1069 (makebench(deltaiter), b'cg1 deltaiter()'),
1070 1070 (makebench(iterchunks), b'cg1 getchunks()'),
1071 1071 (makereadnbytes(8192), b'cg1 read(8k)'),
1072 1072 (makereadnbytes(16384), b'cg1 read(16k)'),
1073 1073 (makereadnbytes(32768), b'cg1 read(32k)'),
1074 1074 (makereadnbytes(131072), b'cg1 read(128k)'),
1075 1075 ]
1076 1076 )
1077 1077 elif isinstance(bundle, bundle2.unbundle20):
1078 1078 benches.extend(
1079 1079 [
1080 1080 (makebench(forwardchunks), b'bundle2 forwardchunks()'),
1081 1081 (makebench(iterparts), b'bundle2 iterparts()'),
1082 1082 (
1083 1083 makebench(iterpartsseekable),
1084 1084 b'bundle2 iterparts() seekable',
1085 1085 ),
1086 1086 (makebench(seek), b'bundle2 part seek()'),
1087 1087 (makepartreadnbytes(8192), b'bundle2 part read(8k)'),
1088 1088 (makepartreadnbytes(16384), b'bundle2 part read(16k)'),
1089 1089 (makepartreadnbytes(32768), b'bundle2 part read(32k)'),
1090 1090 (makepartreadnbytes(131072), b'bundle2 part read(128k)'),
1091 1091 ]
1092 1092 )
1093 1093 elif isinstance(bundle, streamclone.streamcloneapplier):
1094 1094 raise error.Abort(b'stream clone bundles not supported')
1095 1095 else:
1096 1096 raise error.Abort(b'unhandled bundle type: %s' % type(bundle))
1097 1097
1098 1098 for fn, title in benches:
1099 1099 timer, fm = gettimer(ui, opts)
1100 1100 timer(fn, title=title)
1101 1101 fm.end()
1102 1102
1103 1103
1104 1104 @command(
1105 1105 b'perf::changegroupchangelog|perfchangegroupchangelog',
1106 1106 formatteropts
1107 1107 + [
1108 1108 (b'', b'cgversion', b'02', b'changegroup version'),
1109 1109 (b'r', b'rev', b'', b'revisions to add to changegroup'),
1110 1110 ],
1111 1111 )
1112 1112 def perfchangegroupchangelog(ui, repo, cgversion=b'02', rev=None, **opts):
1113 1113 """Benchmark producing a changelog group for a changegroup.
1114 1114
1115 1115 This measures the time spent processing the changelog during a
1116 1116 bundle operation. This occurs during `hg bundle` and on a server
1117 1117 processing a `getbundle` wire protocol request (handles clones
1118 1118 and pull requests).
1119 1119
1120 1120 By default, all revisions are added to the changegroup.
1121 1121 """
1122 1122 opts = _byteskwargs(opts)
1123 1123 cl = repo.changelog
1124 1124 nodes = [cl.lookup(r) for r in repo.revs(rev or b'all()')]
1125 1125 bundler = changegroup.getbundler(cgversion, repo)
1126 1126
1127 1127 def d():
1128 1128 state, chunks = bundler._generatechangelog(cl, nodes)
1129 1129 for chunk in chunks:
1130 1130 pass
1131 1131
1132 1132 timer, fm = gettimer(ui, opts)
1133 1133
1134 1134 # Terminal printing can interfere with timing. So disable it.
1135 1135 with ui.configoverride({(b'progress', b'disable'): True}):
1136 1136 timer(d)
1137 1137
1138 1138 fm.end()
1139 1139
1140 1140
1141 1141 @command(b'perf::dirs|perfdirs', formatteropts)
1142 1142 def perfdirs(ui, repo, **opts):
1143 1143 opts = _byteskwargs(opts)
1144 1144 timer, fm = gettimer(ui, opts)
1145 1145 dirstate = repo.dirstate
1146 1146 b'a' in dirstate
1147 1147
1148 1148 def d():
1149 1149 dirstate.hasdir(b'a')
1150 del dirstate._map._dirs
1150 try:
1151 del dirstate._map._dirs
1152 except AttributeError:
1153 pass
1151 1154
1152 1155 timer(d)
1153 1156 fm.end()
1154 1157
1155 1158
1156 1159 @command(
1157 1160 b'perf::dirstate|perfdirstate',
1158 1161 [
1159 1162 (
1160 1163 b'',
1161 1164 b'iteration',
1162 1165 None,
1163 1166 b'benchmark a full iteration for the dirstate',
1164 1167 ),
1165 1168 (
1166 1169 b'',
1167 1170 b'contains',
1168 1171 None,
1169 1172 b'benchmark a large amount of `nf in dirstate` calls',
1170 1173 ),
1171 1174 ]
1172 1175 + formatteropts,
1173 1176 )
1174 1177 def perfdirstate(ui, repo, **opts):
1175 1178 """benchmap the time of various distate operations
1176 1179
1177 1180 By default benchmark the time necessary to load a dirstate from scratch.
1178 1181 The dirstate is loaded to the point were a "contains" request can be
1179 1182 answered.
1180 1183 """
1181 1184 opts = _byteskwargs(opts)
1182 1185 timer, fm = gettimer(ui, opts)
1183 1186 b"a" in repo.dirstate
1184 1187
1185 1188 if opts[b'iteration'] and opts[b'contains']:
1186 1189 msg = b'only specify one of --iteration or --contains'
1187 1190 raise error.Abort(msg)
1188 1191
1189 1192 if opts[b'iteration']:
1190 1193 setup = None
1191 1194 dirstate = repo.dirstate
1192 1195
1193 1196 def d():
1194 1197 for f in dirstate:
1195 1198 pass
1196 1199
1197 1200 elif opts[b'contains']:
1198 1201 setup = None
1199 1202 dirstate = repo.dirstate
1200 1203 allfiles = list(dirstate)
1201 1204 # also add file path that will be "missing" from the dirstate
1202 1205 allfiles.extend([f[::-1] for f in allfiles])
1203 1206
1204 1207 def d():
1205 1208 for f in allfiles:
1206 1209 f in dirstate
1207 1210
1208 1211 else:
1209 1212
1210 1213 def setup():
1211 1214 repo.dirstate.invalidate()
1212 1215
1213 1216 def d():
1214 1217 b"a" in repo.dirstate
1215 1218
1216 1219 timer(d, setup=setup)
1217 1220 fm.end()
1218 1221
1219 1222
1220 1223 @command(b'perf::dirstatedirs|perfdirstatedirs', formatteropts)
1221 1224 def perfdirstatedirs(ui, repo, **opts):
1222 1225 """benchmap a 'dirstate.hasdir' call from an empty `dirs` cache"""
1223 1226 opts = _byteskwargs(opts)
1224 1227 timer, fm = gettimer(ui, opts)
1225 1228 repo.dirstate.hasdir(b"a")
1226 1229
1227 1230 def setup():
1228 del repo.dirstate._map._dirs
1231 try:
1232 del repo.dirstate._map._dirs
1233 except AttributeError:
1234 pass
1229 1235
1230 1236 def d():
1231 1237 repo.dirstate.hasdir(b"a")
1232 1238
1233 1239 timer(d, setup=setup)
1234 1240 fm.end()
1235 1241
1236 1242
1237 1243 @command(b'perf::dirstatefoldmap|perfdirstatefoldmap', formatteropts)
1238 1244 def perfdirstatefoldmap(ui, repo, **opts):
1239 1245 """benchmap a `dirstate._map.filefoldmap.get()` request
1240 1246
1241 1247 The dirstate filefoldmap cache is dropped between every request.
1242 1248 """
1243 1249 opts = _byteskwargs(opts)
1244 1250 timer, fm = gettimer(ui, opts)
1245 1251 dirstate = repo.dirstate
1246 1252 dirstate._map.filefoldmap.get(b'a')
1247 1253
1248 1254 def setup():
1249 1255 del dirstate._map.filefoldmap
1250 1256
1251 1257 def d():
1252 1258 dirstate._map.filefoldmap.get(b'a')
1253 1259
1254 1260 timer(d, setup=setup)
1255 1261 fm.end()
1256 1262
1257 1263
1258 1264 @command(b'perf::dirfoldmap|perfdirfoldmap', formatteropts)
1259 1265 def perfdirfoldmap(ui, repo, **opts):
1260 1266 """benchmap a `dirstate._map.dirfoldmap.get()` request
1261 1267
1262 1268 The dirstate dirfoldmap cache is dropped between every request.
1263 1269 """
1264 1270 opts = _byteskwargs(opts)
1265 1271 timer, fm = gettimer(ui, opts)
1266 1272 dirstate = repo.dirstate
1267 1273 dirstate._map.dirfoldmap.get(b'a')
1268 1274
1269 1275 def setup():
1270 1276 del dirstate._map.dirfoldmap
1271 del dirstate._map._dirs
1277 try:
1278 del dirstate._map._dirs
1279 except AttributeError:
1280 pass
1272 1281
1273 1282 def d():
1274 1283 dirstate._map.dirfoldmap.get(b'a')
1275 1284
1276 1285 timer(d, setup=setup)
1277 1286 fm.end()
1278 1287
1279 1288
1280 1289 @command(b'perf::dirstatewrite|perfdirstatewrite', formatteropts)
1281 1290 def perfdirstatewrite(ui, repo, **opts):
1282 1291 """benchmap the time it take to write a dirstate on disk"""
1283 1292 opts = _byteskwargs(opts)
1284 1293 timer, fm = gettimer(ui, opts)
1285 1294 ds = repo.dirstate
1286 1295 b"a" in ds
1287 1296
1288 1297 def setup():
1289 1298 ds._dirty = True
1290 1299
1291 1300 def d():
1292 1301 ds.write(repo.currenttransaction())
1293 1302
1294 1303 timer(d, setup=setup)
1295 1304 fm.end()
1296 1305
1297 1306
1298 1307 def _getmergerevs(repo, opts):
1299 1308 """parse command argument to return rev involved in merge
1300 1309
1301 1310 input: options dictionnary with `rev`, `from` and `bse`
1302 1311 output: (localctx, otherctx, basectx)
1303 1312 """
1304 1313 if opts[b'from']:
1305 1314 fromrev = scmutil.revsingle(repo, opts[b'from'])
1306 1315 wctx = repo[fromrev]
1307 1316 else:
1308 1317 wctx = repo[None]
1309 1318 # we don't want working dir files to be stat'd in the benchmark, so
1310 1319 # prime that cache
1311 1320 wctx.dirty()
1312 1321 rctx = scmutil.revsingle(repo, opts[b'rev'], opts[b'rev'])
1313 1322 if opts[b'base']:
1314 1323 fromrev = scmutil.revsingle(repo, opts[b'base'])
1315 1324 ancestor = repo[fromrev]
1316 1325 else:
1317 1326 ancestor = wctx.ancestor(rctx)
1318 1327 return (wctx, rctx, ancestor)
1319 1328
1320 1329
1321 1330 @command(
1322 1331 b'perf::mergecalculate|perfmergecalculate',
1323 1332 [
1324 1333 (b'r', b'rev', b'.', b'rev to merge against'),
1325 1334 (b'', b'from', b'', b'rev to merge from'),
1326 1335 (b'', b'base', b'', b'the revision to use as base'),
1327 1336 ]
1328 1337 + formatteropts,
1329 1338 )
1330 1339 def perfmergecalculate(ui, repo, **opts):
1331 1340 opts = _byteskwargs(opts)
1332 1341 timer, fm = gettimer(ui, opts)
1333 1342
1334 1343 wctx, rctx, ancestor = _getmergerevs(repo, opts)
1335 1344
1336 1345 def d():
1337 1346 # acceptremote is True because we don't want prompts in the middle of
1338 1347 # our benchmark
1339 1348 merge.calculateupdates(
1340 1349 repo,
1341 1350 wctx,
1342 1351 rctx,
1343 1352 [ancestor],
1344 1353 branchmerge=False,
1345 1354 force=False,
1346 1355 acceptremote=True,
1347 1356 followcopies=True,
1348 1357 )
1349 1358
1350 1359 timer(d)
1351 1360 fm.end()
1352 1361
1353 1362
1354 1363 @command(
1355 1364 b'perf::mergecopies|perfmergecopies',
1356 1365 [
1357 1366 (b'r', b'rev', b'.', b'rev to merge against'),
1358 1367 (b'', b'from', b'', b'rev to merge from'),
1359 1368 (b'', b'base', b'', b'the revision to use as base'),
1360 1369 ]
1361 1370 + formatteropts,
1362 1371 )
1363 1372 def perfmergecopies(ui, repo, **opts):
1364 1373 """measure runtime of `copies.mergecopies`"""
1365 1374 opts = _byteskwargs(opts)
1366 1375 timer, fm = gettimer(ui, opts)
1367 1376 wctx, rctx, ancestor = _getmergerevs(repo, opts)
1368 1377
1369 1378 def d():
1370 1379 # acceptremote is True because we don't want prompts in the middle of
1371 1380 # our benchmark
1372 1381 copies.mergecopies(repo, wctx, rctx, ancestor)
1373 1382
1374 1383 timer(d)
1375 1384 fm.end()
1376 1385
1377 1386
1378 1387 @command(b'perf::pathcopies|perfpathcopies', [], b"REV REV")
1379 1388 def perfpathcopies(ui, repo, rev1, rev2, **opts):
1380 1389 """benchmark the copy tracing logic"""
1381 1390 opts = _byteskwargs(opts)
1382 1391 timer, fm = gettimer(ui, opts)
1383 1392 ctx1 = scmutil.revsingle(repo, rev1, rev1)
1384 1393 ctx2 = scmutil.revsingle(repo, rev2, rev2)
1385 1394
1386 1395 def d():
1387 1396 copies.pathcopies(ctx1, ctx2)
1388 1397
1389 1398 timer(d)
1390 1399 fm.end()
1391 1400
1392 1401
1393 1402 @command(
1394 1403 b'perf::phases|perfphases',
1395 1404 [
1396 1405 (b'', b'full', False, b'include file reading time too'),
1397 1406 ],
1398 1407 b"",
1399 1408 )
1400 1409 def perfphases(ui, repo, **opts):
1401 1410 """benchmark phasesets computation"""
1402 1411 opts = _byteskwargs(opts)
1403 1412 timer, fm = gettimer(ui, opts)
1404 1413 _phases = repo._phasecache
1405 1414 full = opts.get(b'full')
1406 1415
1407 1416 def d():
1408 1417 phases = _phases
1409 1418 if full:
1410 1419 clearfilecache(repo, b'_phasecache')
1411 1420 phases = repo._phasecache
1412 1421 phases.invalidate()
1413 1422 phases.loadphaserevs(repo)
1414 1423
1415 1424 timer(d)
1416 1425 fm.end()
1417 1426
1418 1427
1419 1428 @command(b'perf::phasesremote|perfphasesremote', [], b"[DEST]")
1420 1429 def perfphasesremote(ui, repo, dest=None, **opts):
1421 1430 """benchmark time needed to analyse phases of the remote server"""
1422 1431 from mercurial.node import bin
1423 1432 from mercurial import (
1424 1433 exchange,
1425 1434 hg,
1426 1435 phases,
1427 1436 )
1428 1437
1429 1438 opts = _byteskwargs(opts)
1430 1439 timer, fm = gettimer(ui, opts)
1431 1440
1432 1441 path = ui.getpath(dest, default=(b'default-push', b'default'))
1433 1442 if not path:
1434 1443 raise error.Abort(
1435 1444 b'default repository not configured!',
1436 1445 hint=b"see 'hg help config.paths'",
1437 1446 )
1438 1447 dest = path.pushloc or path.loc
1439 1448 ui.statusnoi18n(b'analysing phase of %s\n' % util.hidepassword(dest))
1440 1449 other = hg.peer(repo, opts, dest)
1441 1450
1442 1451 # easier to perform discovery through the operation
1443 1452 op = exchange.pushoperation(repo, other)
1444 1453 exchange._pushdiscoverychangeset(op)
1445 1454
1446 1455 remotesubset = op.fallbackheads
1447 1456
1448 1457 with other.commandexecutor() as e:
1449 1458 remotephases = e.callcommand(
1450 1459 b'listkeys', {b'namespace': b'phases'}
1451 1460 ).result()
1452 1461 del other
1453 1462 publishing = remotephases.get(b'publishing', False)
1454 1463 if publishing:
1455 1464 ui.statusnoi18n(b'publishing: yes\n')
1456 1465 else:
1457 1466 ui.statusnoi18n(b'publishing: no\n')
1458 1467
1459 1468 has_node = getattr(repo.changelog.index, 'has_node', None)
1460 1469 if has_node is None:
1461 1470 has_node = repo.changelog.nodemap.__contains__
1462 1471 nonpublishroots = 0
1463 1472 for nhex, phase in remotephases.iteritems():
1464 1473 if nhex == b'publishing': # ignore data related to publish option
1465 1474 continue
1466 1475 node = bin(nhex)
1467 1476 if has_node(node) and int(phase):
1468 1477 nonpublishroots += 1
1469 1478 ui.statusnoi18n(b'number of roots: %d\n' % len(remotephases))
1470 1479 ui.statusnoi18n(b'number of known non public roots: %d\n' % nonpublishroots)
1471 1480
1472 1481 def d():
1473 1482 phases.remotephasessummary(repo, remotesubset, remotephases)
1474 1483
1475 1484 timer(d)
1476 1485 fm.end()
1477 1486
1478 1487
1479 1488 @command(
1480 1489 b'perf::manifest|perfmanifest',
1481 1490 [
1482 1491 (b'm', b'manifest-rev', False, b'Look up a manifest node revision'),
1483 1492 (b'', b'clear-disk', False, b'clear on-disk caches too'),
1484 1493 ]
1485 1494 + formatteropts,
1486 1495 b'REV|NODE',
1487 1496 )
1488 1497 def perfmanifest(ui, repo, rev, manifest_rev=False, clear_disk=False, **opts):
1489 1498 """benchmark the time to read a manifest from disk and return a usable
1490 1499 dict-like object
1491 1500
1492 1501 Manifest caches are cleared before retrieval."""
1493 1502 opts = _byteskwargs(opts)
1494 1503 timer, fm = gettimer(ui, opts)
1495 1504 if not manifest_rev:
1496 1505 ctx = scmutil.revsingle(repo, rev, rev)
1497 1506 t = ctx.manifestnode()
1498 1507 else:
1499 1508 from mercurial.node import bin
1500 1509
1501 1510 if len(rev) == 40:
1502 1511 t = bin(rev)
1503 1512 else:
1504 1513 try:
1505 1514 rev = int(rev)
1506 1515
1507 1516 if util.safehasattr(repo.manifestlog, b'getstorage'):
1508 1517 t = repo.manifestlog.getstorage(b'').node(rev)
1509 1518 else:
1510 1519 t = repo.manifestlog._revlog.lookup(rev)
1511 1520 except ValueError:
1512 1521 raise error.Abort(
1513 1522 b'manifest revision must be integer or full node'
1514 1523 )
1515 1524
1516 1525 def d():
1517 1526 repo.manifestlog.clearcaches(clear_persisted_data=clear_disk)
1518 1527 repo.manifestlog[t].read()
1519 1528
1520 1529 timer(d)
1521 1530 fm.end()
1522 1531
1523 1532
1524 1533 @command(b'perf::changeset|perfchangeset', formatteropts)
1525 1534 def perfchangeset(ui, repo, rev, **opts):
1526 1535 opts = _byteskwargs(opts)
1527 1536 timer, fm = gettimer(ui, opts)
1528 1537 n = scmutil.revsingle(repo, rev).node()
1529 1538
1530 1539 def d():
1531 1540 repo.changelog.read(n)
1532 1541 # repo.changelog._cache = None
1533 1542
1534 1543 timer(d)
1535 1544 fm.end()
1536 1545
1537 1546
1538 1547 @command(b'perf::ignore|perfignore', formatteropts)
1539 1548 def perfignore(ui, repo, **opts):
1540 1549 """benchmark operation related to computing ignore"""
1541 1550 opts = _byteskwargs(opts)
1542 1551 timer, fm = gettimer(ui, opts)
1543 1552 dirstate = repo.dirstate
1544 1553
1545 1554 def setupone():
1546 1555 dirstate.invalidate()
1547 1556 clearfilecache(dirstate, b'_ignore')
1548 1557
1549 1558 def runone():
1550 1559 dirstate._ignore
1551 1560
1552 1561 timer(runone, setup=setupone, title=b"load")
1553 1562 fm.end()
1554 1563
1555 1564
1556 1565 @command(
1557 1566 b'perf::index|perfindex',
1558 1567 [
1559 1568 (b'', b'rev', [], b'revision to be looked up (default tip)'),
1560 1569 (b'', b'no-lookup', None, b'do not revision lookup post creation'),
1561 1570 ]
1562 1571 + formatteropts,
1563 1572 )
1564 1573 def perfindex(ui, repo, **opts):
1565 1574 """benchmark index creation time followed by a lookup
1566 1575
1567 1576 The default is to look `tip` up. Depending on the index implementation,
1568 1577 the revision looked up can matters. For example, an implementation
1569 1578 scanning the index will have a faster lookup time for `--rev tip` than for
1570 1579 `--rev 0`. The number of looked up revisions and their order can also
1571 1580 matters.
1572 1581
1573 1582 Example of useful set to test:
1574 1583
1575 1584 * tip
1576 1585 * 0
1577 1586 * -10:
1578 1587 * :10
1579 1588 * -10: + :10
1580 1589 * :10: + -10:
1581 1590 * -10000:
1582 1591 * -10000: + 0
1583 1592
1584 1593 It is not currently possible to check for lookup of a missing node. For
1585 1594 deeper lookup benchmarking, checkout the `perfnodemap` command."""
1586 1595 import mercurial.revlog
1587 1596
1588 1597 opts = _byteskwargs(opts)
1589 1598 timer, fm = gettimer(ui, opts)
1590 1599 mercurial.revlog._prereadsize = 2 ** 24 # disable lazy parser in old hg
1591 1600 if opts[b'no_lookup']:
1592 1601 if opts['rev']:
1593 1602 raise error.Abort('--no-lookup and --rev are mutually exclusive')
1594 1603 nodes = []
1595 1604 elif not opts[b'rev']:
1596 1605 nodes = [repo[b"tip"].node()]
1597 1606 else:
1598 1607 revs = scmutil.revrange(repo, opts[b'rev'])
1599 1608 cl = repo.changelog
1600 1609 nodes = [cl.node(r) for r in revs]
1601 1610
1602 1611 unfi = repo.unfiltered()
1603 1612 # find the filecache func directly
1604 1613 # This avoid polluting the benchmark with the filecache logic
1605 1614 makecl = unfi.__class__.changelog.func
1606 1615
1607 1616 def setup():
1608 1617 # probably not necessary, but for good measure
1609 1618 clearchangelog(unfi)
1610 1619
1611 1620 def d():
1612 1621 cl = makecl(unfi)
1613 1622 for n in nodes:
1614 1623 cl.rev(n)
1615 1624
1616 1625 timer(d, setup=setup)
1617 1626 fm.end()
1618 1627
1619 1628
1620 1629 @command(
1621 1630 b'perf::nodemap|perfnodemap',
1622 1631 [
1623 1632 (b'', b'rev', [], b'revision to be looked up (default tip)'),
1624 1633 (b'', b'clear-caches', True, b'clear revlog cache between calls'),
1625 1634 ]
1626 1635 + formatteropts,
1627 1636 )
1628 1637 def perfnodemap(ui, repo, **opts):
1629 1638 """benchmark the time necessary to look up revision from a cold nodemap
1630 1639
1631 1640 Depending on the implementation, the amount and order of revision we look
1632 1641 up can varies. Example of useful set to test:
1633 1642 * tip
1634 1643 * 0
1635 1644 * -10:
1636 1645 * :10
1637 1646 * -10: + :10
1638 1647 * :10: + -10:
1639 1648 * -10000:
1640 1649 * -10000: + 0
1641 1650
1642 1651 The command currently focus on valid binary lookup. Benchmarking for
1643 1652 hexlookup, prefix lookup and missing lookup would also be valuable.
1644 1653 """
1645 1654 import mercurial.revlog
1646 1655
1647 1656 opts = _byteskwargs(opts)
1648 1657 timer, fm = gettimer(ui, opts)
1649 1658 mercurial.revlog._prereadsize = 2 ** 24 # disable lazy parser in old hg
1650 1659
1651 1660 unfi = repo.unfiltered()
1652 1661 clearcaches = opts[b'clear_caches']
1653 1662 # find the filecache func directly
1654 1663 # This avoid polluting the benchmark with the filecache logic
1655 1664 makecl = unfi.__class__.changelog.func
1656 1665 if not opts[b'rev']:
1657 1666 raise error.Abort(b'use --rev to specify revisions to look up')
1658 1667 revs = scmutil.revrange(repo, opts[b'rev'])
1659 1668 cl = repo.changelog
1660 1669 nodes = [cl.node(r) for r in revs]
1661 1670
1662 1671 # use a list to pass reference to a nodemap from one closure to the next
1663 1672 nodeget = [None]
1664 1673
1665 1674 def setnodeget():
1666 1675 # probably not necessary, but for good measure
1667 1676 clearchangelog(unfi)
1668 1677 cl = makecl(unfi)
1669 1678 if util.safehasattr(cl.index, 'get_rev'):
1670 1679 nodeget[0] = cl.index.get_rev
1671 1680 else:
1672 1681 nodeget[0] = cl.nodemap.get
1673 1682
1674 1683 def d():
1675 1684 get = nodeget[0]
1676 1685 for n in nodes:
1677 1686 get(n)
1678 1687
1679 1688 setup = None
1680 1689 if clearcaches:
1681 1690
1682 1691 def setup():
1683 1692 setnodeget()
1684 1693
1685 1694 else:
1686 1695 setnodeget()
1687 1696 d() # prewarm the data structure
1688 1697 timer(d, setup=setup)
1689 1698 fm.end()
1690 1699
1691 1700
1692 1701 @command(b'perf::startup|perfstartup', formatteropts)
1693 1702 def perfstartup(ui, repo, **opts):
1694 1703 opts = _byteskwargs(opts)
1695 1704 timer, fm = gettimer(ui, opts)
1696 1705
1697 1706 def d():
1698 1707 if os.name != 'nt':
1699 1708 os.system(
1700 1709 b"HGRCPATH= %s version -q > /dev/null" % fsencode(sys.argv[0])
1701 1710 )
1702 1711 else:
1703 1712 os.environ['HGRCPATH'] = r' '
1704 1713 os.system("%s version -q > NUL" % sys.argv[0])
1705 1714
1706 1715 timer(d)
1707 1716 fm.end()
1708 1717
1709 1718
1710 1719 @command(b'perf::parents|perfparents', formatteropts)
1711 1720 def perfparents(ui, repo, **opts):
1712 1721 """benchmark the time necessary to fetch one changeset's parents.
1713 1722
1714 1723 The fetch is done using the `node identifier`, traversing all object layers
1715 1724 from the repository object. The first N revisions will be used for this
1716 1725 benchmark. N is controlled by the ``perf.parentscount`` config option
1717 1726 (default: 1000).
1718 1727 """
1719 1728 opts = _byteskwargs(opts)
1720 1729 timer, fm = gettimer(ui, opts)
1721 1730 # control the number of commits perfparents iterates over
1722 1731 # experimental config: perf.parentscount
1723 1732 count = getint(ui, b"perf", b"parentscount", 1000)
1724 1733 if len(repo.changelog) < count:
1725 1734 raise error.Abort(b"repo needs %d commits for this test" % count)
1726 1735 repo = repo.unfiltered()
1727 1736 nl = [repo.changelog.node(i) for i in _xrange(count)]
1728 1737
1729 1738 def d():
1730 1739 for n in nl:
1731 1740 repo.changelog.parents(n)
1732 1741
1733 1742 timer(d)
1734 1743 fm.end()
1735 1744
1736 1745
1737 1746 @command(b'perf::ctxfiles|perfctxfiles', formatteropts)
1738 1747 def perfctxfiles(ui, repo, x, **opts):
1739 1748 opts = _byteskwargs(opts)
1740 1749 x = int(x)
1741 1750 timer, fm = gettimer(ui, opts)
1742 1751
1743 1752 def d():
1744 1753 len(repo[x].files())
1745 1754
1746 1755 timer(d)
1747 1756 fm.end()
1748 1757
1749 1758
1750 1759 @command(b'perf::rawfiles|perfrawfiles', formatteropts)
1751 1760 def perfrawfiles(ui, repo, x, **opts):
1752 1761 opts = _byteskwargs(opts)
1753 1762 x = int(x)
1754 1763 timer, fm = gettimer(ui, opts)
1755 1764 cl = repo.changelog
1756 1765
1757 1766 def d():
1758 1767 len(cl.read(x)[3])
1759 1768
1760 1769 timer(d)
1761 1770 fm.end()
1762 1771
1763 1772
1764 1773 @command(b'perf::lookup|perflookup', formatteropts)
1765 1774 def perflookup(ui, repo, rev, **opts):
1766 1775 opts = _byteskwargs(opts)
1767 1776 timer, fm = gettimer(ui, opts)
1768 1777 timer(lambda: len(repo.lookup(rev)))
1769 1778 fm.end()
1770 1779
1771 1780
1772 1781 @command(
1773 1782 b'perf::linelogedits|perflinelogedits',
1774 1783 [
1775 1784 (b'n', b'edits', 10000, b'number of edits'),
1776 1785 (b'', b'max-hunk-lines', 10, b'max lines in a hunk'),
1777 1786 ],
1778 1787 norepo=True,
1779 1788 )
1780 1789 def perflinelogedits(ui, **opts):
1781 1790 from mercurial import linelog
1782 1791
1783 1792 opts = _byteskwargs(opts)
1784 1793
1785 1794 edits = opts[b'edits']
1786 1795 maxhunklines = opts[b'max_hunk_lines']
1787 1796
1788 1797 maxb1 = 100000
1789 1798 random.seed(0)
1790 1799 randint = random.randint
1791 1800 currentlines = 0
1792 1801 arglist = []
1793 1802 for rev in _xrange(edits):
1794 1803 a1 = randint(0, currentlines)
1795 1804 a2 = randint(a1, min(currentlines, a1 + maxhunklines))
1796 1805 b1 = randint(0, maxb1)
1797 1806 b2 = randint(b1, b1 + maxhunklines)
1798 1807 currentlines += (b2 - b1) - (a2 - a1)
1799 1808 arglist.append((rev, a1, a2, b1, b2))
1800 1809
1801 1810 def d():
1802 1811 ll = linelog.linelog()
1803 1812 for args in arglist:
1804 1813 ll.replacelines(*args)
1805 1814
1806 1815 timer, fm = gettimer(ui, opts)
1807 1816 timer(d)
1808 1817 fm.end()
1809 1818
1810 1819
1811 1820 @command(b'perf::revrange|perfrevrange', formatteropts)
1812 1821 def perfrevrange(ui, repo, *specs, **opts):
1813 1822 opts = _byteskwargs(opts)
1814 1823 timer, fm = gettimer(ui, opts)
1815 1824 revrange = scmutil.revrange
1816 1825 timer(lambda: len(revrange(repo, specs)))
1817 1826 fm.end()
1818 1827
1819 1828
1820 1829 @command(b'perf::nodelookup|perfnodelookup', formatteropts)
1821 1830 def perfnodelookup(ui, repo, rev, **opts):
1822 1831 opts = _byteskwargs(opts)
1823 1832 timer, fm = gettimer(ui, opts)
1824 1833 import mercurial.revlog
1825 1834
1826 1835 mercurial.revlog._prereadsize = 2 ** 24 # disable lazy parser in old hg
1827 1836 n = scmutil.revsingle(repo, rev).node()
1828 1837
1829 1838 try:
1830 1839 cl = revlog(getsvfs(repo), radix=b"00changelog")
1831 1840 except TypeError:
1832 1841 cl = revlog(getsvfs(repo), indexfile=b"00changelog.i")
1833 1842
1834 1843 def d():
1835 1844 cl.rev(n)
1836 1845 clearcaches(cl)
1837 1846
1838 1847 timer(d)
1839 1848 fm.end()
1840 1849
1841 1850
1842 1851 @command(
1843 1852 b'perf::log|perflog',
1844 1853 [(b'', b'rename', False, b'ask log to follow renames')] + formatteropts,
1845 1854 )
1846 1855 def perflog(ui, repo, rev=None, **opts):
1847 1856 opts = _byteskwargs(opts)
1848 1857 if rev is None:
1849 1858 rev = []
1850 1859 timer, fm = gettimer(ui, opts)
1851 1860 ui.pushbuffer()
1852 1861 timer(
1853 1862 lambda: commands.log(
1854 1863 ui, repo, rev=rev, date=b'', user=b'', copies=opts.get(b'rename')
1855 1864 )
1856 1865 )
1857 1866 ui.popbuffer()
1858 1867 fm.end()
1859 1868
1860 1869
1861 1870 @command(b'perf::moonwalk|perfmoonwalk', formatteropts)
1862 1871 def perfmoonwalk(ui, repo, **opts):
1863 1872 """benchmark walking the changelog backwards
1864 1873
1865 1874 This also loads the changelog data for each revision in the changelog.
1866 1875 """
1867 1876 opts = _byteskwargs(opts)
1868 1877 timer, fm = gettimer(ui, opts)
1869 1878
1870 1879 def moonwalk():
1871 1880 for i in repo.changelog.revs(start=(len(repo) - 1), stop=-1):
1872 1881 ctx = repo[i]
1873 1882 ctx.branch() # read changelog data (in addition to the index)
1874 1883
1875 1884 timer(moonwalk)
1876 1885 fm.end()
1877 1886
1878 1887
1879 1888 @command(
1880 1889 b'perf::templating|perftemplating',
1881 1890 [
1882 1891 (b'r', b'rev', [], b'revisions to run the template on'),
1883 1892 ]
1884 1893 + formatteropts,
1885 1894 )
1886 1895 def perftemplating(ui, repo, testedtemplate=None, **opts):
1887 1896 """test the rendering time of a given template"""
1888 1897 if makelogtemplater is None:
1889 1898 raise error.Abort(
1890 1899 b"perftemplating not available with this Mercurial",
1891 1900 hint=b"use 4.3 or later",
1892 1901 )
1893 1902
1894 1903 opts = _byteskwargs(opts)
1895 1904
1896 1905 nullui = ui.copy()
1897 1906 nullui.fout = open(os.devnull, 'wb')
1898 1907 nullui.disablepager()
1899 1908 revs = opts.get(b'rev')
1900 1909 if not revs:
1901 1910 revs = [b'all()']
1902 1911 revs = list(scmutil.revrange(repo, revs))
1903 1912
1904 1913 defaulttemplate = (
1905 1914 b'{date|shortdate} [{rev}:{node|short}]'
1906 1915 b' {author|person}: {desc|firstline}\n'
1907 1916 )
1908 1917 if testedtemplate is None:
1909 1918 testedtemplate = defaulttemplate
1910 1919 displayer = makelogtemplater(nullui, repo, testedtemplate)
1911 1920
1912 1921 def format():
1913 1922 for r in revs:
1914 1923 ctx = repo[r]
1915 1924 displayer.show(ctx)
1916 1925 displayer.flush(ctx)
1917 1926
1918 1927 timer, fm = gettimer(ui, opts)
1919 1928 timer(format)
1920 1929 fm.end()
1921 1930
1922 1931
1923 1932 def _displaystats(ui, opts, entries, data):
1924 1933 # use a second formatter because the data are quite different, not sure
1925 1934 # how it flies with the templater.
1926 1935 fm = ui.formatter(b'perf-stats', opts)
1927 1936 for key, title in entries:
1928 1937 values = data[key]
1929 1938 nbvalues = len(data)
1930 1939 values.sort()
1931 1940 stats = {
1932 1941 'key': key,
1933 1942 'title': title,
1934 1943 'nbitems': len(values),
1935 1944 'min': values[0][0],
1936 1945 '10%': values[(nbvalues * 10) // 100][0],
1937 1946 '25%': values[(nbvalues * 25) // 100][0],
1938 1947 '50%': values[(nbvalues * 50) // 100][0],
1939 1948 '75%': values[(nbvalues * 75) // 100][0],
1940 1949 '80%': values[(nbvalues * 80) // 100][0],
1941 1950 '85%': values[(nbvalues * 85) // 100][0],
1942 1951 '90%': values[(nbvalues * 90) // 100][0],
1943 1952 '95%': values[(nbvalues * 95) // 100][0],
1944 1953 '99%': values[(nbvalues * 99) // 100][0],
1945 1954 'max': values[-1][0],
1946 1955 }
1947 1956 fm.startitem()
1948 1957 fm.data(**stats)
1949 1958 # make node pretty for the human output
1950 1959 fm.plain('### %s (%d items)\n' % (title, len(values)))
1951 1960 lines = [
1952 1961 'min',
1953 1962 '10%',
1954 1963 '25%',
1955 1964 '50%',
1956 1965 '75%',
1957 1966 '80%',
1958 1967 '85%',
1959 1968 '90%',
1960 1969 '95%',
1961 1970 '99%',
1962 1971 'max',
1963 1972 ]
1964 1973 for l in lines:
1965 1974 fm.plain('%s: %s\n' % (l, stats[l]))
1966 1975 fm.end()
1967 1976
1968 1977
1969 1978 @command(
1970 1979 b'perf::helper-mergecopies|perfhelper-mergecopies',
1971 1980 formatteropts
1972 1981 + [
1973 1982 (b'r', b'revs', [], b'restrict search to these revisions'),
1974 1983 (b'', b'timing', False, b'provides extra data (costly)'),
1975 1984 (b'', b'stats', False, b'provides statistic about the measured data'),
1976 1985 ],
1977 1986 )
1978 1987 def perfhelpermergecopies(ui, repo, revs=[], **opts):
1979 1988 """find statistics about potential parameters for `perfmergecopies`
1980 1989
1981 1990 This command find (base, p1, p2) triplet relevant for copytracing
1982 1991 benchmarking in the context of a merge. It reports values for some of the
1983 1992 parameters that impact merge copy tracing time during merge.
1984 1993
1985 1994 If `--timing` is set, rename detection is run and the associated timing
1986 1995 will be reported. The extra details come at the cost of slower command
1987 1996 execution.
1988 1997
1989 1998 Since rename detection is only run once, other factors might easily
1990 1999 affect the precision of the timing. However it should give a good
1991 2000 approximation of which revision triplets are very costly.
1992 2001 """
1993 2002 opts = _byteskwargs(opts)
1994 2003 fm = ui.formatter(b'perf', opts)
1995 2004 dotiming = opts[b'timing']
1996 2005 dostats = opts[b'stats']
1997 2006
1998 2007 output_template = [
1999 2008 ("base", "%(base)12s"),
2000 2009 ("p1", "%(p1.node)12s"),
2001 2010 ("p2", "%(p2.node)12s"),
2002 2011 ("p1.nb-revs", "%(p1.nbrevs)12d"),
2003 2012 ("p1.nb-files", "%(p1.nbmissingfiles)12d"),
2004 2013 ("p1.renames", "%(p1.renamedfiles)12d"),
2005 2014 ("p1.time", "%(p1.time)12.3f"),
2006 2015 ("p2.nb-revs", "%(p2.nbrevs)12d"),
2007 2016 ("p2.nb-files", "%(p2.nbmissingfiles)12d"),
2008 2017 ("p2.renames", "%(p2.renamedfiles)12d"),
2009 2018 ("p2.time", "%(p2.time)12.3f"),
2010 2019 ("renames", "%(nbrenamedfiles)12d"),
2011 2020 ("total.time", "%(time)12.3f"),
2012 2021 ]
2013 2022 if not dotiming:
2014 2023 output_template = [
2015 2024 i
2016 2025 for i in output_template
2017 2026 if not ('time' in i[0] or 'renames' in i[0])
2018 2027 ]
2019 2028 header_names = [h for (h, v) in output_template]
2020 2029 output = ' '.join([v for (h, v) in output_template]) + '\n'
2021 2030 header = ' '.join(['%12s'] * len(header_names)) + '\n'
2022 2031 fm.plain(header % tuple(header_names))
2023 2032
2024 2033 if not revs:
2025 2034 revs = ['all()']
2026 2035 revs = scmutil.revrange(repo, revs)
2027 2036
2028 2037 if dostats:
2029 2038 alldata = {
2030 2039 'nbrevs': [],
2031 2040 'nbmissingfiles': [],
2032 2041 }
2033 2042 if dotiming:
2034 2043 alldata['parentnbrenames'] = []
2035 2044 alldata['totalnbrenames'] = []
2036 2045 alldata['parenttime'] = []
2037 2046 alldata['totaltime'] = []
2038 2047
2039 2048 roi = repo.revs('merge() and %ld', revs)
2040 2049 for r in roi:
2041 2050 ctx = repo[r]
2042 2051 p1 = ctx.p1()
2043 2052 p2 = ctx.p2()
2044 2053 bases = repo.changelog._commonancestorsheads(p1.rev(), p2.rev())
2045 2054 for b in bases:
2046 2055 b = repo[b]
2047 2056 p1missing = copies._computeforwardmissing(b, p1)
2048 2057 p2missing = copies._computeforwardmissing(b, p2)
2049 2058 data = {
2050 2059 b'base': b.hex(),
2051 2060 b'p1.node': p1.hex(),
2052 2061 b'p1.nbrevs': len(repo.revs('only(%d, %d)', p1.rev(), b.rev())),
2053 2062 b'p1.nbmissingfiles': len(p1missing),
2054 2063 b'p2.node': p2.hex(),
2055 2064 b'p2.nbrevs': len(repo.revs('only(%d, %d)', p2.rev(), b.rev())),
2056 2065 b'p2.nbmissingfiles': len(p2missing),
2057 2066 }
2058 2067 if dostats:
2059 2068 if p1missing:
2060 2069 alldata['nbrevs'].append(
2061 2070 (data['p1.nbrevs'], b.hex(), p1.hex())
2062 2071 )
2063 2072 alldata['nbmissingfiles'].append(
2064 2073 (data['p1.nbmissingfiles'], b.hex(), p1.hex())
2065 2074 )
2066 2075 if p2missing:
2067 2076 alldata['nbrevs'].append(
2068 2077 (data['p2.nbrevs'], b.hex(), p2.hex())
2069 2078 )
2070 2079 alldata['nbmissingfiles'].append(
2071 2080 (data['p2.nbmissingfiles'], b.hex(), p2.hex())
2072 2081 )
2073 2082 if dotiming:
2074 2083 begin = util.timer()
2075 2084 mergedata = copies.mergecopies(repo, p1, p2, b)
2076 2085 end = util.timer()
2077 2086 # not very stable timing since we did only one run
2078 2087 data['time'] = end - begin
2079 2088 # mergedata contains five dicts: "copy", "movewithdir",
2080 2089 # "diverge", "renamedelete" and "dirmove".
2081 2090 # The first 4 are about renamed file so lets count that.
2082 2091 renames = len(mergedata[0])
2083 2092 renames += len(mergedata[1])
2084 2093 renames += len(mergedata[2])
2085 2094 renames += len(mergedata[3])
2086 2095 data['nbrenamedfiles'] = renames
2087 2096 begin = util.timer()
2088 2097 p1renames = copies.pathcopies(b, p1)
2089 2098 end = util.timer()
2090 2099 data['p1.time'] = end - begin
2091 2100 begin = util.timer()
2092 2101 p2renames = copies.pathcopies(b, p2)
2093 2102 end = util.timer()
2094 2103 data['p2.time'] = end - begin
2095 2104 data['p1.renamedfiles'] = len(p1renames)
2096 2105 data['p2.renamedfiles'] = len(p2renames)
2097 2106
2098 2107 if dostats:
2099 2108 if p1missing:
2100 2109 alldata['parentnbrenames'].append(
2101 2110 (data['p1.renamedfiles'], b.hex(), p1.hex())
2102 2111 )
2103 2112 alldata['parenttime'].append(
2104 2113 (data['p1.time'], b.hex(), p1.hex())
2105 2114 )
2106 2115 if p2missing:
2107 2116 alldata['parentnbrenames'].append(
2108 2117 (data['p2.renamedfiles'], b.hex(), p2.hex())
2109 2118 )
2110 2119 alldata['parenttime'].append(
2111 2120 (data['p2.time'], b.hex(), p2.hex())
2112 2121 )
2113 2122 if p1missing or p2missing:
2114 2123 alldata['totalnbrenames'].append(
2115 2124 (
2116 2125 data['nbrenamedfiles'],
2117 2126 b.hex(),
2118 2127 p1.hex(),
2119 2128 p2.hex(),
2120 2129 )
2121 2130 )
2122 2131 alldata['totaltime'].append(
2123 2132 (data['time'], b.hex(), p1.hex(), p2.hex())
2124 2133 )
2125 2134 fm.startitem()
2126 2135 fm.data(**data)
2127 2136 # make node pretty for the human output
2128 2137 out = data.copy()
2129 2138 out['base'] = fm.hexfunc(b.node())
2130 2139 out['p1.node'] = fm.hexfunc(p1.node())
2131 2140 out['p2.node'] = fm.hexfunc(p2.node())
2132 2141 fm.plain(output % out)
2133 2142
2134 2143 fm.end()
2135 2144 if dostats:
2136 2145 # use a second formatter because the data are quite different, not sure
2137 2146 # how it flies with the templater.
2138 2147 entries = [
2139 2148 ('nbrevs', 'number of revision covered'),
2140 2149 ('nbmissingfiles', 'number of missing files at head'),
2141 2150 ]
2142 2151 if dotiming:
2143 2152 entries.append(
2144 2153 ('parentnbrenames', 'rename from one parent to base')
2145 2154 )
2146 2155 entries.append(('totalnbrenames', 'total number of renames'))
2147 2156 entries.append(('parenttime', 'time for one parent'))
2148 2157 entries.append(('totaltime', 'time for both parents'))
2149 2158 _displaystats(ui, opts, entries, alldata)
2150 2159
2151 2160
2152 2161 @command(
2153 2162 b'perf::helper-pathcopies|perfhelper-pathcopies',
2154 2163 formatteropts
2155 2164 + [
2156 2165 (b'r', b'revs', [], b'restrict search to these revisions'),
2157 2166 (b'', b'timing', False, b'provides extra data (costly)'),
2158 2167 (b'', b'stats', False, b'provides statistic about the measured data'),
2159 2168 ],
2160 2169 )
2161 2170 def perfhelperpathcopies(ui, repo, revs=[], **opts):
2162 2171 """find statistic about potential parameters for the `perftracecopies`
2163 2172
2164 2173 This command find source-destination pair relevant for copytracing testing.
2165 2174 It report value for some of the parameters that impact copy tracing time.
2166 2175
2167 2176 If `--timing` is set, rename detection is run and the associated timing
2168 2177 will be reported. The extra details comes at the cost of a slower command
2169 2178 execution.
2170 2179
2171 2180 Since the rename detection is only run once, other factors might easily
2172 2181 affect the precision of the timing. However it should give a good
2173 2182 approximation of which revision pairs are very costly.
2174 2183 """
2175 2184 opts = _byteskwargs(opts)
2176 2185 fm = ui.formatter(b'perf', opts)
2177 2186 dotiming = opts[b'timing']
2178 2187 dostats = opts[b'stats']
2179 2188
2180 2189 if dotiming:
2181 2190 header = '%12s %12s %12s %12s %12s %12s\n'
2182 2191 output = (
2183 2192 "%(source)12s %(destination)12s "
2184 2193 "%(nbrevs)12d %(nbmissingfiles)12d "
2185 2194 "%(nbrenamedfiles)12d %(time)18.5f\n"
2186 2195 )
2187 2196 header_names = (
2188 2197 "source",
2189 2198 "destination",
2190 2199 "nb-revs",
2191 2200 "nb-files",
2192 2201 "nb-renames",
2193 2202 "time",
2194 2203 )
2195 2204 fm.plain(header % header_names)
2196 2205 else:
2197 2206 header = '%12s %12s %12s %12s\n'
2198 2207 output = (
2199 2208 "%(source)12s %(destination)12s "
2200 2209 "%(nbrevs)12d %(nbmissingfiles)12d\n"
2201 2210 )
2202 2211 fm.plain(header % ("source", "destination", "nb-revs", "nb-files"))
2203 2212
2204 2213 if not revs:
2205 2214 revs = ['all()']
2206 2215 revs = scmutil.revrange(repo, revs)
2207 2216
2208 2217 if dostats:
2209 2218 alldata = {
2210 2219 'nbrevs': [],
2211 2220 'nbmissingfiles': [],
2212 2221 }
2213 2222 if dotiming:
2214 2223 alldata['nbrenames'] = []
2215 2224 alldata['time'] = []
2216 2225
2217 2226 roi = repo.revs('merge() and %ld', revs)
2218 2227 for r in roi:
2219 2228 ctx = repo[r]
2220 2229 p1 = ctx.p1().rev()
2221 2230 p2 = ctx.p2().rev()
2222 2231 bases = repo.changelog._commonancestorsheads(p1, p2)
2223 2232 for p in (p1, p2):
2224 2233 for b in bases:
2225 2234 base = repo[b]
2226 2235 parent = repo[p]
2227 2236 missing = copies._computeforwardmissing(base, parent)
2228 2237 if not missing:
2229 2238 continue
2230 2239 data = {
2231 2240 b'source': base.hex(),
2232 2241 b'destination': parent.hex(),
2233 2242 b'nbrevs': len(repo.revs('only(%d, %d)', p, b)),
2234 2243 b'nbmissingfiles': len(missing),
2235 2244 }
2236 2245 if dostats:
2237 2246 alldata['nbrevs'].append(
2238 2247 (
2239 2248 data['nbrevs'],
2240 2249 base.hex(),
2241 2250 parent.hex(),
2242 2251 )
2243 2252 )
2244 2253 alldata['nbmissingfiles'].append(
2245 2254 (
2246 2255 data['nbmissingfiles'],
2247 2256 base.hex(),
2248 2257 parent.hex(),
2249 2258 )
2250 2259 )
2251 2260 if dotiming:
2252 2261 begin = util.timer()
2253 2262 renames = copies.pathcopies(base, parent)
2254 2263 end = util.timer()
2255 2264 # not very stable timing since we did only one run
2256 2265 data['time'] = end - begin
2257 2266 data['nbrenamedfiles'] = len(renames)
2258 2267 if dostats:
2259 2268 alldata['time'].append(
2260 2269 (
2261 2270 data['time'],
2262 2271 base.hex(),
2263 2272 parent.hex(),
2264 2273 )
2265 2274 )
2266 2275 alldata['nbrenames'].append(
2267 2276 (
2268 2277 data['nbrenamedfiles'],
2269 2278 base.hex(),
2270 2279 parent.hex(),
2271 2280 )
2272 2281 )
2273 2282 fm.startitem()
2274 2283 fm.data(**data)
2275 2284 out = data.copy()
2276 2285 out['source'] = fm.hexfunc(base.node())
2277 2286 out['destination'] = fm.hexfunc(parent.node())
2278 2287 fm.plain(output % out)
2279 2288
2280 2289 fm.end()
2281 2290 if dostats:
2282 2291 entries = [
2283 2292 ('nbrevs', 'number of revision covered'),
2284 2293 ('nbmissingfiles', 'number of missing files at head'),
2285 2294 ]
2286 2295 if dotiming:
2287 2296 entries.append(('nbrenames', 'renamed files'))
2288 2297 entries.append(('time', 'time'))
2289 2298 _displaystats(ui, opts, entries, alldata)
2290 2299
2291 2300
2292 2301 @command(b'perf::cca|perfcca', formatteropts)
2293 2302 def perfcca(ui, repo, **opts):
2294 2303 opts = _byteskwargs(opts)
2295 2304 timer, fm = gettimer(ui, opts)
2296 2305 timer(lambda: scmutil.casecollisionauditor(ui, False, repo.dirstate))
2297 2306 fm.end()
2298 2307
2299 2308
2300 2309 @command(b'perf::fncacheload|perffncacheload', formatteropts)
2301 2310 def perffncacheload(ui, repo, **opts):
2302 2311 opts = _byteskwargs(opts)
2303 2312 timer, fm = gettimer(ui, opts)
2304 2313 s = repo.store
2305 2314
2306 2315 def d():
2307 2316 s.fncache._load()
2308 2317
2309 2318 timer(d)
2310 2319 fm.end()
2311 2320
2312 2321
2313 2322 @command(b'perf::fncachewrite|perffncachewrite', formatteropts)
2314 2323 def perffncachewrite(ui, repo, **opts):
2315 2324 opts = _byteskwargs(opts)
2316 2325 timer, fm = gettimer(ui, opts)
2317 2326 s = repo.store
2318 2327 lock = repo.lock()
2319 2328 s.fncache._load()
2320 2329 tr = repo.transaction(b'perffncachewrite')
2321 2330 tr.addbackup(b'fncache')
2322 2331
2323 2332 def d():
2324 2333 s.fncache._dirty = True
2325 2334 s.fncache.write(tr)
2326 2335
2327 2336 timer(d)
2328 2337 tr.close()
2329 2338 lock.release()
2330 2339 fm.end()
2331 2340
2332 2341
2333 2342 @command(b'perf::fncacheencode|perffncacheencode', formatteropts)
2334 2343 def perffncacheencode(ui, repo, **opts):
2335 2344 opts = _byteskwargs(opts)
2336 2345 timer, fm = gettimer(ui, opts)
2337 2346 s = repo.store
2338 2347 s.fncache._load()
2339 2348
2340 2349 def d():
2341 2350 for p in s.fncache.entries:
2342 2351 s.encode(p)
2343 2352
2344 2353 timer(d)
2345 2354 fm.end()
2346 2355
2347 2356
2348 2357 def _bdiffworker(q, blocks, xdiff, ready, done):
2349 2358 while not done.is_set():
2350 2359 pair = q.get()
2351 2360 while pair is not None:
2352 2361 if xdiff:
2353 2362 mdiff.bdiff.xdiffblocks(*pair)
2354 2363 elif blocks:
2355 2364 mdiff.bdiff.blocks(*pair)
2356 2365 else:
2357 2366 mdiff.textdiff(*pair)
2358 2367 q.task_done()
2359 2368 pair = q.get()
2360 2369 q.task_done() # for the None one
2361 2370 with ready:
2362 2371 ready.wait()
2363 2372
2364 2373
2365 2374 def _manifestrevision(repo, mnode):
2366 2375 ml = repo.manifestlog
2367 2376
2368 2377 if util.safehasattr(ml, b'getstorage'):
2369 2378 store = ml.getstorage(b'')
2370 2379 else:
2371 2380 store = ml._revlog
2372 2381
2373 2382 return store.revision(mnode)
2374 2383
2375 2384
2376 2385 @command(
2377 2386 b'perf::bdiff|perfbdiff',
2378 2387 revlogopts
2379 2388 + formatteropts
2380 2389 + [
2381 2390 (
2382 2391 b'',
2383 2392 b'count',
2384 2393 1,
2385 2394 b'number of revisions to test (when using --startrev)',
2386 2395 ),
2387 2396 (b'', b'alldata', False, b'test bdiffs for all associated revisions'),
2388 2397 (b'', b'threads', 0, b'number of thread to use (disable with 0)'),
2389 2398 (b'', b'blocks', False, b'test computing diffs into blocks'),
2390 2399 (b'', b'xdiff', False, b'use xdiff algorithm'),
2391 2400 ],
2392 2401 b'-c|-m|FILE REV',
2393 2402 )
2394 2403 def perfbdiff(ui, repo, file_, rev=None, count=None, threads=0, **opts):
2395 2404 """benchmark a bdiff between revisions
2396 2405
2397 2406 By default, benchmark a bdiff between its delta parent and itself.
2398 2407
2399 2408 With ``--count``, benchmark bdiffs between delta parents and self for N
2400 2409 revisions starting at the specified revision.
2401 2410
2402 2411 With ``--alldata``, assume the requested revision is a changeset and
2403 2412 measure bdiffs for all changes related to that changeset (manifest
2404 2413 and filelogs).
2405 2414 """
2406 2415 opts = _byteskwargs(opts)
2407 2416
2408 2417 if opts[b'xdiff'] and not opts[b'blocks']:
2409 2418 raise error.CommandError(b'perfbdiff', b'--xdiff requires --blocks')
2410 2419
2411 2420 if opts[b'alldata']:
2412 2421 opts[b'changelog'] = True
2413 2422
2414 2423 if opts.get(b'changelog') or opts.get(b'manifest'):
2415 2424 file_, rev = None, file_
2416 2425 elif rev is None:
2417 2426 raise error.CommandError(b'perfbdiff', b'invalid arguments')
2418 2427
2419 2428 blocks = opts[b'blocks']
2420 2429 xdiff = opts[b'xdiff']
2421 2430 textpairs = []
2422 2431
2423 2432 r = cmdutil.openrevlog(repo, b'perfbdiff', file_, opts)
2424 2433
2425 2434 startrev = r.rev(r.lookup(rev))
2426 2435 for rev in range(startrev, min(startrev + count, len(r) - 1)):
2427 2436 if opts[b'alldata']:
2428 2437 # Load revisions associated with changeset.
2429 2438 ctx = repo[rev]
2430 2439 mtext = _manifestrevision(repo, ctx.manifestnode())
2431 2440 for pctx in ctx.parents():
2432 2441 pman = _manifestrevision(repo, pctx.manifestnode())
2433 2442 textpairs.append((pman, mtext))
2434 2443
2435 2444 # Load filelog revisions by iterating manifest delta.
2436 2445 man = ctx.manifest()
2437 2446 pman = ctx.p1().manifest()
2438 2447 for filename, change in pman.diff(man).items():
2439 2448 fctx = repo.file(filename)
2440 2449 f1 = fctx.revision(change[0][0] or -1)
2441 2450 f2 = fctx.revision(change[1][0] or -1)
2442 2451 textpairs.append((f1, f2))
2443 2452 else:
2444 2453 dp = r.deltaparent(rev)
2445 2454 textpairs.append((r.revision(dp), r.revision(rev)))
2446 2455
2447 2456 withthreads = threads > 0
2448 2457 if not withthreads:
2449 2458
2450 2459 def d():
2451 2460 for pair in textpairs:
2452 2461 if xdiff:
2453 2462 mdiff.bdiff.xdiffblocks(*pair)
2454 2463 elif blocks:
2455 2464 mdiff.bdiff.blocks(*pair)
2456 2465 else:
2457 2466 mdiff.textdiff(*pair)
2458 2467
2459 2468 else:
2460 2469 q = queue()
2461 2470 for i in _xrange(threads):
2462 2471 q.put(None)
2463 2472 ready = threading.Condition()
2464 2473 done = threading.Event()
2465 2474 for i in _xrange(threads):
2466 2475 threading.Thread(
2467 2476 target=_bdiffworker, args=(q, blocks, xdiff, ready, done)
2468 2477 ).start()
2469 2478 q.join()
2470 2479
2471 2480 def d():
2472 2481 for pair in textpairs:
2473 2482 q.put(pair)
2474 2483 for i in _xrange(threads):
2475 2484 q.put(None)
2476 2485 with ready:
2477 2486 ready.notify_all()
2478 2487 q.join()
2479 2488
2480 2489 timer, fm = gettimer(ui, opts)
2481 2490 timer(d)
2482 2491 fm.end()
2483 2492
2484 2493 if withthreads:
2485 2494 done.set()
2486 2495 for i in _xrange(threads):
2487 2496 q.put(None)
2488 2497 with ready:
2489 2498 ready.notify_all()
2490 2499
2491 2500
2492 2501 @command(
2493 2502 b'perf::unidiff|perfunidiff',
2494 2503 revlogopts
2495 2504 + formatteropts
2496 2505 + [
2497 2506 (
2498 2507 b'',
2499 2508 b'count',
2500 2509 1,
2501 2510 b'number of revisions to test (when using --startrev)',
2502 2511 ),
2503 2512 (b'', b'alldata', False, b'test unidiffs for all associated revisions'),
2504 2513 ],
2505 2514 b'-c|-m|FILE REV',
2506 2515 )
2507 2516 def perfunidiff(ui, repo, file_, rev=None, count=None, **opts):
2508 2517 """benchmark a unified diff between revisions
2509 2518
2510 2519 This doesn't include any copy tracing - it's just a unified diff
2511 2520 of the texts.
2512 2521
2513 2522 By default, benchmark a diff between its delta parent and itself.
2514 2523
2515 2524 With ``--count``, benchmark diffs between delta parents and self for N
2516 2525 revisions starting at the specified revision.
2517 2526
2518 2527 With ``--alldata``, assume the requested revision is a changeset and
2519 2528 measure diffs for all changes related to that changeset (manifest
2520 2529 and filelogs).
2521 2530 """
2522 2531 opts = _byteskwargs(opts)
2523 2532 if opts[b'alldata']:
2524 2533 opts[b'changelog'] = True
2525 2534
2526 2535 if opts.get(b'changelog') or opts.get(b'manifest'):
2527 2536 file_, rev = None, file_
2528 2537 elif rev is None:
2529 2538 raise error.CommandError(b'perfunidiff', b'invalid arguments')
2530 2539
2531 2540 textpairs = []
2532 2541
2533 2542 r = cmdutil.openrevlog(repo, b'perfunidiff', file_, opts)
2534 2543
2535 2544 startrev = r.rev(r.lookup(rev))
2536 2545 for rev in range(startrev, min(startrev + count, len(r) - 1)):
2537 2546 if opts[b'alldata']:
2538 2547 # Load revisions associated with changeset.
2539 2548 ctx = repo[rev]
2540 2549 mtext = _manifestrevision(repo, ctx.manifestnode())
2541 2550 for pctx in ctx.parents():
2542 2551 pman = _manifestrevision(repo, pctx.manifestnode())
2543 2552 textpairs.append((pman, mtext))
2544 2553
2545 2554 # Load filelog revisions by iterating manifest delta.
2546 2555 man = ctx.manifest()
2547 2556 pman = ctx.p1().manifest()
2548 2557 for filename, change in pman.diff(man).items():
2549 2558 fctx = repo.file(filename)
2550 2559 f1 = fctx.revision(change[0][0] or -1)
2551 2560 f2 = fctx.revision(change[1][0] or -1)
2552 2561 textpairs.append((f1, f2))
2553 2562 else:
2554 2563 dp = r.deltaparent(rev)
2555 2564 textpairs.append((r.revision(dp), r.revision(rev)))
2556 2565
2557 2566 def d():
2558 2567 for left, right in textpairs:
2559 2568 # The date strings don't matter, so we pass empty strings.
2560 2569 headerlines, hunks = mdiff.unidiff(
2561 2570 left, b'', right, b'', b'left', b'right', binary=False
2562 2571 )
2563 2572 # consume iterators in roughly the way patch.py does
2564 2573 b'\n'.join(headerlines)
2565 2574 b''.join(sum((list(hlines) for hrange, hlines in hunks), []))
2566 2575
2567 2576 timer, fm = gettimer(ui, opts)
2568 2577 timer(d)
2569 2578 fm.end()
2570 2579
2571 2580
2572 2581 @command(b'perf::diffwd|perfdiffwd', formatteropts)
2573 2582 def perfdiffwd(ui, repo, **opts):
2574 2583 """Profile diff of working directory changes"""
2575 2584 opts = _byteskwargs(opts)
2576 2585 timer, fm = gettimer(ui, opts)
2577 2586 options = {
2578 2587 'w': 'ignore_all_space',
2579 2588 'b': 'ignore_space_change',
2580 2589 'B': 'ignore_blank_lines',
2581 2590 }
2582 2591
2583 2592 for diffopt in ('', 'w', 'b', 'B', 'wB'):
2584 2593 opts = {options[c]: b'1' for c in diffopt}
2585 2594
2586 2595 def d():
2587 2596 ui.pushbuffer()
2588 2597 commands.diff(ui, repo, **opts)
2589 2598 ui.popbuffer()
2590 2599
2591 2600 diffopt = diffopt.encode('ascii')
2592 2601 title = b'diffopts: %s' % (diffopt and (b'-' + diffopt) or b'none')
2593 2602 timer(d, title=title)
2594 2603 fm.end()
2595 2604
2596 2605
2597 2606 @command(
2598 2607 b'perf::revlogindex|perfrevlogindex',
2599 2608 revlogopts + formatteropts,
2600 2609 b'-c|-m|FILE',
2601 2610 )
2602 2611 def perfrevlogindex(ui, repo, file_=None, **opts):
2603 2612 """Benchmark operations against a revlog index.
2604 2613
2605 2614 This tests constructing a revlog instance, reading index data,
2606 2615 parsing index data, and performing various operations related to
2607 2616 index data.
2608 2617 """
2609 2618
2610 2619 opts = _byteskwargs(opts)
2611 2620
2612 2621 rl = cmdutil.openrevlog(repo, b'perfrevlogindex', file_, opts)
2613 2622
2614 2623 opener = getattr(rl, 'opener') # trick linter
2615 2624 # compat with hg <= 5.8
2616 2625 radix = getattr(rl, 'radix', None)
2617 2626 indexfile = getattr(rl, '_indexfile', None)
2618 2627 if indexfile is None:
2619 2628 # compatibility with <= hg-5.8
2620 2629 indexfile = getattr(rl, 'indexfile')
2621 2630 data = opener.read(indexfile)
2622 2631
2623 2632 header = struct.unpack(b'>I', data[0:4])[0]
2624 2633 version = header & 0xFFFF
2625 2634 if version == 1:
2626 2635 inline = header & (1 << 16)
2627 2636 else:
2628 2637 raise error.Abort(b'unsupported revlog version: %d' % version)
2629 2638
2630 2639 parse_index_v1 = getattr(mercurial.revlog, 'parse_index_v1', None)
2631 2640 if parse_index_v1 is None:
2632 2641 parse_index_v1 = mercurial.revlog.revlogio().parseindex
2633 2642
2634 2643 rllen = len(rl)
2635 2644
2636 2645 node0 = rl.node(0)
2637 2646 node25 = rl.node(rllen // 4)
2638 2647 node50 = rl.node(rllen // 2)
2639 2648 node75 = rl.node(rllen // 4 * 3)
2640 2649 node100 = rl.node(rllen - 1)
2641 2650
2642 2651 allrevs = range(rllen)
2643 2652 allrevsrev = list(reversed(allrevs))
2644 2653 allnodes = [rl.node(rev) for rev in range(rllen)]
2645 2654 allnodesrev = list(reversed(allnodes))
2646 2655
2647 2656 def constructor():
2648 2657 if radix is not None:
2649 2658 revlog(opener, radix=radix)
2650 2659 else:
2651 2660 # hg <= 5.8
2652 2661 revlog(opener, indexfile=indexfile)
2653 2662
2654 2663 def read():
2655 2664 with opener(indexfile) as fh:
2656 2665 fh.read()
2657 2666
2658 2667 def parseindex():
2659 2668 parse_index_v1(data, inline)
2660 2669
2661 2670 def getentry(revornode):
2662 2671 index = parse_index_v1(data, inline)[0]
2663 2672 index[revornode]
2664 2673
2665 2674 def getentries(revs, count=1):
2666 2675 index = parse_index_v1(data, inline)[0]
2667 2676
2668 2677 for i in range(count):
2669 2678 for rev in revs:
2670 2679 index[rev]
2671 2680
2672 2681 def resolvenode(node):
2673 2682 index = parse_index_v1(data, inline)[0]
2674 2683 rev = getattr(index, 'rev', None)
2675 2684 if rev is None:
2676 2685 nodemap = getattr(parse_index_v1(data, inline)[0], 'nodemap', None)
2677 2686 # This only works for the C code.
2678 2687 if nodemap is None:
2679 2688 return
2680 2689 rev = nodemap.__getitem__
2681 2690
2682 2691 try:
2683 2692 rev(node)
2684 2693 except error.RevlogError:
2685 2694 pass
2686 2695
2687 2696 def resolvenodes(nodes, count=1):
2688 2697 index = parse_index_v1(data, inline)[0]
2689 2698 rev = getattr(index, 'rev', None)
2690 2699 if rev is None:
2691 2700 nodemap = getattr(parse_index_v1(data, inline)[0], 'nodemap', None)
2692 2701 # This only works for the C code.
2693 2702 if nodemap is None:
2694 2703 return
2695 2704 rev = nodemap.__getitem__
2696 2705
2697 2706 for i in range(count):
2698 2707 for node in nodes:
2699 2708 try:
2700 2709 rev(node)
2701 2710 except error.RevlogError:
2702 2711 pass
2703 2712
2704 2713 benches = [
2705 2714 (constructor, b'revlog constructor'),
2706 2715 (read, b'read'),
2707 2716 (parseindex, b'create index object'),
2708 2717 (lambda: getentry(0), b'retrieve index entry for rev 0'),
2709 2718 (lambda: resolvenode(b'a' * 20), b'look up missing node'),
2710 2719 (lambda: resolvenode(node0), b'look up node at rev 0'),
2711 2720 (lambda: resolvenode(node25), b'look up node at 1/4 len'),
2712 2721 (lambda: resolvenode(node50), b'look up node at 1/2 len'),
2713 2722 (lambda: resolvenode(node75), b'look up node at 3/4 len'),
2714 2723 (lambda: resolvenode(node100), b'look up node at tip'),
2715 2724 # 2x variation is to measure caching impact.
2716 2725 (lambda: resolvenodes(allnodes), b'look up all nodes (forward)'),
2717 2726 (lambda: resolvenodes(allnodes, 2), b'look up all nodes 2x (forward)'),
2718 2727 (lambda: resolvenodes(allnodesrev), b'look up all nodes (reverse)'),
2719 2728 (
2720 2729 lambda: resolvenodes(allnodesrev, 2),
2721 2730 b'look up all nodes 2x (reverse)',
2722 2731 ),
2723 2732 (lambda: getentries(allrevs), b'retrieve all index entries (forward)'),
2724 2733 (
2725 2734 lambda: getentries(allrevs, 2),
2726 2735 b'retrieve all index entries 2x (forward)',
2727 2736 ),
2728 2737 (
2729 2738 lambda: getentries(allrevsrev),
2730 2739 b'retrieve all index entries (reverse)',
2731 2740 ),
2732 2741 (
2733 2742 lambda: getentries(allrevsrev, 2),
2734 2743 b'retrieve all index entries 2x (reverse)',
2735 2744 ),
2736 2745 ]
2737 2746
2738 2747 for fn, title in benches:
2739 2748 timer, fm = gettimer(ui, opts)
2740 2749 timer(fn, title=title)
2741 2750 fm.end()
2742 2751
2743 2752
2744 2753 @command(
2745 2754 b'perf::revlogrevisions|perfrevlogrevisions',
2746 2755 revlogopts
2747 2756 + formatteropts
2748 2757 + [
2749 2758 (b'd', b'dist', 100, b'distance between the revisions'),
2750 2759 (b's', b'startrev', 0, b'revision to start reading at'),
2751 2760 (b'', b'reverse', False, b'read in reverse'),
2752 2761 ],
2753 2762 b'-c|-m|FILE',
2754 2763 )
2755 2764 def perfrevlogrevisions(
2756 2765 ui, repo, file_=None, startrev=0, reverse=False, **opts
2757 2766 ):
2758 2767 """Benchmark reading a series of revisions from a revlog.
2759 2768
2760 2769 By default, we read every ``-d/--dist`` revision from 0 to tip of
2761 2770 the specified revlog.
2762 2771
2763 2772 The start revision can be defined via ``-s/--startrev``.
2764 2773 """
2765 2774 opts = _byteskwargs(opts)
2766 2775
2767 2776 rl = cmdutil.openrevlog(repo, b'perfrevlogrevisions', file_, opts)
2768 2777 rllen = getlen(ui)(rl)
2769 2778
2770 2779 if startrev < 0:
2771 2780 startrev = rllen + startrev
2772 2781
2773 2782 def d():
2774 2783 rl.clearcaches()
2775 2784
2776 2785 beginrev = startrev
2777 2786 endrev = rllen
2778 2787 dist = opts[b'dist']
2779 2788
2780 2789 if reverse:
2781 2790 beginrev, endrev = endrev - 1, beginrev - 1
2782 2791 dist = -1 * dist
2783 2792
2784 2793 for x in _xrange(beginrev, endrev, dist):
2785 2794 # Old revisions don't support passing int.
2786 2795 n = rl.node(x)
2787 2796 rl.revision(n)
2788 2797
2789 2798 timer, fm = gettimer(ui, opts)
2790 2799 timer(d)
2791 2800 fm.end()
2792 2801
2793 2802
2794 2803 @command(
2795 2804 b'perf::revlogwrite|perfrevlogwrite',
2796 2805 revlogopts
2797 2806 + formatteropts
2798 2807 + [
2799 2808 (b's', b'startrev', 1000, b'revision to start writing at'),
2800 2809 (b'', b'stoprev', -1, b'last revision to write'),
2801 2810 (b'', b'count', 3, b'number of passes to perform'),
2802 2811 (b'', b'details', False, b'print timing for every revisions tested'),
2803 2812 (b'', b'source', b'full', b'the kind of data feed in the revlog'),
2804 2813 (b'', b'lazydeltabase', True, b'try the provided delta first'),
2805 2814 (b'', b'clear-caches', True, b'clear revlog cache between calls'),
2806 2815 ],
2807 2816 b'-c|-m|FILE',
2808 2817 )
2809 2818 def perfrevlogwrite(ui, repo, file_=None, startrev=1000, stoprev=-1, **opts):
2810 2819 """Benchmark writing a series of revisions to a revlog.
2811 2820
2812 2821 Possible source values are:
2813 2822 * `full`: add from a full text (default).
2814 2823 * `parent-1`: add from a delta to the first parent
2815 2824 * `parent-2`: add from a delta to the second parent if it exists
2816 2825 (use a delta from the first parent otherwise)
2817 2826 * `parent-smallest`: add from the smallest delta (either p1 or p2)
2818 2827 * `storage`: add from the existing precomputed deltas
2819 2828
2820 2829 Note: This performance command measures performance in a custom way. As a
2821 2830 result some of the global configuration of the 'perf' command does not
2822 2831 apply to it:
2823 2832
2824 2833 * ``pre-run``: disabled
2825 2834
2826 2835 * ``profile-benchmark``: disabled
2827 2836
2828 2837 * ``run-limits``: disabled use --count instead
2829 2838 """
2830 2839 opts = _byteskwargs(opts)
2831 2840
2832 2841 rl = cmdutil.openrevlog(repo, b'perfrevlogwrite', file_, opts)
2833 2842 rllen = getlen(ui)(rl)
2834 2843 if startrev < 0:
2835 2844 startrev = rllen + startrev
2836 2845 if stoprev < 0:
2837 2846 stoprev = rllen + stoprev
2838 2847
2839 2848 lazydeltabase = opts['lazydeltabase']
2840 2849 source = opts['source']
2841 2850 clearcaches = opts['clear_caches']
2842 2851 validsource = (
2843 2852 b'full',
2844 2853 b'parent-1',
2845 2854 b'parent-2',
2846 2855 b'parent-smallest',
2847 2856 b'storage',
2848 2857 )
2849 2858 if source not in validsource:
2850 2859 raise error.Abort('invalid source type: %s' % source)
2851 2860
2852 2861 ### actually gather results
2853 2862 count = opts['count']
2854 2863 if count <= 0:
2855 2864 raise error.Abort('invalide run count: %d' % count)
2856 2865 allresults = []
2857 2866 for c in range(count):
2858 2867 timing = _timeonewrite(
2859 2868 ui,
2860 2869 rl,
2861 2870 source,
2862 2871 startrev,
2863 2872 stoprev,
2864 2873 c + 1,
2865 2874 lazydeltabase=lazydeltabase,
2866 2875 clearcaches=clearcaches,
2867 2876 )
2868 2877 allresults.append(timing)
2869 2878
2870 2879 ### consolidate the results in a single list
2871 2880 results = []
2872 2881 for idx, (rev, t) in enumerate(allresults[0]):
2873 2882 ts = [t]
2874 2883 for other in allresults[1:]:
2875 2884 orev, ot = other[idx]
2876 2885 assert orev == rev
2877 2886 ts.append(ot)
2878 2887 results.append((rev, ts))
2879 2888 resultcount = len(results)
2880 2889
2881 2890 ### Compute and display relevant statistics
2882 2891
2883 2892 # get a formatter
2884 2893 fm = ui.formatter(b'perf', opts)
2885 2894 displayall = ui.configbool(b"perf", b"all-timing", False)
2886 2895
2887 2896 # print individual details if requested
2888 2897 if opts['details']:
2889 2898 for idx, item in enumerate(results, 1):
2890 2899 rev, data = item
2891 2900 title = 'revisions #%d of %d, rev %d' % (idx, resultcount, rev)
2892 2901 formatone(fm, data, title=title, displayall=displayall)
2893 2902
2894 2903 # sorts results by median time
2895 2904 results.sort(key=lambda x: sorted(x[1])[len(x[1]) // 2])
2896 2905 # list of (name, index) to display)
2897 2906 relevants = [
2898 2907 ("min", 0),
2899 2908 ("10%", resultcount * 10 // 100),
2900 2909 ("25%", resultcount * 25 // 100),
2901 2910 ("50%", resultcount * 70 // 100),
2902 2911 ("75%", resultcount * 75 // 100),
2903 2912 ("90%", resultcount * 90 // 100),
2904 2913 ("95%", resultcount * 95 // 100),
2905 2914 ("99%", resultcount * 99 // 100),
2906 2915 ("99.9%", resultcount * 999 // 1000),
2907 2916 ("99.99%", resultcount * 9999 // 10000),
2908 2917 ("99.999%", resultcount * 99999 // 100000),
2909 2918 ("max", -1),
2910 2919 ]
2911 2920 if not ui.quiet:
2912 2921 for name, idx in relevants:
2913 2922 data = results[idx]
2914 2923 title = '%s of %d, rev %d' % (name, resultcount, data[0])
2915 2924 formatone(fm, data[1], title=title, displayall=displayall)
2916 2925
2917 2926 # XXX summing that many float will not be very precise, we ignore this fact
2918 2927 # for now
2919 2928 totaltime = []
2920 2929 for item in allresults:
2921 2930 totaltime.append(
2922 2931 (
2923 2932 sum(x[1][0] for x in item),
2924 2933 sum(x[1][1] for x in item),
2925 2934 sum(x[1][2] for x in item),
2926 2935 )
2927 2936 )
2928 2937 formatone(
2929 2938 fm,
2930 2939 totaltime,
2931 2940 title="total time (%d revs)" % resultcount,
2932 2941 displayall=displayall,
2933 2942 )
2934 2943 fm.end()
2935 2944
2936 2945
2937 2946 class _faketr(object):
2938 2947 def add(s, x, y, z=None):
2939 2948 return None
2940 2949
2941 2950
2942 2951 def _timeonewrite(
2943 2952 ui,
2944 2953 orig,
2945 2954 source,
2946 2955 startrev,
2947 2956 stoprev,
2948 2957 runidx=None,
2949 2958 lazydeltabase=True,
2950 2959 clearcaches=True,
2951 2960 ):
2952 2961 timings = []
2953 2962 tr = _faketr()
2954 2963 with _temprevlog(ui, orig, startrev) as dest:
2955 2964 dest._lazydeltabase = lazydeltabase
2956 2965 revs = list(orig.revs(startrev, stoprev))
2957 2966 total = len(revs)
2958 2967 topic = 'adding'
2959 2968 if runidx is not None:
2960 2969 topic += ' (run #%d)' % runidx
2961 2970 # Support both old and new progress API
2962 2971 if util.safehasattr(ui, 'makeprogress'):
2963 2972 progress = ui.makeprogress(topic, unit='revs', total=total)
2964 2973
2965 2974 def updateprogress(pos):
2966 2975 progress.update(pos)
2967 2976
2968 2977 def completeprogress():
2969 2978 progress.complete()
2970 2979
2971 2980 else:
2972 2981
2973 2982 def updateprogress(pos):
2974 2983 ui.progress(topic, pos, unit='revs', total=total)
2975 2984
2976 2985 def completeprogress():
2977 2986 ui.progress(topic, None, unit='revs', total=total)
2978 2987
2979 2988 for idx, rev in enumerate(revs):
2980 2989 updateprogress(idx)
2981 2990 addargs, addkwargs = _getrevisionseed(orig, rev, tr, source)
2982 2991 if clearcaches:
2983 2992 dest.index.clearcaches()
2984 2993 dest.clearcaches()
2985 2994 with timeone() as r:
2986 2995 dest.addrawrevision(*addargs, **addkwargs)
2987 2996 timings.append((rev, r[0]))
2988 2997 updateprogress(total)
2989 2998 completeprogress()
2990 2999 return timings
2991 3000
2992 3001
2993 3002 def _getrevisionseed(orig, rev, tr, source):
2994 3003 from mercurial.node import nullid
2995 3004
2996 3005 linkrev = orig.linkrev(rev)
2997 3006 node = orig.node(rev)
2998 3007 p1, p2 = orig.parents(node)
2999 3008 flags = orig.flags(rev)
3000 3009 cachedelta = None
3001 3010 text = None
3002 3011
3003 3012 if source == b'full':
3004 3013 text = orig.revision(rev)
3005 3014 elif source == b'parent-1':
3006 3015 baserev = orig.rev(p1)
3007 3016 cachedelta = (baserev, orig.revdiff(p1, rev))
3008 3017 elif source == b'parent-2':
3009 3018 parent = p2
3010 3019 if p2 == nullid:
3011 3020 parent = p1
3012 3021 baserev = orig.rev(parent)
3013 3022 cachedelta = (baserev, orig.revdiff(parent, rev))
3014 3023 elif source == b'parent-smallest':
3015 3024 p1diff = orig.revdiff(p1, rev)
3016 3025 parent = p1
3017 3026 diff = p1diff
3018 3027 if p2 != nullid:
3019 3028 p2diff = orig.revdiff(p2, rev)
3020 3029 if len(p1diff) > len(p2diff):
3021 3030 parent = p2
3022 3031 diff = p2diff
3023 3032 baserev = orig.rev(parent)
3024 3033 cachedelta = (baserev, diff)
3025 3034 elif source == b'storage':
3026 3035 baserev = orig.deltaparent(rev)
3027 3036 cachedelta = (baserev, orig.revdiff(orig.node(baserev), rev))
3028 3037
3029 3038 return (
3030 3039 (text, tr, linkrev, p1, p2),
3031 3040 {'node': node, 'flags': flags, 'cachedelta': cachedelta},
3032 3041 )
3033 3042
3034 3043
3035 3044 @contextlib.contextmanager
3036 3045 def _temprevlog(ui, orig, truncaterev):
3037 3046 from mercurial import vfs as vfsmod
3038 3047
3039 3048 if orig._inline:
3040 3049 raise error.Abort('not supporting inline revlog (yet)')
3041 3050 revlogkwargs = {}
3042 3051 k = 'upperboundcomp'
3043 3052 if util.safehasattr(orig, k):
3044 3053 revlogkwargs[k] = getattr(orig, k)
3045 3054
3046 3055 indexfile = getattr(orig, '_indexfile', None)
3047 3056 if indexfile is None:
3048 3057 # compatibility with <= hg-5.8
3049 3058 indexfile = getattr(orig, 'indexfile')
3050 3059 origindexpath = orig.opener.join(indexfile)
3051 3060
3052 3061 datafile = getattr(orig, '_datafile', getattr(orig, 'datafile'))
3053 3062 origdatapath = orig.opener.join(datafile)
3054 3063 radix = b'revlog'
3055 3064 indexname = b'revlog.i'
3056 3065 dataname = b'revlog.d'
3057 3066
3058 3067 tmpdir = tempfile.mkdtemp(prefix='tmp-hgperf-')
3059 3068 try:
3060 3069 # copy the data file in a temporary directory
3061 3070 ui.debug('copying data in %s\n' % tmpdir)
3062 3071 destindexpath = os.path.join(tmpdir, 'revlog.i')
3063 3072 destdatapath = os.path.join(tmpdir, 'revlog.d')
3064 3073 shutil.copyfile(origindexpath, destindexpath)
3065 3074 shutil.copyfile(origdatapath, destdatapath)
3066 3075
3067 3076 # remove the data we want to add again
3068 3077 ui.debug('truncating data to be rewritten\n')
3069 3078 with open(destindexpath, 'ab') as index:
3070 3079 index.seek(0)
3071 3080 index.truncate(truncaterev * orig._io.size)
3072 3081 with open(destdatapath, 'ab') as data:
3073 3082 data.seek(0)
3074 3083 data.truncate(orig.start(truncaterev))
3075 3084
3076 3085 # instantiate a new revlog from the temporary copy
3077 3086 ui.debug('truncating adding to be rewritten\n')
3078 3087 vfs = vfsmod.vfs(tmpdir)
3079 3088 vfs.options = getattr(orig.opener, 'options', None)
3080 3089
3081 3090 try:
3082 3091 dest = revlog(vfs, radix=radix, **revlogkwargs)
3083 3092 except TypeError:
3084 3093 dest = revlog(
3085 3094 vfs, indexfile=indexname, datafile=dataname, **revlogkwargs
3086 3095 )
3087 3096 if dest._inline:
3088 3097 raise error.Abort('not supporting inline revlog (yet)')
3089 3098 # make sure internals are initialized
3090 3099 dest.revision(len(dest) - 1)
3091 3100 yield dest
3092 3101 del dest, vfs
3093 3102 finally:
3094 3103 shutil.rmtree(tmpdir, True)
3095 3104
3096 3105
3097 3106 @command(
3098 3107 b'perf::revlogchunks|perfrevlogchunks',
3099 3108 revlogopts
3100 3109 + formatteropts
3101 3110 + [
3102 3111 (b'e', b'engines', b'', b'compression engines to use'),
3103 3112 (b's', b'startrev', 0, b'revision to start at'),
3104 3113 ],
3105 3114 b'-c|-m|FILE',
3106 3115 )
3107 3116 def perfrevlogchunks(ui, repo, file_=None, engines=None, startrev=0, **opts):
3108 3117 """Benchmark operations on revlog chunks.
3109 3118
3110 3119 Logically, each revlog is a collection of fulltext revisions. However,
3111 3120 stored within each revlog are "chunks" of possibly compressed data. This
3112 3121 data needs to be read and decompressed or compressed and written.
3113 3122
3114 3123 This command measures the time it takes to read+decompress and recompress
3115 3124 chunks in a revlog. It effectively isolates I/O and compression performance.
3116 3125 For measurements of higher-level operations like resolving revisions,
3117 3126 see ``perfrevlogrevisions`` and ``perfrevlogrevision``.
3118 3127 """
3119 3128 opts = _byteskwargs(opts)
3120 3129
3121 3130 rl = cmdutil.openrevlog(repo, b'perfrevlogchunks', file_, opts)
3122 3131
3123 3132 # _chunkraw was renamed to _getsegmentforrevs.
3124 3133 try:
3125 3134 segmentforrevs = rl._getsegmentforrevs
3126 3135 except AttributeError:
3127 3136 segmentforrevs = rl._chunkraw
3128 3137
3129 3138 # Verify engines argument.
3130 3139 if engines:
3131 3140 engines = {e.strip() for e in engines.split(b',')}
3132 3141 for engine in engines:
3133 3142 try:
3134 3143 util.compressionengines[engine]
3135 3144 except KeyError:
3136 3145 raise error.Abort(b'unknown compression engine: %s' % engine)
3137 3146 else:
3138 3147 engines = []
3139 3148 for e in util.compengines:
3140 3149 engine = util.compengines[e]
3141 3150 try:
3142 3151 if engine.available():
3143 3152 engine.revlogcompressor().compress(b'dummy')
3144 3153 engines.append(e)
3145 3154 except NotImplementedError:
3146 3155 pass
3147 3156
3148 3157 revs = list(rl.revs(startrev, len(rl) - 1))
3149 3158
3150 3159 def rlfh(rl):
3151 3160 if rl._inline:
3152 3161 indexfile = getattr(rl, '_indexfile', None)
3153 3162 if indexfile is None:
3154 3163 # compatibility with <= hg-5.8
3155 3164 indexfile = getattr(rl, 'indexfile')
3156 3165 return getsvfs(repo)(indexfile)
3157 3166 else:
3158 3167 datafile = getattr(rl, 'datafile', getattr(rl, 'datafile'))
3159 3168 return getsvfs(repo)(datafile)
3160 3169
3161 3170 def doread():
3162 3171 rl.clearcaches()
3163 3172 for rev in revs:
3164 3173 segmentforrevs(rev, rev)
3165 3174
3166 3175 def doreadcachedfh():
3167 3176 rl.clearcaches()
3168 3177 fh = rlfh(rl)
3169 3178 for rev in revs:
3170 3179 segmentforrevs(rev, rev, df=fh)
3171 3180
3172 3181 def doreadbatch():
3173 3182 rl.clearcaches()
3174 3183 segmentforrevs(revs[0], revs[-1])
3175 3184
3176 3185 def doreadbatchcachedfh():
3177 3186 rl.clearcaches()
3178 3187 fh = rlfh(rl)
3179 3188 segmentforrevs(revs[0], revs[-1], df=fh)
3180 3189
3181 3190 def dochunk():
3182 3191 rl.clearcaches()
3183 3192 fh = rlfh(rl)
3184 3193 for rev in revs:
3185 3194 rl._chunk(rev, df=fh)
3186 3195
3187 3196 chunks = [None]
3188 3197
3189 3198 def dochunkbatch():
3190 3199 rl.clearcaches()
3191 3200 fh = rlfh(rl)
3192 3201 # Save chunks as a side-effect.
3193 3202 chunks[0] = rl._chunks(revs, df=fh)
3194 3203
3195 3204 def docompress(compressor):
3196 3205 rl.clearcaches()
3197 3206
3198 3207 try:
3199 3208 # Swap in the requested compression engine.
3200 3209 oldcompressor = rl._compressor
3201 3210 rl._compressor = compressor
3202 3211 for chunk in chunks[0]:
3203 3212 rl.compress(chunk)
3204 3213 finally:
3205 3214 rl._compressor = oldcompressor
3206 3215
3207 3216 benches = [
3208 3217 (lambda: doread(), b'read'),
3209 3218 (lambda: doreadcachedfh(), b'read w/ reused fd'),
3210 3219 (lambda: doreadbatch(), b'read batch'),
3211 3220 (lambda: doreadbatchcachedfh(), b'read batch w/ reused fd'),
3212 3221 (lambda: dochunk(), b'chunk'),
3213 3222 (lambda: dochunkbatch(), b'chunk batch'),
3214 3223 ]
3215 3224
3216 3225 for engine in sorted(engines):
3217 3226 compressor = util.compengines[engine].revlogcompressor()
3218 3227 benches.append(
3219 3228 (
3220 3229 functools.partial(docompress, compressor),
3221 3230 b'compress w/ %s' % engine,
3222 3231 )
3223 3232 )
3224 3233
3225 3234 for fn, title in benches:
3226 3235 timer, fm = gettimer(ui, opts)
3227 3236 timer(fn, title=title)
3228 3237 fm.end()
3229 3238
3230 3239
3231 3240 @command(
3232 3241 b'perf::revlogrevision|perfrevlogrevision',
3233 3242 revlogopts
3234 3243 + formatteropts
3235 3244 + [(b'', b'cache', False, b'use caches instead of clearing')],
3236 3245 b'-c|-m|FILE REV',
3237 3246 )
3238 3247 def perfrevlogrevision(ui, repo, file_, rev=None, cache=None, **opts):
3239 3248 """Benchmark obtaining a revlog revision.
3240 3249
3241 3250 Obtaining a revlog revision consists of roughly the following steps:
3242 3251
3243 3252 1. Compute the delta chain
3244 3253 2. Slice the delta chain if applicable
3245 3254 3. Obtain the raw chunks for that delta chain
3246 3255 4. Decompress each raw chunk
3247 3256 5. Apply binary patches to obtain fulltext
3248 3257 6. Verify hash of fulltext
3249 3258
3250 3259 This command measures the time spent in each of these phases.
3251 3260 """
3252 3261 opts = _byteskwargs(opts)
3253 3262
3254 3263 if opts.get(b'changelog') or opts.get(b'manifest'):
3255 3264 file_, rev = None, file_
3256 3265 elif rev is None:
3257 3266 raise error.CommandError(b'perfrevlogrevision', b'invalid arguments')
3258 3267
3259 3268 r = cmdutil.openrevlog(repo, b'perfrevlogrevision', file_, opts)
3260 3269
3261 3270 # _chunkraw was renamed to _getsegmentforrevs.
3262 3271 try:
3263 3272 segmentforrevs = r._getsegmentforrevs
3264 3273 except AttributeError:
3265 3274 segmentforrevs = r._chunkraw
3266 3275
3267 3276 node = r.lookup(rev)
3268 3277 rev = r.rev(node)
3269 3278
3270 3279 def getrawchunks(data, chain):
3271 3280 start = r.start
3272 3281 length = r.length
3273 3282 inline = r._inline
3274 3283 try:
3275 3284 iosize = r.index.entry_size
3276 3285 except AttributeError:
3277 3286 iosize = r._io.size
3278 3287 buffer = util.buffer
3279 3288
3280 3289 chunks = []
3281 3290 ladd = chunks.append
3282 3291 for idx, item in enumerate(chain):
3283 3292 offset = start(item[0])
3284 3293 bits = data[idx]
3285 3294 for rev in item:
3286 3295 chunkstart = start(rev)
3287 3296 if inline:
3288 3297 chunkstart += (rev + 1) * iosize
3289 3298 chunklength = length(rev)
3290 3299 ladd(buffer(bits, chunkstart - offset, chunklength))
3291 3300
3292 3301 return chunks
3293 3302
3294 3303 def dodeltachain(rev):
3295 3304 if not cache:
3296 3305 r.clearcaches()
3297 3306 r._deltachain(rev)
3298 3307
3299 3308 def doread(chain):
3300 3309 if not cache:
3301 3310 r.clearcaches()
3302 3311 for item in slicedchain:
3303 3312 segmentforrevs(item[0], item[-1])
3304 3313
3305 3314 def doslice(r, chain, size):
3306 3315 for s in slicechunk(r, chain, targetsize=size):
3307 3316 pass
3308 3317
3309 3318 def dorawchunks(data, chain):
3310 3319 if not cache:
3311 3320 r.clearcaches()
3312 3321 getrawchunks(data, chain)
3313 3322
3314 3323 def dodecompress(chunks):
3315 3324 decomp = r.decompress
3316 3325 for chunk in chunks:
3317 3326 decomp(chunk)
3318 3327
3319 3328 def dopatch(text, bins):
3320 3329 if not cache:
3321 3330 r.clearcaches()
3322 3331 mdiff.patches(text, bins)
3323 3332
3324 3333 def dohash(text):
3325 3334 if not cache:
3326 3335 r.clearcaches()
3327 3336 r.checkhash(text, node, rev=rev)
3328 3337
3329 3338 def dorevision():
3330 3339 if not cache:
3331 3340 r.clearcaches()
3332 3341 r.revision(node)
3333 3342
3334 3343 try:
3335 3344 from mercurial.revlogutils.deltas import slicechunk
3336 3345 except ImportError:
3337 3346 slicechunk = getattr(revlog, '_slicechunk', None)
3338 3347
3339 3348 size = r.length(rev)
3340 3349 chain = r._deltachain(rev)[0]
3341 3350 if not getattr(r, '_withsparseread', False):
3342 3351 slicedchain = (chain,)
3343 3352 else:
3344 3353 slicedchain = tuple(slicechunk(r, chain, targetsize=size))
3345 3354 data = [segmentforrevs(seg[0], seg[-1])[1] for seg in slicedchain]
3346 3355 rawchunks = getrawchunks(data, slicedchain)
3347 3356 bins = r._chunks(chain)
3348 3357 text = bytes(bins[0])
3349 3358 bins = bins[1:]
3350 3359 text = mdiff.patches(text, bins)
3351 3360
3352 3361 benches = [
3353 3362 (lambda: dorevision(), b'full'),
3354 3363 (lambda: dodeltachain(rev), b'deltachain'),
3355 3364 (lambda: doread(chain), b'read'),
3356 3365 ]
3357 3366
3358 3367 if getattr(r, '_withsparseread', False):
3359 3368 slicing = (lambda: doslice(r, chain, size), b'slice-sparse-chain')
3360 3369 benches.append(slicing)
3361 3370
3362 3371 benches.extend(
3363 3372 [
3364 3373 (lambda: dorawchunks(data, slicedchain), b'rawchunks'),
3365 3374 (lambda: dodecompress(rawchunks), b'decompress'),
3366 3375 (lambda: dopatch(text, bins), b'patch'),
3367 3376 (lambda: dohash(text), b'hash'),
3368 3377 ]
3369 3378 )
3370 3379
3371 3380 timer, fm = gettimer(ui, opts)
3372 3381 for fn, title in benches:
3373 3382 timer(fn, title=title)
3374 3383 fm.end()
3375 3384
3376 3385
3377 3386 @command(
3378 3387 b'perf::revset|perfrevset',
3379 3388 [
3380 3389 (b'C', b'clear', False, b'clear volatile cache between each call.'),
3381 3390 (b'', b'contexts', False, b'obtain changectx for each revision'),
3382 3391 ]
3383 3392 + formatteropts,
3384 3393 b"REVSET",
3385 3394 )
3386 3395 def perfrevset(ui, repo, expr, clear=False, contexts=False, **opts):
3387 3396 """benchmark the execution time of a revset
3388 3397
3389 3398 Use the --clean option if need to evaluate the impact of build volatile
3390 3399 revisions set cache on the revset execution. Volatile cache hold filtered
3391 3400 and obsolete related cache."""
3392 3401 opts = _byteskwargs(opts)
3393 3402
3394 3403 timer, fm = gettimer(ui, opts)
3395 3404
3396 3405 def d():
3397 3406 if clear:
3398 3407 repo.invalidatevolatilesets()
3399 3408 if contexts:
3400 3409 for ctx in repo.set(expr):
3401 3410 pass
3402 3411 else:
3403 3412 for r in repo.revs(expr):
3404 3413 pass
3405 3414
3406 3415 timer(d)
3407 3416 fm.end()
3408 3417
3409 3418
3410 3419 @command(
3411 3420 b'perf::volatilesets|perfvolatilesets',
3412 3421 [
3413 3422 (b'', b'clear-obsstore', False, b'drop obsstore between each call.'),
3414 3423 ]
3415 3424 + formatteropts,
3416 3425 )
3417 3426 def perfvolatilesets(ui, repo, *names, **opts):
3418 3427 """benchmark the computation of various volatile set
3419 3428
3420 3429 Volatile set computes element related to filtering and obsolescence."""
3421 3430 opts = _byteskwargs(opts)
3422 3431 timer, fm = gettimer(ui, opts)
3423 3432 repo = repo.unfiltered()
3424 3433
3425 3434 def getobs(name):
3426 3435 def d():
3427 3436 repo.invalidatevolatilesets()
3428 3437 if opts[b'clear_obsstore']:
3429 3438 clearfilecache(repo, b'obsstore')
3430 3439 obsolete.getrevs(repo, name)
3431 3440
3432 3441 return d
3433 3442
3434 3443 allobs = sorted(obsolete.cachefuncs)
3435 3444 if names:
3436 3445 allobs = [n for n in allobs if n in names]
3437 3446
3438 3447 for name in allobs:
3439 3448 timer(getobs(name), title=name)
3440 3449
3441 3450 def getfiltered(name):
3442 3451 def d():
3443 3452 repo.invalidatevolatilesets()
3444 3453 if opts[b'clear_obsstore']:
3445 3454 clearfilecache(repo, b'obsstore')
3446 3455 repoview.filterrevs(repo, name)
3447 3456
3448 3457 return d
3449 3458
3450 3459 allfilter = sorted(repoview.filtertable)
3451 3460 if names:
3452 3461 allfilter = [n for n in allfilter if n in names]
3453 3462
3454 3463 for name in allfilter:
3455 3464 timer(getfiltered(name), title=name)
3456 3465 fm.end()
3457 3466
3458 3467
3459 3468 @command(
3460 3469 b'perf::branchmap|perfbranchmap',
3461 3470 [
3462 3471 (b'f', b'full', False, b'Includes build time of subset'),
3463 3472 (
3464 3473 b'',
3465 3474 b'clear-revbranch',
3466 3475 False,
3467 3476 b'purge the revbranch cache between computation',
3468 3477 ),
3469 3478 ]
3470 3479 + formatteropts,
3471 3480 )
3472 3481 def perfbranchmap(ui, repo, *filternames, **opts):
3473 3482 """benchmark the update of a branchmap
3474 3483
3475 3484 This benchmarks the full repo.branchmap() call with read and write disabled
3476 3485 """
3477 3486 opts = _byteskwargs(opts)
3478 3487 full = opts.get(b"full", False)
3479 3488 clear_revbranch = opts.get(b"clear_revbranch", False)
3480 3489 timer, fm = gettimer(ui, opts)
3481 3490
3482 3491 def getbranchmap(filtername):
3483 3492 """generate a benchmark function for the filtername"""
3484 3493 if filtername is None:
3485 3494 view = repo
3486 3495 else:
3487 3496 view = repo.filtered(filtername)
3488 3497 if util.safehasattr(view._branchcaches, '_per_filter'):
3489 3498 filtered = view._branchcaches._per_filter
3490 3499 else:
3491 3500 # older versions
3492 3501 filtered = view._branchcaches
3493 3502
3494 3503 def d():
3495 3504 if clear_revbranch:
3496 3505 repo.revbranchcache()._clear()
3497 3506 if full:
3498 3507 view._branchcaches.clear()
3499 3508 else:
3500 3509 filtered.pop(filtername, None)
3501 3510 view.branchmap()
3502 3511
3503 3512 return d
3504 3513
3505 3514 # add filter in smaller subset to bigger subset
3506 3515 possiblefilters = set(repoview.filtertable)
3507 3516 if filternames:
3508 3517 possiblefilters &= set(filternames)
3509 3518 subsettable = getbranchmapsubsettable()
3510 3519 allfilters = []
3511 3520 while possiblefilters:
3512 3521 for name in possiblefilters:
3513 3522 subset = subsettable.get(name)
3514 3523 if subset not in possiblefilters:
3515 3524 break
3516 3525 else:
3517 3526 assert False, b'subset cycle %s!' % possiblefilters
3518 3527 allfilters.append(name)
3519 3528 possiblefilters.remove(name)
3520 3529
3521 3530 # warm the cache
3522 3531 if not full:
3523 3532 for name in allfilters:
3524 3533 repo.filtered(name).branchmap()
3525 3534 if not filternames or b'unfiltered' in filternames:
3526 3535 # add unfiltered
3527 3536 allfilters.append(None)
3528 3537
3529 3538 if util.safehasattr(branchmap.branchcache, 'fromfile'):
3530 3539 branchcacheread = safeattrsetter(branchmap.branchcache, b'fromfile')
3531 3540 branchcacheread.set(classmethod(lambda *args: None))
3532 3541 else:
3533 3542 # older versions
3534 3543 branchcacheread = safeattrsetter(branchmap, b'read')
3535 3544 branchcacheread.set(lambda *args: None)
3536 3545 branchcachewrite = safeattrsetter(branchmap.branchcache, b'write')
3537 3546 branchcachewrite.set(lambda *args: None)
3538 3547 try:
3539 3548 for name in allfilters:
3540 3549 printname = name
3541 3550 if name is None:
3542 3551 printname = b'unfiltered'
3543 3552 timer(getbranchmap(name), title=printname)
3544 3553 finally:
3545 3554 branchcacheread.restore()
3546 3555 branchcachewrite.restore()
3547 3556 fm.end()
3548 3557
3549 3558
3550 3559 @command(
3551 3560 b'perf::branchmapupdate|perfbranchmapupdate',
3552 3561 [
3553 3562 (b'', b'base', [], b'subset of revision to start from'),
3554 3563 (b'', b'target', [], b'subset of revision to end with'),
3555 3564 (b'', b'clear-caches', False, b'clear cache between each runs'),
3556 3565 ]
3557 3566 + formatteropts,
3558 3567 )
3559 3568 def perfbranchmapupdate(ui, repo, base=(), target=(), **opts):
3560 3569 """benchmark branchmap update from for <base> revs to <target> revs
3561 3570
3562 3571 If `--clear-caches` is passed, the following items will be reset before
3563 3572 each update:
3564 3573 * the changelog instance and associated indexes
3565 3574 * the rev-branch-cache instance
3566 3575
3567 3576 Examples:
3568 3577
3569 3578 # update for the one last revision
3570 3579 $ hg perfbranchmapupdate --base 'not tip' --target 'tip'
3571 3580
3572 3581 $ update for change coming with a new branch
3573 3582 $ hg perfbranchmapupdate --base 'stable' --target 'default'
3574 3583 """
3575 3584 from mercurial import branchmap
3576 3585 from mercurial import repoview
3577 3586
3578 3587 opts = _byteskwargs(opts)
3579 3588 timer, fm = gettimer(ui, opts)
3580 3589 clearcaches = opts[b'clear_caches']
3581 3590 unfi = repo.unfiltered()
3582 3591 x = [None] # used to pass data between closure
3583 3592
3584 3593 # we use a `list` here to avoid possible side effect from smartset
3585 3594 baserevs = list(scmutil.revrange(repo, base))
3586 3595 targetrevs = list(scmutil.revrange(repo, target))
3587 3596 if not baserevs:
3588 3597 raise error.Abort(b'no revisions selected for --base')
3589 3598 if not targetrevs:
3590 3599 raise error.Abort(b'no revisions selected for --target')
3591 3600
3592 3601 # make sure the target branchmap also contains the one in the base
3593 3602 targetrevs = list(set(baserevs) | set(targetrevs))
3594 3603 targetrevs.sort()
3595 3604
3596 3605 cl = repo.changelog
3597 3606 allbaserevs = list(cl.ancestors(baserevs, inclusive=True))
3598 3607 allbaserevs.sort()
3599 3608 alltargetrevs = frozenset(cl.ancestors(targetrevs, inclusive=True))
3600 3609
3601 3610 newrevs = list(alltargetrevs.difference(allbaserevs))
3602 3611 newrevs.sort()
3603 3612
3604 3613 allrevs = frozenset(unfi.changelog.revs())
3605 3614 basefilterrevs = frozenset(allrevs.difference(allbaserevs))
3606 3615 targetfilterrevs = frozenset(allrevs.difference(alltargetrevs))
3607 3616
3608 3617 def basefilter(repo, visibilityexceptions=None):
3609 3618 return basefilterrevs
3610 3619
3611 3620 def targetfilter(repo, visibilityexceptions=None):
3612 3621 return targetfilterrevs
3613 3622
3614 3623 msg = b'benchmark of branchmap with %d revisions with %d new ones\n'
3615 3624 ui.status(msg % (len(allbaserevs), len(newrevs)))
3616 3625 if targetfilterrevs:
3617 3626 msg = b'(%d revisions still filtered)\n'
3618 3627 ui.status(msg % len(targetfilterrevs))
3619 3628
3620 3629 try:
3621 3630 repoview.filtertable[b'__perf_branchmap_update_base'] = basefilter
3622 3631 repoview.filtertable[b'__perf_branchmap_update_target'] = targetfilter
3623 3632
3624 3633 baserepo = repo.filtered(b'__perf_branchmap_update_base')
3625 3634 targetrepo = repo.filtered(b'__perf_branchmap_update_target')
3626 3635
3627 3636 # try to find an existing branchmap to reuse
3628 3637 subsettable = getbranchmapsubsettable()
3629 3638 candidatefilter = subsettable.get(None)
3630 3639 while candidatefilter is not None:
3631 3640 candidatebm = repo.filtered(candidatefilter).branchmap()
3632 3641 if candidatebm.validfor(baserepo):
3633 3642 filtered = repoview.filterrevs(repo, candidatefilter)
3634 3643 missing = [r for r in allbaserevs if r in filtered]
3635 3644 base = candidatebm.copy()
3636 3645 base.update(baserepo, missing)
3637 3646 break
3638 3647 candidatefilter = subsettable.get(candidatefilter)
3639 3648 else:
3640 3649 # no suitable subset where found
3641 3650 base = branchmap.branchcache()
3642 3651 base.update(baserepo, allbaserevs)
3643 3652
3644 3653 def setup():
3645 3654 x[0] = base.copy()
3646 3655 if clearcaches:
3647 3656 unfi._revbranchcache = None
3648 3657 clearchangelog(repo)
3649 3658
3650 3659 def bench():
3651 3660 x[0].update(targetrepo, newrevs)
3652 3661
3653 3662 timer(bench, setup=setup)
3654 3663 fm.end()
3655 3664 finally:
3656 3665 repoview.filtertable.pop(b'__perf_branchmap_update_base', None)
3657 3666 repoview.filtertable.pop(b'__perf_branchmap_update_target', None)
3658 3667
3659 3668
3660 3669 @command(
3661 3670 b'perf::branchmapload|perfbranchmapload',
3662 3671 [
3663 3672 (b'f', b'filter', b'', b'Specify repoview filter'),
3664 3673 (b'', b'list', False, b'List brachmap filter caches'),
3665 3674 (b'', b'clear-revlogs', False, b'refresh changelog and manifest'),
3666 3675 ]
3667 3676 + formatteropts,
3668 3677 )
3669 3678 def perfbranchmapload(ui, repo, filter=b'', list=False, **opts):
3670 3679 """benchmark reading the branchmap"""
3671 3680 opts = _byteskwargs(opts)
3672 3681 clearrevlogs = opts[b'clear_revlogs']
3673 3682
3674 3683 if list:
3675 3684 for name, kind, st in repo.cachevfs.readdir(stat=True):
3676 3685 if name.startswith(b'branch2'):
3677 3686 filtername = name.partition(b'-')[2] or b'unfiltered'
3678 3687 ui.status(
3679 3688 b'%s - %s\n' % (filtername, util.bytecount(st.st_size))
3680 3689 )
3681 3690 return
3682 3691 if not filter:
3683 3692 filter = None
3684 3693 subsettable = getbranchmapsubsettable()
3685 3694 if filter is None:
3686 3695 repo = repo.unfiltered()
3687 3696 else:
3688 3697 repo = repoview.repoview(repo, filter)
3689 3698
3690 3699 repo.branchmap() # make sure we have a relevant, up to date branchmap
3691 3700
3692 3701 try:
3693 3702 fromfile = branchmap.branchcache.fromfile
3694 3703 except AttributeError:
3695 3704 # older versions
3696 3705 fromfile = branchmap.read
3697 3706
3698 3707 currentfilter = filter
3699 3708 # try once without timer, the filter may not be cached
3700 3709 while fromfile(repo) is None:
3701 3710 currentfilter = subsettable.get(currentfilter)
3702 3711 if currentfilter is None:
3703 3712 raise error.Abort(
3704 3713 b'No branchmap cached for %s repo' % (filter or b'unfiltered')
3705 3714 )
3706 3715 repo = repo.filtered(currentfilter)
3707 3716 timer, fm = gettimer(ui, opts)
3708 3717
3709 3718 def setup():
3710 3719 if clearrevlogs:
3711 3720 clearchangelog(repo)
3712 3721
3713 3722 def bench():
3714 3723 fromfile(repo)
3715 3724
3716 3725 timer(bench, setup=setup)
3717 3726 fm.end()
3718 3727
3719 3728
3720 3729 @command(b'perf::loadmarkers|perfloadmarkers')
3721 3730 def perfloadmarkers(ui, repo):
3722 3731 """benchmark the time to parse the on-disk markers for a repo
3723 3732
3724 3733 Result is the number of markers in the repo."""
3725 3734 timer, fm = gettimer(ui)
3726 3735 svfs = getsvfs(repo)
3727 3736 timer(lambda: len(obsolete.obsstore(repo, svfs)))
3728 3737 fm.end()
3729 3738
3730 3739
3731 3740 @command(
3732 3741 b'perf::lrucachedict|perflrucachedict',
3733 3742 formatteropts
3734 3743 + [
3735 3744 (b'', b'costlimit', 0, b'maximum total cost of items in cache'),
3736 3745 (b'', b'mincost', 0, b'smallest cost of items in cache'),
3737 3746 (b'', b'maxcost', 100, b'maximum cost of items in cache'),
3738 3747 (b'', b'size', 4, b'size of cache'),
3739 3748 (b'', b'gets', 10000, b'number of key lookups'),
3740 3749 (b'', b'sets', 10000, b'number of key sets'),
3741 3750 (b'', b'mixed', 10000, b'number of mixed mode operations'),
3742 3751 (
3743 3752 b'',
3744 3753 b'mixedgetfreq',
3745 3754 50,
3746 3755 b'frequency of get vs set ops in mixed mode',
3747 3756 ),
3748 3757 ],
3749 3758 norepo=True,
3750 3759 )
3751 3760 def perflrucache(
3752 3761 ui,
3753 3762 mincost=0,
3754 3763 maxcost=100,
3755 3764 costlimit=0,
3756 3765 size=4,
3757 3766 gets=10000,
3758 3767 sets=10000,
3759 3768 mixed=10000,
3760 3769 mixedgetfreq=50,
3761 3770 **opts
3762 3771 ):
3763 3772 opts = _byteskwargs(opts)
3764 3773
3765 3774 def doinit():
3766 3775 for i in _xrange(10000):
3767 3776 util.lrucachedict(size)
3768 3777
3769 3778 costrange = list(range(mincost, maxcost + 1))
3770 3779
3771 3780 values = []
3772 3781 for i in _xrange(size):
3773 3782 values.append(random.randint(0, _maxint))
3774 3783
3775 3784 # Get mode fills the cache and tests raw lookup performance with no
3776 3785 # eviction.
3777 3786 getseq = []
3778 3787 for i in _xrange(gets):
3779 3788 getseq.append(random.choice(values))
3780 3789
3781 3790 def dogets():
3782 3791 d = util.lrucachedict(size)
3783 3792 for v in values:
3784 3793 d[v] = v
3785 3794 for key in getseq:
3786 3795 value = d[key]
3787 3796 value # silence pyflakes warning
3788 3797
3789 3798 def dogetscost():
3790 3799 d = util.lrucachedict(size, maxcost=costlimit)
3791 3800 for i, v in enumerate(values):
3792 3801 d.insert(v, v, cost=costs[i])
3793 3802 for key in getseq:
3794 3803 try:
3795 3804 value = d[key]
3796 3805 value # silence pyflakes warning
3797 3806 except KeyError:
3798 3807 pass
3799 3808
3800 3809 # Set mode tests insertion speed with cache eviction.
3801 3810 setseq = []
3802 3811 costs = []
3803 3812 for i in _xrange(sets):
3804 3813 setseq.append(random.randint(0, _maxint))
3805 3814 costs.append(random.choice(costrange))
3806 3815
3807 3816 def doinserts():
3808 3817 d = util.lrucachedict(size)
3809 3818 for v in setseq:
3810 3819 d.insert(v, v)
3811 3820
3812 3821 def doinsertscost():
3813 3822 d = util.lrucachedict(size, maxcost=costlimit)
3814 3823 for i, v in enumerate(setseq):
3815 3824 d.insert(v, v, cost=costs[i])
3816 3825
3817 3826 def dosets():
3818 3827 d = util.lrucachedict(size)
3819 3828 for v in setseq:
3820 3829 d[v] = v
3821 3830
3822 3831 # Mixed mode randomly performs gets and sets with eviction.
3823 3832 mixedops = []
3824 3833 for i in _xrange(mixed):
3825 3834 r = random.randint(0, 100)
3826 3835 if r < mixedgetfreq:
3827 3836 op = 0
3828 3837 else:
3829 3838 op = 1
3830 3839
3831 3840 mixedops.append(
3832 3841 (op, random.randint(0, size * 2), random.choice(costrange))
3833 3842 )
3834 3843
3835 3844 def domixed():
3836 3845 d = util.lrucachedict(size)
3837 3846
3838 3847 for op, v, cost in mixedops:
3839 3848 if op == 0:
3840 3849 try:
3841 3850 d[v]
3842 3851 except KeyError:
3843 3852 pass
3844 3853 else:
3845 3854 d[v] = v
3846 3855
3847 3856 def domixedcost():
3848 3857 d = util.lrucachedict(size, maxcost=costlimit)
3849 3858
3850 3859 for op, v, cost in mixedops:
3851 3860 if op == 0:
3852 3861 try:
3853 3862 d[v]
3854 3863 except KeyError:
3855 3864 pass
3856 3865 else:
3857 3866 d.insert(v, v, cost=cost)
3858 3867
3859 3868 benches = [
3860 3869 (doinit, b'init'),
3861 3870 ]
3862 3871
3863 3872 if costlimit:
3864 3873 benches.extend(
3865 3874 [
3866 3875 (dogetscost, b'gets w/ cost limit'),
3867 3876 (doinsertscost, b'inserts w/ cost limit'),
3868 3877 (domixedcost, b'mixed w/ cost limit'),
3869 3878 ]
3870 3879 )
3871 3880 else:
3872 3881 benches.extend(
3873 3882 [
3874 3883 (dogets, b'gets'),
3875 3884 (doinserts, b'inserts'),
3876 3885 (dosets, b'sets'),
3877 3886 (domixed, b'mixed'),
3878 3887 ]
3879 3888 )
3880 3889
3881 3890 for fn, title in benches:
3882 3891 timer, fm = gettimer(ui, opts)
3883 3892 timer(fn, title=title)
3884 3893 fm.end()
3885 3894
3886 3895
3887 3896 @command(
3888 3897 b'perf::write|perfwrite',
3889 3898 formatteropts
3890 3899 + [
3891 3900 (b'', b'write-method', b'write', b'ui write method'),
3892 3901 (b'', b'nlines', 100, b'number of lines'),
3893 3902 (b'', b'nitems', 100, b'number of items (per line)'),
3894 3903 (b'', b'item', b'x', b'item that is written'),
3895 3904 (b'', b'batch-line', None, b'pass whole line to write method at once'),
3896 3905 (b'', b'flush-line', None, b'flush after each line'),
3897 3906 ],
3898 3907 )
3899 3908 def perfwrite(ui, repo, **opts):
3900 3909 """microbenchmark ui.write (and others)"""
3901 3910 opts = _byteskwargs(opts)
3902 3911
3903 3912 write = getattr(ui, _sysstr(opts[b'write_method']))
3904 3913 nlines = int(opts[b'nlines'])
3905 3914 nitems = int(opts[b'nitems'])
3906 3915 item = opts[b'item']
3907 3916 batch_line = opts.get(b'batch_line')
3908 3917 flush_line = opts.get(b'flush_line')
3909 3918
3910 3919 if batch_line:
3911 3920 line = item * nitems + b'\n'
3912 3921
3913 3922 def benchmark():
3914 3923 for i in pycompat.xrange(nlines):
3915 3924 if batch_line:
3916 3925 write(line)
3917 3926 else:
3918 3927 for i in pycompat.xrange(nitems):
3919 3928 write(item)
3920 3929 write(b'\n')
3921 3930 if flush_line:
3922 3931 ui.flush()
3923 3932 ui.flush()
3924 3933
3925 3934 timer, fm = gettimer(ui, opts)
3926 3935 timer(benchmark)
3927 3936 fm.end()
3928 3937
3929 3938
3930 3939 def uisetup(ui):
3931 3940 if util.safehasattr(cmdutil, b'openrevlog') and not util.safehasattr(
3932 3941 commands, b'debugrevlogopts'
3933 3942 ):
3934 3943 # for "historical portability":
3935 3944 # In this case, Mercurial should be 1.9 (or a79fea6b3e77) -
3936 3945 # 3.7 (or 5606f7d0d063). Therefore, '--dir' option for
3937 3946 # openrevlog() should cause failure, because it has been
3938 3947 # available since 3.5 (or 49c583ca48c4).
3939 3948 def openrevlog(orig, repo, cmd, file_, opts):
3940 3949 if opts.get(b'dir') and not util.safehasattr(repo, b'dirlog'):
3941 3950 raise error.Abort(
3942 3951 b"This version doesn't support --dir option",
3943 3952 hint=b"use 3.5 or later",
3944 3953 )
3945 3954 return orig(repo, cmd, file_, opts)
3946 3955
3947 3956 extensions.wrapfunction(cmdutil, b'openrevlog', openrevlog)
3948 3957
3949 3958
3950 3959 @command(
3951 3960 b'perf::progress|perfprogress',
3952 3961 formatteropts
3953 3962 + [
3954 3963 (b'', b'topic', b'topic', b'topic for progress messages'),
3955 3964 (b'c', b'total', 1000000, b'total value we are progressing to'),
3956 3965 ],
3957 3966 norepo=True,
3958 3967 )
3959 3968 def perfprogress(ui, topic=None, total=None, **opts):
3960 3969 """printing of progress bars"""
3961 3970 opts = _byteskwargs(opts)
3962 3971
3963 3972 timer, fm = gettimer(ui, opts)
3964 3973
3965 3974 def doprogress():
3966 3975 with ui.makeprogress(topic, total=total) as progress:
3967 3976 for i in _xrange(total):
3968 3977 progress.increment()
3969 3978
3970 3979 timer(doprogress)
3971 3980 fm.end()
@@ -1,1993 +1,1983
1 1 # dirstate.py - working directory tracking for mercurial
2 2 #
3 3 # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import collections
11 11 import contextlib
12 12 import errno
13 13 import os
14 14 import stat
15 15
16 16 from .i18n import _
17 17 from .pycompat import delattr
18 18
19 19 from hgdemandimport import tracing
20 20
21 21 from . import (
22 22 encoding,
23 23 error,
24 24 match as matchmod,
25 25 pathutil,
26 26 policy,
27 27 pycompat,
28 28 scmutil,
29 29 sparse,
30 30 txnutil,
31 31 util,
32 32 )
33 33
34 34 from .interfaces import (
35 35 dirstate as intdirstate,
36 36 util as interfaceutil,
37 37 )
38 38
39 39 parsers = policy.importmod('parsers')
40 40 rustmod = policy.importrust('dirstate')
41 41
42 42 SUPPORTS_DIRSTATE_V2 = rustmod is not None
43 43
44 44 propertycache = util.propertycache
45 45 filecache = scmutil.filecache
46 46 _rangemask = 0x7FFFFFFF
47 47
48 48 dirstatetuple = parsers.dirstatetuple
49 49
50 50
51 51 class repocache(filecache):
52 52 """filecache for files in .hg/"""
53 53
54 54 def join(self, obj, fname):
55 55 return obj._opener.join(fname)
56 56
57 57
58 58 class rootcache(filecache):
59 59 """filecache for files in the repository root"""
60 60
61 61 def join(self, obj, fname):
62 62 return obj._join(fname)
63 63
64 64
65 65 def _getfsnow(vfs):
66 66 '''Get "now" timestamp on filesystem'''
67 67 tmpfd, tmpname = vfs.mkstemp()
68 68 try:
69 69 return os.fstat(tmpfd)[stat.ST_MTIME]
70 70 finally:
71 71 os.close(tmpfd)
72 72 vfs.unlink(tmpname)
73 73
74 74
75 75 @interfaceutil.implementer(intdirstate.idirstate)
76 76 class dirstate(object):
77 77 def __init__(
78 78 self,
79 79 opener,
80 80 ui,
81 81 root,
82 82 validate,
83 83 sparsematchfn,
84 84 nodeconstants,
85 85 use_dirstate_v2,
86 86 ):
87 87 """Create a new dirstate object.
88 88
89 89 opener is an open()-like callable that can be used to open the
90 90 dirstate file; root is the root of the directory tracked by
91 91 the dirstate.
92 92 """
93 93 self._use_dirstate_v2 = use_dirstate_v2
94 94 self._nodeconstants = nodeconstants
95 95 self._opener = opener
96 96 self._validate = validate
97 97 self._root = root
98 98 self._sparsematchfn = sparsematchfn
99 99 # ntpath.join(root, '') of Python 2.7.9 does not add sep if root is
100 100 # UNC path pointing to root share (issue4557)
101 101 self._rootdir = pathutil.normasprefix(root)
102 102 self._dirty = False
103 103 self._lastnormaltime = 0
104 104 self._ui = ui
105 105 self._filecache = {}
106 106 self._parentwriters = 0
107 107 self._filename = b'dirstate'
108 108 self._pendingfilename = b'%s.pending' % self._filename
109 109 self._plchangecallbacks = {}
110 110 self._origpl = None
111 111 self._updatedfiles = set()
112 112 self._mapcls = dirstatemap
113 113 # Access and cache cwd early, so we don't access it for the first time
114 114 # after a working-copy update caused it to not exist (accessing it then
115 115 # raises an exception).
116 116 self._cwd
117 117
118 118 def prefetch_parents(self):
119 119 """make sure the parents are loaded
120 120
121 121 Used to avoid a race condition.
122 122 """
123 123 self._pl
124 124
125 125 @contextlib.contextmanager
126 126 def parentchange(self):
127 127 """Context manager for handling dirstate parents.
128 128
129 129 If an exception occurs in the scope of the context manager,
130 130 the incoherent dirstate won't be written when wlock is
131 131 released.
132 132 """
133 133 self._parentwriters += 1
134 134 yield
135 135 # Typically we want the "undo" step of a context manager in a
136 136 # finally block so it happens even when an exception
137 137 # occurs. In this case, however, we only want to decrement
138 138 # parentwriters if the code in the with statement exits
139 139 # normally, so we don't have a try/finally here on purpose.
140 140 self._parentwriters -= 1
141 141
142 142 def pendingparentchange(self):
143 143 """Returns true if the dirstate is in the middle of a set of changes
144 144 that modify the dirstate parent.
145 145 """
146 146 return self._parentwriters > 0
147 147
148 148 @propertycache
149 149 def _map(self):
150 150 """Return the dirstate contents (see documentation for dirstatemap)."""
151 151 self._map = self._mapcls(
152 152 self._ui,
153 153 self._opener,
154 154 self._root,
155 155 self._nodeconstants,
156 156 self._use_dirstate_v2,
157 157 )
158 158 return self._map
159 159
160 160 @property
161 161 def _sparsematcher(self):
162 162 """The matcher for the sparse checkout.
163 163
164 164 The working directory may not include every file from a manifest. The
165 165 matcher obtained by this property will match a path if it is to be
166 166 included in the working directory.
167 167 """
168 168 # TODO there is potential to cache this property. For now, the matcher
169 169 # is resolved on every access. (But the called function does use a
170 170 # cache to keep the lookup fast.)
171 171 return self._sparsematchfn()
172 172
173 173 @repocache(b'branch')
174 174 def _branch(self):
175 175 try:
176 176 return self._opener.read(b"branch").strip() or b"default"
177 177 except IOError as inst:
178 178 if inst.errno != errno.ENOENT:
179 179 raise
180 180 return b"default"
181 181
182 182 @property
183 183 def _pl(self):
184 184 return self._map.parents()
185 185
186 186 def hasdir(self, d):
187 187 return self._map.hastrackeddir(d)
188 188
189 189 @rootcache(b'.hgignore')
190 190 def _ignore(self):
191 191 files = self._ignorefiles()
192 192 if not files:
193 193 return matchmod.never()
194 194
195 195 pats = [b'include:%s' % f for f in files]
196 196 return matchmod.match(self._root, b'', [], pats, warn=self._ui.warn)
197 197
198 198 @propertycache
199 199 def _slash(self):
200 200 return self._ui.configbool(b'ui', b'slash') and pycompat.ossep != b'/'
201 201
202 202 @propertycache
203 203 def _checklink(self):
204 204 return util.checklink(self._root)
205 205
206 206 @propertycache
207 207 def _checkexec(self):
208 208 return bool(util.checkexec(self._root))
209 209
210 210 @propertycache
211 211 def _checkcase(self):
212 212 return not util.fscasesensitive(self._join(b'.hg'))
213 213
214 214 def _join(self, f):
215 215 # much faster than os.path.join()
216 216 # it's safe because f is always a relative path
217 217 return self._rootdir + f
218 218
219 219 def flagfunc(self, buildfallback):
220 220 if self._checklink and self._checkexec:
221 221
222 222 def f(x):
223 223 try:
224 224 st = os.lstat(self._join(x))
225 225 if util.statislink(st):
226 226 return b'l'
227 227 if util.statisexec(st):
228 228 return b'x'
229 229 except OSError:
230 230 pass
231 231 return b''
232 232
233 233 return f
234 234
235 235 fallback = buildfallback()
236 236 if self._checklink:
237 237
238 238 def f(x):
239 239 if os.path.islink(self._join(x)):
240 240 return b'l'
241 241 if b'x' in fallback(x):
242 242 return b'x'
243 243 return b''
244 244
245 245 return f
246 246 if self._checkexec:
247 247
248 248 def f(x):
249 249 if b'l' in fallback(x):
250 250 return b'l'
251 251 if util.isexec(self._join(x)):
252 252 return b'x'
253 253 return b''
254 254
255 255 return f
256 256 else:
257 257 return fallback
258 258
259 259 @propertycache
260 260 def _cwd(self):
261 261 # internal config: ui.forcecwd
262 262 forcecwd = self._ui.config(b'ui', b'forcecwd')
263 263 if forcecwd:
264 264 return forcecwd
265 265 return encoding.getcwd()
266 266
267 267 def getcwd(self):
268 268 """Return the path from which a canonical path is calculated.
269 269
270 270 This path should be used to resolve file patterns or to convert
271 271 canonical paths back to file paths for display. It shouldn't be
272 272 used to get real file paths. Use vfs functions instead.
273 273 """
274 274 cwd = self._cwd
275 275 if cwd == self._root:
276 276 return b''
277 277 # self._root ends with a path separator if self._root is '/' or 'C:\'
278 278 rootsep = self._root
279 279 if not util.endswithsep(rootsep):
280 280 rootsep += pycompat.ossep
281 281 if cwd.startswith(rootsep):
282 282 return cwd[len(rootsep) :]
283 283 else:
284 284 # we're outside the repo. return an absolute path.
285 285 return cwd
286 286
287 287 def pathto(self, f, cwd=None):
288 288 if cwd is None:
289 289 cwd = self.getcwd()
290 290 path = util.pathto(self._root, cwd, f)
291 291 if self._slash:
292 292 return util.pconvert(path)
293 293 return path
294 294
295 295 def __getitem__(self, key):
296 296 """Return the current state of key (a filename) in the dirstate.
297 297
298 298 States are:
299 299 n normal
300 300 m needs merging
301 301 r marked for removal
302 302 a marked for addition
303 303 ? not tracked
304 304 """
305 305 return self._map.get(key, (b"?",))[0]
306 306
307 307 def __contains__(self, key):
308 308 return key in self._map
309 309
310 310 def __iter__(self):
311 311 return iter(sorted(self._map))
312 312
313 313 def items(self):
314 314 return pycompat.iteritems(self._map)
315 315
316 316 iteritems = items
317 317
318 318 def directories(self):
319 319 return self._map.directories()
320 320
321 321 def parents(self):
322 322 return [self._validate(p) for p in self._pl]
323 323
324 324 def p1(self):
325 325 return self._validate(self._pl[0])
326 326
327 327 def p2(self):
328 328 return self._validate(self._pl[1])
329 329
330 330 def branch(self):
331 331 return encoding.tolocal(self._branch)
332 332
333 333 def setparents(self, p1, p2=None):
334 334 """Set dirstate parents to p1 and p2.
335 335
336 336 When moving from two parents to one, 'm' merged entries a
337 337 adjusted to normal and previous copy records discarded and
338 338 returned by the call.
339 339
340 340 See localrepo.setparents()
341 341 """
342 342 if p2 is None:
343 343 p2 = self._nodeconstants.nullid
344 344 if self._parentwriters == 0:
345 345 raise ValueError(
346 346 b"cannot set dirstate parent outside of "
347 347 b"dirstate.parentchange context manager"
348 348 )
349 349
350 350 self._dirty = True
351 351 oldp2 = self._pl[1]
352 352 if self._origpl is None:
353 353 self._origpl = self._pl
354 354 self._map.setparents(p1, p2)
355 355 copies = {}
356 356 if (
357 357 oldp2 != self._nodeconstants.nullid
358 358 and p2 == self._nodeconstants.nullid
359 359 ):
360 360 candidatefiles = self._map.non_normal_or_other_parent_paths()
361 361
362 362 for f in candidatefiles:
363 363 s = self._map.get(f)
364 364 if s is None:
365 365 continue
366 366
367 367 # Discard 'm' markers when moving away from a merge state
368 368 if s[0] == b'm':
369 369 source = self._map.copymap.get(f)
370 370 if source:
371 371 copies[f] = source
372 372 self.normallookup(f)
373 373 # Also fix up otherparent markers
374 374 elif s[0] == b'n' and s[2] == -2:
375 375 source = self._map.copymap.get(f)
376 376 if source:
377 377 copies[f] = source
378 378 self.add(f)
379 379 return copies
380 380
381 381 def setbranch(self, branch):
382 382 self.__class__._branch.set(self, encoding.fromlocal(branch))
383 383 f = self._opener(b'branch', b'w', atomictemp=True, checkambig=True)
384 384 try:
385 385 f.write(self._branch + b'\n')
386 386 f.close()
387 387
388 388 # make sure filecache has the correct stat info for _branch after
389 389 # replacing the underlying file
390 390 ce = self._filecache[b'_branch']
391 391 if ce:
392 392 ce.refresh()
393 393 except: # re-raises
394 394 f.discard()
395 395 raise
396 396
397 397 def invalidate(self):
398 398 """Causes the next access to reread the dirstate.
399 399
400 400 This is different from localrepo.invalidatedirstate() because it always
401 401 rereads the dirstate. Use localrepo.invalidatedirstate() if you want to
402 402 check whether the dirstate has changed before rereading it."""
403 403
404 404 for a in ("_map", "_branch", "_ignore"):
405 405 if a in self.__dict__:
406 406 delattr(self, a)
407 407 self._lastnormaltime = 0
408 408 self._dirty = False
409 409 self._updatedfiles.clear()
410 410 self._parentwriters = 0
411 411 self._origpl = None
412 412
413 413 def copy(self, source, dest):
414 414 """Mark dest as a copy of source. Unmark dest if source is None."""
415 415 if source == dest:
416 416 return
417 417 self._dirty = True
418 418 if source is not None:
419 419 self._map.copymap[dest] = source
420 420 self._updatedfiles.add(source)
421 421 self._updatedfiles.add(dest)
422 422 elif self._map.copymap.pop(dest, None):
423 423 self._updatedfiles.add(dest)
424 424
425 425 def copied(self, file):
426 426 return self._map.copymap.get(file, None)
427 427
428 428 def copies(self):
429 429 return self._map.copymap
430 430
431 431 def _addpath(self, f, state, mode, size, mtime):
432 432 oldstate = self[f]
433 433 if state == b'a' or oldstate == b'r':
434 434 scmutil.checkfilename(f)
435 435 if self._map.hastrackeddir(f):
436 436 raise error.Abort(
437 437 _(b'directory %r already in dirstate') % pycompat.bytestr(f)
438 438 )
439 439 # shadows
440 440 for d in pathutil.finddirs(f):
441 441 if self._map.hastrackeddir(d):
442 442 break
443 443 entry = self._map.get(d)
444 444 if entry is not None and entry[0] != b'r':
445 445 raise error.Abort(
446 446 _(b'file %r in dirstate clashes with %r')
447 447 % (pycompat.bytestr(d), pycompat.bytestr(f))
448 448 )
449 449 self._dirty = True
450 450 self._updatedfiles.add(f)
451 451 self._map.addfile(f, oldstate, state, mode, size, mtime)
452 452
453 453 def normal(self, f, parentfiledata=None):
454 454 """Mark a file normal and clean.
455 455
456 456 parentfiledata: (mode, size, mtime) of the clean file
457 457
458 458 parentfiledata should be computed from memory (for mode,
459 459 size), as or close as possible from the point where we
460 460 determined the file was clean, to limit the risk of the
461 461 file having been changed by an external process between the
462 462 moment where the file was determined to be clean and now."""
463 463 if parentfiledata:
464 464 (mode, size, mtime) = parentfiledata
465 465 else:
466 466 s = os.lstat(self._join(f))
467 467 mode = s.st_mode
468 468 size = s.st_size
469 469 mtime = s[stat.ST_MTIME]
470 470 self._addpath(f, b'n', mode, size & _rangemask, mtime & _rangemask)
471 471 self._map.copymap.pop(f, None)
472 472 if f in self._map.nonnormalset:
473 473 self._map.nonnormalset.remove(f)
474 474 if mtime > self._lastnormaltime:
475 475 # Remember the most recent modification timeslot for status(),
476 476 # to make sure we won't miss future size-preserving file content
477 477 # modifications that happen within the same timeslot.
478 478 self._lastnormaltime = mtime
479 479
480 480 def normallookup(self, f):
481 481 '''Mark a file normal, but possibly dirty.'''
482 482 if self._pl[1] != self._nodeconstants.nullid:
483 483 # if there is a merge going on and the file was either
484 484 # in state 'm' (-1) or coming from other parent (-2) before
485 485 # being removed, restore that state.
486 486 entry = self._map.get(f)
487 487 if entry is not None:
488 488 if entry[0] == b'r' and entry[2] in (-1, -2):
489 489 source = self._map.copymap.get(f)
490 490 if entry[2] == -1:
491 491 self.merge(f)
492 492 elif entry[2] == -2:
493 493 self.otherparent(f)
494 494 if source:
495 495 self.copy(source, f)
496 496 return
497 497 if entry[0] == b'm' or entry[0] == b'n' and entry[2] == -2:
498 498 return
499 499 self._addpath(f, b'n', 0, -1, -1)
500 500 self._map.copymap.pop(f, None)
501 501
502 502 def otherparent(self, f):
503 503 '''Mark as coming from the other parent, always dirty.'''
504 504 if self._pl[1] == self._nodeconstants.nullid:
505 505 raise error.Abort(
506 506 _(b"setting %r to other parent only allowed in merges") % f
507 507 )
508 508 if f in self and self[f] == b'n':
509 509 # merge-like
510 510 self._addpath(f, b'm', 0, -2, -1)
511 511 else:
512 512 # add-like
513 513 self._addpath(f, b'n', 0, -2, -1)
514 514 self._map.copymap.pop(f, None)
515 515
516 516 def add(self, f):
517 517 '''Mark a file added.'''
518 518 self._addpath(f, b'a', 0, -1, -1)
519 519 self._map.copymap.pop(f, None)
520 520
521 521 def remove(self, f):
522 522 '''Mark a file removed.'''
523 523 self._dirty = True
524 524 oldstate = self[f]
525 525 size = 0
526 526 if self._pl[1] != self._nodeconstants.nullid:
527 527 entry = self._map.get(f)
528 528 if entry is not None:
529 529 # backup the previous state
530 530 if entry[0] == b'm': # merge
531 531 size = -1
532 532 elif entry[0] == b'n' and entry[2] == -2: # other parent
533 533 size = -2
534 534 self._map.otherparentset.add(f)
535 535 self._updatedfiles.add(f)
536 536 self._map.removefile(f, oldstate, size)
537 537 if size == 0:
538 538 self._map.copymap.pop(f, None)
539 539
540 540 def merge(self, f):
541 541 '''Mark a file merged.'''
542 542 if self._pl[1] == self._nodeconstants.nullid:
543 543 return self.normallookup(f)
544 544 return self.otherparent(f)
545 545
546 546 def drop(self, f):
547 547 '''Drop a file from the dirstate'''
548 548 oldstate = self[f]
549 549 if self._map.dropfile(f, oldstate):
550 550 self._dirty = True
551 551 self._updatedfiles.add(f)
552 552 self._map.copymap.pop(f, None)
553 553
554 554 def _discoverpath(self, path, normed, ignoremissing, exists, storemap):
555 555 if exists is None:
556 556 exists = os.path.lexists(os.path.join(self._root, path))
557 557 if not exists:
558 558 # Maybe a path component exists
559 559 if not ignoremissing and b'/' in path:
560 560 d, f = path.rsplit(b'/', 1)
561 561 d = self._normalize(d, False, ignoremissing, None)
562 562 folded = d + b"/" + f
563 563 else:
564 564 # No path components, preserve original case
565 565 folded = path
566 566 else:
567 567 # recursively normalize leading directory components
568 568 # against dirstate
569 569 if b'/' in normed:
570 570 d, f = normed.rsplit(b'/', 1)
571 571 d = self._normalize(d, False, ignoremissing, True)
572 572 r = self._root + b"/" + d
573 573 folded = d + b"/" + util.fspath(f, r)
574 574 else:
575 575 folded = util.fspath(normed, self._root)
576 576 storemap[normed] = folded
577 577
578 578 return folded
579 579
580 580 def _normalizefile(self, path, isknown, ignoremissing=False, exists=None):
581 581 normed = util.normcase(path)
582 582 folded = self._map.filefoldmap.get(normed, None)
583 583 if folded is None:
584 584 if isknown:
585 585 folded = path
586 586 else:
587 587 folded = self._discoverpath(
588 588 path, normed, ignoremissing, exists, self._map.filefoldmap
589 589 )
590 590 return folded
591 591
592 592 def _normalize(self, path, isknown, ignoremissing=False, exists=None):
593 593 normed = util.normcase(path)
594 594 folded = self._map.filefoldmap.get(normed, None)
595 595 if folded is None:
596 596 folded = self._map.dirfoldmap.get(normed, None)
597 597 if folded is None:
598 598 if isknown:
599 599 folded = path
600 600 else:
601 601 # store discovered result in dirfoldmap so that future
602 602 # normalizefile calls don't start matching directories
603 603 folded = self._discoverpath(
604 604 path, normed, ignoremissing, exists, self._map.dirfoldmap
605 605 )
606 606 return folded
607 607
608 608 def normalize(self, path, isknown=False, ignoremissing=False):
609 609 """
610 610 normalize the case of a pathname when on a casefolding filesystem
611 611
612 612 isknown specifies whether the filename came from walking the
613 613 disk, to avoid extra filesystem access.
614 614
615 615 If ignoremissing is True, missing path are returned
616 616 unchanged. Otherwise, we try harder to normalize possibly
617 617 existing path components.
618 618
619 619 The normalized case is determined based on the following precedence:
620 620
621 621 - version of name already stored in the dirstate
622 622 - version of name stored on disk
623 623 - version provided via command arguments
624 624 """
625 625
626 626 if self._checkcase:
627 627 return self._normalize(path, isknown, ignoremissing)
628 628 return path
629 629
630 630 def clear(self):
631 631 self._map.clear()
632 632 self._lastnormaltime = 0
633 633 self._updatedfiles.clear()
634 634 self._dirty = True
635 635
636 636 def rebuild(self, parent, allfiles, changedfiles=None):
637 637 if changedfiles is None:
638 638 # Rebuild entire dirstate
639 639 to_lookup = allfiles
640 640 to_drop = []
641 641 lastnormaltime = self._lastnormaltime
642 642 self.clear()
643 643 self._lastnormaltime = lastnormaltime
644 644 elif len(changedfiles) < 10:
645 645 # Avoid turning allfiles into a set, which can be expensive if it's
646 646 # large.
647 647 to_lookup = []
648 648 to_drop = []
649 649 for f in changedfiles:
650 650 if f in allfiles:
651 651 to_lookup.append(f)
652 652 else:
653 653 to_drop.append(f)
654 654 else:
655 655 changedfilesset = set(changedfiles)
656 656 to_lookup = changedfilesset & set(allfiles)
657 657 to_drop = changedfilesset - to_lookup
658 658
659 659 if self._origpl is None:
660 660 self._origpl = self._pl
661 661 self._map.setparents(parent, self._nodeconstants.nullid)
662 662
663 663 for f in to_lookup:
664 664 self.normallookup(f)
665 665 for f in to_drop:
666 666 self.drop(f)
667 667
668 668 self._dirty = True
669 669
670 670 def identity(self):
671 671 """Return identity of dirstate itself to detect changing in storage
672 672
673 673 If identity of previous dirstate is equal to this, writing
674 674 changes based on the former dirstate out can keep consistency.
675 675 """
676 676 return self._map.identity
677 677
678 678 def write(self, tr):
679 679 if not self._dirty:
680 680 return
681 681
682 682 filename = self._filename
683 683 if tr:
684 684 # 'dirstate.write()' is not only for writing in-memory
685 685 # changes out, but also for dropping ambiguous timestamp.
686 686 # delayed writing re-raise "ambiguous timestamp issue".
687 687 # See also the wiki page below for detail:
688 688 # https://www.mercurial-scm.org/wiki/DirstateTransactionPlan
689 689
690 690 # emulate dropping timestamp in 'parsers.pack_dirstate'
691 691 now = _getfsnow(self._opener)
692 692 self._map.clearambiguoustimes(self._updatedfiles, now)
693 693
694 694 # emulate that all 'dirstate.normal' results are written out
695 695 self._lastnormaltime = 0
696 696 self._updatedfiles.clear()
697 697
698 698 # delay writing in-memory changes out
699 699 tr.addfilegenerator(
700 700 b'dirstate',
701 701 (self._filename,),
702 702 self._writedirstate,
703 703 location=b'plain',
704 704 )
705 705 return
706 706
707 707 st = self._opener(filename, b"w", atomictemp=True, checkambig=True)
708 708 self._writedirstate(st)
709 709
710 710 def addparentchangecallback(self, category, callback):
711 711 """add a callback to be called when the wd parents are changed
712 712
713 713 Callback will be called with the following arguments:
714 714 dirstate, (oldp1, oldp2), (newp1, newp2)
715 715
716 716 Category is a unique identifier to allow overwriting an old callback
717 717 with a newer callback.
718 718 """
719 719 self._plchangecallbacks[category] = callback
720 720
721 721 def _writedirstate(self, st):
722 722 # notify callbacks about parents change
723 723 if self._origpl is not None and self._origpl != self._pl:
724 724 for c, callback in sorted(
725 725 pycompat.iteritems(self._plchangecallbacks)
726 726 ):
727 727 callback(self, self._origpl, self._pl)
728 728 self._origpl = None
729 729 # use the modification time of the newly created temporary file as the
730 730 # filesystem's notion of 'now'
731 731 now = util.fstat(st)[stat.ST_MTIME] & _rangemask
732 732
733 733 # enough 'delaywrite' prevents 'pack_dirstate' from dropping
734 734 # timestamp of each entries in dirstate, because of 'now > mtime'
735 735 delaywrite = self._ui.configint(b'debug', b'dirstate.delaywrite')
736 736 if delaywrite > 0:
737 737 # do we have any files to delay for?
738 738 for f, e in pycompat.iteritems(self._map):
739 739 if e[0] == b'n' and e[3] == now:
740 740 import time # to avoid useless import
741 741
742 742 # rather than sleep n seconds, sleep until the next
743 743 # multiple of n seconds
744 744 clock = time.time()
745 745 start = int(clock) - (int(clock) % delaywrite)
746 746 end = start + delaywrite
747 747 time.sleep(end - clock)
748 748 now = end # trust our estimate that the end is near now
749 749 break
750 750
751 751 self._map.write(st, now)
752 752 self._lastnormaltime = 0
753 753 self._dirty = False
754 754
755 755 def _dirignore(self, f):
756 756 if self._ignore(f):
757 757 return True
758 758 for p in pathutil.finddirs(f):
759 759 if self._ignore(p):
760 760 return True
761 761 return False
762 762
763 763 def _ignorefiles(self):
764 764 files = []
765 765 if os.path.exists(self._join(b'.hgignore')):
766 766 files.append(self._join(b'.hgignore'))
767 767 for name, path in self._ui.configitems(b"ui"):
768 768 if name == b'ignore' or name.startswith(b'ignore.'):
769 769 # we need to use os.path.join here rather than self._join
770 770 # because path is arbitrary and user-specified
771 771 files.append(os.path.join(self._rootdir, util.expandpath(path)))
772 772 return files
773 773
774 774 def _ignorefileandline(self, f):
775 775 files = collections.deque(self._ignorefiles())
776 776 visited = set()
777 777 while files:
778 778 i = files.popleft()
779 779 patterns = matchmod.readpatternfile(
780 780 i, self._ui.warn, sourceinfo=True
781 781 )
782 782 for pattern, lineno, line in patterns:
783 783 kind, p = matchmod._patsplit(pattern, b'glob')
784 784 if kind == b"subinclude":
785 785 if p not in visited:
786 786 files.append(p)
787 787 continue
788 788 m = matchmod.match(
789 789 self._root, b'', [], [pattern], warn=self._ui.warn
790 790 )
791 791 if m(f):
792 792 return (i, lineno, line)
793 793 visited.add(i)
794 794 return (None, -1, b"")
795 795
796 796 def _walkexplicit(self, match, subrepos):
797 797 """Get stat data about the files explicitly specified by match.
798 798
799 799 Return a triple (results, dirsfound, dirsnotfound).
800 800 - results is a mapping from filename to stat result. It also contains
801 801 listings mapping subrepos and .hg to None.
802 802 - dirsfound is a list of files found to be directories.
803 803 - dirsnotfound is a list of files that the dirstate thinks are
804 804 directories and that were not found."""
805 805
806 806 def badtype(mode):
807 807 kind = _(b'unknown')
808 808 if stat.S_ISCHR(mode):
809 809 kind = _(b'character device')
810 810 elif stat.S_ISBLK(mode):
811 811 kind = _(b'block device')
812 812 elif stat.S_ISFIFO(mode):
813 813 kind = _(b'fifo')
814 814 elif stat.S_ISSOCK(mode):
815 815 kind = _(b'socket')
816 816 elif stat.S_ISDIR(mode):
817 817 kind = _(b'directory')
818 818 return _(b'unsupported file type (type is %s)') % kind
819 819
820 820 badfn = match.bad
821 821 dmap = self._map
822 822 lstat = os.lstat
823 823 getkind = stat.S_IFMT
824 824 dirkind = stat.S_IFDIR
825 825 regkind = stat.S_IFREG
826 826 lnkkind = stat.S_IFLNK
827 827 join = self._join
828 828 dirsfound = []
829 829 foundadd = dirsfound.append
830 830 dirsnotfound = []
831 831 notfoundadd = dirsnotfound.append
832 832
833 833 if not match.isexact() and self._checkcase:
834 834 normalize = self._normalize
835 835 else:
836 836 normalize = None
837 837
838 838 files = sorted(match.files())
839 839 subrepos.sort()
840 840 i, j = 0, 0
841 841 while i < len(files) and j < len(subrepos):
842 842 subpath = subrepos[j] + b"/"
843 843 if files[i] < subpath:
844 844 i += 1
845 845 continue
846 846 while i < len(files) and files[i].startswith(subpath):
847 847 del files[i]
848 848 j += 1
849 849
850 850 if not files or b'' in files:
851 851 files = [b'']
852 852 # constructing the foldmap is expensive, so don't do it for the
853 853 # common case where files is ['']
854 854 normalize = None
855 855 results = dict.fromkeys(subrepos)
856 856 results[b'.hg'] = None
857 857
858 858 for ff in files:
859 859 if normalize:
860 860 nf = normalize(ff, False, True)
861 861 else:
862 862 nf = ff
863 863 if nf in results:
864 864 continue
865 865
866 866 try:
867 867 st = lstat(join(nf))
868 868 kind = getkind(st.st_mode)
869 869 if kind == dirkind:
870 870 if nf in dmap:
871 871 # file replaced by dir on disk but still in dirstate
872 872 results[nf] = None
873 873 foundadd((nf, ff))
874 874 elif kind == regkind or kind == lnkkind:
875 875 results[nf] = st
876 876 else:
877 877 badfn(ff, badtype(kind))
878 878 if nf in dmap:
879 879 results[nf] = None
880 880 except OSError as inst: # nf not found on disk - it is dirstate only
881 881 if nf in dmap: # does it exactly match a missing file?
882 882 results[nf] = None
883 883 else: # does it match a missing directory?
884 884 if self._map.hasdir(nf):
885 885 notfoundadd(nf)
886 886 else:
887 887 badfn(ff, encoding.strtolocal(inst.strerror))
888 888
889 889 # match.files() may contain explicitly-specified paths that shouldn't
890 890 # be taken; drop them from the list of files found. dirsfound/notfound
891 891 # aren't filtered here because they will be tested later.
892 892 if match.anypats():
893 893 for f in list(results):
894 894 if f == b'.hg' or f in subrepos:
895 895 # keep sentinel to disable further out-of-repo walks
896 896 continue
897 897 if not match(f):
898 898 del results[f]
899 899
900 900 # Case insensitive filesystems cannot rely on lstat() failing to detect
901 901 # a case-only rename. Prune the stat object for any file that does not
902 902 # match the case in the filesystem, if there are multiple files that
903 903 # normalize to the same path.
904 904 if match.isexact() and self._checkcase:
905 905 normed = {}
906 906
907 907 for f, st in pycompat.iteritems(results):
908 908 if st is None:
909 909 continue
910 910
911 911 nc = util.normcase(f)
912 912 paths = normed.get(nc)
913 913
914 914 if paths is None:
915 915 paths = set()
916 916 normed[nc] = paths
917 917
918 918 paths.add(f)
919 919
920 920 for norm, paths in pycompat.iteritems(normed):
921 921 if len(paths) > 1:
922 922 for path in paths:
923 923 folded = self._discoverpath(
924 924 path, norm, True, None, self._map.dirfoldmap
925 925 )
926 926 if path != folded:
927 927 results[path] = None
928 928
929 929 return results, dirsfound, dirsnotfound
930 930
931 931 def walk(self, match, subrepos, unknown, ignored, full=True):
932 932 """
933 933 Walk recursively through the directory tree, finding all files
934 934 matched by match.
935 935
936 936 If full is False, maybe skip some known-clean files.
937 937
938 938 Return a dict mapping filename to stat-like object (either
939 939 mercurial.osutil.stat instance or return value of os.stat()).
940 940
941 941 """
942 942 # full is a flag that extensions that hook into walk can use -- this
943 943 # implementation doesn't use it at all. This satisfies the contract
944 944 # because we only guarantee a "maybe".
945 945
946 946 if ignored:
947 947 ignore = util.never
948 948 dirignore = util.never
949 949 elif unknown:
950 950 ignore = self._ignore
951 951 dirignore = self._dirignore
952 952 else:
953 953 # if not unknown and not ignored, drop dir recursion and step 2
954 954 ignore = util.always
955 955 dirignore = util.always
956 956
957 957 matchfn = match.matchfn
958 958 matchalways = match.always()
959 959 matchtdir = match.traversedir
960 960 dmap = self._map
961 961 listdir = util.listdir
962 962 lstat = os.lstat
963 963 dirkind = stat.S_IFDIR
964 964 regkind = stat.S_IFREG
965 965 lnkkind = stat.S_IFLNK
966 966 join = self._join
967 967
968 968 exact = skipstep3 = False
969 969 if match.isexact(): # match.exact
970 970 exact = True
971 971 dirignore = util.always # skip step 2
972 972 elif match.prefix(): # match.match, no patterns
973 973 skipstep3 = True
974 974
975 975 if not exact and self._checkcase:
976 976 normalize = self._normalize
977 977 normalizefile = self._normalizefile
978 978 skipstep3 = False
979 979 else:
980 980 normalize = self._normalize
981 981 normalizefile = None
982 982
983 983 # step 1: find all explicit files
984 984 results, work, dirsnotfound = self._walkexplicit(match, subrepos)
985 985 if matchtdir:
986 986 for d in work:
987 987 matchtdir(d[0])
988 988 for d in dirsnotfound:
989 989 matchtdir(d)
990 990
991 991 skipstep3 = skipstep3 and not (work or dirsnotfound)
992 992 work = [d for d in work if not dirignore(d[0])]
993 993
994 994 # step 2: visit subdirectories
995 995 def traverse(work, alreadynormed):
996 996 wadd = work.append
997 997 while work:
998 998 tracing.counter('dirstate.walk work', len(work))
999 999 nd = work.pop()
1000 1000 visitentries = match.visitchildrenset(nd)
1001 1001 if not visitentries:
1002 1002 continue
1003 1003 if visitentries == b'this' or visitentries == b'all':
1004 1004 visitentries = None
1005 1005 skip = None
1006 1006 if nd != b'':
1007 1007 skip = b'.hg'
1008 1008 try:
1009 1009 with tracing.log('dirstate.walk.traverse listdir %s', nd):
1010 1010 entries = listdir(join(nd), stat=True, skip=skip)
1011 1011 except OSError as inst:
1012 1012 if inst.errno in (errno.EACCES, errno.ENOENT):
1013 1013 match.bad(
1014 1014 self.pathto(nd), encoding.strtolocal(inst.strerror)
1015 1015 )
1016 1016 continue
1017 1017 raise
1018 1018 for f, kind, st in entries:
1019 1019 # Some matchers may return files in the visitentries set,
1020 1020 # instead of 'this', if the matcher explicitly mentions them
1021 1021 # and is not an exactmatcher. This is acceptable; we do not
1022 1022 # make any hard assumptions about file-or-directory below
1023 1023 # based on the presence of `f` in visitentries. If
1024 1024 # visitchildrenset returned a set, we can always skip the
1025 1025 # entries *not* in the set it provided regardless of whether
1026 1026 # they're actually a file or a directory.
1027 1027 if visitentries and f not in visitentries:
1028 1028 continue
1029 1029 if normalizefile:
1030 1030 # even though f might be a directory, we're only
1031 1031 # interested in comparing it to files currently in the
1032 1032 # dmap -- therefore normalizefile is enough
1033 1033 nf = normalizefile(
1034 1034 nd and (nd + b"/" + f) or f, True, True
1035 1035 )
1036 1036 else:
1037 1037 nf = nd and (nd + b"/" + f) or f
1038 1038 if nf not in results:
1039 1039 if kind == dirkind:
1040 1040 if not ignore(nf):
1041 1041 if matchtdir:
1042 1042 matchtdir(nf)
1043 1043 wadd(nf)
1044 1044 if nf in dmap and (matchalways or matchfn(nf)):
1045 1045 results[nf] = None
1046 1046 elif kind == regkind or kind == lnkkind:
1047 1047 if nf in dmap:
1048 1048 if matchalways or matchfn(nf):
1049 1049 results[nf] = st
1050 1050 elif (matchalways or matchfn(nf)) and not ignore(
1051 1051 nf
1052 1052 ):
1053 1053 # unknown file -- normalize if necessary
1054 1054 if not alreadynormed:
1055 1055 nf = normalize(nf, False, True)
1056 1056 results[nf] = st
1057 1057 elif nf in dmap and (matchalways or matchfn(nf)):
1058 1058 results[nf] = None
1059 1059
1060 1060 for nd, d in work:
1061 1061 # alreadynormed means that processwork doesn't have to do any
1062 1062 # expensive directory normalization
1063 1063 alreadynormed = not normalize or nd == d
1064 1064 traverse([d], alreadynormed)
1065 1065
1066 1066 for s in subrepos:
1067 1067 del results[s]
1068 1068 del results[b'.hg']
1069 1069
1070 1070 # step 3: visit remaining files from dmap
1071 1071 if not skipstep3 and not exact:
1072 1072 # If a dmap file is not in results yet, it was either
1073 1073 # a) not matching matchfn b) ignored, c) missing, or d) under a
1074 1074 # symlink directory.
1075 1075 if not results and matchalways:
1076 1076 visit = [f for f in dmap]
1077 1077 else:
1078 1078 visit = [f for f in dmap if f not in results and matchfn(f)]
1079 1079 visit.sort()
1080 1080
1081 1081 if unknown:
1082 1082 # unknown == True means we walked all dirs under the roots
1083 1083 # that wasn't ignored, and everything that matched was stat'ed
1084 1084 # and is already in results.
1085 1085 # The rest must thus be ignored or under a symlink.
1086 1086 audit_path = pathutil.pathauditor(self._root, cached=True)
1087 1087
1088 1088 for nf in iter(visit):
1089 1089 # If a stat for the same file was already added with a
1090 1090 # different case, don't add one for this, since that would
1091 1091 # make it appear as if the file exists under both names
1092 1092 # on disk.
1093 1093 if (
1094 1094 normalizefile
1095 1095 and normalizefile(nf, True, True) in results
1096 1096 ):
1097 1097 results[nf] = None
1098 1098 # Report ignored items in the dmap as long as they are not
1099 1099 # under a symlink directory.
1100 1100 elif audit_path.check(nf):
1101 1101 try:
1102 1102 results[nf] = lstat(join(nf))
1103 1103 # file was just ignored, no links, and exists
1104 1104 except OSError:
1105 1105 # file doesn't exist
1106 1106 results[nf] = None
1107 1107 else:
1108 1108 # It's either missing or under a symlink directory
1109 1109 # which we in this case report as missing
1110 1110 results[nf] = None
1111 1111 else:
1112 1112 # We may not have walked the full directory tree above,
1113 1113 # so stat and check everything we missed.
1114 1114 iv = iter(visit)
1115 1115 for st in util.statfiles([join(i) for i in visit]):
1116 1116 results[next(iv)] = st
1117 1117 return results
1118 1118
1119 1119 def _rust_status(self, matcher, list_clean, list_ignored, list_unknown):
1120 1120 # Force Rayon (Rust parallelism library) to respect the number of
1121 1121 # workers. This is a temporary workaround until Rust code knows
1122 1122 # how to read the config file.
1123 1123 numcpus = self._ui.configint(b"worker", b"numcpus")
1124 1124 if numcpus is not None:
1125 1125 encoding.environ.setdefault(b'RAYON_NUM_THREADS', b'%d' % numcpus)
1126 1126
1127 1127 workers_enabled = self._ui.configbool(b"worker", b"enabled", True)
1128 1128 if not workers_enabled:
1129 1129 encoding.environ[b"RAYON_NUM_THREADS"] = b"1"
1130 1130
1131 1131 (
1132 1132 lookup,
1133 1133 modified,
1134 1134 added,
1135 1135 removed,
1136 1136 deleted,
1137 1137 clean,
1138 1138 ignored,
1139 1139 unknown,
1140 1140 warnings,
1141 1141 bad,
1142 1142 traversed,
1143 1143 dirty,
1144 1144 ) = rustmod.status(
1145 1145 self._map._rustmap,
1146 1146 matcher,
1147 1147 self._rootdir,
1148 1148 self._ignorefiles(),
1149 1149 self._checkexec,
1150 1150 self._lastnormaltime,
1151 1151 bool(list_clean),
1152 1152 bool(list_ignored),
1153 1153 bool(list_unknown),
1154 1154 bool(matcher.traversedir),
1155 1155 )
1156 1156
1157 1157 self._dirty |= dirty
1158 1158
1159 1159 if matcher.traversedir:
1160 1160 for dir in traversed:
1161 1161 matcher.traversedir(dir)
1162 1162
1163 1163 if self._ui.warn:
1164 1164 for item in warnings:
1165 1165 if isinstance(item, tuple):
1166 1166 file_path, syntax = item
1167 1167 msg = _(b"%s: ignoring invalid syntax '%s'\n") % (
1168 1168 file_path,
1169 1169 syntax,
1170 1170 )
1171 1171 self._ui.warn(msg)
1172 1172 else:
1173 1173 msg = _(b"skipping unreadable pattern file '%s': %s\n")
1174 1174 self._ui.warn(
1175 1175 msg
1176 1176 % (
1177 1177 pathutil.canonpath(
1178 1178 self._rootdir, self._rootdir, item
1179 1179 ),
1180 1180 b"No such file or directory",
1181 1181 )
1182 1182 )
1183 1183
1184 1184 for (fn, message) in bad:
1185 1185 matcher.bad(fn, encoding.strtolocal(message))
1186 1186
1187 1187 status = scmutil.status(
1188 1188 modified=modified,
1189 1189 added=added,
1190 1190 removed=removed,
1191 1191 deleted=deleted,
1192 1192 unknown=unknown,
1193 1193 ignored=ignored,
1194 1194 clean=clean,
1195 1195 )
1196 1196 return (lookup, status)
1197 1197
1198 1198 def status(self, match, subrepos, ignored, clean, unknown):
1199 1199 """Determine the status of the working copy relative to the
1200 1200 dirstate and return a pair of (unsure, status), where status is of type
1201 1201 scmutil.status and:
1202 1202
1203 1203 unsure:
1204 1204 files that might have been modified since the dirstate was
1205 1205 written, but need to be read to be sure (size is the same
1206 1206 but mtime differs)
1207 1207 status.modified:
1208 1208 files that have definitely been modified since the dirstate
1209 1209 was written (different size or mode)
1210 1210 status.clean:
1211 1211 files that have definitely not been modified since the
1212 1212 dirstate was written
1213 1213 """
1214 1214 listignored, listclean, listunknown = ignored, clean, unknown
1215 1215 lookup, modified, added, unknown, ignored = [], [], [], [], []
1216 1216 removed, deleted, clean = [], [], []
1217 1217
1218 1218 dmap = self._map
1219 1219 dmap.preload()
1220 1220
1221 1221 use_rust = True
1222 1222
1223 1223 allowed_matchers = (
1224 1224 matchmod.alwaysmatcher,
1225 1225 matchmod.exactmatcher,
1226 1226 matchmod.includematcher,
1227 1227 )
1228 1228
1229 1229 if rustmod is None:
1230 1230 use_rust = False
1231 1231 elif self._checkcase:
1232 1232 # Case-insensitive filesystems are not handled yet
1233 1233 use_rust = False
1234 1234 elif subrepos:
1235 1235 use_rust = False
1236 1236 elif sparse.enabled:
1237 1237 use_rust = False
1238 1238 elif not isinstance(match, allowed_matchers):
1239 1239 # Some matchers have yet to be implemented
1240 1240 use_rust = False
1241 1241
1242 1242 if use_rust:
1243 1243 try:
1244 1244 return self._rust_status(
1245 1245 match, listclean, listignored, listunknown
1246 1246 )
1247 1247 except rustmod.FallbackError:
1248 1248 pass
1249 1249
1250 1250 def noop(f):
1251 1251 pass
1252 1252
1253 1253 dcontains = dmap.__contains__
1254 1254 dget = dmap.__getitem__
1255 1255 ladd = lookup.append # aka "unsure"
1256 1256 madd = modified.append
1257 1257 aadd = added.append
1258 1258 uadd = unknown.append if listunknown else noop
1259 1259 iadd = ignored.append if listignored else noop
1260 1260 radd = removed.append
1261 1261 dadd = deleted.append
1262 1262 cadd = clean.append if listclean else noop
1263 1263 mexact = match.exact
1264 1264 dirignore = self._dirignore
1265 1265 checkexec = self._checkexec
1266 1266 copymap = self._map.copymap
1267 1267 lastnormaltime = self._lastnormaltime
1268 1268
1269 1269 # We need to do full walks when either
1270 1270 # - we're listing all clean files, or
1271 1271 # - match.traversedir does something, because match.traversedir should
1272 1272 # be called for every dir in the working dir
1273 1273 full = listclean or match.traversedir is not None
1274 1274 for fn, st in pycompat.iteritems(
1275 1275 self.walk(match, subrepos, listunknown, listignored, full=full)
1276 1276 ):
1277 1277 if not dcontains(fn):
1278 1278 if (listignored or mexact(fn)) and dirignore(fn):
1279 1279 if listignored:
1280 1280 iadd(fn)
1281 1281 else:
1282 1282 uadd(fn)
1283 1283 continue
1284 1284
1285 1285 # This is equivalent to 'state, mode, size, time = dmap[fn]' but not
1286 1286 # written like that for performance reasons. dmap[fn] is not a
1287 1287 # Python tuple in compiled builds. The CPython UNPACK_SEQUENCE
1288 1288 # opcode has fast paths when the value to be unpacked is a tuple or
1289 1289 # a list, but falls back to creating a full-fledged iterator in
1290 1290 # general. That is much slower than simply accessing and storing the
1291 1291 # tuple members one by one.
1292 1292 t = dget(fn)
1293 1293 state = t[0]
1294 1294 mode = t[1]
1295 1295 size = t[2]
1296 1296 time = t[3]
1297 1297
1298 1298 if not st and state in b"nma":
1299 1299 dadd(fn)
1300 1300 elif state == b'n':
1301 1301 if (
1302 1302 size >= 0
1303 1303 and (
1304 1304 (size != st.st_size and size != st.st_size & _rangemask)
1305 1305 or ((mode ^ st.st_mode) & 0o100 and checkexec)
1306 1306 )
1307 1307 or size == -2 # other parent
1308 1308 or fn in copymap
1309 1309 ):
1310 1310 if stat.S_ISLNK(st.st_mode) and size != st.st_size:
1311 1311 # issue6456: Size returned may be longer due to
1312 1312 # encryption on EXT-4 fscrypt, undecided.
1313 1313 ladd(fn)
1314 1314 else:
1315 1315 madd(fn)
1316 1316 elif (
1317 1317 time != st[stat.ST_MTIME]
1318 1318 and time != st[stat.ST_MTIME] & _rangemask
1319 1319 ):
1320 1320 ladd(fn)
1321 1321 elif st[stat.ST_MTIME] == lastnormaltime:
1322 1322 # fn may have just been marked as normal and it may have
1323 1323 # changed in the same second without changing its size.
1324 1324 # This can happen if we quickly do multiple commits.
1325 1325 # Force lookup, so we don't miss such a racy file change.
1326 1326 ladd(fn)
1327 1327 elif listclean:
1328 1328 cadd(fn)
1329 1329 elif state == b'm':
1330 1330 madd(fn)
1331 1331 elif state == b'a':
1332 1332 aadd(fn)
1333 1333 elif state == b'r':
1334 1334 radd(fn)
1335 1335 status = scmutil.status(
1336 1336 modified, added, removed, deleted, unknown, ignored, clean
1337 1337 )
1338 1338 return (lookup, status)
1339 1339
1340 1340 def matches(self, match):
1341 1341 """
1342 1342 return files in the dirstate (in whatever state) filtered by match
1343 1343 """
1344 1344 dmap = self._map
1345 1345 if rustmod is not None:
1346 1346 dmap = self._map._rustmap
1347 1347
1348 1348 if match.always():
1349 1349 return dmap.keys()
1350 1350 files = match.files()
1351 1351 if match.isexact():
1352 1352 # fast path -- filter the other way around, since typically files is
1353 1353 # much smaller than dmap
1354 1354 return [f for f in files if f in dmap]
1355 1355 if match.prefix() and all(fn in dmap for fn in files):
1356 1356 # fast path -- all the values are known to be files, so just return
1357 1357 # that
1358 1358 return list(files)
1359 1359 return [f for f in dmap if match(f)]
1360 1360
1361 1361 def _actualfilename(self, tr):
1362 1362 if tr:
1363 1363 return self._pendingfilename
1364 1364 else:
1365 1365 return self._filename
1366 1366
1367 1367 def savebackup(self, tr, backupname):
1368 1368 '''Save current dirstate into backup file'''
1369 1369 filename = self._actualfilename(tr)
1370 1370 assert backupname != filename
1371 1371
1372 1372 # use '_writedirstate' instead of 'write' to write changes certainly,
1373 1373 # because the latter omits writing out if transaction is running.
1374 1374 # output file will be used to create backup of dirstate at this point.
1375 1375 if self._dirty or not self._opener.exists(filename):
1376 1376 self._writedirstate(
1377 1377 self._opener(filename, b"w", atomictemp=True, checkambig=True)
1378 1378 )
1379 1379
1380 1380 if tr:
1381 1381 # ensure that subsequent tr.writepending returns True for
1382 1382 # changes written out above, even if dirstate is never
1383 1383 # changed after this
1384 1384 tr.addfilegenerator(
1385 1385 b'dirstate',
1386 1386 (self._filename,),
1387 1387 self._writedirstate,
1388 1388 location=b'plain',
1389 1389 )
1390 1390
1391 1391 # ensure that pending file written above is unlinked at
1392 1392 # failure, even if tr.writepending isn't invoked until the
1393 1393 # end of this transaction
1394 1394 tr.registertmp(filename, location=b'plain')
1395 1395
1396 1396 self._opener.tryunlink(backupname)
1397 1397 # hardlink backup is okay because _writedirstate is always called
1398 1398 # with an "atomictemp=True" file.
1399 1399 util.copyfile(
1400 1400 self._opener.join(filename),
1401 1401 self._opener.join(backupname),
1402 1402 hardlink=True,
1403 1403 )
1404 1404
1405 1405 def restorebackup(self, tr, backupname):
1406 1406 '''Restore dirstate by backup file'''
1407 1407 # this "invalidate()" prevents "wlock.release()" from writing
1408 1408 # changes of dirstate out after restoring from backup file
1409 1409 self.invalidate()
1410 1410 filename = self._actualfilename(tr)
1411 1411 o = self._opener
1412 1412 if util.samefile(o.join(backupname), o.join(filename)):
1413 1413 o.unlink(backupname)
1414 1414 else:
1415 1415 o.rename(backupname, filename, checkambig=True)
1416 1416
1417 1417 def clearbackup(self, tr, backupname):
1418 1418 '''Clear backup file'''
1419 1419 self._opener.unlink(backupname)
1420 1420
1421 1421
1422 1422 class dirstatemap(object):
1423 1423 """Map encapsulating the dirstate's contents.
1424 1424
1425 1425 The dirstate contains the following state:
1426 1426
1427 1427 - `identity` is the identity of the dirstate file, which can be used to
1428 1428 detect when changes have occurred to the dirstate file.
1429 1429
1430 1430 - `parents` is a pair containing the parents of the working copy. The
1431 1431 parents are updated by calling `setparents`.
1432 1432
1433 1433 - the state map maps filenames to tuples of (state, mode, size, mtime),
1434 1434 where state is a single character representing 'normal', 'added',
1435 1435 'removed', or 'merged'. It is read by treating the dirstate as a
1436 1436 dict. File state is updated by calling the `addfile`, `removefile` and
1437 1437 `dropfile` methods.
1438 1438
1439 1439 - `copymap` maps destination filenames to their source filename.
1440 1440
1441 1441 The dirstate also provides the following views onto the state:
1442 1442
1443 1443 - `nonnormalset` is a set of the filenames that have state other
1444 1444 than 'normal', or are normal but have an mtime of -1 ('normallookup').
1445 1445
1446 1446 - `otherparentset` is a set of the filenames that are marked as coming
1447 1447 from the second parent when the dirstate is currently being merged.
1448 1448
1449 1449 - `filefoldmap` is a dict mapping normalized filenames to the denormalized
1450 1450 form that they appear as in the dirstate.
1451 1451
1452 1452 - `dirfoldmap` is a dict mapping normalized directory names to the
1453 1453 denormalized form that they appear as in the dirstate.
1454 1454 """
1455 1455
1456 1456 def __init__(self, ui, opener, root, nodeconstants, use_dirstate_v2):
1457 1457 self._ui = ui
1458 1458 self._opener = opener
1459 1459 self._root = root
1460 1460 self._filename = b'dirstate'
1461 1461 self._nodelen = 20
1462 1462 self._nodeconstants = nodeconstants
1463 1463 assert (
1464 1464 not use_dirstate_v2
1465 1465 ), "should have detected unsupported requirement"
1466 1466
1467 1467 self._parents = None
1468 1468 self._dirtyparents = False
1469 1469
1470 1470 # for consistent view between _pl() and _read() invocations
1471 1471 self._pendingmode = None
1472 1472
1473 1473 @propertycache
1474 1474 def _map(self):
1475 1475 self._map = {}
1476 1476 self.read()
1477 1477 return self._map
1478 1478
1479 1479 @propertycache
1480 1480 def copymap(self):
1481 1481 self.copymap = {}
1482 1482 self._map
1483 1483 return self.copymap
1484 1484
1485 1485 def directories(self):
1486 1486 # Rust / dirstate-v2 only
1487 1487 return []
1488 1488
1489 1489 def clear(self):
1490 1490 self._map.clear()
1491 1491 self.copymap.clear()
1492 1492 self.setparents(self._nodeconstants.nullid, self._nodeconstants.nullid)
1493 1493 util.clearcachedproperty(self, b"_dirs")
1494 1494 util.clearcachedproperty(self, b"_alldirs")
1495 1495 util.clearcachedproperty(self, b"filefoldmap")
1496 1496 util.clearcachedproperty(self, b"dirfoldmap")
1497 1497 util.clearcachedproperty(self, b"nonnormalset")
1498 1498 util.clearcachedproperty(self, b"otherparentset")
1499 1499
1500 1500 def items(self):
1501 1501 return pycompat.iteritems(self._map)
1502 1502
1503 1503 # forward for python2,3 compat
1504 1504 iteritems = items
1505 1505
1506 1506 def __len__(self):
1507 1507 return len(self._map)
1508 1508
1509 1509 def __iter__(self):
1510 1510 return iter(self._map)
1511 1511
1512 1512 def get(self, key, default=None):
1513 1513 return self._map.get(key, default)
1514 1514
1515 1515 def __contains__(self, key):
1516 1516 return key in self._map
1517 1517
1518 1518 def __getitem__(self, key):
1519 1519 return self._map[key]
1520 1520
1521 1521 def keys(self):
1522 1522 return self._map.keys()
1523 1523
1524 1524 def preload(self):
1525 1525 """Loads the underlying data, if it's not already loaded"""
1526 1526 self._map
1527 1527
1528 1528 def addfile(self, f, oldstate, state, mode, size, mtime):
1529 1529 """Add a tracked file to the dirstate."""
1530 1530 if oldstate in b"?r" and "_dirs" in self.__dict__:
1531 1531 self._dirs.addpath(f)
1532 1532 if oldstate == b"?" and "_alldirs" in self.__dict__:
1533 1533 self._alldirs.addpath(f)
1534 1534 self._map[f] = dirstatetuple(state, mode, size, mtime)
1535 1535 if state != b'n' or mtime == -1:
1536 1536 self.nonnormalset.add(f)
1537 1537 if size == -2:
1538 1538 self.otherparentset.add(f)
1539 1539
1540 1540 def removefile(self, f, oldstate, size):
1541 1541 """
1542 1542 Mark a file as removed in the dirstate.
1543 1543
1544 1544 The `size` parameter is used to store sentinel values that indicate
1545 1545 the file's previous state. In the future, we should refactor this
1546 1546 to be more explicit about what that state is.
1547 1547 """
1548 1548 if oldstate not in b"?r" and "_dirs" in self.__dict__:
1549 1549 self._dirs.delpath(f)
1550 1550 if oldstate == b"?" and "_alldirs" in self.__dict__:
1551 1551 self._alldirs.addpath(f)
1552 1552 if "filefoldmap" in self.__dict__:
1553 1553 normed = util.normcase(f)
1554 1554 self.filefoldmap.pop(normed, None)
1555 1555 self._map[f] = dirstatetuple(b'r', 0, size, 0)
1556 1556 self.nonnormalset.add(f)
1557 1557
1558 1558 def dropfile(self, f, oldstate):
1559 1559 """
1560 1560 Remove a file from the dirstate. Returns True if the file was
1561 1561 previously recorded.
1562 1562 """
1563 1563 exists = self._map.pop(f, None) is not None
1564 1564 if exists:
1565 1565 if oldstate != b"r" and "_dirs" in self.__dict__:
1566 1566 self._dirs.delpath(f)
1567 1567 if "_alldirs" in self.__dict__:
1568 1568 self._alldirs.delpath(f)
1569 1569 if "filefoldmap" in self.__dict__:
1570 1570 normed = util.normcase(f)
1571 1571 self.filefoldmap.pop(normed, None)
1572 1572 self.nonnormalset.discard(f)
1573 1573 return exists
1574 1574
1575 1575 def clearambiguoustimes(self, files, now):
1576 1576 for f in files:
1577 1577 e = self.get(f)
1578 1578 if e is not None and e[0] == b'n' and e[3] == now:
1579 1579 self._map[f] = dirstatetuple(e[0], e[1], e[2], -1)
1580 1580 self.nonnormalset.add(f)
1581 1581
1582 1582 def nonnormalentries(self):
1583 1583 '''Compute the nonnormal dirstate entries from the dmap'''
1584 1584 try:
1585 1585 return parsers.nonnormalotherparententries(self._map)
1586 1586 except AttributeError:
1587 1587 nonnorm = set()
1588 1588 otherparent = set()
1589 1589 for fname, e in pycompat.iteritems(self._map):
1590 1590 if e[0] != b'n' or e[3] == -1:
1591 1591 nonnorm.add(fname)
1592 1592 if e[0] == b'n' and e[2] == -2:
1593 1593 otherparent.add(fname)
1594 1594 return nonnorm, otherparent
1595 1595
1596 1596 @propertycache
1597 1597 def filefoldmap(self):
1598 1598 """Returns a dictionary mapping normalized case paths to their
1599 1599 non-normalized versions.
1600 1600 """
1601 1601 try:
1602 1602 makefilefoldmap = parsers.make_file_foldmap
1603 1603 except AttributeError:
1604 1604 pass
1605 1605 else:
1606 1606 return makefilefoldmap(
1607 1607 self._map, util.normcasespec, util.normcasefallback
1608 1608 )
1609 1609
1610 1610 f = {}
1611 1611 normcase = util.normcase
1612 1612 for name, s in pycompat.iteritems(self._map):
1613 1613 if s[0] != b'r':
1614 1614 f[normcase(name)] = name
1615 1615 f[b'.'] = b'.' # prevents useless util.fspath() invocation
1616 1616 return f
1617 1617
1618 1618 def hastrackeddir(self, d):
1619 1619 """
1620 1620 Returns True if the dirstate contains a tracked (not removed) file
1621 1621 in this directory.
1622 1622 """
1623 1623 return d in self._dirs
1624 1624
1625 1625 def hasdir(self, d):
1626 1626 """
1627 1627 Returns True if the dirstate contains a file (tracked or removed)
1628 1628 in this directory.
1629 1629 """
1630 1630 return d in self._alldirs
1631 1631
1632 1632 @propertycache
1633 1633 def _dirs(self):
1634 1634 return pathutil.dirs(self._map, b'r')
1635 1635
1636 1636 @propertycache
1637 1637 def _alldirs(self):
1638 1638 return pathutil.dirs(self._map)
1639 1639
1640 1640 def _opendirstatefile(self):
1641 1641 fp, mode = txnutil.trypending(self._root, self._opener, self._filename)
1642 1642 if self._pendingmode is not None and self._pendingmode != mode:
1643 1643 fp.close()
1644 1644 raise error.Abort(
1645 1645 _(b'working directory state may be changed parallelly')
1646 1646 )
1647 1647 self._pendingmode = mode
1648 1648 return fp
1649 1649
1650 1650 def parents(self):
1651 1651 if not self._parents:
1652 1652 try:
1653 1653 fp = self._opendirstatefile()
1654 1654 st = fp.read(2 * self._nodelen)
1655 1655 fp.close()
1656 1656 except IOError as err:
1657 1657 if err.errno != errno.ENOENT:
1658 1658 raise
1659 1659 # File doesn't exist, so the current state is empty
1660 1660 st = b''
1661 1661
1662 1662 l = len(st)
1663 1663 if l == self._nodelen * 2:
1664 1664 self._parents = (
1665 1665 st[: self._nodelen],
1666 1666 st[self._nodelen : 2 * self._nodelen],
1667 1667 )
1668 1668 elif l == 0:
1669 1669 self._parents = (
1670 1670 self._nodeconstants.nullid,
1671 1671 self._nodeconstants.nullid,
1672 1672 )
1673 1673 else:
1674 1674 raise error.Abort(
1675 1675 _(b'working directory state appears damaged!')
1676 1676 )
1677 1677
1678 1678 return self._parents
1679 1679
1680 1680 def setparents(self, p1, p2):
1681 1681 self._parents = (p1, p2)
1682 1682 self._dirtyparents = True
1683 1683
1684 1684 def read(self):
1685 1685 # ignore HG_PENDING because identity is used only for writing
1686 1686 self.identity = util.filestat.frompath(
1687 1687 self._opener.join(self._filename)
1688 1688 )
1689 1689
1690 1690 try:
1691 1691 fp = self._opendirstatefile()
1692 1692 try:
1693 1693 st = fp.read()
1694 1694 finally:
1695 1695 fp.close()
1696 1696 except IOError as err:
1697 1697 if err.errno != errno.ENOENT:
1698 1698 raise
1699 1699 return
1700 1700 if not st:
1701 1701 return
1702 1702
1703 1703 if util.safehasattr(parsers, b'dict_new_presized'):
1704 1704 # Make an estimate of the number of files in the dirstate based on
1705 1705 # its size. This trades wasting some memory for avoiding costly
1706 1706 # resizes. Each entry have a prefix of 17 bytes followed by one or
1707 1707 # two path names. Studies on various large-scale real-world repositories
1708 1708 # found 54 bytes a reasonable upper limit for the average path names.
1709 1709 # Copy entries are ignored for the sake of this estimate.
1710 1710 self._map = parsers.dict_new_presized(len(st) // 71)
1711 1711
1712 1712 # Python's garbage collector triggers a GC each time a certain number
1713 1713 # of container objects (the number being defined by
1714 1714 # gc.get_threshold()) are allocated. parse_dirstate creates a tuple
1715 1715 # for each file in the dirstate. The C version then immediately marks
1716 1716 # them as not to be tracked by the collector. However, this has no
1717 1717 # effect on when GCs are triggered, only on what objects the GC looks
1718 1718 # into. This means that O(number of files) GCs are unavoidable.
1719 1719 # Depending on when in the process's lifetime the dirstate is parsed,
1720 1720 # this can get very expensive. As a workaround, disable GC while
1721 1721 # parsing the dirstate.
1722 1722 #
1723 1723 # (we cannot decorate the function directly since it is in a C module)
1724 1724 parse_dirstate = util.nogc(parsers.parse_dirstate)
1725 1725 p = parse_dirstate(self._map, self.copymap, st)
1726 1726 if not self._dirtyparents:
1727 1727 self.setparents(*p)
1728 1728
1729 1729 # Avoid excess attribute lookups by fast pathing certain checks
1730 1730 self.__contains__ = self._map.__contains__
1731 1731 self.__getitem__ = self._map.__getitem__
1732 1732 self.get = self._map.get
1733 1733
1734 1734 def write(self, st, now):
1735 1735 st.write(
1736 1736 parsers.pack_dirstate(self._map, self.copymap, self.parents(), now)
1737 1737 )
1738 1738 st.close()
1739 1739 self._dirtyparents = False
1740 1740 self.nonnormalset, self.otherparentset = self.nonnormalentries()
1741 1741
1742 1742 @propertycache
1743 1743 def nonnormalset(self):
1744 1744 nonnorm, otherparents = self.nonnormalentries()
1745 1745 self.otherparentset = otherparents
1746 1746 return nonnorm
1747 1747
1748 1748 @propertycache
1749 1749 def otherparentset(self):
1750 1750 nonnorm, otherparents = self.nonnormalentries()
1751 1751 self.nonnormalset = nonnorm
1752 1752 return otherparents
1753 1753
1754 1754 def non_normal_or_other_parent_paths(self):
1755 1755 return self.nonnormalset.union(self.otherparentset)
1756 1756
1757 1757 @propertycache
1758 1758 def identity(self):
1759 1759 self._map
1760 1760 return self.identity
1761 1761
1762 1762 @propertycache
1763 1763 def dirfoldmap(self):
1764 1764 f = {}
1765 1765 normcase = util.normcase
1766 1766 for name in self._dirs:
1767 1767 f[normcase(name)] = name
1768 1768 return f
1769 1769
1770 1770
1771 1771 if rustmod is not None:
1772 1772
1773 1773 class dirstatemap(object):
1774 1774 def __init__(self, ui, opener, root, nodeconstants, use_dirstate_v2):
1775 1775 self._use_dirstate_v2 = use_dirstate_v2
1776 1776 self._nodeconstants = nodeconstants
1777 1777 self._ui = ui
1778 1778 self._opener = opener
1779 1779 self._root = root
1780 1780 self._filename = b'dirstate'
1781 1781 self._nodelen = 20 # Also update Rust code when changing this!
1782 1782 self._parents = None
1783 1783 self._dirtyparents = False
1784 1784
1785 1785 # for consistent view between _pl() and _read() invocations
1786 1786 self._pendingmode = None
1787 1787
1788 1788 self._use_dirstate_tree = self._ui.configbool(
1789 1789 b"experimental",
1790 1790 b"dirstate-tree.in-memory",
1791 1791 False,
1792 1792 )
1793 1793
1794 1794 def addfile(self, *args, **kwargs):
1795 1795 return self._rustmap.addfile(*args, **kwargs)
1796 1796
1797 1797 def removefile(self, *args, **kwargs):
1798 1798 return self._rustmap.removefile(*args, **kwargs)
1799 1799
1800 1800 def dropfile(self, *args, **kwargs):
1801 1801 return self._rustmap.dropfile(*args, **kwargs)
1802 1802
1803 1803 def clearambiguoustimes(self, *args, **kwargs):
1804 1804 return self._rustmap.clearambiguoustimes(*args, **kwargs)
1805 1805
1806 1806 def nonnormalentries(self):
1807 1807 return self._rustmap.nonnormalentries()
1808 1808
1809 1809 def get(self, *args, **kwargs):
1810 1810 return self._rustmap.get(*args, **kwargs)
1811 1811
1812 1812 @property
1813 1813 def copymap(self):
1814 1814 return self._rustmap.copymap()
1815 1815
1816 1816 def directories(self):
1817 1817 return self._rustmap.directories()
1818 1818
1819 1819 def preload(self):
1820 1820 self._rustmap
1821 1821
1822 1822 def clear(self):
1823 1823 self._rustmap.clear()
1824 1824 self.setparents(
1825 1825 self._nodeconstants.nullid, self._nodeconstants.nullid
1826 1826 )
1827 1827 util.clearcachedproperty(self, b"_dirs")
1828 1828 util.clearcachedproperty(self, b"_alldirs")
1829 1829 util.clearcachedproperty(self, b"dirfoldmap")
1830 1830
1831 1831 def items(self):
1832 1832 return self._rustmap.items()
1833 1833
1834 1834 def keys(self):
1835 1835 return iter(self._rustmap)
1836 1836
1837 1837 def __contains__(self, key):
1838 1838 return key in self._rustmap
1839 1839
1840 1840 def __getitem__(self, item):
1841 1841 return self._rustmap[item]
1842 1842
1843 1843 def __len__(self):
1844 1844 return len(self._rustmap)
1845 1845
1846 1846 def __iter__(self):
1847 1847 return iter(self._rustmap)
1848 1848
1849 1849 # forward for python2,3 compat
1850 1850 iteritems = items
1851 1851
1852 1852 def _opendirstatefile(self):
1853 1853 fp, mode = txnutil.trypending(
1854 1854 self._root, self._opener, self._filename
1855 1855 )
1856 1856 if self._pendingmode is not None and self._pendingmode != mode:
1857 1857 fp.close()
1858 1858 raise error.Abort(
1859 1859 _(b'working directory state may be changed parallelly')
1860 1860 )
1861 1861 self._pendingmode = mode
1862 1862 return fp
1863 1863
1864 1864 def setparents(self, p1, p2):
1865 1865 self._parents = (p1, p2)
1866 1866 self._dirtyparents = True
1867 1867
1868 1868 def parents(self):
1869 1869 if not self._parents:
1870 1870 if self._use_dirstate_v2:
1871 1871 offset = len(rustmod.V2_FORMAT_MARKER)
1872 1872 else:
1873 1873 offset = 0
1874 1874 read_len = offset + self._nodelen * 2
1875 1875 try:
1876 1876 fp = self._opendirstatefile()
1877 1877 st = fp.read(read_len)
1878 1878 fp.close()
1879 1879 except IOError as err:
1880 1880 if err.errno != errno.ENOENT:
1881 1881 raise
1882 1882 # File doesn't exist, so the current state is empty
1883 1883 st = b''
1884 1884
1885 1885 l = len(st)
1886 1886 if l == read_len:
1887 1887 st = st[offset:]
1888 1888 self._parents = (
1889 1889 st[: self._nodelen],
1890 1890 st[self._nodelen : 2 * self._nodelen],
1891 1891 )
1892 1892 elif l == 0:
1893 1893 self._parents = (
1894 1894 self._nodeconstants.nullid,
1895 1895 self._nodeconstants.nullid,
1896 1896 )
1897 1897 else:
1898 1898 raise error.Abort(
1899 1899 _(b'working directory state appears damaged!')
1900 1900 )
1901 1901
1902 1902 return self._parents
1903 1903
1904 1904 @propertycache
1905 1905 def _rustmap(self):
1906 1906 """
1907 1907 Fills the Dirstatemap when called.
1908 1908 """
1909 1909 # ignore HG_PENDING because identity is used only for writing
1910 1910 self.identity = util.filestat.frompath(
1911 1911 self._opener.join(self._filename)
1912 1912 )
1913 1913
1914 1914 try:
1915 1915 fp = self._opendirstatefile()
1916 1916 try:
1917 1917 st = fp.read()
1918 1918 finally:
1919 1919 fp.close()
1920 1920 except IOError as err:
1921 1921 if err.errno != errno.ENOENT:
1922 1922 raise
1923 1923 st = b''
1924 1924
1925 1925 self._rustmap, parents = rustmod.DirstateMap.new(
1926 1926 self._use_dirstate_tree, self._use_dirstate_v2, st
1927 1927 )
1928 1928
1929 1929 if parents and not self._dirtyparents:
1930 1930 self.setparents(*parents)
1931 1931
1932 1932 self.__contains__ = self._rustmap.__contains__
1933 1933 self.__getitem__ = self._rustmap.__getitem__
1934 1934 self.get = self._rustmap.get
1935 1935 return self._rustmap
1936 1936
1937 1937 def write(self, st, now):
1938 1938 parents = self.parents()
1939 1939 packed = self._rustmap.write(
1940 1940 self._use_dirstate_v2, parents[0], parents[1], now
1941 1941 )
1942 1942 st.write(packed)
1943 1943 st.close()
1944 1944 self._dirtyparents = False
1945 1945
1946 1946 @propertycache
1947 1947 def filefoldmap(self):
1948 1948 """Returns a dictionary mapping normalized case paths to their
1949 1949 non-normalized versions.
1950 1950 """
1951 1951 return self._rustmap.filefoldmapasdict()
1952 1952
1953 1953 def hastrackeddir(self, d):
1954 self._dirs # Trigger Python's propertycache
1955 1954 return self._rustmap.hastrackeddir(d)
1956 1955
1957 1956 def hasdir(self, d):
1958 self._dirs # Trigger Python's propertycache
1959 1957 return self._rustmap.hasdir(d)
1960 1958
1961 1959 @propertycache
1962 def _dirs(self):
1963 return self._rustmap.getdirs()
1964
1965 @propertycache
1966 def _alldirs(self):
1967 return self._rustmap.getalldirs()
1968
1969 @propertycache
1970 1960 def identity(self):
1971 1961 self._rustmap
1972 1962 return self.identity
1973 1963
1974 1964 @property
1975 1965 def nonnormalset(self):
1976 1966 nonnorm = self._rustmap.non_normal_entries()
1977 1967 return nonnorm
1978 1968
1979 1969 @propertycache
1980 1970 def otherparentset(self):
1981 1971 otherparents = self._rustmap.other_parent_entries()
1982 1972 return otherparents
1983 1973
1984 1974 def non_normal_or_other_parent_paths(self):
1985 1975 return self._rustmap.non_normal_or_other_parent_paths()
1986 1976
1987 1977 @propertycache
1988 1978 def dirfoldmap(self):
1989 1979 f = {}
1990 1980 normcase = util.normcase
1991 for name in self._dirs:
1981 for name, _pseudo_entry in self.directories():
1992 1982 f[normcase(name)] = name
1993 1983 return f
@@ -1,1130 +1,1118
1 1 use bytes_cast::BytesCast;
2 2 use micro_timer::timed;
3 3 use std::borrow::Cow;
4 4 use std::convert::TryInto;
5 5 use std::path::PathBuf;
6 6
7 7 use super::on_disk;
8 8 use super::on_disk::DirstateV2ParseError;
9 9 use super::path_with_basename::WithBasename;
10 10 use crate::dirstate::parsers::pack_entry;
11 11 use crate::dirstate::parsers::packed_entry_size;
12 12 use crate::dirstate::parsers::parse_dirstate_entries;
13 13 use crate::dirstate::parsers::Timestamp;
14 14 use crate::matchers::Matcher;
15 15 use crate::utils::hg_path::{HgPath, HgPathBuf};
16 16 use crate::CopyMapIter;
17 17 use crate::DirstateEntry;
18 18 use crate::DirstateError;
19 19 use crate::DirstateParents;
20 20 use crate::DirstateStatus;
21 21 use crate::EntryState;
22 22 use crate::FastHashMap;
23 23 use crate::PatternFileWarning;
24 24 use crate::StateMapIter;
25 25 use crate::StatusError;
26 26 use crate::StatusOptions;
27 27
28 28 pub struct DirstateMap<'on_disk> {
29 29 /// Contents of the `.hg/dirstate` file
30 30 pub(super) on_disk: &'on_disk [u8],
31 31
32 32 pub(super) root: ChildNodes<'on_disk>,
33 33
34 34 /// Number of nodes anywhere in the tree that have `.entry.is_some()`.
35 35 pub(super) nodes_with_entry_count: u32,
36 36
37 37 /// Number of nodes anywhere in the tree that have
38 38 /// `.copy_source.is_some()`.
39 39 pub(super) nodes_with_copy_source_count: u32,
40 40
41 41 /// See on_disk::Header
42 42 pub(super) ignore_patterns_hash: on_disk::IgnorePatternsHash,
43 43 }
44 44
45 45 /// Using a plain `HgPathBuf` of the full path from the repository root as a
46 46 /// map key would also work: all paths in a given map have the same parent
47 47 /// path, so comparing full paths gives the same result as comparing base
48 48 /// names. However `HashMap` would waste time always re-hashing the same
49 49 /// string prefix.
50 50 pub(super) type NodeKey<'on_disk> = WithBasename<Cow<'on_disk, HgPath>>;
51 51
52 52 /// Similar to `&'tree Cow<'on_disk, HgPath>`, but can also be returned
53 53 /// for on-disk nodes that don’t actually have a `Cow` to borrow.
54 54 pub(super) enum BorrowedPath<'tree, 'on_disk> {
55 55 InMemory(&'tree HgPathBuf),
56 56 OnDisk(&'on_disk HgPath),
57 57 }
58 58
59 59 pub(super) enum ChildNodes<'on_disk> {
60 60 InMemory(FastHashMap<NodeKey<'on_disk>, Node<'on_disk>>),
61 61 OnDisk(&'on_disk [on_disk::Node]),
62 62 }
63 63
64 64 pub(super) enum ChildNodesRef<'tree, 'on_disk> {
65 65 InMemory(&'tree FastHashMap<NodeKey<'on_disk>, Node<'on_disk>>),
66 66 OnDisk(&'on_disk [on_disk::Node]),
67 67 }
68 68
69 69 pub(super) enum NodeRef<'tree, 'on_disk> {
70 70 InMemory(&'tree NodeKey<'on_disk>, &'tree Node<'on_disk>),
71 71 OnDisk(&'on_disk on_disk::Node),
72 72 }
73 73
74 74 impl<'tree, 'on_disk> BorrowedPath<'tree, 'on_disk> {
75 75 pub fn detach_from_tree(&self) -> Cow<'on_disk, HgPath> {
76 76 match *self {
77 77 BorrowedPath::InMemory(in_memory) => Cow::Owned(in_memory.clone()),
78 78 BorrowedPath::OnDisk(on_disk) => Cow::Borrowed(on_disk),
79 79 }
80 80 }
81 81 }
82 82
83 83 impl<'tree, 'on_disk> std::ops::Deref for BorrowedPath<'tree, 'on_disk> {
84 84 type Target = HgPath;
85 85
86 86 fn deref(&self) -> &HgPath {
87 87 match *self {
88 88 BorrowedPath::InMemory(in_memory) => in_memory,
89 89 BorrowedPath::OnDisk(on_disk) => on_disk,
90 90 }
91 91 }
92 92 }
93 93
94 94 impl Default for ChildNodes<'_> {
95 95 fn default() -> Self {
96 96 ChildNodes::InMemory(Default::default())
97 97 }
98 98 }
99 99
100 100 impl<'on_disk> ChildNodes<'on_disk> {
101 101 pub(super) fn as_ref<'tree>(
102 102 &'tree self,
103 103 ) -> ChildNodesRef<'tree, 'on_disk> {
104 104 match self {
105 105 ChildNodes::InMemory(nodes) => ChildNodesRef::InMemory(nodes),
106 106 ChildNodes::OnDisk(nodes) => ChildNodesRef::OnDisk(nodes),
107 107 }
108 108 }
109 109
110 110 pub(super) fn is_empty(&self) -> bool {
111 111 match self {
112 112 ChildNodes::InMemory(nodes) => nodes.is_empty(),
113 113 ChildNodes::OnDisk(nodes) => nodes.is_empty(),
114 114 }
115 115 }
116 116
117 117 pub(super) fn make_mut(
118 118 &mut self,
119 119 on_disk: &'on_disk [u8],
120 120 ) -> Result<
121 121 &mut FastHashMap<NodeKey<'on_disk>, Node<'on_disk>>,
122 122 DirstateV2ParseError,
123 123 > {
124 124 match self {
125 125 ChildNodes::InMemory(nodes) => Ok(nodes),
126 126 ChildNodes::OnDisk(nodes) => {
127 127 let nodes = nodes
128 128 .iter()
129 129 .map(|node| {
130 130 Ok((
131 131 node.path(on_disk)?,
132 132 node.to_in_memory_node(on_disk)?,
133 133 ))
134 134 })
135 135 .collect::<Result<_, _>>()?;
136 136 *self = ChildNodes::InMemory(nodes);
137 137 match self {
138 138 ChildNodes::InMemory(nodes) => Ok(nodes),
139 139 ChildNodes::OnDisk(_) => unreachable!(),
140 140 }
141 141 }
142 142 }
143 143 }
144 144 }
145 145
146 146 impl<'tree, 'on_disk> ChildNodesRef<'tree, 'on_disk> {
147 147 pub(super) fn get(
148 148 &self,
149 149 base_name: &HgPath,
150 150 on_disk: &'on_disk [u8],
151 151 ) -> Result<Option<NodeRef<'tree, 'on_disk>>, DirstateV2ParseError> {
152 152 match self {
153 153 ChildNodesRef::InMemory(nodes) => Ok(nodes
154 154 .get_key_value(base_name)
155 155 .map(|(k, v)| NodeRef::InMemory(k, v))),
156 156 ChildNodesRef::OnDisk(nodes) => {
157 157 let mut parse_result = Ok(());
158 158 let search_result = nodes.binary_search_by(|node| {
159 159 match node.base_name(on_disk) {
160 160 Ok(node_base_name) => node_base_name.cmp(base_name),
161 161 Err(e) => {
162 162 parse_result = Err(e);
163 163 // Dummy comparison result, `search_result` won’t
164 164 // be used since `parse_result` is an error
165 165 std::cmp::Ordering::Equal
166 166 }
167 167 }
168 168 });
169 169 parse_result.map(|()| {
170 170 search_result.ok().map(|i| NodeRef::OnDisk(&nodes[i]))
171 171 })
172 172 }
173 173 }
174 174 }
175 175
176 176 /// Iterate in undefined order
177 177 pub(super) fn iter(
178 178 &self,
179 179 ) -> impl Iterator<Item = NodeRef<'tree, 'on_disk>> {
180 180 match self {
181 181 ChildNodesRef::InMemory(nodes) => itertools::Either::Left(
182 182 nodes.iter().map(|(k, v)| NodeRef::InMemory(k, v)),
183 183 ),
184 184 ChildNodesRef::OnDisk(nodes) => {
185 185 itertools::Either::Right(nodes.iter().map(NodeRef::OnDisk))
186 186 }
187 187 }
188 188 }
189 189
190 190 /// Iterate in parallel in undefined order
191 191 pub(super) fn par_iter(
192 192 &self,
193 193 ) -> impl rayon::iter::ParallelIterator<Item = NodeRef<'tree, 'on_disk>>
194 194 {
195 195 use rayon::prelude::*;
196 196 match self {
197 197 ChildNodesRef::InMemory(nodes) => rayon::iter::Either::Left(
198 198 nodes.par_iter().map(|(k, v)| NodeRef::InMemory(k, v)),
199 199 ),
200 200 ChildNodesRef::OnDisk(nodes) => rayon::iter::Either::Right(
201 201 nodes.par_iter().map(NodeRef::OnDisk),
202 202 ),
203 203 }
204 204 }
205 205
206 206 pub(super) fn sorted(&self) -> Vec<NodeRef<'tree, 'on_disk>> {
207 207 match self {
208 208 ChildNodesRef::InMemory(nodes) => {
209 209 let mut vec: Vec<_> = nodes
210 210 .iter()
211 211 .map(|(k, v)| NodeRef::InMemory(k, v))
212 212 .collect();
213 213 fn sort_key<'a>(node: &'a NodeRef) -> &'a HgPath {
214 214 match node {
215 215 NodeRef::InMemory(path, _node) => path.base_name(),
216 216 NodeRef::OnDisk(_) => unreachable!(),
217 217 }
218 218 }
219 219 // `sort_unstable_by_key` doesn’t allow keys borrowing from the
220 220 // value: https://github.com/rust-lang/rust/issues/34162
221 221 vec.sort_unstable_by(|a, b| sort_key(a).cmp(sort_key(b)));
222 222 vec
223 223 }
224 224 ChildNodesRef::OnDisk(nodes) => {
225 225 // Nodes on disk are already sorted
226 226 nodes.iter().map(NodeRef::OnDisk).collect()
227 227 }
228 228 }
229 229 }
230 230 }
231 231
232 232 impl<'tree, 'on_disk> NodeRef<'tree, 'on_disk> {
233 233 pub(super) fn full_path(
234 234 &self,
235 235 on_disk: &'on_disk [u8],
236 236 ) -> Result<&'tree HgPath, DirstateV2ParseError> {
237 237 match self {
238 238 NodeRef::InMemory(path, _node) => Ok(path.full_path()),
239 239 NodeRef::OnDisk(node) => node.full_path(on_disk),
240 240 }
241 241 }
242 242
243 243 /// Returns a `BorrowedPath`, which can be turned into a `Cow<'on_disk,
244 244 /// HgPath>` detached from `'tree`
245 245 pub(super) fn full_path_borrowed(
246 246 &self,
247 247 on_disk: &'on_disk [u8],
248 248 ) -> Result<BorrowedPath<'tree, 'on_disk>, DirstateV2ParseError> {
249 249 match self {
250 250 NodeRef::InMemory(path, _node) => match path.full_path() {
251 251 Cow::Borrowed(on_disk) => Ok(BorrowedPath::OnDisk(on_disk)),
252 252 Cow::Owned(in_memory) => Ok(BorrowedPath::InMemory(in_memory)),
253 253 },
254 254 NodeRef::OnDisk(node) => {
255 255 Ok(BorrowedPath::OnDisk(node.full_path(on_disk)?))
256 256 }
257 257 }
258 258 }
259 259
260 260 pub(super) fn base_name(
261 261 &self,
262 262 on_disk: &'on_disk [u8],
263 263 ) -> Result<&'tree HgPath, DirstateV2ParseError> {
264 264 match self {
265 265 NodeRef::InMemory(path, _node) => Ok(path.base_name()),
266 266 NodeRef::OnDisk(node) => node.base_name(on_disk),
267 267 }
268 268 }
269 269
270 270 pub(super) fn children(
271 271 &self,
272 272 on_disk: &'on_disk [u8],
273 273 ) -> Result<ChildNodesRef<'tree, 'on_disk>, DirstateV2ParseError> {
274 274 match self {
275 275 NodeRef::InMemory(_path, node) => Ok(node.children.as_ref()),
276 276 NodeRef::OnDisk(node) => {
277 277 Ok(ChildNodesRef::OnDisk(node.children(on_disk)?))
278 278 }
279 279 }
280 280 }
281 281
282 282 pub(super) fn has_copy_source(&self) -> bool {
283 283 match self {
284 284 NodeRef::InMemory(_path, node) => node.copy_source.is_some(),
285 285 NodeRef::OnDisk(node) => node.has_copy_source(),
286 286 }
287 287 }
288 288
289 289 pub(super) fn copy_source(
290 290 &self,
291 291 on_disk: &'on_disk [u8],
292 292 ) -> Result<Option<&'tree HgPath>, DirstateV2ParseError> {
293 293 match self {
294 294 NodeRef::InMemory(_path, node) => {
295 295 Ok(node.copy_source.as_ref().map(|s| &**s))
296 296 }
297 297 NodeRef::OnDisk(node) => node.copy_source(on_disk),
298 298 }
299 299 }
300 300
301 301 pub(super) fn entry(
302 302 &self,
303 303 ) -> Result<Option<DirstateEntry>, DirstateV2ParseError> {
304 304 match self {
305 305 NodeRef::InMemory(_path, node) => {
306 306 Ok(node.data.as_entry().copied())
307 307 }
308 308 NodeRef::OnDisk(node) => node.entry(),
309 309 }
310 310 }
311 311
312 312 pub(super) fn state(
313 313 &self,
314 314 ) -> Result<Option<EntryState>, DirstateV2ParseError> {
315 315 match self {
316 316 NodeRef::InMemory(_path, node) => {
317 317 Ok(node.data.as_entry().map(|entry| entry.state))
318 318 }
319 319 NodeRef::OnDisk(node) => node.state(),
320 320 }
321 321 }
322 322
323 323 pub(super) fn cached_directory_mtime(
324 324 &self,
325 325 ) -> Option<&'tree on_disk::Timestamp> {
326 326 match self {
327 327 NodeRef::InMemory(_path, node) => match &node.data {
328 328 NodeData::CachedDirectory { mtime } => Some(mtime),
329 329 _ => None,
330 330 },
331 331 NodeRef::OnDisk(node) => node.cached_directory_mtime(),
332 332 }
333 333 }
334 334
335 335 pub(super) fn tracked_descendants_count(&self) -> u32 {
336 336 match self {
337 337 NodeRef::InMemory(_path, node) => node.tracked_descendants_count,
338 338 NodeRef::OnDisk(node) => node.tracked_descendants_count.get(),
339 339 }
340 340 }
341 341 }
342 342
343 343 /// Represents a file or a directory
344 344 #[derive(Default)]
345 345 pub(super) struct Node<'on_disk> {
346 346 pub(super) data: NodeData,
347 347
348 348 pub(super) copy_source: Option<Cow<'on_disk, HgPath>>,
349 349
350 350 pub(super) children: ChildNodes<'on_disk>,
351 351
352 352 /// How many (non-inclusive) descendants of this node are tracked files
353 353 pub(super) tracked_descendants_count: u32,
354 354 }
355 355
356 356 pub(super) enum NodeData {
357 357 Entry(DirstateEntry),
358 358 CachedDirectory { mtime: on_disk::Timestamp },
359 359 None,
360 360 }
361 361
362 362 impl Default for NodeData {
363 363 fn default() -> Self {
364 364 NodeData::None
365 365 }
366 366 }
367 367
368 368 impl NodeData {
369 369 fn has_entry(&self) -> bool {
370 370 match self {
371 371 NodeData::Entry(_) => true,
372 372 _ => false,
373 373 }
374 374 }
375 375
376 376 fn as_entry(&self) -> Option<&DirstateEntry> {
377 377 match self {
378 378 NodeData::Entry(entry) => Some(entry),
379 379 _ => None,
380 380 }
381 381 }
382 382 }
383 383
384 384 impl<'on_disk> DirstateMap<'on_disk> {
385 385 pub(super) fn empty(on_disk: &'on_disk [u8]) -> Self {
386 386 Self {
387 387 on_disk,
388 388 root: ChildNodes::default(),
389 389 nodes_with_entry_count: 0,
390 390 nodes_with_copy_source_count: 0,
391 391 ignore_patterns_hash: [0; on_disk::IGNORE_PATTERNS_HASH_LEN],
392 392 }
393 393 }
394 394
395 395 #[timed]
396 396 pub fn new_v2(
397 397 on_disk: &'on_disk [u8],
398 398 ) -> Result<(Self, Option<DirstateParents>), DirstateError> {
399 399 Ok(on_disk::read(on_disk)?)
400 400 }
401 401
402 402 #[timed]
403 403 pub fn new_v1(
404 404 on_disk: &'on_disk [u8],
405 405 ) -> Result<(Self, Option<DirstateParents>), DirstateError> {
406 406 let mut map = Self::empty(on_disk);
407 407 if map.on_disk.is_empty() {
408 408 return Ok((map, None));
409 409 }
410 410
411 411 let parents = parse_dirstate_entries(
412 412 map.on_disk,
413 413 |path, entry, copy_source| {
414 414 let tracked = entry.state.is_tracked();
415 415 let node = Self::get_or_insert_node(
416 416 map.on_disk,
417 417 &mut map.root,
418 418 path,
419 419 WithBasename::to_cow_borrowed,
420 420 |ancestor| {
421 421 if tracked {
422 422 ancestor.tracked_descendants_count += 1
423 423 }
424 424 },
425 425 )?;
426 426 assert!(
427 427 !node.data.has_entry(),
428 428 "duplicate dirstate entry in read"
429 429 );
430 430 assert!(
431 431 node.copy_source.is_none(),
432 432 "duplicate dirstate entry in read"
433 433 );
434 434 node.data = NodeData::Entry(*entry);
435 435 node.copy_source = copy_source.map(Cow::Borrowed);
436 436 map.nodes_with_entry_count += 1;
437 437 if copy_source.is_some() {
438 438 map.nodes_with_copy_source_count += 1
439 439 }
440 440 Ok(())
441 441 },
442 442 )?;
443 443 let parents = Some(parents.clone());
444 444
445 445 Ok((map, parents))
446 446 }
447 447
448 448 fn get_node<'tree>(
449 449 &'tree self,
450 450 path: &HgPath,
451 451 ) -> Result<Option<NodeRef<'tree, 'on_disk>>, DirstateV2ParseError> {
452 452 let mut children = self.root.as_ref();
453 453 let mut components = path.components();
454 454 let mut component =
455 455 components.next().expect("expected at least one components");
456 456 loop {
457 457 if let Some(child) = children.get(component, self.on_disk)? {
458 458 if let Some(next_component) = components.next() {
459 459 component = next_component;
460 460 children = child.children(self.on_disk)?;
461 461 } else {
462 462 return Ok(Some(child));
463 463 }
464 464 } else {
465 465 return Ok(None);
466 466 }
467 467 }
468 468 }
469 469
470 470 /// Returns a mutable reference to the node at `path` if it exists
471 471 ///
472 472 /// This takes `root` instead of `&mut self` so that callers can mutate
473 473 /// other fields while the returned borrow is still valid
474 474 fn get_node_mut<'tree>(
475 475 on_disk: &'on_disk [u8],
476 476 root: &'tree mut ChildNodes<'on_disk>,
477 477 path: &HgPath,
478 478 ) -> Result<Option<&'tree mut Node<'on_disk>>, DirstateV2ParseError> {
479 479 let mut children = root;
480 480 let mut components = path.components();
481 481 let mut component =
482 482 components.next().expect("expected at least one components");
483 483 loop {
484 484 if let Some(child) = children.make_mut(on_disk)?.get_mut(component)
485 485 {
486 486 if let Some(next_component) = components.next() {
487 487 component = next_component;
488 488 children = &mut child.children;
489 489 } else {
490 490 return Ok(Some(child));
491 491 }
492 492 } else {
493 493 return Ok(None);
494 494 }
495 495 }
496 496 }
497 497
498 498 pub(super) fn get_or_insert<'tree, 'path>(
499 499 &'tree mut self,
500 500 path: &HgPath,
501 501 ) -> Result<&'tree mut Node<'on_disk>, DirstateV2ParseError> {
502 502 Self::get_or_insert_node(
503 503 self.on_disk,
504 504 &mut self.root,
505 505 path,
506 506 WithBasename::to_cow_owned,
507 507 |_| {},
508 508 )
509 509 }
510 510
511 511 pub(super) fn get_or_insert_node<'tree, 'path>(
512 512 on_disk: &'on_disk [u8],
513 513 root: &'tree mut ChildNodes<'on_disk>,
514 514 path: &'path HgPath,
515 515 to_cow: impl Fn(
516 516 WithBasename<&'path HgPath>,
517 517 ) -> WithBasename<Cow<'on_disk, HgPath>>,
518 518 mut each_ancestor: impl FnMut(&mut Node),
519 519 ) -> Result<&'tree mut Node<'on_disk>, DirstateV2ParseError> {
520 520 let mut child_nodes = root;
521 521 let mut inclusive_ancestor_paths =
522 522 WithBasename::inclusive_ancestors_of(path);
523 523 let mut ancestor_path = inclusive_ancestor_paths
524 524 .next()
525 525 .expect("expected at least one inclusive ancestor");
526 526 loop {
527 527 // TODO: can we avoid allocating an owned key in cases where the
528 528 // map already contains that key, without introducing double
529 529 // lookup?
530 530 let child_node = child_nodes
531 531 .make_mut(on_disk)?
532 532 .entry(to_cow(ancestor_path))
533 533 .or_default();
534 534 if let Some(next) = inclusive_ancestor_paths.next() {
535 535 each_ancestor(child_node);
536 536 ancestor_path = next;
537 537 child_nodes = &mut child_node.children;
538 538 } else {
539 539 return Ok(child_node);
540 540 }
541 541 }
542 542 }
543 543
544 544 fn add_or_remove_file(
545 545 &mut self,
546 546 path: &HgPath,
547 547 old_state: EntryState,
548 548 new_entry: DirstateEntry,
549 549 ) -> Result<(), DirstateV2ParseError> {
550 550 let tracked_count_increment =
551 551 match (old_state.is_tracked(), new_entry.state.is_tracked()) {
552 552 (false, true) => 1,
553 553 (true, false) => -1,
554 554 _ => 0,
555 555 };
556 556
557 557 let node = Self::get_or_insert_node(
558 558 self.on_disk,
559 559 &mut self.root,
560 560 path,
561 561 WithBasename::to_cow_owned,
562 562 |ancestor| {
563 563 // We can’t use `+= increment` because the counter is unsigned,
564 564 // and we want debug builds to detect accidental underflow
565 565 // through zero
566 566 match tracked_count_increment {
567 567 1 => ancestor.tracked_descendants_count += 1,
568 568 -1 => ancestor.tracked_descendants_count -= 1,
569 569 _ => {}
570 570 }
571 571 },
572 572 )?;
573 573 if !node.data.has_entry() {
574 574 self.nodes_with_entry_count += 1
575 575 }
576 576 node.data = NodeData::Entry(new_entry);
577 577 Ok(())
578 578 }
579 579
580 580 fn iter_nodes<'tree>(
581 581 &'tree self,
582 582 ) -> impl Iterator<
583 583 Item = Result<NodeRef<'tree, 'on_disk>, DirstateV2ParseError>,
584 584 > + 'tree {
585 585 // Depth first tree traversal.
586 586 //
587 587 // If we could afford internal iteration and recursion,
588 588 // this would look like:
589 589 //
590 590 // ```
591 591 // fn traverse_children(
592 592 // children: &ChildNodes,
593 593 // each: &mut impl FnMut(&Node),
594 594 // ) {
595 595 // for child in children.values() {
596 596 // traverse_children(&child.children, each);
597 597 // each(child);
598 598 // }
599 599 // }
600 600 // ```
601 601 //
602 602 // However we want an external iterator and therefore can’t use the
603 603 // call stack. Use an explicit stack instead:
604 604 let mut stack = Vec::new();
605 605 let mut iter = self.root.as_ref().iter();
606 606 std::iter::from_fn(move || {
607 607 while let Some(child_node) = iter.next() {
608 608 let children = match child_node.children(self.on_disk) {
609 609 Ok(children) => children,
610 610 Err(error) => return Some(Err(error)),
611 611 };
612 612 // Pseudo-recursion
613 613 let new_iter = children.iter();
614 614 let old_iter = std::mem::replace(&mut iter, new_iter);
615 615 stack.push((child_node, old_iter));
616 616 }
617 617 // Found the end of a `children.iter()` iterator.
618 618 if let Some((child_node, next_iter)) = stack.pop() {
619 619 // "Return" from pseudo-recursion by restoring state from the
620 620 // explicit stack
621 621 iter = next_iter;
622 622
623 623 Some(Ok(child_node))
624 624 } else {
625 625 // Reached the bottom of the stack, we’re done
626 626 None
627 627 }
628 628 })
629 629 }
630 630
631 631 fn clear_known_ambiguous_mtimes(
632 632 &mut self,
633 633 paths: &[impl AsRef<HgPath>],
634 634 ) -> Result<(), DirstateV2ParseError> {
635 635 for path in paths {
636 636 if let Some(node) = Self::get_node_mut(
637 637 self.on_disk,
638 638 &mut self.root,
639 639 path.as_ref(),
640 640 )? {
641 641 if let NodeData::Entry(entry) = &mut node.data {
642 642 entry.clear_mtime();
643 643 }
644 644 }
645 645 }
646 646 Ok(())
647 647 }
648 648
649 649 /// Return a faillilble iterator of full paths of nodes that have an
650 650 /// `entry` for which the given `predicate` returns true.
651 651 ///
652 652 /// Fallibility means that each iterator item is a `Result`, which may
653 653 /// indicate a parse error of the on-disk dirstate-v2 format. Such errors
654 654 /// should only happen if Mercurial is buggy or a repository is corrupted.
655 655 fn filter_full_paths<'tree>(
656 656 &'tree self,
657 657 predicate: impl Fn(&DirstateEntry) -> bool + 'tree,
658 658 ) -> impl Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + 'tree
659 659 {
660 660 filter_map_results(self.iter_nodes(), move |node| {
661 661 if let Some(entry) = node.entry()? {
662 662 if predicate(&entry) {
663 663 return Ok(Some(node.full_path(self.on_disk)?));
664 664 }
665 665 }
666 666 Ok(None)
667 667 })
668 668 }
669 669 }
670 670
671 671 /// Like `Iterator::filter_map`, but over a fallible iterator of `Result`s.
672 672 ///
673 673 /// The callback is only called for incoming `Ok` values. Errors are passed
674 674 /// through as-is. In order to let it use the `?` operator the callback is
675 675 /// expected to return a `Result` of `Option`, instead of an `Option` of
676 676 /// `Result`.
677 677 fn filter_map_results<'a, I, F, A, B, E>(
678 678 iter: I,
679 679 f: F,
680 680 ) -> impl Iterator<Item = Result<B, E>> + 'a
681 681 where
682 682 I: Iterator<Item = Result<A, E>> + 'a,
683 683 F: Fn(A) -> Result<Option<B>, E> + 'a,
684 684 {
685 685 iter.filter_map(move |result| match result {
686 686 Ok(node) => f(node).transpose(),
687 687 Err(e) => Some(Err(e)),
688 688 })
689 689 }
690 690
691 691 impl<'on_disk> super::dispatch::DirstateMapMethods for DirstateMap<'on_disk> {
692 692 fn clear(&mut self) {
693 693 self.root = Default::default();
694 694 self.nodes_with_entry_count = 0;
695 695 self.nodes_with_copy_source_count = 0;
696 696 }
697 697
698 698 fn add_file(
699 699 &mut self,
700 700 filename: &HgPath,
701 701 old_state: EntryState,
702 702 entry: DirstateEntry,
703 703 ) -> Result<(), DirstateError> {
704 704 Ok(self.add_or_remove_file(filename, old_state, entry)?)
705 705 }
706 706
707 707 fn remove_file(
708 708 &mut self,
709 709 filename: &HgPath,
710 710 old_state: EntryState,
711 711 size: i32,
712 712 ) -> Result<(), DirstateError> {
713 713 let entry = DirstateEntry {
714 714 state: EntryState::Removed,
715 715 mode: 0,
716 716 size,
717 717 mtime: 0,
718 718 };
719 719 Ok(self.add_or_remove_file(filename, old_state, entry)?)
720 720 }
721 721
722 722 fn drop_file(
723 723 &mut self,
724 724 filename: &HgPath,
725 725 old_state: EntryState,
726 726 ) -> Result<bool, DirstateError> {
727 727 struct Dropped {
728 728 was_tracked: bool,
729 729 had_entry: bool,
730 730 had_copy_source: bool,
731 731 }
732 732
733 733 /// If this returns `Ok(Some((dropped, removed)))`, then
734 734 ///
735 735 /// * `dropped` is about the leaf node that was at `filename`
736 736 /// * `removed` is whether this particular level of recursion just
737 737 /// removed a node in `nodes`.
738 738 fn recur<'on_disk>(
739 739 on_disk: &'on_disk [u8],
740 740 nodes: &mut ChildNodes<'on_disk>,
741 741 path: &HgPath,
742 742 ) -> Result<Option<(Dropped, bool)>, DirstateV2ParseError> {
743 743 let (first_path_component, rest_of_path) =
744 744 path.split_first_component();
745 745 let node = if let Some(node) =
746 746 nodes.make_mut(on_disk)?.get_mut(first_path_component)
747 747 {
748 748 node
749 749 } else {
750 750 return Ok(None);
751 751 };
752 752 let dropped;
753 753 if let Some(rest) = rest_of_path {
754 754 if let Some((d, removed)) =
755 755 recur(on_disk, &mut node.children, rest)?
756 756 {
757 757 dropped = d;
758 758 if dropped.was_tracked {
759 759 node.tracked_descendants_count -= 1;
760 760 }
761 761
762 762 // Directory caches must be invalidated when removing a
763 763 // child node
764 764 if removed {
765 765 if let NodeData::CachedDirectory { .. } = &node.data {
766 766 node.data = NodeData::None
767 767 }
768 768 }
769 769 } else {
770 770 return Ok(None);
771 771 }
772 772 } else {
773 773 let had_entry = node.data.has_entry();
774 774 if had_entry {
775 775 node.data = NodeData::None
776 776 }
777 777 dropped = Dropped {
778 778 was_tracked: node
779 779 .data
780 780 .as_entry()
781 781 .map_or(false, |entry| entry.state.is_tracked()),
782 782 had_entry,
783 783 had_copy_source: node.copy_source.take().is_some(),
784 784 };
785 785 }
786 786 // After recursion, for both leaf (rest_of_path is None) nodes and
787 787 // parent nodes, remove a node if it just became empty.
788 788 let remove = !node.data.has_entry()
789 789 && node.copy_source.is_none()
790 790 && node.children.is_empty();
791 791 if remove {
792 792 nodes.make_mut(on_disk)?.remove(first_path_component);
793 793 }
794 794 Ok(Some((dropped, remove)))
795 795 }
796 796
797 797 if let Some((dropped, _removed)) =
798 798 recur(self.on_disk, &mut self.root, filename)?
799 799 {
800 800 if dropped.had_entry {
801 801 self.nodes_with_entry_count -= 1
802 802 }
803 803 if dropped.had_copy_source {
804 804 self.nodes_with_copy_source_count -= 1
805 805 }
806 806 Ok(dropped.had_entry)
807 807 } else {
808 808 debug_assert!(!old_state.is_tracked());
809 809 Ok(false)
810 810 }
811 811 }
812 812
813 813 fn clear_ambiguous_times(
814 814 &mut self,
815 815 filenames: Vec<HgPathBuf>,
816 816 now: i32,
817 817 ) -> Result<(), DirstateV2ParseError> {
818 818 for filename in filenames {
819 819 if let Some(node) =
820 820 Self::get_node_mut(self.on_disk, &mut self.root, &filename)?
821 821 {
822 822 if let NodeData::Entry(entry) = &mut node.data {
823 823 entry.clear_ambiguous_mtime(now);
824 824 }
825 825 }
826 826 }
827 827 Ok(())
828 828 }
829 829
830 830 fn non_normal_entries_contains(
831 831 &mut self,
832 832 key: &HgPath,
833 833 ) -> Result<bool, DirstateV2ParseError> {
834 834 Ok(if let Some(node) = self.get_node(key)? {
835 835 node.entry()?.map_or(false, |entry| entry.is_non_normal())
836 836 } else {
837 837 false
838 838 })
839 839 }
840 840
841 841 fn non_normal_entries_remove(&mut self, _key: &HgPath) {
842 842 // Do nothing, this `DirstateMap` does not have a separate "non normal
843 843 // entries" set that need to be kept up to date
844 844 }
845 845
846 846 fn non_normal_or_other_parent_paths(
847 847 &mut self,
848 848 ) -> Box<dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + '_>
849 849 {
850 850 Box::new(self.filter_full_paths(|entry| {
851 851 entry.is_non_normal() || entry.is_from_other_parent()
852 852 }))
853 853 }
854 854
855 855 fn set_non_normal_other_parent_entries(&mut self, _force: bool) {
856 856 // Do nothing, this `DirstateMap` does not have a separate "non normal
857 857 // entries" and "from other parent" sets that need to be recomputed
858 858 }
859 859
860 860 fn iter_non_normal_paths(
861 861 &mut self,
862 862 ) -> Box<
863 863 dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + Send + '_,
864 864 > {
865 865 self.iter_non_normal_paths_panic()
866 866 }
867 867
868 868 fn iter_non_normal_paths_panic(
869 869 &self,
870 870 ) -> Box<
871 871 dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + Send + '_,
872 872 > {
873 873 Box::new(self.filter_full_paths(|entry| entry.is_non_normal()))
874 874 }
875 875
876 876 fn iter_other_parent_paths(
877 877 &mut self,
878 878 ) -> Box<
879 879 dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + Send + '_,
880 880 > {
881 881 Box::new(self.filter_full_paths(|entry| entry.is_from_other_parent()))
882 882 }
883 883
884 884 fn has_tracked_dir(
885 885 &mut self,
886 886 directory: &HgPath,
887 887 ) -> Result<bool, DirstateError> {
888 888 if let Some(node) = self.get_node(directory)? {
889 889 // A node without a `DirstateEntry` was created to hold child
890 890 // nodes, and is therefore a directory.
891 891 let state = node.state()?;
892 892 Ok(state.is_none() && node.tracked_descendants_count() > 0)
893 893 } else {
894 894 Ok(false)
895 895 }
896 896 }
897 897
898 898 fn has_dir(&mut self, directory: &HgPath) -> Result<bool, DirstateError> {
899 899 if let Some(node) = self.get_node(directory)? {
900 900 // A node without a `DirstateEntry` was created to hold child
901 901 // nodes, and is therefore a directory.
902 902 Ok(node.state()?.is_none())
903 903 } else {
904 904 Ok(false)
905 905 }
906 906 }
907 907
908 908 #[timed]
909 909 fn pack_v1(
910 910 &mut self,
911 911 parents: DirstateParents,
912 912 now: Timestamp,
913 913 ) -> Result<Vec<u8>, DirstateError> {
914 914 let now: i32 = now.0.try_into().expect("time overflow");
915 915 let mut ambiguous_mtimes = Vec::new();
916 916 // Optizimation (to be measured?): pre-compute size to avoid `Vec`
917 917 // reallocations
918 918 let mut size = parents.as_bytes().len();
919 919 for node in self.iter_nodes() {
920 920 let node = node?;
921 921 if let Some(entry) = node.entry()? {
922 922 size += packed_entry_size(
923 923 node.full_path(self.on_disk)?,
924 924 node.copy_source(self.on_disk)?,
925 925 );
926 926 if entry.mtime_is_ambiguous(now) {
927 927 ambiguous_mtimes.push(
928 928 node.full_path_borrowed(self.on_disk)?
929 929 .detach_from_tree(),
930 930 )
931 931 }
932 932 }
933 933 }
934 934 self.clear_known_ambiguous_mtimes(&ambiguous_mtimes)?;
935 935
936 936 let mut packed = Vec::with_capacity(size);
937 937 packed.extend(parents.as_bytes());
938 938
939 939 for node in self.iter_nodes() {
940 940 let node = node?;
941 941 if let Some(entry) = node.entry()? {
942 942 pack_entry(
943 943 node.full_path(self.on_disk)?,
944 944 &entry,
945 945 node.copy_source(self.on_disk)?,
946 946 &mut packed,
947 947 );
948 948 }
949 949 }
950 950 Ok(packed)
951 951 }
952 952
953 953 #[timed]
954 954 fn pack_v2(
955 955 &mut self,
956 956 parents: DirstateParents,
957 957 now: Timestamp,
958 958 ) -> Result<Vec<u8>, DirstateError> {
959 959 // TODO: how do we want to handle this in 2038?
960 960 let now: i32 = now.0.try_into().expect("time overflow");
961 961 let mut paths = Vec::new();
962 962 for node in self.iter_nodes() {
963 963 let node = node?;
964 964 if let Some(entry) = node.entry()? {
965 965 if entry.mtime_is_ambiguous(now) {
966 966 paths.push(
967 967 node.full_path_borrowed(self.on_disk)?
968 968 .detach_from_tree(),
969 969 )
970 970 }
971 971 }
972 972 }
973 973 // Borrow of `self` ends here since we collect cloned paths
974 974
975 975 self.clear_known_ambiguous_mtimes(&paths)?;
976 976
977 977 on_disk::write(self, parents)
978 978 }
979 979
980 fn set_all_dirs(&mut self) -> Result<(), DirstateError> {
981 // Do nothing, this `DirstateMap` does not a separate `all_dirs` that
982 // needs to be recomputed
983 Ok(())
984 }
985
986 fn set_dirs(&mut self) -> Result<(), DirstateError> {
987 // Do nothing, this `DirstateMap` does not a separate `dirs` that needs
988 // to be recomputed
989 Ok(())
990 }
991
992 980 fn status<'a>(
993 981 &'a mut self,
994 982 matcher: &'a (dyn Matcher + Sync),
995 983 root_dir: PathBuf,
996 984 ignore_files: Vec<PathBuf>,
997 985 options: StatusOptions,
998 986 ) -> Result<(DirstateStatus<'a>, Vec<PatternFileWarning>), StatusError>
999 987 {
1000 988 super::status::status(self, matcher, root_dir, ignore_files, options)
1001 989 }
1002 990
1003 991 fn copy_map_len(&self) -> usize {
1004 992 self.nodes_with_copy_source_count as usize
1005 993 }
1006 994
1007 995 fn copy_map_iter(&self) -> CopyMapIter<'_> {
1008 996 Box::new(filter_map_results(self.iter_nodes(), move |node| {
1009 997 Ok(if let Some(source) = node.copy_source(self.on_disk)? {
1010 998 Some((node.full_path(self.on_disk)?, source))
1011 999 } else {
1012 1000 None
1013 1001 })
1014 1002 }))
1015 1003 }
1016 1004
1017 1005 fn copy_map_contains_key(
1018 1006 &self,
1019 1007 key: &HgPath,
1020 1008 ) -> Result<bool, DirstateV2ParseError> {
1021 1009 Ok(if let Some(node) = self.get_node(key)? {
1022 1010 node.has_copy_source()
1023 1011 } else {
1024 1012 false
1025 1013 })
1026 1014 }
1027 1015
1028 1016 fn copy_map_get(
1029 1017 &self,
1030 1018 key: &HgPath,
1031 1019 ) -> Result<Option<&HgPath>, DirstateV2ParseError> {
1032 1020 if let Some(node) = self.get_node(key)? {
1033 1021 if let Some(source) = node.copy_source(self.on_disk)? {
1034 1022 return Ok(Some(source));
1035 1023 }
1036 1024 }
1037 1025 Ok(None)
1038 1026 }
1039 1027
1040 1028 fn copy_map_remove(
1041 1029 &mut self,
1042 1030 key: &HgPath,
1043 1031 ) -> Result<Option<HgPathBuf>, DirstateV2ParseError> {
1044 1032 let count = &mut self.nodes_with_copy_source_count;
1045 1033 Ok(
1046 1034 Self::get_node_mut(self.on_disk, &mut self.root, key)?.and_then(
1047 1035 |node| {
1048 1036 if node.copy_source.is_some() {
1049 1037 *count -= 1
1050 1038 }
1051 1039 node.copy_source.take().map(Cow::into_owned)
1052 1040 },
1053 1041 ),
1054 1042 )
1055 1043 }
1056 1044
1057 1045 fn copy_map_insert(
1058 1046 &mut self,
1059 1047 key: HgPathBuf,
1060 1048 value: HgPathBuf,
1061 1049 ) -> Result<Option<HgPathBuf>, DirstateV2ParseError> {
1062 1050 let node = Self::get_or_insert_node(
1063 1051 self.on_disk,
1064 1052 &mut self.root,
1065 1053 &key,
1066 1054 WithBasename::to_cow_owned,
1067 1055 |_ancestor| {},
1068 1056 )?;
1069 1057 if node.copy_source.is_none() {
1070 1058 self.nodes_with_copy_source_count += 1
1071 1059 }
1072 1060 Ok(node.copy_source.replace(value.into()).map(Cow::into_owned))
1073 1061 }
1074 1062
1075 1063 fn len(&self) -> usize {
1076 1064 self.nodes_with_entry_count as usize
1077 1065 }
1078 1066
1079 1067 fn contains_key(
1080 1068 &self,
1081 1069 key: &HgPath,
1082 1070 ) -> Result<bool, DirstateV2ParseError> {
1083 1071 Ok(self.get(key)?.is_some())
1084 1072 }
1085 1073
1086 1074 fn get(
1087 1075 &self,
1088 1076 key: &HgPath,
1089 1077 ) -> Result<Option<DirstateEntry>, DirstateV2ParseError> {
1090 1078 Ok(if let Some(node) = self.get_node(key)? {
1091 1079 node.entry()?
1092 1080 } else {
1093 1081 None
1094 1082 })
1095 1083 }
1096 1084
1097 1085 fn iter(&self) -> StateMapIter<'_> {
1098 1086 Box::new(filter_map_results(self.iter_nodes(), move |node| {
1099 1087 Ok(if let Some(entry) = node.entry()? {
1100 1088 Some((node.full_path(self.on_disk)?, entry))
1101 1089 } else {
1102 1090 None
1103 1091 })
1104 1092 }))
1105 1093 }
1106 1094
1107 1095 fn iter_directories(
1108 1096 &self,
1109 1097 ) -> Box<
1110 1098 dyn Iterator<
1111 1099 Item = Result<
1112 1100 (&HgPath, Option<Timestamp>),
1113 1101 DirstateV2ParseError,
1114 1102 >,
1115 1103 > + Send
1116 1104 + '_,
1117 1105 > {
1118 1106 Box::new(filter_map_results(self.iter_nodes(), move |node| {
1119 1107 Ok(if node.state()?.is_none() {
1120 1108 Some((
1121 1109 node.full_path(self.on_disk)?,
1122 1110 node.cached_directory_mtime()
1123 1111 .map(|mtime| Timestamp(mtime.seconds())),
1124 1112 ))
1125 1113 } else {
1126 1114 None
1127 1115 })
1128 1116 }))
1129 1117 }
1130 1118 }
@@ -1,379 +1,367
1 1 use std::path::PathBuf;
2 2
3 3 use crate::dirstate::parsers::Timestamp;
4 4 use crate::dirstate_tree::on_disk::DirstateV2ParseError;
5 5 use crate::matchers::Matcher;
6 6 use crate::utils::hg_path::{HgPath, HgPathBuf};
7 7 use crate::CopyMapIter;
8 8 use crate::DirstateEntry;
9 9 use crate::DirstateError;
10 10 use crate::DirstateMap;
11 11 use crate::DirstateParents;
12 12 use crate::DirstateStatus;
13 13 use crate::EntryState;
14 14 use crate::PatternFileWarning;
15 15 use crate::StateMapIter;
16 16 use crate::StatusError;
17 17 use crate::StatusOptions;
18 18
19 19 pub trait DirstateMapMethods {
20 20 fn clear(&mut self);
21 21
22 22 fn add_file(
23 23 &mut self,
24 24 filename: &HgPath,
25 25 old_state: EntryState,
26 26 entry: DirstateEntry,
27 27 ) -> Result<(), DirstateError>;
28 28
29 29 fn remove_file(
30 30 &mut self,
31 31 filename: &HgPath,
32 32 old_state: EntryState,
33 33 size: i32,
34 34 ) -> Result<(), DirstateError>;
35 35
36 36 fn drop_file(
37 37 &mut self,
38 38 filename: &HgPath,
39 39 old_state: EntryState,
40 40 ) -> Result<bool, DirstateError>;
41 41
42 42 fn clear_ambiguous_times(
43 43 &mut self,
44 44 filenames: Vec<HgPathBuf>,
45 45 now: i32,
46 46 ) -> Result<(), DirstateV2ParseError>;
47 47
48 48 fn non_normal_entries_contains(
49 49 &mut self,
50 50 key: &HgPath,
51 51 ) -> Result<bool, DirstateV2ParseError>;
52 52
53 53 fn non_normal_entries_remove(&mut self, key: &HgPath);
54 54
55 55 fn non_normal_or_other_parent_paths(
56 56 &mut self,
57 57 ) -> Box<dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + '_>;
58 58
59 59 fn set_non_normal_other_parent_entries(&mut self, force: bool);
60 60
61 61 fn iter_non_normal_paths(
62 62 &mut self,
63 63 ) -> Box<
64 64 dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + Send + '_,
65 65 >;
66 66
67 67 fn iter_non_normal_paths_panic(
68 68 &self,
69 69 ) -> Box<
70 70 dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + Send + '_,
71 71 >;
72 72
73 73 fn iter_other_parent_paths(
74 74 &mut self,
75 75 ) -> Box<
76 76 dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + Send + '_,
77 77 >;
78 78
79 79 fn has_tracked_dir(
80 80 &mut self,
81 81 directory: &HgPath,
82 82 ) -> Result<bool, DirstateError>;
83 83
84 84 fn has_dir(&mut self, directory: &HgPath) -> Result<bool, DirstateError>;
85 85
86 86 fn pack_v1(
87 87 &mut self,
88 88 parents: DirstateParents,
89 89 now: Timestamp,
90 90 ) -> Result<Vec<u8>, DirstateError>;
91 91
92 92 fn pack_v2(
93 93 &mut self,
94 94 parents: DirstateParents,
95 95 now: Timestamp,
96 96 ) -> Result<Vec<u8>, DirstateError>;
97 97
98 fn set_all_dirs(&mut self) -> Result<(), DirstateError>;
99
100 fn set_dirs(&mut self) -> Result<(), DirstateError>;
101
102 98 fn status<'a>(
103 99 &'a mut self,
104 100 matcher: &'a (dyn Matcher + Sync),
105 101 root_dir: PathBuf,
106 102 ignore_files: Vec<PathBuf>,
107 103 options: StatusOptions,
108 104 ) -> Result<(DirstateStatus<'a>, Vec<PatternFileWarning>), StatusError>;
109 105
110 106 fn copy_map_len(&self) -> usize;
111 107
112 108 fn copy_map_iter(&self) -> CopyMapIter<'_>;
113 109
114 110 fn copy_map_contains_key(
115 111 &self,
116 112 key: &HgPath,
117 113 ) -> Result<bool, DirstateV2ParseError>;
118 114
119 115 fn copy_map_get(
120 116 &self,
121 117 key: &HgPath,
122 118 ) -> Result<Option<&HgPath>, DirstateV2ParseError>;
123 119
124 120 fn copy_map_remove(
125 121 &mut self,
126 122 key: &HgPath,
127 123 ) -> Result<Option<HgPathBuf>, DirstateV2ParseError>;
128 124
129 125 fn copy_map_insert(
130 126 &mut self,
131 127 key: HgPathBuf,
132 128 value: HgPathBuf,
133 129 ) -> Result<Option<HgPathBuf>, DirstateV2ParseError>;
134 130
135 131 fn len(&self) -> usize;
136 132
137 133 fn contains_key(&self, key: &HgPath)
138 134 -> Result<bool, DirstateV2ParseError>;
139 135
140 136 fn get(
141 137 &self,
142 138 key: &HgPath,
143 139 ) -> Result<Option<DirstateEntry>, DirstateV2ParseError>;
144 140
145 141 fn iter(&self) -> StateMapIter<'_>;
146 142
147 143 fn iter_directories(
148 144 &self,
149 145 ) -> Box<
150 146 dyn Iterator<
151 147 Item = Result<
152 148 (&HgPath, Option<Timestamp>),
153 149 DirstateV2ParseError,
154 150 >,
155 151 > + Send
156 152 + '_,
157 153 >;
158 154 }
159 155
160 156 impl DirstateMapMethods for DirstateMap {
161 157 fn clear(&mut self) {
162 158 self.clear()
163 159 }
164 160
165 161 fn add_file(
166 162 &mut self,
167 163 filename: &HgPath,
168 164 old_state: EntryState,
169 165 entry: DirstateEntry,
170 166 ) -> Result<(), DirstateError> {
171 167 self.add_file(filename, old_state, entry)
172 168 }
173 169
174 170 fn remove_file(
175 171 &mut self,
176 172 filename: &HgPath,
177 173 old_state: EntryState,
178 174 size: i32,
179 175 ) -> Result<(), DirstateError> {
180 176 self.remove_file(filename, old_state, size)
181 177 }
182 178
183 179 fn drop_file(
184 180 &mut self,
185 181 filename: &HgPath,
186 182 old_state: EntryState,
187 183 ) -> Result<bool, DirstateError> {
188 184 self.drop_file(filename, old_state)
189 185 }
190 186
191 187 fn clear_ambiguous_times(
192 188 &mut self,
193 189 filenames: Vec<HgPathBuf>,
194 190 now: i32,
195 191 ) -> Result<(), DirstateV2ParseError> {
196 192 Ok(self.clear_ambiguous_times(filenames, now))
197 193 }
198 194
199 195 fn non_normal_entries_contains(
200 196 &mut self,
201 197 key: &HgPath,
202 198 ) -> Result<bool, DirstateV2ParseError> {
203 199 let (non_normal, _other_parent) =
204 200 self.get_non_normal_other_parent_entries();
205 201 Ok(non_normal.contains(key))
206 202 }
207 203
208 204 fn non_normal_entries_remove(&mut self, key: &HgPath) {
209 205 self.non_normal_entries_remove(key)
210 206 }
211 207
212 208 fn non_normal_or_other_parent_paths(
213 209 &mut self,
214 210 ) -> Box<dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + '_>
215 211 {
216 212 let (non_normal, other_parent) =
217 213 self.get_non_normal_other_parent_entries();
218 214 Box::new(non_normal.union(other_parent).map(|p| Ok(&**p)))
219 215 }
220 216
221 217 fn set_non_normal_other_parent_entries(&mut self, force: bool) {
222 218 self.set_non_normal_other_parent_entries(force)
223 219 }
224 220
225 221 fn iter_non_normal_paths(
226 222 &mut self,
227 223 ) -> Box<
228 224 dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + Send + '_,
229 225 > {
230 226 let (non_normal, _other_parent) =
231 227 self.get_non_normal_other_parent_entries();
232 228 Box::new(non_normal.iter().map(|p| Ok(&**p)))
233 229 }
234 230
235 231 fn iter_non_normal_paths_panic(
236 232 &self,
237 233 ) -> Box<
238 234 dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + Send + '_,
239 235 > {
240 236 let (non_normal, _other_parent) =
241 237 self.get_non_normal_other_parent_entries_panic();
242 238 Box::new(non_normal.iter().map(|p| Ok(&**p)))
243 239 }
244 240
245 241 fn iter_other_parent_paths(
246 242 &mut self,
247 243 ) -> Box<
248 244 dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + Send + '_,
249 245 > {
250 246 let (_non_normal, other_parent) =
251 247 self.get_non_normal_other_parent_entries();
252 248 Box::new(other_parent.iter().map(|p| Ok(&**p)))
253 249 }
254 250
255 251 fn has_tracked_dir(
256 252 &mut self,
257 253 directory: &HgPath,
258 254 ) -> Result<bool, DirstateError> {
259 255 self.has_tracked_dir(directory)
260 256 }
261 257
262 258 fn has_dir(&mut self, directory: &HgPath) -> Result<bool, DirstateError> {
263 259 self.has_dir(directory)
264 260 }
265 261
266 262 fn pack_v1(
267 263 &mut self,
268 264 parents: DirstateParents,
269 265 now: Timestamp,
270 266 ) -> Result<Vec<u8>, DirstateError> {
271 267 self.pack(parents, now)
272 268 }
273 269
274 270 fn pack_v2(
275 271 &mut self,
276 272 _parents: DirstateParents,
277 273 _now: Timestamp,
278 274 ) -> Result<Vec<u8>, DirstateError> {
279 275 panic!(
280 276 "should have used dirstate_tree::DirstateMap to use the v2 format"
281 277 )
282 278 }
283 279
284 fn set_all_dirs(&mut self) -> Result<(), DirstateError> {
285 self.set_all_dirs()
286 }
287
288 fn set_dirs(&mut self) -> Result<(), DirstateError> {
289 self.set_dirs()
290 }
291
292 280 fn status<'a>(
293 281 &'a mut self,
294 282 matcher: &'a (dyn Matcher + Sync),
295 283 root_dir: PathBuf,
296 284 ignore_files: Vec<PathBuf>,
297 285 options: StatusOptions,
298 286 ) -> Result<(DirstateStatus<'a>, Vec<PatternFileWarning>), StatusError>
299 287 {
300 288 crate::status(self, matcher, root_dir, ignore_files, options)
301 289 }
302 290
303 291 fn copy_map_len(&self) -> usize {
304 292 self.copy_map.len()
305 293 }
306 294
307 295 fn copy_map_iter(&self) -> CopyMapIter<'_> {
308 296 Box::new(
309 297 self.copy_map
310 298 .iter()
311 299 .map(|(key, value)| Ok((&**key, &**value))),
312 300 )
313 301 }
314 302
315 303 fn copy_map_contains_key(
316 304 &self,
317 305 key: &HgPath,
318 306 ) -> Result<bool, DirstateV2ParseError> {
319 307 Ok(self.copy_map.contains_key(key))
320 308 }
321 309
322 310 fn copy_map_get(
323 311 &self,
324 312 key: &HgPath,
325 313 ) -> Result<Option<&HgPath>, DirstateV2ParseError> {
326 314 Ok(self.copy_map.get(key).map(|p| &**p))
327 315 }
328 316
329 317 fn copy_map_remove(
330 318 &mut self,
331 319 key: &HgPath,
332 320 ) -> Result<Option<HgPathBuf>, DirstateV2ParseError> {
333 321 Ok(self.copy_map.remove(key))
334 322 }
335 323
336 324 fn copy_map_insert(
337 325 &mut self,
338 326 key: HgPathBuf,
339 327 value: HgPathBuf,
340 328 ) -> Result<Option<HgPathBuf>, DirstateV2ParseError> {
341 329 Ok(self.copy_map.insert(key, value))
342 330 }
343 331
344 332 fn len(&self) -> usize {
345 333 (&**self).len()
346 334 }
347 335
348 336 fn contains_key(
349 337 &self,
350 338 key: &HgPath,
351 339 ) -> Result<bool, DirstateV2ParseError> {
352 340 Ok((&**self).contains_key(key))
353 341 }
354 342
355 343 fn get(
356 344 &self,
357 345 key: &HgPath,
358 346 ) -> Result<Option<DirstateEntry>, DirstateV2ParseError> {
359 347 Ok((&**self).get(key).cloned())
360 348 }
361 349
362 350 fn iter(&self) -> StateMapIter<'_> {
363 351 Box::new((&**self).iter().map(|(key, value)| Ok((&**key, *value))))
364 352 }
365 353
366 354 fn iter_directories(
367 355 &self,
368 356 ) -> Box<
369 357 dyn Iterator<
370 358 Item = Result<
371 359 (&HgPath, Option<Timestamp>),
372 360 DirstateV2ParseError,
373 361 >,
374 362 > + Send
375 363 + '_,
376 364 > {
377 365 Box::new(std::iter::empty())
378 366 }
379 367 }
@@ -1,602 +1,568
1 1 // dirstate_map.rs
2 2 //
3 3 // Copyright 2019 Raphaël Gomès <rgomes@octobus.net>
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 the `hg::dirstate::dirstate_map` file provided by the
9 9 //! `hg-core` package.
10 10
11 11 use std::cell::{RefCell, RefMut};
12 12 use std::convert::TryInto;
13 13
14 14 use cpython::{
15 15 exc, ObjectProtocol, PyBool, PyBytes, PyClone, PyDict, PyErr, PyList,
16 16 PyObject, PyResult, PySet, PyString, Python, PythonObject, ToPyObject,
17 17 UnsafePyLeaked,
18 18 };
19 19
20 20 use crate::{
21 21 dirstate::copymap::{CopyMap, CopyMapItemsIterator, CopyMapKeysIterator},
22 dirstate::make_dirstate_tuple,
22 23 dirstate::non_normal_entries::{
23 24 NonNormalEntries, NonNormalEntriesIterator,
24 25 },
25 26 dirstate::owning::OwningDirstateMap,
26 dirstate::{dirs_multiset::Dirs, make_dirstate_tuple},
27 27 parsers::dirstate_parents_to_pytuple,
28 28 };
29 29 use hg::{
30 30 dirstate::parsers::Timestamp,
31 31 dirstate_tree::dispatch::DirstateMapMethods,
32 32 dirstate_tree::on_disk::DirstateV2ParseError,
33 33 errors::HgError,
34 34 revlog::Node,
35 35 utils::files::normalize_case,
36 36 utils::hg_path::{HgPath, HgPathBuf},
37 DirsMultiset, DirstateEntry, DirstateError,
38 DirstateMap as RustDirstateMap, DirstateParents, EntryState, StateMapIter,
37 DirstateEntry, DirstateError, DirstateMap as RustDirstateMap,
38 DirstateParents, EntryState, StateMapIter,
39 39 };
40 40
41 41 // TODO
42 42 // This object needs to share references to multiple members of its Rust
43 43 // inner struct, namely `copy_map`, `dirs` and `all_dirs`.
44 44 // Right now `CopyMap` is done, but it needs to have an explicit reference
45 45 // to `RustDirstateMap` which itself needs to have an encapsulation for
46 46 // every method in `CopyMap` (copymapcopy, etc.).
47 47 // This is ugly and hard to maintain.
48 48 // The same logic applies to `dirs` and `all_dirs`, however the `Dirs`
49 49 // `py_class!` is already implemented and does not mention
50 50 // `RustDirstateMap`, rightfully so.
51 51 // All attributes also have to have a separate refcount data attribute for
52 52 // leaks, with all methods that go along for reference sharing.
53 53 py_class!(pub class DirstateMap |py| {
54 54 @shared data inner: Box<dyn DirstateMapMethods + Send>;
55 55
56 56 /// Returns a `(dirstate_map, parents)` tuple
57 57 @staticmethod
58 58 def new(
59 59 use_dirstate_tree: bool,
60 60 use_dirstate_v2: bool,
61 61 on_disk: PyBytes,
62 62 ) -> PyResult<PyObject> {
63 63 let dirstate_error = |e: DirstateError| {
64 64 PyErr::new::<exc::OSError, _>(py, format!("Dirstate error: {:?}", e))
65 65 };
66 66 let (inner, parents) = if use_dirstate_tree || use_dirstate_v2 {
67 67 let (map, parents) =
68 68 OwningDirstateMap::new(py, on_disk, use_dirstate_v2)
69 69 .map_err(dirstate_error)?;
70 70 (Box::new(map) as _, parents)
71 71 } else {
72 72 let bytes = on_disk.data(py);
73 73 let mut map = RustDirstateMap::default();
74 74 let parents = map.read(bytes).map_err(dirstate_error)?;
75 75 (Box::new(map) as _, parents)
76 76 };
77 77 let map = Self::create_instance(py, inner)?;
78 78 let parents = parents.map(|p| dirstate_parents_to_pytuple(py, &p));
79 79 Ok((map, parents).to_py_object(py).into_object())
80 80 }
81 81
82 82 def clear(&self) -> PyResult<PyObject> {
83 83 self.inner(py).borrow_mut().clear();
84 84 Ok(py.None())
85 85 }
86 86
87 87 def get(
88 88 &self,
89 89 key: PyObject,
90 90 default: Option<PyObject> = None
91 91 ) -> PyResult<Option<PyObject>> {
92 92 let key = key.extract::<PyBytes>(py)?;
93 93 match self
94 94 .inner(py)
95 95 .borrow()
96 96 .get(HgPath::new(key.data(py)))
97 97 .map_err(|e| v2_error(py, e))?
98 98 {
99 99 Some(entry) => {
100 100 Ok(Some(make_dirstate_tuple(py, &entry)?))
101 101 },
102 102 None => Ok(default)
103 103 }
104 104 }
105 105
106 106 def addfile(
107 107 &self,
108 108 f: PyObject,
109 109 oldstate: PyObject,
110 110 state: PyObject,
111 111 mode: PyObject,
112 112 size: PyObject,
113 113 mtime: PyObject
114 114 ) -> PyResult<PyObject> {
115 115 self.inner(py).borrow_mut().add_file(
116 116 HgPath::new(f.extract::<PyBytes>(py)?.data(py)),
117 117 oldstate.extract::<PyBytes>(py)?.data(py)[0]
118 118 .try_into()
119 119 .map_err(|e: HgError| {
120 120 PyErr::new::<exc::ValueError, _>(py, e.to_string())
121 121 })?,
122 122 DirstateEntry {
123 123 state: state.extract::<PyBytes>(py)?.data(py)[0]
124 124 .try_into()
125 125 .map_err(|e: HgError| {
126 126 PyErr::new::<exc::ValueError, _>(py, e.to_string())
127 127 })?,
128 128 mode: mode.extract(py)?,
129 129 size: size.extract(py)?,
130 130 mtime: mtime.extract(py)?,
131 131 },
132 132 ).and(Ok(py.None())).or_else(|e: DirstateError| {
133 133 Err(PyErr::new::<exc::ValueError, _>(py, e.to_string()))
134 134 })
135 135 }
136 136
137 137 def removefile(
138 138 &self,
139 139 f: PyObject,
140 140 oldstate: PyObject,
141 141 size: PyObject
142 142 ) -> PyResult<PyObject> {
143 143 self.inner(py).borrow_mut()
144 144 .remove_file(
145 145 HgPath::new(f.extract::<PyBytes>(py)?.data(py)),
146 146 oldstate.extract::<PyBytes>(py)?.data(py)[0]
147 147 .try_into()
148 148 .map_err(|e: HgError| {
149 149 PyErr::new::<exc::ValueError, _>(py, e.to_string())
150 150 })?,
151 151 size.extract(py)?,
152 152 )
153 153 .or_else(|_| {
154 154 Err(PyErr::new::<exc::OSError, _>(
155 155 py,
156 156 "Dirstate error".to_string(),
157 157 ))
158 158 })?;
159 159 Ok(py.None())
160 160 }
161 161
162 162 def dropfile(
163 163 &self,
164 164 f: PyObject,
165 165 oldstate: PyObject
166 166 ) -> PyResult<PyBool> {
167 167 self.inner(py).borrow_mut()
168 168 .drop_file(
169 169 HgPath::new(f.extract::<PyBytes>(py)?.data(py)),
170 170 oldstate.extract::<PyBytes>(py)?.data(py)[0]
171 171 .try_into()
172 172 .map_err(|e: HgError| {
173 173 PyErr::new::<exc::ValueError, _>(py, e.to_string())
174 174 })?,
175 175 )
176 176 .and_then(|b| Ok(b.to_py_object(py)))
177 177 .or_else(|e| {
178 178 Err(PyErr::new::<exc::OSError, _>(
179 179 py,
180 180 format!("Dirstate error: {}", e.to_string()),
181 181 ))
182 182 })
183 183 }
184 184
185 185 def clearambiguoustimes(
186 186 &self,
187 187 files: PyObject,
188 188 now: PyObject
189 189 ) -> PyResult<PyObject> {
190 190 let files: PyResult<Vec<HgPathBuf>> = files
191 191 .iter(py)?
192 192 .map(|filename| {
193 193 Ok(HgPathBuf::from_bytes(
194 194 filename?.extract::<PyBytes>(py)?.data(py),
195 195 ))
196 196 })
197 197 .collect();
198 198 self.inner(py)
199 199 .borrow_mut()
200 200 .clear_ambiguous_times(files?, now.extract(py)?)
201 201 .map_err(|e| v2_error(py, e))?;
202 202 Ok(py.None())
203 203 }
204 204
205 205 def other_parent_entries(&self) -> PyResult<PyObject> {
206 206 let mut inner_shared = self.inner(py).borrow_mut();
207 207 let set = PySet::empty(py)?;
208 208 for path in inner_shared.iter_other_parent_paths() {
209 209 let path = path.map_err(|e| v2_error(py, e))?;
210 210 set.add(py, PyBytes::new(py, path.as_bytes()))?;
211 211 }
212 212 Ok(set.into_object())
213 213 }
214 214
215 215 def non_normal_entries(&self) -> PyResult<NonNormalEntries> {
216 216 NonNormalEntries::from_inner(py, self.clone_ref(py))
217 217 }
218 218
219 219 def non_normal_entries_contains(&self, key: PyObject) -> PyResult<bool> {
220 220 let key = key.extract::<PyBytes>(py)?;
221 221 self.inner(py)
222 222 .borrow_mut()
223 223 .non_normal_entries_contains(HgPath::new(key.data(py)))
224 224 .map_err(|e| v2_error(py, e))
225 225 }
226 226
227 227 def non_normal_entries_display(&self) -> PyResult<PyString> {
228 228 let mut inner = self.inner(py).borrow_mut();
229 229 let paths = inner
230 230 .iter_non_normal_paths()
231 231 .collect::<Result<Vec<_>, _>>()
232 232 .map_err(|e| v2_error(py, e))?;
233 233 let formatted = format!("NonNormalEntries: {}", hg::utils::join_display(paths, ", "));
234 234 Ok(PyString::new(py, &formatted))
235 235 }
236 236
237 237 def non_normal_entries_remove(&self, key: PyObject) -> PyResult<PyObject> {
238 238 let key = key.extract::<PyBytes>(py)?;
239 239 self
240 240 .inner(py)
241 241 .borrow_mut()
242 242 .non_normal_entries_remove(HgPath::new(key.data(py)));
243 243 Ok(py.None())
244 244 }
245 245
246 246 def non_normal_or_other_parent_paths(&self) -> PyResult<PyList> {
247 247 let mut inner = self.inner(py).borrow_mut();
248 248
249 249 let ret = PyList::new(py, &[]);
250 250 for filename in inner.non_normal_or_other_parent_paths() {
251 251 let filename = filename.map_err(|e| v2_error(py, e))?;
252 252 let as_pystring = PyBytes::new(py, filename.as_bytes());
253 253 ret.append(py, as_pystring.into_object());
254 254 }
255 255 Ok(ret)
256 256 }
257 257
258 258 def non_normal_entries_iter(&self) -> PyResult<NonNormalEntriesIterator> {
259 259 // Make sure the sets are defined before we no longer have a mutable
260 260 // reference to the dmap.
261 261 self.inner(py)
262 262 .borrow_mut()
263 263 .set_non_normal_other_parent_entries(false);
264 264
265 265 let leaked_ref = self.inner(py).leak_immutable();
266 266
267 267 NonNormalEntriesIterator::from_inner(py, unsafe {
268 268 leaked_ref.map(py, |o| {
269 269 o.iter_non_normal_paths_panic()
270 270 })
271 271 })
272 272 }
273 273
274 274 def hastrackeddir(&self, d: PyObject) -> PyResult<PyBool> {
275 275 let d = d.extract::<PyBytes>(py)?;
276 276 Ok(self.inner(py).borrow_mut()
277 277 .has_tracked_dir(HgPath::new(d.data(py)))
278 278 .map_err(|e| {
279 279 PyErr::new::<exc::ValueError, _>(py, e.to_string())
280 280 })?
281 281 .to_py_object(py))
282 282 }
283 283
284 284 def hasdir(&self, d: PyObject) -> PyResult<PyBool> {
285 285 let d = d.extract::<PyBytes>(py)?;
286 286 Ok(self.inner(py).borrow_mut()
287 287 .has_dir(HgPath::new(d.data(py)))
288 288 .map_err(|e| {
289 289 PyErr::new::<exc::ValueError, _>(py, e.to_string())
290 290 })?
291 291 .to_py_object(py))
292 292 }
293 293
294 294 def write(
295 295 &self,
296 296 use_dirstate_v2: bool,
297 297 p1: PyObject,
298 298 p2: PyObject,
299 299 now: PyObject
300 300 ) -> PyResult<PyBytes> {
301 301 let now = Timestamp(now.extract(py)?);
302 302 let parents = DirstateParents {
303 303 p1: extract_node_id(py, &p1)?,
304 304 p2: extract_node_id(py, &p2)?,
305 305 };
306 306
307 307 let mut inner = self.inner(py).borrow_mut();
308 308 let result = if use_dirstate_v2 {
309 309 inner.pack_v2(parents, now)
310 310 } else {
311 311 inner.pack_v1(parents, now)
312 312 };
313 313 match result {
314 314 Ok(packed) => Ok(PyBytes::new(py, &packed)),
315 315 Err(_) => Err(PyErr::new::<exc::OSError, _>(
316 316 py,
317 317 "Dirstate error".to_string(),
318 318 )),
319 319 }
320 320 }
321 321
322 322 def filefoldmapasdict(&self) -> PyResult<PyDict> {
323 323 let dict = PyDict::new(py);
324 324 for item in self.inner(py).borrow_mut().iter() {
325 325 let (path, entry) = item.map_err(|e| v2_error(py, e))?;
326 326 if entry.state != EntryState::Removed {
327 327 let key = normalize_case(path);
328 328 let value = path;
329 329 dict.set_item(
330 330 py,
331 331 PyBytes::new(py, key.as_bytes()).into_object(),
332 332 PyBytes::new(py, value.as_bytes()).into_object(),
333 333 )?;
334 334 }
335 335 }
336 336 Ok(dict)
337 337 }
338 338
339 339 def __len__(&self) -> PyResult<usize> {
340 340 Ok(self.inner(py).borrow().len())
341 341 }
342 342
343 343 def __contains__(&self, key: PyObject) -> PyResult<bool> {
344 344 let key = key.extract::<PyBytes>(py)?;
345 345 self.inner(py)
346 346 .borrow()
347 347 .contains_key(HgPath::new(key.data(py)))
348 348 .map_err(|e| v2_error(py, e))
349 349 }
350 350
351 351 def __getitem__(&self, key: PyObject) -> PyResult<PyObject> {
352 352 let key = key.extract::<PyBytes>(py)?;
353 353 let key = HgPath::new(key.data(py));
354 354 match self
355 355 .inner(py)
356 356 .borrow()
357 357 .get(key)
358 358 .map_err(|e| v2_error(py, e))?
359 359 {
360 360 Some(entry) => {
361 361 Ok(make_dirstate_tuple(py, &entry)?)
362 362 },
363 363 None => Err(PyErr::new::<exc::KeyError, _>(
364 364 py,
365 365 String::from_utf8_lossy(key.as_bytes()),
366 366 )),
367 367 }
368 368 }
369 369
370 370 def keys(&self) -> PyResult<DirstateMapKeysIterator> {
371 371 let leaked_ref = self.inner(py).leak_immutable();
372 372 DirstateMapKeysIterator::from_inner(
373 373 py,
374 374 unsafe { leaked_ref.map(py, |o| o.iter()) },
375 375 )
376 376 }
377 377
378 378 def items(&self) -> PyResult<DirstateMapItemsIterator> {
379 379 let leaked_ref = self.inner(py).leak_immutable();
380 380 DirstateMapItemsIterator::from_inner(
381 381 py,
382 382 unsafe { leaked_ref.map(py, |o| o.iter()) },
383 383 )
384 384 }
385 385
386 386 def __iter__(&self) -> PyResult<DirstateMapKeysIterator> {
387 387 let leaked_ref = self.inner(py).leak_immutable();
388 388 DirstateMapKeysIterator::from_inner(
389 389 py,
390 390 unsafe { leaked_ref.map(py, |o| o.iter()) },
391 391 )
392 392 }
393 393
394 def getdirs(&self) -> PyResult<Dirs> {
395 // TODO don't copy, share the reference
396 self.inner(py).borrow_mut().set_dirs()
397 .map_err(|e| {
398 PyErr::new::<exc::ValueError, _>(py, e.to_string())
399 })?;
400 Dirs::from_inner(
401 py,
402 DirsMultiset::from_dirstate(
403 self.inner(py).borrow().iter(),
404 Some(EntryState::Removed),
405 )
406 .map_err(|e| {
407 PyErr::new::<exc::ValueError, _>(py, e.to_string())
408 })?,
409 )
410 }
411 def getalldirs(&self) -> PyResult<Dirs> {
412 // TODO don't copy, share the reference
413 self.inner(py).borrow_mut().set_all_dirs()
414 .map_err(|e| {
415 PyErr::new::<exc::ValueError, _>(py, e.to_string())
416 })?;
417 Dirs::from_inner(
418 py,
419 DirsMultiset::from_dirstate(
420 self.inner(py).borrow().iter(),
421 None,
422 ).map_err(|e| {
423 PyErr::new::<exc::ValueError, _>(py, e.to_string())
424 })?,
425 )
426 }
427
428 394 // TODO all copymap* methods, see docstring above
429 395 def copymapcopy(&self) -> PyResult<PyDict> {
430 396 let dict = PyDict::new(py);
431 397 for item in self.inner(py).borrow().copy_map_iter() {
432 398 let (key, value) = item.map_err(|e| v2_error(py, e))?;
433 399 dict.set_item(
434 400 py,
435 401 PyBytes::new(py, key.as_bytes()),
436 402 PyBytes::new(py, value.as_bytes()),
437 403 )?;
438 404 }
439 405 Ok(dict)
440 406 }
441 407
442 408 def copymapgetitem(&self, key: PyObject) -> PyResult<PyBytes> {
443 409 let key = key.extract::<PyBytes>(py)?;
444 410 match self
445 411 .inner(py)
446 412 .borrow()
447 413 .copy_map_get(HgPath::new(key.data(py)))
448 414 .map_err(|e| v2_error(py, e))?
449 415 {
450 416 Some(copy) => Ok(PyBytes::new(py, copy.as_bytes())),
451 417 None => Err(PyErr::new::<exc::KeyError, _>(
452 418 py,
453 419 String::from_utf8_lossy(key.data(py)),
454 420 )),
455 421 }
456 422 }
457 423 def copymap(&self) -> PyResult<CopyMap> {
458 424 CopyMap::from_inner(py, self.clone_ref(py))
459 425 }
460 426
461 427 def copymaplen(&self) -> PyResult<usize> {
462 428 Ok(self.inner(py).borrow().copy_map_len())
463 429 }
464 430 def copymapcontains(&self, key: PyObject) -> PyResult<bool> {
465 431 let key = key.extract::<PyBytes>(py)?;
466 432 self.inner(py)
467 433 .borrow()
468 434 .copy_map_contains_key(HgPath::new(key.data(py)))
469 435 .map_err(|e| v2_error(py, e))
470 436 }
471 437 def copymapget(
472 438 &self,
473 439 key: PyObject,
474 440 default: Option<PyObject>
475 441 ) -> PyResult<Option<PyObject>> {
476 442 let key = key.extract::<PyBytes>(py)?;
477 443 match self
478 444 .inner(py)
479 445 .borrow()
480 446 .copy_map_get(HgPath::new(key.data(py)))
481 447 .map_err(|e| v2_error(py, e))?
482 448 {
483 449 Some(copy) => Ok(Some(
484 450 PyBytes::new(py, copy.as_bytes()).into_object(),
485 451 )),
486 452 None => Ok(default),
487 453 }
488 454 }
489 455 def copymapsetitem(
490 456 &self,
491 457 key: PyObject,
492 458 value: PyObject
493 459 ) -> PyResult<PyObject> {
494 460 let key = key.extract::<PyBytes>(py)?;
495 461 let value = value.extract::<PyBytes>(py)?;
496 462 self.inner(py)
497 463 .borrow_mut()
498 464 .copy_map_insert(
499 465 HgPathBuf::from_bytes(key.data(py)),
500 466 HgPathBuf::from_bytes(value.data(py)),
501 467 )
502 468 .map_err(|e| v2_error(py, e))?;
503 469 Ok(py.None())
504 470 }
505 471 def copymappop(
506 472 &self,
507 473 key: PyObject,
508 474 default: Option<PyObject>
509 475 ) -> PyResult<Option<PyObject>> {
510 476 let key = key.extract::<PyBytes>(py)?;
511 477 match self
512 478 .inner(py)
513 479 .borrow_mut()
514 480 .copy_map_remove(HgPath::new(key.data(py)))
515 481 .map_err(|e| v2_error(py, e))?
516 482 {
517 483 Some(_) => Ok(None),
518 484 None => Ok(default),
519 485 }
520 486 }
521 487
522 488 def copymapiter(&self) -> PyResult<CopyMapKeysIterator> {
523 489 let leaked_ref = self.inner(py).leak_immutable();
524 490 CopyMapKeysIterator::from_inner(
525 491 py,
526 492 unsafe { leaked_ref.map(py, |o| o.copy_map_iter()) },
527 493 )
528 494 }
529 495
530 496 def copymapitemsiter(&self) -> PyResult<CopyMapItemsIterator> {
531 497 let leaked_ref = self.inner(py).leak_immutable();
532 498 CopyMapItemsIterator::from_inner(
533 499 py,
534 500 unsafe { leaked_ref.map(py, |o| o.copy_map_iter()) },
535 501 )
536 502 }
537 503
538 504 def directories(&self) -> PyResult<PyList> {
539 505 let dirs = PyList::new(py, &[]);
540 506 for item in self.inner(py).borrow().iter_directories() {
541 507 let (path, mtime) = item.map_err(|e| v2_error(py, e))?;
542 508 let path = PyBytes::new(py, path.as_bytes());
543 509 let mtime = mtime.map(|t| t.0).unwrap_or(-1);
544 510 let tuple = (path, (b'd', 0, 0, mtime));
545 511 dirs.append(py, tuple.to_py_object(py).into_object())
546 512 }
547 513 Ok(dirs)
548 514 }
549 515
550 516 });
551 517
552 518 impl DirstateMap {
553 519 pub fn get_inner_mut<'a>(
554 520 &'a self,
555 521 py: Python<'a>,
556 522 ) -> RefMut<'a, Box<dyn DirstateMapMethods + Send>> {
557 523 self.inner(py).borrow_mut()
558 524 }
559 525 fn translate_key(
560 526 py: Python,
561 527 res: Result<(&HgPath, DirstateEntry), DirstateV2ParseError>,
562 528 ) -> PyResult<Option<PyBytes>> {
563 529 let (f, _entry) = res.map_err(|e| v2_error(py, e))?;
564 530 Ok(Some(PyBytes::new(py, f.as_bytes())))
565 531 }
566 532 fn translate_key_value(
567 533 py: Python,
568 534 res: Result<(&HgPath, DirstateEntry), DirstateV2ParseError>,
569 535 ) -> PyResult<Option<(PyBytes, PyObject)>> {
570 536 let (f, entry) = res.map_err(|e| v2_error(py, e))?;
571 537 Ok(Some((
572 538 PyBytes::new(py, f.as_bytes()),
573 539 make_dirstate_tuple(py, &entry)?,
574 540 )))
575 541 }
576 542 }
577 543
578 544 py_shared_iterator!(
579 545 DirstateMapKeysIterator,
580 546 UnsafePyLeaked<StateMapIter<'static>>,
581 547 DirstateMap::translate_key,
582 548 Option<PyBytes>
583 549 );
584 550
585 551 py_shared_iterator!(
586 552 DirstateMapItemsIterator,
587 553 UnsafePyLeaked<StateMapIter<'static>>,
588 554 DirstateMap::translate_key_value,
589 555 Option<(PyBytes, PyObject)>
590 556 );
591 557
592 558 fn extract_node_id(py: Python, obj: &PyObject) -> PyResult<Node> {
593 559 let bytes = obj.extract::<PyBytes>(py)?;
594 560 match bytes.data(py).try_into() {
595 561 Ok(s) => Ok(s),
596 562 Err(e) => Err(PyErr::new::<exc::ValueError, _>(py, e.to_string())),
597 563 }
598 564 }
599 565
600 566 pub(super) fn v2_error(py: Python<'_>, _: DirstateV2ParseError) -> PyErr {
601 567 PyErr::new::<exc::ValueError, _>(py, "corrupted dirstate-v2")
602 568 }
@@ -1,223 +1,215
1 1 use crate::dirstate::owning::OwningDirstateMap;
2 2 use hg::dirstate::parsers::Timestamp;
3 3 use hg::dirstate_tree::dispatch::DirstateMapMethods;
4 4 use hg::dirstate_tree::on_disk::DirstateV2ParseError;
5 5 use hg::matchers::Matcher;
6 6 use hg::utils::hg_path::{HgPath, HgPathBuf};
7 7 use hg::CopyMapIter;
8 8 use hg::DirstateEntry;
9 9 use hg::DirstateError;
10 10 use hg::DirstateParents;
11 11 use hg::DirstateStatus;
12 12 use hg::EntryState;
13 13 use hg::PatternFileWarning;
14 14 use hg::StateMapIter;
15 15 use hg::StatusError;
16 16 use hg::StatusOptions;
17 17 use std::path::PathBuf;
18 18
19 19 impl DirstateMapMethods for OwningDirstateMap {
20 20 fn clear(&mut self) {
21 21 self.get_mut().clear()
22 22 }
23 23
24 24 fn add_file(
25 25 &mut self,
26 26 filename: &HgPath,
27 27 old_state: EntryState,
28 28 entry: DirstateEntry,
29 29 ) -> Result<(), DirstateError> {
30 30 self.get_mut().add_file(filename, old_state, entry)
31 31 }
32 32
33 33 fn remove_file(
34 34 &mut self,
35 35 filename: &HgPath,
36 36 old_state: EntryState,
37 37 size: i32,
38 38 ) -> Result<(), DirstateError> {
39 39 self.get_mut().remove_file(filename, old_state, size)
40 40 }
41 41
42 42 fn drop_file(
43 43 &mut self,
44 44 filename: &HgPath,
45 45 old_state: EntryState,
46 46 ) -> Result<bool, DirstateError> {
47 47 self.get_mut().drop_file(filename, old_state)
48 48 }
49 49
50 50 fn clear_ambiguous_times(
51 51 &mut self,
52 52 filenames: Vec<HgPathBuf>,
53 53 now: i32,
54 54 ) -> Result<(), DirstateV2ParseError> {
55 55 self.get_mut().clear_ambiguous_times(filenames, now)
56 56 }
57 57
58 58 fn non_normal_entries_contains(
59 59 &mut self,
60 60 key: &HgPath,
61 61 ) -> Result<bool, DirstateV2ParseError> {
62 62 self.get_mut().non_normal_entries_contains(key)
63 63 }
64 64
65 65 fn non_normal_entries_remove(&mut self, key: &HgPath) {
66 66 self.get_mut().non_normal_entries_remove(key)
67 67 }
68 68
69 69 fn non_normal_or_other_parent_paths(
70 70 &mut self,
71 71 ) -> Box<dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + '_>
72 72 {
73 73 self.get_mut().non_normal_or_other_parent_paths()
74 74 }
75 75
76 76 fn set_non_normal_other_parent_entries(&mut self, force: bool) {
77 77 self.get_mut().set_non_normal_other_parent_entries(force)
78 78 }
79 79
80 80 fn iter_non_normal_paths(
81 81 &mut self,
82 82 ) -> Box<
83 83 dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + Send + '_,
84 84 > {
85 85 self.get_mut().iter_non_normal_paths()
86 86 }
87 87
88 88 fn iter_non_normal_paths_panic(
89 89 &self,
90 90 ) -> Box<
91 91 dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + Send + '_,
92 92 > {
93 93 self.get().iter_non_normal_paths_panic()
94 94 }
95 95
96 96 fn iter_other_parent_paths(
97 97 &mut self,
98 98 ) -> Box<
99 99 dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + Send + '_,
100 100 > {
101 101 self.get_mut().iter_other_parent_paths()
102 102 }
103 103
104 104 fn has_tracked_dir(
105 105 &mut self,
106 106 directory: &HgPath,
107 107 ) -> Result<bool, DirstateError> {
108 108 self.get_mut().has_tracked_dir(directory)
109 109 }
110 110
111 111 fn has_dir(&mut self, directory: &HgPath) -> Result<bool, DirstateError> {
112 112 self.get_mut().has_dir(directory)
113 113 }
114 114
115 115 fn pack_v1(
116 116 &mut self,
117 117 parents: DirstateParents,
118 118 now: Timestamp,
119 119 ) -> Result<Vec<u8>, DirstateError> {
120 120 self.get_mut().pack_v1(parents, now)
121 121 }
122 122
123 123 fn pack_v2(
124 124 &mut self,
125 125 parents: DirstateParents,
126 126 now: Timestamp,
127 127 ) -> Result<Vec<u8>, DirstateError> {
128 128 self.get_mut().pack_v2(parents, now)
129 129 }
130 130
131 fn set_all_dirs(&mut self) -> Result<(), DirstateError> {
132 self.get_mut().set_all_dirs()
133 }
134
135 fn set_dirs(&mut self) -> Result<(), DirstateError> {
136 self.get_mut().set_dirs()
137 }
138
139 131 fn status<'a>(
140 132 &'a mut self,
141 133 matcher: &'a (dyn Matcher + Sync),
142 134 root_dir: PathBuf,
143 135 ignore_files: Vec<PathBuf>,
144 136 options: StatusOptions,
145 137 ) -> Result<(DirstateStatus<'a>, Vec<PatternFileWarning>), StatusError>
146 138 {
147 139 self.get_mut()
148 140 .status(matcher, root_dir, ignore_files, options)
149 141 }
150 142
151 143 fn copy_map_len(&self) -> usize {
152 144 self.get().copy_map_len()
153 145 }
154 146
155 147 fn copy_map_iter(&self) -> CopyMapIter<'_> {
156 148 self.get().copy_map_iter()
157 149 }
158 150
159 151 fn copy_map_contains_key(
160 152 &self,
161 153 key: &HgPath,
162 154 ) -> Result<bool, DirstateV2ParseError> {
163 155 self.get().copy_map_contains_key(key)
164 156 }
165 157
166 158 fn copy_map_get(
167 159 &self,
168 160 key: &HgPath,
169 161 ) -> Result<Option<&HgPath>, DirstateV2ParseError> {
170 162 self.get().copy_map_get(key)
171 163 }
172 164
173 165 fn copy_map_remove(
174 166 &mut self,
175 167 key: &HgPath,
176 168 ) -> Result<Option<HgPathBuf>, DirstateV2ParseError> {
177 169 self.get_mut().copy_map_remove(key)
178 170 }
179 171
180 172 fn copy_map_insert(
181 173 &mut self,
182 174 key: HgPathBuf,
183 175 value: HgPathBuf,
184 176 ) -> Result<Option<HgPathBuf>, DirstateV2ParseError> {
185 177 self.get_mut().copy_map_insert(key, value)
186 178 }
187 179
188 180 fn len(&self) -> usize {
189 181 self.get().len()
190 182 }
191 183
192 184 fn contains_key(
193 185 &self,
194 186 key: &HgPath,
195 187 ) -> Result<bool, DirstateV2ParseError> {
196 188 self.get().contains_key(key)
197 189 }
198 190
199 191 fn get(
200 192 &self,
201 193 key: &HgPath,
202 194 ) -> Result<Option<DirstateEntry>, DirstateV2ParseError> {
203 195 self.get().get(key)
204 196 }
205 197
206 198 fn iter(&self) -> StateMapIter<'_> {
207 199 self.get().iter()
208 200 }
209 201
210 202 fn iter_directories(
211 203 &self,
212 204 ) -> Box<
213 205 dyn Iterator<
214 206 Item = Result<
215 207 (&HgPath, Option<Timestamp>),
216 208 DirstateV2ParseError,
217 209 >,
218 210 > + Send
219 211 + '_,
220 212 > {
221 213 self.get().iter_directories()
222 214 }
223 215 }
General Comments 0
You need to be logged in to leave comments. Login now