##// END OF EJS Templates
config: add experimental argument to the config registrar...
Navaneeth Suresh -
r43028:9f2189b6 default
parent child Browse files
Show More
@@ -1,3063 +1,3092 b''
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 from mercurial import (
70 70 changegroup,
71 71 cmdutil,
72 72 commands,
73 73 copies,
74 74 error,
75 75 extensions,
76 76 hg,
77 77 mdiff,
78 78 merge,
79 79 revlog,
80 80 util,
81 81 )
82 82
83 83 # for "historical portability":
84 84 # try to import modules separately (in dict order), and ignore
85 85 # failure, because these aren't available with early Mercurial
86 86 try:
87 87 from mercurial import branchmap # since 2.5 (or bcee63733aad)
88 88 except ImportError:
89 89 pass
90 90 try:
91 91 from mercurial import obsolete # since 2.3 (or ad0d6c2b3279)
92 92 except ImportError:
93 93 pass
94 94 try:
95 95 from mercurial import registrar # since 3.7 (or 37d50250b696)
96 96 dir(registrar) # forcibly load it
97 97 except ImportError:
98 98 registrar = None
99 99 try:
100 100 from mercurial import repoview # since 2.5 (or 3a6ddacb7198)
101 101 except ImportError:
102 102 pass
103 103 try:
104 104 from mercurial.utils import repoviewutil # since 5.0
105 105 except ImportError:
106 106 repoviewutil = None
107 107 try:
108 108 from mercurial import scmutil # since 1.9 (or 8b252e826c68)
109 109 except ImportError:
110 110 pass
111 111 try:
112 112 from mercurial import setdiscovery # since 1.9 (or cb98fed52495)
113 113 except ImportError:
114 114 pass
115 115
116 116 try:
117 117 from mercurial import profiling
118 118 except ImportError:
119 119 profiling = None
120 120
121 121 def identity(a):
122 122 return a
123 123
124 124 try:
125 125 from mercurial import pycompat
126 126 getargspec = pycompat.getargspec # added to module after 4.5
127 127 _byteskwargs = pycompat.byteskwargs # since 4.1 (or fbc3f73dc802)
128 128 _sysstr = pycompat.sysstr # since 4.0 (or 2219f4f82ede)
129 129 _xrange = pycompat.xrange # since 4.8 (or 7eba8f83129b)
130 130 fsencode = pycompat.fsencode # since 3.9 (or f4a5e0e86a7e)
131 131 if pycompat.ispy3:
132 132 _maxint = sys.maxsize # per py3 docs for replacing maxint
133 133 else:
134 134 _maxint = sys.maxint
135 135 except (ImportError, AttributeError):
136 136 import inspect
137 137 getargspec = inspect.getargspec
138 138 _byteskwargs = identity
139 139 fsencode = identity # no py3 support
140 140 _maxint = sys.maxint # no py3 support
141 141 _sysstr = lambda x: x # no py3 support
142 142 _xrange = xrange
143 143
144 144 try:
145 145 # 4.7+
146 146 queue = pycompat.queue.Queue
147 147 except (AttributeError, ImportError):
148 148 # <4.7.
149 149 try:
150 150 queue = pycompat.queue
151 151 except (AttributeError, ImportError):
152 152 queue = util.queue
153 153
154 154 try:
155 155 from mercurial import logcmdutil
156 156 makelogtemplater = logcmdutil.maketemplater
157 157 except (AttributeError, ImportError):
158 158 try:
159 159 makelogtemplater = cmdutil.makelogtemplater
160 160 except (AttributeError, ImportError):
161 161 makelogtemplater = None
162 162
163 163 # for "historical portability":
164 164 # define util.safehasattr forcibly, because util.safehasattr has been
165 165 # available since 1.9.3 (or 94b200a11cf7)
166 166 _undefined = object()
167 167 def safehasattr(thing, attr):
168 168 return getattr(thing, _sysstr(attr), _undefined) is not _undefined
169 169 setattr(util, 'safehasattr', safehasattr)
170 170
171 171 # for "historical portability":
172 172 # define util.timer forcibly, because util.timer has been available
173 173 # since ae5d60bb70c9
174 174 if safehasattr(time, 'perf_counter'):
175 175 util.timer = time.perf_counter
176 176 elif os.name == b'nt':
177 177 util.timer = time.clock
178 178 else:
179 179 util.timer = time.time
180 180
181 181 # for "historical portability":
182 182 # use locally defined empty option list, if formatteropts isn't
183 183 # available, because commands.formatteropts has been available since
184 184 # 3.2 (or 7a7eed5176a4), even though formatting itself has been
185 185 # available since 2.2 (or ae5f92e154d3)
186 186 formatteropts = getattr(cmdutil, "formatteropts",
187 187 getattr(commands, "formatteropts", []))
188 188
189 189 # for "historical portability":
190 190 # use locally defined option list, if debugrevlogopts isn't available,
191 191 # because commands.debugrevlogopts has been available since 3.7 (or
192 192 # 5606f7d0d063), even though cmdutil.openrevlog() has been available
193 193 # since 1.9 (or a79fea6b3e77).
194 194 revlogopts = getattr(cmdutil, "debugrevlogopts",
195 195 getattr(commands, "debugrevlogopts", [
196 196 (b'c', b'changelog', False, (b'open changelog')),
197 197 (b'm', b'manifest', False, (b'open manifest')),
198 198 (b'', b'dir', False, (b'open directory manifest')),
199 199 ]))
200 200
201 201 cmdtable = {}
202 202
203 203 # for "historical portability":
204 204 # define parsealiases locally, because cmdutil.parsealiases has been
205 205 # available since 1.5 (or 6252852b4332)
206 206 def parsealiases(cmd):
207 207 return cmd.split(b"|")
208 208
209 209 if safehasattr(registrar, 'command'):
210 210 command = registrar.command(cmdtable)
211 211 elif safehasattr(cmdutil, 'command'):
212 212 command = cmdutil.command(cmdtable)
213 213 if b'norepo' not in getargspec(command).args:
214 214 # for "historical portability":
215 215 # wrap original cmdutil.command, because "norepo" option has
216 216 # been available since 3.1 (or 75a96326cecb)
217 217 _command = command
218 218 def command(name, options=(), synopsis=None, norepo=False):
219 219 if norepo:
220 220 commands.norepo += b' %s' % b' '.join(parsealiases(name))
221 221 return _command(name, list(options), synopsis)
222 222 else:
223 223 # for "historical portability":
224 224 # define "@command" annotation locally, because cmdutil.command
225 225 # has been available since 1.9 (or 2daa5179e73f)
226 226 def command(name, options=(), synopsis=None, norepo=False):
227 227 def decorator(func):
228 228 if synopsis:
229 229 cmdtable[name] = func, list(options), synopsis
230 230 else:
231 231 cmdtable[name] = func, list(options)
232 232 if norepo:
233 233 commands.norepo += b' %s' % b' '.join(parsealiases(name))
234 234 return func
235 235 return decorator
236 236
237 237 try:
238 238 import mercurial.registrar
239 239 import mercurial.configitems
240 240 configtable = {}
241 241 configitem = mercurial.registrar.configitem(configtable)
242 242 configitem(b'perf', b'presleep',
243 243 default=mercurial.configitems.dynamicdefault,
244 experimental=True,
245 )
246 configitem(b'perf', b'stub',
247 default=mercurial.configitems.dynamicdefault,
248 experimental=True,
249 )
250 configitem(b'perf', b'parentscount',
251 default=mercurial.configitems.dynamicdefault,
252 experimental=True,
253 )
254 configitem(b'perf', b'all-timing',
255 default=mercurial.configitems.dynamicdefault,
256 experimental=True,
257 )
258 configitem(b'perf', b'pre-run',
259 default=mercurial.configitems.dynamicdefault,
260 )
261 configitem(b'perf', b'profile-benchmark',
262 default=mercurial.configitems.dynamicdefault,
263 )
264 configitem(b'perf', b'run-limits',
265 default=mercurial.configitems.dynamicdefault,
266 experimental=True,
267 )
268 except (ImportError, AttributeError):
269 pass
270 except TypeError:
271 # compatibility fix for a11fd395e83f
272 # hg version: 5.2
273 configitem(b'perf', b'presleep',
274 default=mercurial.configitems.dynamicdefault,
244 275 )
245 276 configitem(b'perf', b'stub',
246 277 default=mercurial.configitems.dynamicdefault,
247 278 )
248 279 configitem(b'perf', b'parentscount',
249 280 default=mercurial.configitems.dynamicdefault,
250 281 )
251 282 configitem(b'perf', b'all-timing',
252 283 default=mercurial.configitems.dynamicdefault,
253 284 )
254 285 configitem(b'perf', b'pre-run',
255 286 default=mercurial.configitems.dynamicdefault,
256 287 )
257 288 configitem(b'perf', b'profile-benchmark',
258 289 default=mercurial.configitems.dynamicdefault,
259 290 )
260 291 configitem(b'perf', b'run-limits',
261 292 default=mercurial.configitems.dynamicdefault,
262 293 )
263 except (ImportError, AttributeError):
264 pass
265 294
266 295 def getlen(ui):
267 296 if ui.configbool(b"perf", b"stub", False):
268 297 return lambda x: 1
269 298 return len
270 299
271 300 class noop(object):
272 301 """dummy context manager"""
273 302 def __enter__(self):
274 303 pass
275 304 def __exit__(self, *args):
276 305 pass
277 306
278 307 NOOPCTX = noop()
279 308
280 309 def gettimer(ui, opts=None):
281 310 """return a timer function and formatter: (timer, formatter)
282 311
283 312 This function exists to gather the creation of formatter in a single
284 313 place instead of duplicating it in all performance commands."""
285 314
286 315 # enforce an idle period before execution to counteract power management
287 316 # experimental config: perf.presleep
288 317 time.sleep(getint(ui, b"perf", b"presleep", 1))
289 318
290 319 if opts is None:
291 320 opts = {}
292 321 # redirect all to stderr unless buffer api is in use
293 322 if not ui._buffers:
294 323 ui = ui.copy()
295 324 uifout = safeattrsetter(ui, b'fout', ignoremissing=True)
296 325 if uifout:
297 326 # for "historical portability":
298 327 # ui.fout/ferr have been available since 1.9 (or 4e1ccd4c2b6d)
299 328 uifout.set(ui.ferr)
300 329
301 330 # get a formatter
302 331 uiformatter = getattr(ui, 'formatter', None)
303 332 if uiformatter:
304 333 fm = uiformatter(b'perf', opts)
305 334 else:
306 335 # for "historical portability":
307 336 # define formatter locally, because ui.formatter has been
308 337 # available since 2.2 (or ae5f92e154d3)
309 338 from mercurial import node
310 339 class defaultformatter(object):
311 340 """Minimized composition of baseformatter and plainformatter
312 341 """
313 342 def __init__(self, ui, topic, opts):
314 343 self._ui = ui
315 344 if ui.debugflag:
316 345 self.hexfunc = node.hex
317 346 else:
318 347 self.hexfunc = node.short
319 348 def __nonzero__(self):
320 349 return False
321 350 __bool__ = __nonzero__
322 351 def startitem(self):
323 352 pass
324 353 def data(self, **data):
325 354 pass
326 355 def write(self, fields, deftext, *fielddata, **opts):
327 356 self._ui.write(deftext % fielddata, **opts)
328 357 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
329 358 if cond:
330 359 self._ui.write(deftext % fielddata, **opts)
331 360 def plain(self, text, **opts):
332 361 self._ui.write(text, **opts)
333 362 def end(self):
334 363 pass
335 364 fm = defaultformatter(ui, b'perf', opts)
336 365
337 366 # stub function, runs code only once instead of in a loop
338 367 # experimental config: perf.stub
339 368 if ui.configbool(b"perf", b"stub", False):
340 369 return functools.partial(stub_timer, fm), fm
341 370
342 371 # experimental config: perf.all-timing
343 372 displayall = ui.configbool(b"perf", b"all-timing", False)
344 373
345 374 # experimental config: perf.run-limits
346 375 limitspec = ui.configlist(b"perf", b"run-limits", [])
347 376 limits = []
348 377 for item in limitspec:
349 378 parts = item.split(b'-', 1)
350 379 if len(parts) < 2:
351 380 ui.warn((b'malformatted run limit entry, missing "-": %s\n'
352 381 % item))
353 382 continue
354 383 try:
355 384 time_limit = float(pycompat.sysstr(parts[0]))
356 385 except ValueError as e:
357 386 ui.warn((b'malformatted run limit entry, %s: %s\n'
358 387 % (pycompat.bytestr(e), item)))
359 388 continue
360 389 try:
361 390 run_limit = int(pycompat.sysstr(parts[1]))
362 391 except ValueError as e:
363 392 ui.warn((b'malformatted run limit entry, %s: %s\n'
364 393 % (pycompat.bytestr(e), item)))
365 394 continue
366 395 limits.append((time_limit, run_limit))
367 396 if not limits:
368 397 limits = DEFAULTLIMITS
369 398
370 399 profiler = None
371 400 if profiling is not None:
372 401 if ui.configbool(b"perf", b"profile-benchmark", False):
373 402 profiler = profiling.profile(ui)
374 403
375 404 prerun = getint(ui, b"perf", b"pre-run", 0)
376 405 t = functools.partial(_timer, fm, displayall=displayall, limits=limits,
377 406 prerun=prerun, profiler=profiler)
378 407 return t, fm
379 408
380 409 def stub_timer(fm, func, setup=None, title=None):
381 410 if setup is not None:
382 411 setup()
383 412 func()
384 413
385 414 @contextlib.contextmanager
386 415 def timeone():
387 416 r = []
388 417 ostart = os.times()
389 418 cstart = util.timer()
390 419 yield r
391 420 cstop = util.timer()
392 421 ostop = os.times()
393 422 a, b = ostart, ostop
394 423 r.append((cstop - cstart, b[0] - a[0], b[1]-a[1]))
395 424
396 425
397 426 # list of stop condition (elapsed time, minimal run count)
398 427 DEFAULTLIMITS = (
399 428 (3.0, 100),
400 429 (10.0, 3),
401 430 )
402 431
403 432 def _timer(fm, func, setup=None, title=None, displayall=False,
404 433 limits=DEFAULTLIMITS, prerun=0, profiler=None):
405 434 gc.collect()
406 435 results = []
407 436 begin = util.timer()
408 437 count = 0
409 438 if profiler is None:
410 439 profiler = NOOPCTX
411 440 for i in range(prerun):
412 441 if setup is not None:
413 442 setup()
414 443 func()
415 444 keepgoing = True
416 445 while keepgoing:
417 446 if setup is not None:
418 447 setup()
419 448 with profiler:
420 449 with timeone() as item:
421 450 r = func()
422 451 profiler = NOOPCTX
423 452 count += 1
424 453 results.append(item[0])
425 454 cstop = util.timer()
426 455 # Look for a stop condition.
427 456 elapsed = cstop - begin
428 457 for t, mincount in limits:
429 458 if elapsed >= t and count >= mincount:
430 459 keepgoing = False
431 460 break
432 461
433 462 formatone(fm, results, title=title, result=r,
434 463 displayall=displayall)
435 464
436 465 def formatone(fm, timings, title=None, result=None, displayall=False):
437 466
438 467 count = len(timings)
439 468
440 469 fm.startitem()
441 470
442 471 if title:
443 472 fm.write(b'title', b'! %s\n', title)
444 473 if result:
445 474 fm.write(b'result', b'! result: %s\n', result)
446 475 def display(role, entry):
447 476 prefix = b''
448 477 if role != b'best':
449 478 prefix = b'%s.' % role
450 479 fm.plain(b'!')
451 480 fm.write(prefix + b'wall', b' wall %f', entry[0])
452 481 fm.write(prefix + b'comb', b' comb %f', entry[1] + entry[2])
453 482 fm.write(prefix + b'user', b' user %f', entry[1])
454 483 fm.write(prefix + b'sys', b' sys %f', entry[2])
455 484 fm.write(prefix + b'count', b' (%s of %%d)' % role, count)
456 485 fm.plain(b'\n')
457 486 timings.sort()
458 487 min_val = timings[0]
459 488 display(b'best', min_val)
460 489 if displayall:
461 490 max_val = timings[-1]
462 491 display(b'max', max_val)
463 492 avg = tuple([sum(x) / count for x in zip(*timings)])
464 493 display(b'avg', avg)
465 494 median = timings[len(timings) // 2]
466 495 display(b'median', median)
467 496
468 497 # utilities for historical portability
469 498
470 499 def getint(ui, section, name, default):
471 500 # for "historical portability":
472 501 # ui.configint has been available since 1.9 (or fa2b596db182)
473 502 v = ui.config(section, name, None)
474 503 if v is None:
475 504 return default
476 505 try:
477 506 return int(v)
478 507 except ValueError:
479 508 raise error.ConfigError((b"%s.%s is not an integer ('%s')")
480 509 % (section, name, v))
481 510
482 511 def safeattrsetter(obj, name, ignoremissing=False):
483 512 """Ensure that 'obj' has 'name' attribute before subsequent setattr
484 513
485 514 This function is aborted, if 'obj' doesn't have 'name' attribute
486 515 at runtime. This avoids overlooking removal of an attribute, which
487 516 breaks assumption of performance measurement, in the future.
488 517
489 518 This function returns the object to (1) assign a new value, and
490 519 (2) restore an original value to the attribute.
491 520
492 521 If 'ignoremissing' is true, missing 'name' attribute doesn't cause
493 522 abortion, and this function returns None. This is useful to
494 523 examine an attribute, which isn't ensured in all Mercurial
495 524 versions.
496 525 """
497 526 if not util.safehasattr(obj, name):
498 527 if ignoremissing:
499 528 return None
500 529 raise error.Abort((b"missing attribute %s of %s might break assumption"
501 530 b" of performance measurement") % (name, obj))
502 531
503 532 origvalue = getattr(obj, _sysstr(name))
504 533 class attrutil(object):
505 534 def set(self, newvalue):
506 535 setattr(obj, _sysstr(name), newvalue)
507 536 def restore(self):
508 537 setattr(obj, _sysstr(name), origvalue)
509 538
510 539 return attrutil()
511 540
512 541 # utilities to examine each internal API changes
513 542
514 543 def getbranchmapsubsettable():
515 544 # for "historical portability":
516 545 # subsettable is defined in:
517 546 # - branchmap since 2.9 (or 175c6fd8cacc)
518 547 # - repoview since 2.5 (or 59a9f18d4587)
519 548 # - repoviewutil since 5.0
520 549 for mod in (branchmap, repoview, repoviewutil):
521 550 subsettable = getattr(mod, 'subsettable', None)
522 551 if subsettable:
523 552 return subsettable
524 553
525 554 # bisecting in bcee63733aad::59a9f18d4587 can reach here (both
526 555 # branchmap and repoview modules exist, but subsettable attribute
527 556 # doesn't)
528 557 raise error.Abort((b"perfbranchmap not available with this Mercurial"),
529 558 hint=b"use 2.5 or later")
530 559
531 560 def getsvfs(repo):
532 561 """Return appropriate object to access files under .hg/store
533 562 """
534 563 # for "historical portability":
535 564 # repo.svfs has been available since 2.3 (or 7034365089bf)
536 565 svfs = getattr(repo, 'svfs', None)
537 566 if svfs:
538 567 return svfs
539 568 else:
540 569 return getattr(repo, 'sopener')
541 570
542 571 def getvfs(repo):
543 572 """Return appropriate object to access files under .hg
544 573 """
545 574 # for "historical portability":
546 575 # repo.vfs has been available since 2.3 (or 7034365089bf)
547 576 vfs = getattr(repo, 'vfs', None)
548 577 if vfs:
549 578 return vfs
550 579 else:
551 580 return getattr(repo, 'opener')
552 581
553 582 def repocleartagscachefunc(repo):
554 583 """Return the function to clear tags cache according to repo internal API
555 584 """
556 585 if util.safehasattr(repo, b'_tagscache'): # since 2.0 (or 9dca7653b525)
557 586 # in this case, setattr(repo, '_tagscache', None) or so isn't
558 587 # correct way to clear tags cache, because existing code paths
559 588 # expect _tagscache to be a structured object.
560 589 def clearcache():
561 590 # _tagscache has been filteredpropertycache since 2.5 (or
562 591 # 98c867ac1330), and delattr() can't work in such case
563 592 if b'_tagscache' in vars(repo):
564 593 del repo.__dict__[b'_tagscache']
565 594 return clearcache
566 595
567 596 repotags = safeattrsetter(repo, b'_tags', ignoremissing=True)
568 597 if repotags: # since 1.4 (or 5614a628d173)
569 598 return lambda : repotags.set(None)
570 599
571 600 repotagscache = safeattrsetter(repo, b'tagscache', ignoremissing=True)
572 601 if repotagscache: # since 0.6 (or d7df759d0e97)
573 602 return lambda : repotagscache.set(None)
574 603
575 604 # Mercurial earlier than 0.6 (or d7df759d0e97) logically reaches
576 605 # this point, but it isn't so problematic, because:
577 606 # - repo.tags of such Mercurial isn't "callable", and repo.tags()
578 607 # in perftags() causes failure soon
579 608 # - perf.py itself has been available since 1.1 (or eb240755386d)
580 609 raise error.Abort((b"tags API of this hg command is unknown"))
581 610
582 611 # utilities to clear cache
583 612
584 613 def clearfilecache(obj, attrname):
585 614 unfiltered = getattr(obj, 'unfiltered', None)
586 615 if unfiltered is not None:
587 616 obj = obj.unfiltered()
588 617 if attrname in vars(obj):
589 618 delattr(obj, attrname)
590 619 obj._filecache.pop(attrname, None)
591 620
592 621 def clearchangelog(repo):
593 622 if repo is not repo.unfiltered():
594 623 object.__setattr__(repo, r'_clcachekey', None)
595 624 object.__setattr__(repo, r'_clcache', None)
596 625 clearfilecache(repo.unfiltered(), 'changelog')
597 626
598 627 # perf commands
599 628
600 629 @command(b'perfwalk', formatteropts)
601 630 def perfwalk(ui, repo, *pats, **opts):
602 631 opts = _byteskwargs(opts)
603 632 timer, fm = gettimer(ui, opts)
604 633 m = scmutil.match(repo[None], pats, {})
605 634 timer(lambda: len(list(repo.dirstate.walk(m, subrepos=[], unknown=True,
606 635 ignored=False))))
607 636 fm.end()
608 637
609 638 @command(b'perfannotate', formatteropts)
610 639 def perfannotate(ui, repo, f, **opts):
611 640 opts = _byteskwargs(opts)
612 641 timer, fm = gettimer(ui, opts)
613 642 fc = repo[b'.'][f]
614 643 timer(lambda: len(fc.annotate(True)))
615 644 fm.end()
616 645
617 646 @command(b'perfstatus',
618 647 [(b'u', b'unknown', False,
619 648 b'ask status to look for unknown files')] + formatteropts)
620 649 def perfstatus(ui, repo, **opts):
621 650 opts = _byteskwargs(opts)
622 651 #m = match.always(repo.root, repo.getcwd())
623 652 #timer(lambda: sum(map(len, repo.dirstate.status(m, [], False, False,
624 653 # False))))
625 654 timer, fm = gettimer(ui, opts)
626 655 timer(lambda: sum(map(len, repo.status(unknown=opts[b'unknown']))))
627 656 fm.end()
628 657
629 658 @command(b'perfaddremove', formatteropts)
630 659 def perfaddremove(ui, repo, **opts):
631 660 opts = _byteskwargs(opts)
632 661 timer, fm = gettimer(ui, opts)
633 662 try:
634 663 oldquiet = repo.ui.quiet
635 664 repo.ui.quiet = True
636 665 matcher = scmutil.match(repo[None])
637 666 opts[b'dry_run'] = True
638 667 if b'uipathfn' in getargspec(scmutil.addremove).args:
639 668 uipathfn = scmutil.getuipathfn(repo)
640 669 timer(lambda: scmutil.addremove(repo, matcher, b"", uipathfn, opts))
641 670 else:
642 671 timer(lambda: scmutil.addremove(repo, matcher, b"", opts))
643 672 finally:
644 673 repo.ui.quiet = oldquiet
645 674 fm.end()
646 675
647 676 def clearcaches(cl):
648 677 # behave somewhat consistently across internal API changes
649 678 if util.safehasattr(cl, b'clearcaches'):
650 679 cl.clearcaches()
651 680 elif util.safehasattr(cl, b'_nodecache'):
652 681 from mercurial.node import nullid, nullrev
653 682 cl._nodecache = {nullid: nullrev}
654 683 cl._nodepos = None
655 684
656 685 @command(b'perfheads', formatteropts)
657 686 def perfheads(ui, repo, **opts):
658 687 """benchmark the computation of a changelog heads"""
659 688 opts = _byteskwargs(opts)
660 689 timer, fm = gettimer(ui, opts)
661 690 cl = repo.changelog
662 691 def s():
663 692 clearcaches(cl)
664 693 def d():
665 694 len(cl.headrevs())
666 695 timer(d, setup=s)
667 696 fm.end()
668 697
669 698 @command(b'perftags', formatteropts+
670 699 [
671 700 (b'', b'clear-revlogs', False, b'refresh changelog and manifest'),
672 701 ])
673 702 def perftags(ui, repo, **opts):
674 703 opts = _byteskwargs(opts)
675 704 timer, fm = gettimer(ui, opts)
676 705 repocleartagscache = repocleartagscachefunc(repo)
677 706 clearrevlogs = opts[b'clear_revlogs']
678 707 def s():
679 708 if clearrevlogs:
680 709 clearchangelog(repo)
681 710 clearfilecache(repo.unfiltered(), 'manifest')
682 711 repocleartagscache()
683 712 def t():
684 713 return len(repo.tags())
685 714 timer(t, setup=s)
686 715 fm.end()
687 716
688 717 @command(b'perfancestors', formatteropts)
689 718 def perfancestors(ui, repo, **opts):
690 719 opts = _byteskwargs(opts)
691 720 timer, fm = gettimer(ui, opts)
692 721 heads = repo.changelog.headrevs()
693 722 def d():
694 723 for a in repo.changelog.ancestors(heads):
695 724 pass
696 725 timer(d)
697 726 fm.end()
698 727
699 728 @command(b'perfancestorset', formatteropts)
700 729 def perfancestorset(ui, repo, revset, **opts):
701 730 opts = _byteskwargs(opts)
702 731 timer, fm = gettimer(ui, opts)
703 732 revs = repo.revs(revset)
704 733 heads = repo.changelog.headrevs()
705 734 def d():
706 735 s = repo.changelog.ancestors(heads)
707 736 for rev in revs:
708 737 rev in s
709 738 timer(d)
710 739 fm.end()
711 740
712 741 @command(b'perfdiscovery', formatteropts, b'PATH')
713 742 def perfdiscovery(ui, repo, path, **opts):
714 743 """benchmark discovery between local repo and the peer at given path
715 744 """
716 745 repos = [repo, None]
717 746 timer, fm = gettimer(ui, opts)
718 747 path = ui.expandpath(path)
719 748
720 749 def s():
721 750 repos[1] = hg.peer(ui, opts, path)
722 751 def d():
723 752 setdiscovery.findcommonheads(ui, *repos)
724 753 timer(d, setup=s)
725 754 fm.end()
726 755
727 756 @command(b'perfbookmarks', formatteropts +
728 757 [
729 758 (b'', b'clear-revlogs', False, b'refresh changelog and manifest'),
730 759 ])
731 760 def perfbookmarks(ui, repo, **opts):
732 761 """benchmark parsing bookmarks from disk to memory"""
733 762 opts = _byteskwargs(opts)
734 763 timer, fm = gettimer(ui, opts)
735 764
736 765 clearrevlogs = opts[b'clear_revlogs']
737 766 def s():
738 767 if clearrevlogs:
739 768 clearchangelog(repo)
740 769 clearfilecache(repo, b'_bookmarks')
741 770 def d():
742 771 repo._bookmarks
743 772 timer(d, setup=s)
744 773 fm.end()
745 774
746 775 @command(b'perfbundleread', formatteropts, b'BUNDLE')
747 776 def perfbundleread(ui, repo, bundlepath, **opts):
748 777 """Benchmark reading of bundle files.
749 778
750 779 This command is meant to isolate the I/O part of bundle reading as
751 780 much as possible.
752 781 """
753 782 from mercurial import (
754 783 bundle2,
755 784 exchange,
756 785 streamclone,
757 786 )
758 787
759 788 opts = _byteskwargs(opts)
760 789
761 790 def makebench(fn):
762 791 def run():
763 792 with open(bundlepath, b'rb') as fh:
764 793 bundle = exchange.readbundle(ui, fh, bundlepath)
765 794 fn(bundle)
766 795
767 796 return run
768 797
769 798 def makereadnbytes(size):
770 799 def run():
771 800 with open(bundlepath, b'rb') as fh:
772 801 bundle = exchange.readbundle(ui, fh, bundlepath)
773 802 while bundle.read(size):
774 803 pass
775 804
776 805 return run
777 806
778 807 def makestdioread(size):
779 808 def run():
780 809 with open(bundlepath, b'rb') as fh:
781 810 while fh.read(size):
782 811 pass
783 812
784 813 return run
785 814
786 815 # bundle1
787 816
788 817 def deltaiter(bundle):
789 818 for delta in bundle.deltaiter():
790 819 pass
791 820
792 821 def iterchunks(bundle):
793 822 for chunk in bundle.getchunks():
794 823 pass
795 824
796 825 # bundle2
797 826
798 827 def forwardchunks(bundle):
799 828 for chunk in bundle._forwardchunks():
800 829 pass
801 830
802 831 def iterparts(bundle):
803 832 for part in bundle.iterparts():
804 833 pass
805 834
806 835 def iterpartsseekable(bundle):
807 836 for part in bundle.iterparts(seekable=True):
808 837 pass
809 838
810 839 def seek(bundle):
811 840 for part in bundle.iterparts(seekable=True):
812 841 part.seek(0, os.SEEK_END)
813 842
814 843 def makepartreadnbytes(size):
815 844 def run():
816 845 with open(bundlepath, b'rb') as fh:
817 846 bundle = exchange.readbundle(ui, fh, bundlepath)
818 847 for part in bundle.iterparts():
819 848 while part.read(size):
820 849 pass
821 850
822 851 return run
823 852
824 853 benches = [
825 854 (makestdioread(8192), b'read(8k)'),
826 855 (makestdioread(16384), b'read(16k)'),
827 856 (makestdioread(32768), b'read(32k)'),
828 857 (makestdioread(131072), b'read(128k)'),
829 858 ]
830 859
831 860 with open(bundlepath, b'rb') as fh:
832 861 bundle = exchange.readbundle(ui, fh, bundlepath)
833 862
834 863 if isinstance(bundle, changegroup.cg1unpacker):
835 864 benches.extend([
836 865 (makebench(deltaiter), b'cg1 deltaiter()'),
837 866 (makebench(iterchunks), b'cg1 getchunks()'),
838 867 (makereadnbytes(8192), b'cg1 read(8k)'),
839 868 (makereadnbytes(16384), b'cg1 read(16k)'),
840 869 (makereadnbytes(32768), b'cg1 read(32k)'),
841 870 (makereadnbytes(131072), b'cg1 read(128k)'),
842 871 ])
843 872 elif isinstance(bundle, bundle2.unbundle20):
844 873 benches.extend([
845 874 (makebench(forwardchunks), b'bundle2 forwardchunks()'),
846 875 (makebench(iterparts), b'bundle2 iterparts()'),
847 876 (makebench(iterpartsseekable), b'bundle2 iterparts() seekable'),
848 877 (makebench(seek), b'bundle2 part seek()'),
849 878 (makepartreadnbytes(8192), b'bundle2 part read(8k)'),
850 879 (makepartreadnbytes(16384), b'bundle2 part read(16k)'),
851 880 (makepartreadnbytes(32768), b'bundle2 part read(32k)'),
852 881 (makepartreadnbytes(131072), b'bundle2 part read(128k)'),
853 882 ])
854 883 elif isinstance(bundle, streamclone.streamcloneapplier):
855 884 raise error.Abort(b'stream clone bundles not supported')
856 885 else:
857 886 raise error.Abort(b'unhandled bundle type: %s' % type(bundle))
858 887
859 888 for fn, title in benches:
860 889 timer, fm = gettimer(ui, opts)
861 890 timer(fn, title=title)
862 891 fm.end()
863 892
864 893 @command(b'perfchangegroupchangelog', formatteropts +
865 894 [(b'', b'cgversion', b'02', b'changegroup version'),
866 895 (b'r', b'rev', b'', b'revisions to add to changegroup')])
867 896 def perfchangegroupchangelog(ui, repo, cgversion=b'02', rev=None, **opts):
868 897 """Benchmark producing a changelog group for a changegroup.
869 898
870 899 This measures the time spent processing the changelog during a
871 900 bundle operation. This occurs during `hg bundle` and on a server
872 901 processing a `getbundle` wire protocol request (handles clones
873 902 and pull requests).
874 903
875 904 By default, all revisions are added to the changegroup.
876 905 """
877 906 opts = _byteskwargs(opts)
878 907 cl = repo.changelog
879 908 nodes = [cl.lookup(r) for r in repo.revs(rev or b'all()')]
880 909 bundler = changegroup.getbundler(cgversion, repo)
881 910
882 911 def d():
883 912 state, chunks = bundler._generatechangelog(cl, nodes)
884 913 for chunk in chunks:
885 914 pass
886 915
887 916 timer, fm = gettimer(ui, opts)
888 917
889 918 # Terminal printing can interfere with timing. So disable it.
890 919 with ui.configoverride({(b'progress', b'disable'): True}):
891 920 timer(d)
892 921
893 922 fm.end()
894 923
895 924 @command(b'perfdirs', formatteropts)
896 925 def perfdirs(ui, repo, **opts):
897 926 opts = _byteskwargs(opts)
898 927 timer, fm = gettimer(ui, opts)
899 928 dirstate = repo.dirstate
900 929 b'a' in dirstate
901 930 def d():
902 931 dirstate.hasdir(b'a')
903 932 del dirstate._map._dirs
904 933 timer(d)
905 934 fm.end()
906 935
907 936 @command(b'perfdirstate', formatteropts)
908 937 def perfdirstate(ui, repo, **opts):
909 938 opts = _byteskwargs(opts)
910 939 timer, fm = gettimer(ui, opts)
911 940 b"a" in repo.dirstate
912 941 def d():
913 942 repo.dirstate.invalidate()
914 943 b"a" in repo.dirstate
915 944 timer(d)
916 945 fm.end()
917 946
918 947 @command(b'perfdirstatedirs', formatteropts)
919 948 def perfdirstatedirs(ui, repo, **opts):
920 949 opts = _byteskwargs(opts)
921 950 timer, fm = gettimer(ui, opts)
922 951 b"a" in repo.dirstate
923 952 def d():
924 953 repo.dirstate.hasdir(b"a")
925 954 del repo.dirstate._map._dirs
926 955 timer(d)
927 956 fm.end()
928 957
929 958 @command(b'perfdirstatefoldmap', formatteropts)
930 959 def perfdirstatefoldmap(ui, repo, **opts):
931 960 opts = _byteskwargs(opts)
932 961 timer, fm = gettimer(ui, opts)
933 962 dirstate = repo.dirstate
934 963 b'a' in dirstate
935 964 def d():
936 965 dirstate._map.filefoldmap.get(b'a')
937 966 del dirstate._map.filefoldmap
938 967 timer(d)
939 968 fm.end()
940 969
941 970 @command(b'perfdirfoldmap', formatteropts)
942 971 def perfdirfoldmap(ui, repo, **opts):
943 972 opts = _byteskwargs(opts)
944 973 timer, fm = gettimer(ui, opts)
945 974 dirstate = repo.dirstate
946 975 b'a' in dirstate
947 976 def d():
948 977 dirstate._map.dirfoldmap.get(b'a')
949 978 del dirstate._map.dirfoldmap
950 979 del dirstate._map._dirs
951 980 timer(d)
952 981 fm.end()
953 982
954 983 @command(b'perfdirstatewrite', formatteropts)
955 984 def perfdirstatewrite(ui, repo, **opts):
956 985 opts = _byteskwargs(opts)
957 986 timer, fm = gettimer(ui, opts)
958 987 ds = repo.dirstate
959 988 b"a" in ds
960 989 def d():
961 990 ds._dirty = True
962 991 ds.write(repo.currenttransaction())
963 992 timer(d)
964 993 fm.end()
965 994
966 995 def _getmergerevs(repo, opts):
967 996 """parse command argument to return rev involved in merge
968 997
969 998 input: options dictionnary with `rev`, `from` and `bse`
970 999 output: (localctx, otherctx, basectx)
971 1000 """
972 1001 if opts[b'from']:
973 1002 fromrev = scmutil.revsingle(repo, opts[b'from'])
974 1003 wctx = repo[fromrev]
975 1004 else:
976 1005 wctx = repo[None]
977 1006 # we don't want working dir files to be stat'd in the benchmark, so
978 1007 # prime that cache
979 1008 wctx.dirty()
980 1009 rctx = scmutil.revsingle(repo, opts[b'rev'], opts[b'rev'])
981 1010 if opts[b'base']:
982 1011 fromrev = scmutil.revsingle(repo, opts[b'base'])
983 1012 ancestor = repo[fromrev]
984 1013 else:
985 1014 ancestor = wctx.ancestor(rctx)
986 1015 return (wctx, rctx, ancestor)
987 1016
988 1017 @command(b'perfmergecalculate',
989 1018 [
990 1019 (b'r', b'rev', b'.', b'rev to merge against'),
991 1020 (b'', b'from', b'', b'rev to merge from'),
992 1021 (b'', b'base', b'', b'the revision to use as base'),
993 1022 ] + formatteropts)
994 1023 def perfmergecalculate(ui, repo, **opts):
995 1024 opts = _byteskwargs(opts)
996 1025 timer, fm = gettimer(ui, opts)
997 1026
998 1027 wctx, rctx, ancestor = _getmergerevs(repo, opts)
999 1028 def d():
1000 1029 # acceptremote is True because we don't want prompts in the middle of
1001 1030 # our benchmark
1002 1031 merge.calculateupdates(repo, wctx, rctx, [ancestor], False, False,
1003 1032 acceptremote=True, followcopies=True)
1004 1033 timer(d)
1005 1034 fm.end()
1006 1035
1007 1036 @command(b'perfmergecopies',
1008 1037 [
1009 1038 (b'r', b'rev', b'.', b'rev to merge against'),
1010 1039 (b'', b'from', b'', b'rev to merge from'),
1011 1040 (b'', b'base', b'', b'the revision to use as base'),
1012 1041 ] + formatteropts)
1013 1042 def perfmergecopies(ui, repo, **opts):
1014 1043 """measure runtime of `copies.mergecopies`"""
1015 1044 opts = _byteskwargs(opts)
1016 1045 timer, fm = gettimer(ui, opts)
1017 1046 wctx, rctx, ancestor = _getmergerevs(repo, opts)
1018 1047 def d():
1019 1048 # acceptremote is True because we don't want prompts in the middle of
1020 1049 # our benchmark
1021 1050 copies.mergecopies(repo, wctx, rctx, ancestor)
1022 1051 timer(d)
1023 1052 fm.end()
1024 1053
1025 1054 @command(b'perfpathcopies', [], b"REV REV")
1026 1055 def perfpathcopies(ui, repo, rev1, rev2, **opts):
1027 1056 """benchmark the copy tracing logic"""
1028 1057 opts = _byteskwargs(opts)
1029 1058 timer, fm = gettimer(ui, opts)
1030 1059 ctx1 = scmutil.revsingle(repo, rev1, rev1)
1031 1060 ctx2 = scmutil.revsingle(repo, rev2, rev2)
1032 1061 def d():
1033 1062 copies.pathcopies(ctx1, ctx2)
1034 1063 timer(d)
1035 1064 fm.end()
1036 1065
1037 1066 @command(b'perfphases',
1038 1067 [(b'', b'full', False, b'include file reading time too'),
1039 1068 ], b"")
1040 1069 def perfphases(ui, repo, **opts):
1041 1070 """benchmark phasesets computation"""
1042 1071 opts = _byteskwargs(opts)
1043 1072 timer, fm = gettimer(ui, opts)
1044 1073 _phases = repo._phasecache
1045 1074 full = opts.get(b'full')
1046 1075 def d():
1047 1076 phases = _phases
1048 1077 if full:
1049 1078 clearfilecache(repo, b'_phasecache')
1050 1079 phases = repo._phasecache
1051 1080 phases.invalidate()
1052 1081 phases.loadphaserevs(repo)
1053 1082 timer(d)
1054 1083 fm.end()
1055 1084
1056 1085 @command(b'perfphasesremote',
1057 1086 [], b"[DEST]")
1058 1087 def perfphasesremote(ui, repo, dest=None, **opts):
1059 1088 """benchmark time needed to analyse phases of the remote server"""
1060 1089 from mercurial.node import (
1061 1090 bin,
1062 1091 )
1063 1092 from mercurial import (
1064 1093 exchange,
1065 1094 hg,
1066 1095 phases,
1067 1096 )
1068 1097 opts = _byteskwargs(opts)
1069 1098 timer, fm = gettimer(ui, opts)
1070 1099
1071 1100 path = ui.paths.getpath(dest, default=(b'default-push', b'default'))
1072 1101 if not path:
1073 1102 raise error.Abort((b'default repository not configured!'),
1074 1103 hint=(b"see 'hg help config.paths'"))
1075 1104 dest = path.pushloc or path.loc
1076 1105 ui.status((b'analysing phase of %s\n') % util.hidepassword(dest))
1077 1106 other = hg.peer(repo, opts, dest)
1078 1107
1079 1108 # easier to perform discovery through the operation
1080 1109 op = exchange.pushoperation(repo, other)
1081 1110 exchange._pushdiscoverychangeset(op)
1082 1111
1083 1112 remotesubset = op.fallbackheads
1084 1113
1085 1114 with other.commandexecutor() as e:
1086 1115 remotephases = e.callcommand(b'listkeys',
1087 1116 {b'namespace': b'phases'}).result()
1088 1117 del other
1089 1118 publishing = remotephases.get(b'publishing', False)
1090 1119 if publishing:
1091 1120 ui.status((b'publishing: yes\n'))
1092 1121 else:
1093 1122 ui.status((b'publishing: no\n'))
1094 1123
1095 1124 nodemap = repo.changelog.nodemap
1096 1125 nonpublishroots = 0
1097 1126 for nhex, phase in remotephases.iteritems():
1098 1127 if nhex == b'publishing': # ignore data related to publish option
1099 1128 continue
1100 1129 node = bin(nhex)
1101 1130 if node in nodemap and int(phase):
1102 1131 nonpublishroots += 1
1103 1132 ui.status((b'number of roots: %d\n') % len(remotephases))
1104 1133 ui.status((b'number of known non public roots: %d\n') % nonpublishroots)
1105 1134 def d():
1106 1135 phases.remotephasessummary(repo,
1107 1136 remotesubset,
1108 1137 remotephases)
1109 1138 timer(d)
1110 1139 fm.end()
1111 1140
1112 1141 @command(b'perfmanifest',[
1113 1142 (b'm', b'manifest-rev', False, b'Look up a manifest node revision'),
1114 1143 (b'', b'clear-disk', False, b'clear on-disk caches too'),
1115 1144 ] + formatteropts, b'REV|NODE')
1116 1145 def perfmanifest(ui, repo, rev, manifest_rev=False, clear_disk=False, **opts):
1117 1146 """benchmark the time to read a manifest from disk and return a usable
1118 1147 dict-like object
1119 1148
1120 1149 Manifest caches are cleared before retrieval."""
1121 1150 opts = _byteskwargs(opts)
1122 1151 timer, fm = gettimer(ui, opts)
1123 1152 if not manifest_rev:
1124 1153 ctx = scmutil.revsingle(repo, rev, rev)
1125 1154 t = ctx.manifestnode()
1126 1155 else:
1127 1156 from mercurial.node import bin
1128 1157
1129 1158 if len(rev) == 40:
1130 1159 t = bin(rev)
1131 1160 else:
1132 1161 try:
1133 1162 rev = int(rev)
1134 1163
1135 1164 if util.safehasattr(repo.manifestlog, b'getstorage'):
1136 1165 t = repo.manifestlog.getstorage(b'').node(rev)
1137 1166 else:
1138 1167 t = repo.manifestlog._revlog.lookup(rev)
1139 1168 except ValueError:
1140 1169 raise error.Abort(b'manifest revision must be integer or full '
1141 1170 b'node')
1142 1171 def d():
1143 1172 repo.manifestlog.clearcaches(clear_persisted_data=clear_disk)
1144 1173 repo.manifestlog[t].read()
1145 1174 timer(d)
1146 1175 fm.end()
1147 1176
1148 1177 @command(b'perfchangeset', formatteropts)
1149 1178 def perfchangeset(ui, repo, rev, **opts):
1150 1179 opts = _byteskwargs(opts)
1151 1180 timer, fm = gettimer(ui, opts)
1152 1181 n = scmutil.revsingle(repo, rev).node()
1153 1182 def d():
1154 1183 repo.changelog.read(n)
1155 1184 #repo.changelog._cache = None
1156 1185 timer(d)
1157 1186 fm.end()
1158 1187
1159 1188 @command(b'perfignore', formatteropts)
1160 1189 def perfignore(ui, repo, **opts):
1161 1190 """benchmark operation related to computing ignore"""
1162 1191 opts = _byteskwargs(opts)
1163 1192 timer, fm = gettimer(ui, opts)
1164 1193 dirstate = repo.dirstate
1165 1194
1166 1195 def setupone():
1167 1196 dirstate.invalidate()
1168 1197 clearfilecache(dirstate, b'_ignore')
1169 1198
1170 1199 def runone():
1171 1200 dirstate._ignore
1172 1201
1173 1202 timer(runone, setup=setupone, title=b"load")
1174 1203 fm.end()
1175 1204
1176 1205 @command(b'perfindex', [
1177 1206 (b'', b'rev', [], b'revision to be looked up (default tip)'),
1178 1207 (b'', b'no-lookup', None, b'do not revision lookup post creation'),
1179 1208 ] + formatteropts)
1180 1209 def perfindex(ui, repo, **opts):
1181 1210 """benchmark index creation time followed by a lookup
1182 1211
1183 1212 The default is to look `tip` up. Depending on the index implementation,
1184 1213 the revision looked up can matters. For example, an implementation
1185 1214 scanning the index will have a faster lookup time for `--rev tip` than for
1186 1215 `--rev 0`. The number of looked up revisions and their order can also
1187 1216 matters.
1188 1217
1189 1218 Example of useful set to test:
1190 1219 * tip
1191 1220 * 0
1192 1221 * -10:
1193 1222 * :10
1194 1223 * -10: + :10
1195 1224 * :10: + -10:
1196 1225 * -10000:
1197 1226 * -10000: + 0
1198 1227
1199 1228 It is not currently possible to check for lookup of a missing node. For
1200 1229 deeper lookup benchmarking, checkout the `perfnodemap` command."""
1201 1230 import mercurial.revlog
1202 1231 opts = _byteskwargs(opts)
1203 1232 timer, fm = gettimer(ui, opts)
1204 1233 mercurial.revlog._prereadsize = 2**24 # disable lazy parser in old hg
1205 1234 if opts[b'no_lookup']:
1206 1235 if opts['rev']:
1207 1236 raise error.Abort('--no-lookup and --rev are mutually exclusive')
1208 1237 nodes = []
1209 1238 elif not opts[b'rev']:
1210 1239 nodes = [repo[b"tip"].node()]
1211 1240 else:
1212 1241 revs = scmutil.revrange(repo, opts[b'rev'])
1213 1242 cl = repo.changelog
1214 1243 nodes = [cl.node(r) for r in revs]
1215 1244
1216 1245 unfi = repo.unfiltered()
1217 1246 # find the filecache func directly
1218 1247 # This avoid polluting the benchmark with the filecache logic
1219 1248 makecl = unfi.__class__.changelog.func
1220 1249 def setup():
1221 1250 # probably not necessary, but for good measure
1222 1251 clearchangelog(unfi)
1223 1252 def d():
1224 1253 cl = makecl(unfi)
1225 1254 for n in nodes:
1226 1255 cl.rev(n)
1227 1256 timer(d, setup=setup)
1228 1257 fm.end()
1229 1258
1230 1259 @command(b'perfnodemap', [
1231 1260 (b'', b'rev', [], b'revision to be looked up (default tip)'),
1232 1261 (b'', b'clear-caches', True, b'clear revlog cache between calls'),
1233 1262 ] + formatteropts)
1234 1263 def perfnodemap(ui, repo, **opts):
1235 1264 """benchmark the time necessary to look up revision from a cold nodemap
1236 1265
1237 1266 Depending on the implementation, the amount and order of revision we look
1238 1267 up can varies. Example of useful set to test:
1239 1268 * tip
1240 1269 * 0
1241 1270 * -10:
1242 1271 * :10
1243 1272 * -10: + :10
1244 1273 * :10: + -10:
1245 1274 * -10000:
1246 1275 * -10000: + 0
1247 1276
1248 1277 The command currently focus on valid binary lookup. Benchmarking for
1249 1278 hexlookup, prefix lookup and missing lookup would also be valuable.
1250 1279 """
1251 1280 import mercurial.revlog
1252 1281 opts = _byteskwargs(opts)
1253 1282 timer, fm = gettimer(ui, opts)
1254 1283 mercurial.revlog._prereadsize = 2**24 # disable lazy parser in old hg
1255 1284
1256 1285 unfi = repo.unfiltered()
1257 1286 clearcaches = opts['clear_caches']
1258 1287 # find the filecache func directly
1259 1288 # This avoid polluting the benchmark with the filecache logic
1260 1289 makecl = unfi.__class__.changelog.func
1261 1290 if not opts[b'rev']:
1262 1291 raise error.Abort('use --rev to specify revisions to look up')
1263 1292 revs = scmutil.revrange(repo, opts[b'rev'])
1264 1293 cl = repo.changelog
1265 1294 nodes = [cl.node(r) for r in revs]
1266 1295
1267 1296 # use a list to pass reference to a nodemap from one closure to the next
1268 1297 nodeget = [None]
1269 1298 def setnodeget():
1270 1299 # probably not necessary, but for good measure
1271 1300 clearchangelog(unfi)
1272 1301 nodeget[0] = makecl(unfi).nodemap.get
1273 1302
1274 1303 def d():
1275 1304 get = nodeget[0]
1276 1305 for n in nodes:
1277 1306 get(n)
1278 1307
1279 1308 setup = None
1280 1309 if clearcaches:
1281 1310 def setup():
1282 1311 setnodeget()
1283 1312 else:
1284 1313 setnodeget()
1285 1314 d() # prewarm the data structure
1286 1315 timer(d, setup=setup)
1287 1316 fm.end()
1288 1317
1289 1318 @command(b'perfstartup', formatteropts)
1290 1319 def perfstartup(ui, repo, **opts):
1291 1320 opts = _byteskwargs(opts)
1292 1321 timer, fm = gettimer(ui, opts)
1293 1322 def d():
1294 1323 if os.name != r'nt':
1295 1324 os.system(b"HGRCPATH= %s version -q > /dev/null" %
1296 1325 fsencode(sys.argv[0]))
1297 1326 else:
1298 1327 os.environ[r'HGRCPATH'] = r' '
1299 1328 os.system(r"%s version -q > NUL" % sys.argv[0])
1300 1329 timer(d)
1301 1330 fm.end()
1302 1331
1303 1332 @command(b'perfparents', formatteropts)
1304 1333 def perfparents(ui, repo, **opts):
1305 1334 """benchmark the time necessary to fetch one changeset's parents.
1306 1335
1307 1336 The fetch is done using the `node identifier`, traversing all object layers
1308 1337 from the repository object. The first N revisions will be used for this
1309 1338 benchmark. N is controlled by the ``perf.parentscount`` config option
1310 1339 (default: 1000).
1311 1340 """
1312 1341 opts = _byteskwargs(opts)
1313 1342 timer, fm = gettimer(ui, opts)
1314 1343 # control the number of commits perfparents iterates over
1315 1344 # experimental config: perf.parentscount
1316 1345 count = getint(ui, b"perf", b"parentscount", 1000)
1317 1346 if len(repo.changelog) < count:
1318 1347 raise error.Abort(b"repo needs %d commits for this test" % count)
1319 1348 repo = repo.unfiltered()
1320 1349 nl = [repo.changelog.node(i) for i in _xrange(count)]
1321 1350 def d():
1322 1351 for n in nl:
1323 1352 repo.changelog.parents(n)
1324 1353 timer(d)
1325 1354 fm.end()
1326 1355
1327 1356 @command(b'perfctxfiles', formatteropts)
1328 1357 def perfctxfiles(ui, repo, x, **opts):
1329 1358 opts = _byteskwargs(opts)
1330 1359 x = int(x)
1331 1360 timer, fm = gettimer(ui, opts)
1332 1361 def d():
1333 1362 len(repo[x].files())
1334 1363 timer(d)
1335 1364 fm.end()
1336 1365
1337 1366 @command(b'perfrawfiles', formatteropts)
1338 1367 def perfrawfiles(ui, repo, x, **opts):
1339 1368 opts = _byteskwargs(opts)
1340 1369 x = int(x)
1341 1370 timer, fm = gettimer(ui, opts)
1342 1371 cl = repo.changelog
1343 1372 def d():
1344 1373 len(cl.read(x)[3])
1345 1374 timer(d)
1346 1375 fm.end()
1347 1376
1348 1377 @command(b'perflookup', formatteropts)
1349 1378 def perflookup(ui, repo, rev, **opts):
1350 1379 opts = _byteskwargs(opts)
1351 1380 timer, fm = gettimer(ui, opts)
1352 1381 timer(lambda: len(repo.lookup(rev)))
1353 1382 fm.end()
1354 1383
1355 1384 @command(b'perflinelogedits',
1356 1385 [(b'n', b'edits', 10000, b'number of edits'),
1357 1386 (b'', b'max-hunk-lines', 10, b'max lines in a hunk'),
1358 1387 ], norepo=True)
1359 1388 def perflinelogedits(ui, **opts):
1360 1389 from mercurial import linelog
1361 1390
1362 1391 opts = _byteskwargs(opts)
1363 1392
1364 1393 edits = opts[b'edits']
1365 1394 maxhunklines = opts[b'max_hunk_lines']
1366 1395
1367 1396 maxb1 = 100000
1368 1397 random.seed(0)
1369 1398 randint = random.randint
1370 1399 currentlines = 0
1371 1400 arglist = []
1372 1401 for rev in _xrange(edits):
1373 1402 a1 = randint(0, currentlines)
1374 1403 a2 = randint(a1, min(currentlines, a1 + maxhunklines))
1375 1404 b1 = randint(0, maxb1)
1376 1405 b2 = randint(b1, b1 + maxhunklines)
1377 1406 currentlines += (b2 - b1) - (a2 - a1)
1378 1407 arglist.append((rev, a1, a2, b1, b2))
1379 1408
1380 1409 def d():
1381 1410 ll = linelog.linelog()
1382 1411 for args in arglist:
1383 1412 ll.replacelines(*args)
1384 1413
1385 1414 timer, fm = gettimer(ui, opts)
1386 1415 timer(d)
1387 1416 fm.end()
1388 1417
1389 1418 @command(b'perfrevrange', formatteropts)
1390 1419 def perfrevrange(ui, repo, *specs, **opts):
1391 1420 opts = _byteskwargs(opts)
1392 1421 timer, fm = gettimer(ui, opts)
1393 1422 revrange = scmutil.revrange
1394 1423 timer(lambda: len(revrange(repo, specs)))
1395 1424 fm.end()
1396 1425
1397 1426 @command(b'perfnodelookup', formatteropts)
1398 1427 def perfnodelookup(ui, repo, rev, **opts):
1399 1428 opts = _byteskwargs(opts)
1400 1429 timer, fm = gettimer(ui, opts)
1401 1430 import mercurial.revlog
1402 1431 mercurial.revlog._prereadsize = 2**24 # disable lazy parser in old hg
1403 1432 n = scmutil.revsingle(repo, rev).node()
1404 1433 cl = mercurial.revlog.revlog(getsvfs(repo), b"00changelog.i")
1405 1434 def d():
1406 1435 cl.rev(n)
1407 1436 clearcaches(cl)
1408 1437 timer(d)
1409 1438 fm.end()
1410 1439
1411 1440 @command(b'perflog',
1412 1441 [(b'', b'rename', False, b'ask log to follow renames')
1413 1442 ] + formatteropts)
1414 1443 def perflog(ui, repo, rev=None, **opts):
1415 1444 opts = _byteskwargs(opts)
1416 1445 if rev is None:
1417 1446 rev=[]
1418 1447 timer, fm = gettimer(ui, opts)
1419 1448 ui.pushbuffer()
1420 1449 timer(lambda: commands.log(ui, repo, rev=rev, date=b'', user=b'',
1421 1450 copies=opts.get(b'rename')))
1422 1451 ui.popbuffer()
1423 1452 fm.end()
1424 1453
1425 1454 @command(b'perfmoonwalk', formatteropts)
1426 1455 def perfmoonwalk(ui, repo, **opts):
1427 1456 """benchmark walking the changelog backwards
1428 1457
1429 1458 This also loads the changelog data for each revision in the changelog.
1430 1459 """
1431 1460 opts = _byteskwargs(opts)
1432 1461 timer, fm = gettimer(ui, opts)
1433 1462 def moonwalk():
1434 1463 for i in repo.changelog.revs(start=(len(repo) - 1), stop=-1):
1435 1464 ctx = repo[i]
1436 1465 ctx.branch() # read changelog data (in addition to the index)
1437 1466 timer(moonwalk)
1438 1467 fm.end()
1439 1468
1440 1469 @command(b'perftemplating',
1441 1470 [(b'r', b'rev', [], b'revisions to run the template on'),
1442 1471 ] + formatteropts)
1443 1472 def perftemplating(ui, repo, testedtemplate=None, **opts):
1444 1473 """test the rendering time of a given template"""
1445 1474 if makelogtemplater is None:
1446 1475 raise error.Abort((b"perftemplating not available with this Mercurial"),
1447 1476 hint=b"use 4.3 or later")
1448 1477
1449 1478 opts = _byteskwargs(opts)
1450 1479
1451 1480 nullui = ui.copy()
1452 1481 nullui.fout = open(os.devnull, r'wb')
1453 1482 nullui.disablepager()
1454 1483 revs = opts.get(b'rev')
1455 1484 if not revs:
1456 1485 revs = [b'all()']
1457 1486 revs = list(scmutil.revrange(repo, revs))
1458 1487
1459 1488 defaulttemplate = (b'{date|shortdate} [{rev}:{node|short}]'
1460 1489 b' {author|person}: {desc|firstline}\n')
1461 1490 if testedtemplate is None:
1462 1491 testedtemplate = defaulttemplate
1463 1492 displayer = makelogtemplater(nullui, repo, testedtemplate)
1464 1493 def format():
1465 1494 for r in revs:
1466 1495 ctx = repo[r]
1467 1496 displayer.show(ctx)
1468 1497 displayer.flush(ctx)
1469 1498
1470 1499 timer, fm = gettimer(ui, opts)
1471 1500 timer(format)
1472 1501 fm.end()
1473 1502
1474 1503 @command(b'perfhelper-mergecopies', formatteropts +
1475 1504 [
1476 1505 (b'r', b'revs', [], b'restrict search to these revisions'),
1477 1506 (b'', b'timing', False, b'provides extra data (costly)'),
1478 1507 ])
1479 1508 def perfhelpermergecopies(ui, repo, revs=[], **opts):
1480 1509 """find statistics about potential parameters for `perfmergecopies`
1481 1510
1482 1511 This command find (base, p1, p2) triplet relevant for copytracing
1483 1512 benchmarking in the context of a merge. It reports values for some of the
1484 1513 parameters that impact merge copy tracing time during merge.
1485 1514
1486 1515 If `--timing` is set, rename detection is run and the associated timing
1487 1516 will be reported. The extra details come at the cost of slower command
1488 1517 execution.
1489 1518
1490 1519 Since rename detection is only run once, other factors might easily
1491 1520 affect the precision of the timing. However it should give a good
1492 1521 approximation of which revision triplets are very costly.
1493 1522 """
1494 1523 opts = _byteskwargs(opts)
1495 1524 fm = ui.formatter(b'perf', opts)
1496 1525 dotiming = opts[b'timing']
1497 1526
1498 1527 output_template = [
1499 1528 ("base", "%(base)12s"),
1500 1529 ("p1", "%(p1.node)12s"),
1501 1530 ("p2", "%(p2.node)12s"),
1502 1531 ("p1.nb-revs", "%(p1.nbrevs)12d"),
1503 1532 ("p1.nb-files", "%(p1.nbmissingfiles)12d"),
1504 1533 ("p1.renames", "%(p1.renamedfiles)12d"),
1505 1534 ("p1.time", "%(p1.time)12.3f"),
1506 1535 ("p2.nb-revs", "%(p2.nbrevs)12d"),
1507 1536 ("p2.nb-files", "%(p2.nbmissingfiles)12d"),
1508 1537 ("p2.renames", "%(p2.renamedfiles)12d"),
1509 1538 ("p2.time", "%(p2.time)12.3f"),
1510 1539 ("renames", "%(nbrenamedfiles)12d"),
1511 1540 ("total.time", "%(time)12.3f"),
1512 1541 ]
1513 1542 if not dotiming:
1514 1543 output_template = [i for i in output_template
1515 1544 if not ('time' in i[0] or 'renames' in i[0])]
1516 1545 header_names = [h for (h, v) in output_template]
1517 1546 output = ' '.join([v for (h, v) in output_template]) + '\n'
1518 1547 header = ' '.join(['%12s'] * len(header_names)) + '\n'
1519 1548 fm.plain(header % tuple(header_names))
1520 1549
1521 1550 if not revs:
1522 1551 revs = ['all()']
1523 1552 revs = scmutil.revrange(repo, revs)
1524 1553
1525 1554 roi = repo.revs('merge() and %ld', revs)
1526 1555 for r in roi:
1527 1556 ctx = repo[r]
1528 1557 p1 = ctx.p1()
1529 1558 p2 = ctx.p2()
1530 1559 bases = repo.changelog._commonancestorsheads(p1.rev(), p2.rev())
1531 1560 for b in bases:
1532 1561 b = repo[b]
1533 1562 p1missing = copies._computeforwardmissing(b, p1)
1534 1563 p2missing = copies._computeforwardmissing(b, p2)
1535 1564 data = {
1536 1565 b'base': b.hex(),
1537 1566 b'p1.node': p1.hex(),
1538 1567 b'p1.nbrevs': len(repo.revs('%d::%d', b.rev(), p1.rev())),
1539 1568 b'p1.nbmissingfiles': len(p1missing),
1540 1569 b'p2.node': p2.hex(),
1541 1570 b'p2.nbrevs': len(repo.revs('%d::%d', b.rev(), p2.rev())),
1542 1571 b'p2.nbmissingfiles': len(p2missing),
1543 1572 }
1544 1573 if dotiming:
1545 1574 begin = util.timer()
1546 1575 mergedata = copies.mergecopies(repo, p1, p2, b)
1547 1576 end = util.timer()
1548 1577 # not very stable timing since we did only one run
1549 1578 data['time'] = end - begin
1550 1579 # mergedata contains five dicts: "copy", "movewithdir",
1551 1580 # "diverge", "renamedelete" and "dirmove".
1552 1581 # The first 4 are about renamed file so lets count that.
1553 1582 renames = len(mergedata[0])
1554 1583 renames += len(mergedata[1])
1555 1584 renames += len(mergedata[2])
1556 1585 renames += len(mergedata[3])
1557 1586 data['nbrenamedfiles'] = renames
1558 1587 begin = util.timer()
1559 1588 p1renames = copies.pathcopies(b, p1)
1560 1589 end = util.timer()
1561 1590 data['p1.time'] = end - begin
1562 1591 begin = util.timer()
1563 1592 p2renames = copies.pathcopies(b, p2)
1564 1593 data['p2.time'] = end - begin
1565 1594 end = util.timer()
1566 1595 data['p1.renamedfiles'] = len(p1renames)
1567 1596 data['p2.renamedfiles'] = len(p2renames)
1568 1597 fm.startitem()
1569 1598 fm.data(**data)
1570 1599 # make node pretty for the human output
1571 1600 out = data.copy()
1572 1601 out['base'] = fm.hexfunc(b.node())
1573 1602 out['p1.node'] = fm.hexfunc(p1.node())
1574 1603 out['p2.node'] = fm.hexfunc(p2.node())
1575 1604 fm.plain(output % out)
1576 1605
1577 1606 fm.end()
1578 1607
1579 1608 @command(b'perfhelper-pathcopies', formatteropts +
1580 1609 [
1581 1610 (b'r', b'revs', [], b'restrict search to these revisions'),
1582 1611 (b'', b'timing', False, b'provides extra data (costly)'),
1583 1612 ])
1584 1613 def perfhelperpathcopies(ui, repo, revs=[], **opts):
1585 1614 """find statistic about potential parameters for the `perftracecopies`
1586 1615
1587 1616 This command find source-destination pair relevant for copytracing testing.
1588 1617 It report value for some of the parameters that impact copy tracing time.
1589 1618
1590 1619 If `--timing` is set, rename detection is run and the associated timing
1591 1620 will be reported. The extra details comes at the cost of a slower command
1592 1621 execution.
1593 1622
1594 1623 Since the rename detection is only run once, other factors might easily
1595 1624 affect the precision of the timing. However it should give a good
1596 1625 approximation of which revision pairs are very costly.
1597 1626 """
1598 1627 opts = _byteskwargs(opts)
1599 1628 fm = ui.formatter(b'perf', opts)
1600 1629 dotiming = opts[b'timing']
1601 1630
1602 1631 if dotiming:
1603 1632 header = '%12s %12s %12s %12s %12s %12s\n'
1604 1633 output = ("%(source)12s %(destination)12s "
1605 1634 "%(nbrevs)12d %(nbmissingfiles)12d "
1606 1635 "%(nbrenamedfiles)12d %(time)18.5f\n")
1607 1636 header_names = ("source", "destination", "nb-revs", "nb-files",
1608 1637 "nb-renames", "time")
1609 1638 fm.plain(header % header_names)
1610 1639 else:
1611 1640 header = '%12s %12s %12s %12s\n'
1612 1641 output = ("%(source)12s %(destination)12s "
1613 1642 "%(nbrevs)12d %(nbmissingfiles)12d\n")
1614 1643 fm.plain(header % ("source", "destination", "nb-revs", "nb-files"))
1615 1644
1616 1645 if not revs:
1617 1646 revs = ['all()']
1618 1647 revs = scmutil.revrange(repo, revs)
1619 1648
1620 1649 roi = repo.revs('merge() and %ld', revs)
1621 1650 for r in roi:
1622 1651 ctx = repo[r]
1623 1652 p1 = ctx.p1().rev()
1624 1653 p2 = ctx.p2().rev()
1625 1654 bases = repo.changelog._commonancestorsheads(p1, p2)
1626 1655 for p in (p1, p2):
1627 1656 for b in bases:
1628 1657 base = repo[b]
1629 1658 parent = repo[p]
1630 1659 missing = copies._computeforwardmissing(base, parent)
1631 1660 if not missing:
1632 1661 continue
1633 1662 data = {
1634 1663 b'source': base.hex(),
1635 1664 b'destination': parent.hex(),
1636 1665 b'nbrevs': len(repo.revs('%d::%d', b, p)),
1637 1666 b'nbmissingfiles': len(missing),
1638 1667 }
1639 1668 if dotiming:
1640 1669 begin = util.timer()
1641 1670 renames = copies.pathcopies(base, parent)
1642 1671 end = util.timer()
1643 1672 # not very stable timing since we did only one run
1644 1673 data['time'] = end - begin
1645 1674 data['nbrenamedfiles'] = len(renames)
1646 1675 fm.startitem()
1647 1676 fm.data(**data)
1648 1677 out = data.copy()
1649 1678 out['source'] = fm.hexfunc(base.node())
1650 1679 out['destination'] = fm.hexfunc(parent.node())
1651 1680 fm.plain(output % out)
1652 1681
1653 1682 fm.end()
1654 1683
1655 1684 @command(b'perfcca', formatteropts)
1656 1685 def perfcca(ui, repo, **opts):
1657 1686 opts = _byteskwargs(opts)
1658 1687 timer, fm = gettimer(ui, opts)
1659 1688 timer(lambda: scmutil.casecollisionauditor(ui, False, repo.dirstate))
1660 1689 fm.end()
1661 1690
1662 1691 @command(b'perffncacheload', formatteropts)
1663 1692 def perffncacheload(ui, repo, **opts):
1664 1693 opts = _byteskwargs(opts)
1665 1694 timer, fm = gettimer(ui, opts)
1666 1695 s = repo.store
1667 1696 def d():
1668 1697 s.fncache._load()
1669 1698 timer(d)
1670 1699 fm.end()
1671 1700
1672 1701 @command(b'perffncachewrite', formatteropts)
1673 1702 def perffncachewrite(ui, repo, **opts):
1674 1703 opts = _byteskwargs(opts)
1675 1704 timer, fm = gettimer(ui, opts)
1676 1705 s = repo.store
1677 1706 lock = repo.lock()
1678 1707 s.fncache._load()
1679 1708 tr = repo.transaction(b'perffncachewrite')
1680 1709 tr.addbackup(b'fncache')
1681 1710 def d():
1682 1711 s.fncache._dirty = True
1683 1712 s.fncache.write(tr)
1684 1713 timer(d)
1685 1714 tr.close()
1686 1715 lock.release()
1687 1716 fm.end()
1688 1717
1689 1718 @command(b'perffncacheencode', formatteropts)
1690 1719 def perffncacheencode(ui, repo, **opts):
1691 1720 opts = _byteskwargs(opts)
1692 1721 timer, fm = gettimer(ui, opts)
1693 1722 s = repo.store
1694 1723 s.fncache._load()
1695 1724 def d():
1696 1725 for p in s.fncache.entries:
1697 1726 s.encode(p)
1698 1727 timer(d)
1699 1728 fm.end()
1700 1729
1701 1730 def _bdiffworker(q, blocks, xdiff, ready, done):
1702 1731 while not done.is_set():
1703 1732 pair = q.get()
1704 1733 while pair is not None:
1705 1734 if xdiff:
1706 1735 mdiff.bdiff.xdiffblocks(*pair)
1707 1736 elif blocks:
1708 1737 mdiff.bdiff.blocks(*pair)
1709 1738 else:
1710 1739 mdiff.textdiff(*pair)
1711 1740 q.task_done()
1712 1741 pair = q.get()
1713 1742 q.task_done() # for the None one
1714 1743 with ready:
1715 1744 ready.wait()
1716 1745
1717 1746 def _manifestrevision(repo, mnode):
1718 1747 ml = repo.manifestlog
1719 1748
1720 1749 if util.safehasattr(ml, b'getstorage'):
1721 1750 store = ml.getstorage(b'')
1722 1751 else:
1723 1752 store = ml._revlog
1724 1753
1725 1754 return store.revision(mnode)
1726 1755
1727 1756 @command(b'perfbdiff', revlogopts + formatteropts + [
1728 1757 (b'', b'count', 1, b'number of revisions to test (when using --startrev)'),
1729 1758 (b'', b'alldata', False, b'test bdiffs for all associated revisions'),
1730 1759 (b'', b'threads', 0, b'number of thread to use (disable with 0)'),
1731 1760 (b'', b'blocks', False, b'test computing diffs into blocks'),
1732 1761 (b'', b'xdiff', False, b'use xdiff algorithm'),
1733 1762 ],
1734 1763
1735 1764 b'-c|-m|FILE REV')
1736 1765 def perfbdiff(ui, repo, file_, rev=None, count=None, threads=0, **opts):
1737 1766 """benchmark a bdiff between revisions
1738 1767
1739 1768 By default, benchmark a bdiff between its delta parent and itself.
1740 1769
1741 1770 With ``--count``, benchmark bdiffs between delta parents and self for N
1742 1771 revisions starting at the specified revision.
1743 1772
1744 1773 With ``--alldata``, assume the requested revision is a changeset and
1745 1774 measure bdiffs for all changes related to that changeset (manifest
1746 1775 and filelogs).
1747 1776 """
1748 1777 opts = _byteskwargs(opts)
1749 1778
1750 1779 if opts[b'xdiff'] and not opts[b'blocks']:
1751 1780 raise error.CommandError(b'perfbdiff', b'--xdiff requires --blocks')
1752 1781
1753 1782 if opts[b'alldata']:
1754 1783 opts[b'changelog'] = True
1755 1784
1756 1785 if opts.get(b'changelog') or opts.get(b'manifest'):
1757 1786 file_, rev = None, file_
1758 1787 elif rev is None:
1759 1788 raise error.CommandError(b'perfbdiff', b'invalid arguments')
1760 1789
1761 1790 blocks = opts[b'blocks']
1762 1791 xdiff = opts[b'xdiff']
1763 1792 textpairs = []
1764 1793
1765 1794 r = cmdutil.openrevlog(repo, b'perfbdiff', file_, opts)
1766 1795
1767 1796 startrev = r.rev(r.lookup(rev))
1768 1797 for rev in range(startrev, min(startrev + count, len(r) - 1)):
1769 1798 if opts[b'alldata']:
1770 1799 # Load revisions associated with changeset.
1771 1800 ctx = repo[rev]
1772 1801 mtext = _manifestrevision(repo, ctx.manifestnode())
1773 1802 for pctx in ctx.parents():
1774 1803 pman = _manifestrevision(repo, pctx.manifestnode())
1775 1804 textpairs.append((pman, mtext))
1776 1805
1777 1806 # Load filelog revisions by iterating manifest delta.
1778 1807 man = ctx.manifest()
1779 1808 pman = ctx.p1().manifest()
1780 1809 for filename, change in pman.diff(man).items():
1781 1810 fctx = repo.file(filename)
1782 1811 f1 = fctx.revision(change[0][0] or -1)
1783 1812 f2 = fctx.revision(change[1][0] or -1)
1784 1813 textpairs.append((f1, f2))
1785 1814 else:
1786 1815 dp = r.deltaparent(rev)
1787 1816 textpairs.append((r.revision(dp), r.revision(rev)))
1788 1817
1789 1818 withthreads = threads > 0
1790 1819 if not withthreads:
1791 1820 def d():
1792 1821 for pair in textpairs:
1793 1822 if xdiff:
1794 1823 mdiff.bdiff.xdiffblocks(*pair)
1795 1824 elif blocks:
1796 1825 mdiff.bdiff.blocks(*pair)
1797 1826 else:
1798 1827 mdiff.textdiff(*pair)
1799 1828 else:
1800 1829 q = queue()
1801 1830 for i in _xrange(threads):
1802 1831 q.put(None)
1803 1832 ready = threading.Condition()
1804 1833 done = threading.Event()
1805 1834 for i in _xrange(threads):
1806 1835 threading.Thread(target=_bdiffworker,
1807 1836 args=(q, blocks, xdiff, ready, done)).start()
1808 1837 q.join()
1809 1838 def d():
1810 1839 for pair in textpairs:
1811 1840 q.put(pair)
1812 1841 for i in _xrange(threads):
1813 1842 q.put(None)
1814 1843 with ready:
1815 1844 ready.notify_all()
1816 1845 q.join()
1817 1846 timer, fm = gettimer(ui, opts)
1818 1847 timer(d)
1819 1848 fm.end()
1820 1849
1821 1850 if withthreads:
1822 1851 done.set()
1823 1852 for i in _xrange(threads):
1824 1853 q.put(None)
1825 1854 with ready:
1826 1855 ready.notify_all()
1827 1856
1828 1857 @command(b'perfunidiff', revlogopts + formatteropts + [
1829 1858 (b'', b'count', 1, b'number of revisions to test (when using --startrev)'),
1830 1859 (b'', b'alldata', False, b'test unidiffs for all associated revisions'),
1831 1860 ], b'-c|-m|FILE REV')
1832 1861 def perfunidiff(ui, repo, file_, rev=None, count=None, **opts):
1833 1862 """benchmark a unified diff between revisions
1834 1863
1835 1864 This doesn't include any copy tracing - it's just a unified diff
1836 1865 of the texts.
1837 1866
1838 1867 By default, benchmark a diff between its delta parent and itself.
1839 1868
1840 1869 With ``--count``, benchmark diffs between delta parents and self for N
1841 1870 revisions starting at the specified revision.
1842 1871
1843 1872 With ``--alldata``, assume the requested revision is a changeset and
1844 1873 measure diffs for all changes related to that changeset (manifest
1845 1874 and filelogs).
1846 1875 """
1847 1876 opts = _byteskwargs(opts)
1848 1877 if opts[b'alldata']:
1849 1878 opts[b'changelog'] = True
1850 1879
1851 1880 if opts.get(b'changelog') or opts.get(b'manifest'):
1852 1881 file_, rev = None, file_
1853 1882 elif rev is None:
1854 1883 raise error.CommandError(b'perfunidiff', b'invalid arguments')
1855 1884
1856 1885 textpairs = []
1857 1886
1858 1887 r = cmdutil.openrevlog(repo, b'perfunidiff', file_, opts)
1859 1888
1860 1889 startrev = r.rev(r.lookup(rev))
1861 1890 for rev in range(startrev, min(startrev + count, len(r) - 1)):
1862 1891 if opts[b'alldata']:
1863 1892 # Load revisions associated with changeset.
1864 1893 ctx = repo[rev]
1865 1894 mtext = _manifestrevision(repo, ctx.manifestnode())
1866 1895 for pctx in ctx.parents():
1867 1896 pman = _manifestrevision(repo, pctx.manifestnode())
1868 1897 textpairs.append((pman, mtext))
1869 1898
1870 1899 # Load filelog revisions by iterating manifest delta.
1871 1900 man = ctx.manifest()
1872 1901 pman = ctx.p1().manifest()
1873 1902 for filename, change in pman.diff(man).items():
1874 1903 fctx = repo.file(filename)
1875 1904 f1 = fctx.revision(change[0][0] or -1)
1876 1905 f2 = fctx.revision(change[1][0] or -1)
1877 1906 textpairs.append((f1, f2))
1878 1907 else:
1879 1908 dp = r.deltaparent(rev)
1880 1909 textpairs.append((r.revision(dp), r.revision(rev)))
1881 1910
1882 1911 def d():
1883 1912 for left, right in textpairs:
1884 1913 # The date strings don't matter, so we pass empty strings.
1885 1914 headerlines, hunks = mdiff.unidiff(
1886 1915 left, b'', right, b'', b'left', b'right', binary=False)
1887 1916 # consume iterators in roughly the way patch.py does
1888 1917 b'\n'.join(headerlines)
1889 1918 b''.join(sum((list(hlines) for hrange, hlines in hunks), []))
1890 1919 timer, fm = gettimer(ui, opts)
1891 1920 timer(d)
1892 1921 fm.end()
1893 1922
1894 1923 @command(b'perfdiffwd', formatteropts)
1895 1924 def perfdiffwd(ui, repo, **opts):
1896 1925 """Profile diff of working directory changes"""
1897 1926 opts = _byteskwargs(opts)
1898 1927 timer, fm = gettimer(ui, opts)
1899 1928 options = {
1900 1929 'w': 'ignore_all_space',
1901 1930 'b': 'ignore_space_change',
1902 1931 'B': 'ignore_blank_lines',
1903 1932 }
1904 1933
1905 1934 for diffopt in ('', 'w', 'b', 'B', 'wB'):
1906 1935 opts = dict((options[c], b'1') for c in diffopt)
1907 1936 def d():
1908 1937 ui.pushbuffer()
1909 1938 commands.diff(ui, repo, **opts)
1910 1939 ui.popbuffer()
1911 1940 diffopt = diffopt.encode('ascii')
1912 1941 title = b'diffopts: %s' % (diffopt and (b'-' + diffopt) or b'none')
1913 1942 timer(d, title=title)
1914 1943 fm.end()
1915 1944
1916 1945 @command(b'perfrevlogindex', revlogopts + formatteropts,
1917 1946 b'-c|-m|FILE')
1918 1947 def perfrevlogindex(ui, repo, file_=None, **opts):
1919 1948 """Benchmark operations against a revlog index.
1920 1949
1921 1950 This tests constructing a revlog instance, reading index data,
1922 1951 parsing index data, and performing various operations related to
1923 1952 index data.
1924 1953 """
1925 1954
1926 1955 opts = _byteskwargs(opts)
1927 1956
1928 1957 rl = cmdutil.openrevlog(repo, b'perfrevlogindex', file_, opts)
1929 1958
1930 1959 opener = getattr(rl, 'opener') # trick linter
1931 1960 indexfile = rl.indexfile
1932 1961 data = opener.read(indexfile)
1933 1962
1934 1963 header = struct.unpack(b'>I', data[0:4])[0]
1935 1964 version = header & 0xFFFF
1936 1965 if version == 1:
1937 1966 revlogio = revlog.revlogio()
1938 1967 inline = header & (1 << 16)
1939 1968 else:
1940 1969 raise error.Abort((b'unsupported revlog version: %d') % version)
1941 1970
1942 1971 rllen = len(rl)
1943 1972
1944 1973 node0 = rl.node(0)
1945 1974 node25 = rl.node(rllen // 4)
1946 1975 node50 = rl.node(rllen // 2)
1947 1976 node75 = rl.node(rllen // 4 * 3)
1948 1977 node100 = rl.node(rllen - 1)
1949 1978
1950 1979 allrevs = range(rllen)
1951 1980 allrevsrev = list(reversed(allrevs))
1952 1981 allnodes = [rl.node(rev) for rev in range(rllen)]
1953 1982 allnodesrev = list(reversed(allnodes))
1954 1983
1955 1984 def constructor():
1956 1985 revlog.revlog(opener, indexfile)
1957 1986
1958 1987 def read():
1959 1988 with opener(indexfile) as fh:
1960 1989 fh.read()
1961 1990
1962 1991 def parseindex():
1963 1992 revlogio.parseindex(data, inline)
1964 1993
1965 1994 def getentry(revornode):
1966 1995 index = revlogio.parseindex(data, inline)[0]
1967 1996 index[revornode]
1968 1997
1969 1998 def getentries(revs, count=1):
1970 1999 index = revlogio.parseindex(data, inline)[0]
1971 2000
1972 2001 for i in range(count):
1973 2002 for rev in revs:
1974 2003 index[rev]
1975 2004
1976 2005 def resolvenode(node):
1977 2006 nodemap = revlogio.parseindex(data, inline)[1]
1978 2007 # This only works for the C code.
1979 2008 if nodemap is None:
1980 2009 return
1981 2010
1982 2011 try:
1983 2012 nodemap[node]
1984 2013 except error.RevlogError:
1985 2014 pass
1986 2015
1987 2016 def resolvenodes(nodes, count=1):
1988 2017 nodemap = revlogio.parseindex(data, inline)[1]
1989 2018 if nodemap is None:
1990 2019 return
1991 2020
1992 2021 for i in range(count):
1993 2022 for node in nodes:
1994 2023 try:
1995 2024 nodemap[node]
1996 2025 except error.RevlogError:
1997 2026 pass
1998 2027
1999 2028 benches = [
2000 2029 (constructor, b'revlog constructor'),
2001 2030 (read, b'read'),
2002 2031 (parseindex, b'create index object'),
2003 2032 (lambda: getentry(0), b'retrieve index entry for rev 0'),
2004 2033 (lambda: resolvenode(b'a' * 20), b'look up missing node'),
2005 2034 (lambda: resolvenode(node0), b'look up node at rev 0'),
2006 2035 (lambda: resolvenode(node25), b'look up node at 1/4 len'),
2007 2036 (lambda: resolvenode(node50), b'look up node at 1/2 len'),
2008 2037 (lambda: resolvenode(node75), b'look up node at 3/4 len'),
2009 2038 (lambda: resolvenode(node100), b'look up node at tip'),
2010 2039 # 2x variation is to measure caching impact.
2011 2040 (lambda: resolvenodes(allnodes),
2012 2041 b'look up all nodes (forward)'),
2013 2042 (lambda: resolvenodes(allnodes, 2),
2014 2043 b'look up all nodes 2x (forward)'),
2015 2044 (lambda: resolvenodes(allnodesrev),
2016 2045 b'look up all nodes (reverse)'),
2017 2046 (lambda: resolvenodes(allnodesrev, 2),
2018 2047 b'look up all nodes 2x (reverse)'),
2019 2048 (lambda: getentries(allrevs),
2020 2049 b'retrieve all index entries (forward)'),
2021 2050 (lambda: getentries(allrevs, 2),
2022 2051 b'retrieve all index entries 2x (forward)'),
2023 2052 (lambda: getentries(allrevsrev),
2024 2053 b'retrieve all index entries (reverse)'),
2025 2054 (lambda: getentries(allrevsrev, 2),
2026 2055 b'retrieve all index entries 2x (reverse)'),
2027 2056 ]
2028 2057
2029 2058 for fn, title in benches:
2030 2059 timer, fm = gettimer(ui, opts)
2031 2060 timer(fn, title=title)
2032 2061 fm.end()
2033 2062
2034 2063 @command(b'perfrevlogrevisions', revlogopts + formatteropts +
2035 2064 [(b'd', b'dist', 100, b'distance between the revisions'),
2036 2065 (b's', b'startrev', 0, b'revision to start reading at'),
2037 2066 (b'', b'reverse', False, b'read in reverse')],
2038 2067 b'-c|-m|FILE')
2039 2068 def perfrevlogrevisions(ui, repo, file_=None, startrev=0, reverse=False,
2040 2069 **opts):
2041 2070 """Benchmark reading a series of revisions from a revlog.
2042 2071
2043 2072 By default, we read every ``-d/--dist`` revision from 0 to tip of
2044 2073 the specified revlog.
2045 2074
2046 2075 The start revision can be defined via ``-s/--startrev``.
2047 2076 """
2048 2077 opts = _byteskwargs(opts)
2049 2078
2050 2079 rl = cmdutil.openrevlog(repo, b'perfrevlogrevisions', file_, opts)
2051 2080 rllen = getlen(ui)(rl)
2052 2081
2053 2082 if startrev < 0:
2054 2083 startrev = rllen + startrev
2055 2084
2056 2085 def d():
2057 2086 rl.clearcaches()
2058 2087
2059 2088 beginrev = startrev
2060 2089 endrev = rllen
2061 2090 dist = opts[b'dist']
2062 2091
2063 2092 if reverse:
2064 2093 beginrev, endrev = endrev - 1, beginrev - 1
2065 2094 dist = -1 * dist
2066 2095
2067 2096 for x in _xrange(beginrev, endrev, dist):
2068 2097 # Old revisions don't support passing int.
2069 2098 n = rl.node(x)
2070 2099 rl.revision(n)
2071 2100
2072 2101 timer, fm = gettimer(ui, opts)
2073 2102 timer(d)
2074 2103 fm.end()
2075 2104
2076 2105 @command(b'perfrevlogwrite', revlogopts + formatteropts +
2077 2106 [(b's', b'startrev', 1000, b'revision to start writing at'),
2078 2107 (b'', b'stoprev', -1, b'last revision to write'),
2079 2108 (b'', b'count', 3, b'number of passes to perform'),
2080 2109 (b'', b'details', False, b'print timing for every revisions tested'),
2081 2110 (b'', b'source', b'full', b'the kind of data feed in the revlog'),
2082 2111 (b'', b'lazydeltabase', True, b'try the provided delta first'),
2083 2112 (b'', b'clear-caches', True, b'clear revlog cache between calls'),
2084 2113 ],
2085 2114 b'-c|-m|FILE')
2086 2115 def perfrevlogwrite(ui, repo, file_=None, startrev=1000, stoprev=-1, **opts):
2087 2116 """Benchmark writing a series of revisions to a revlog.
2088 2117
2089 2118 Possible source values are:
2090 2119 * `full`: add from a full text (default).
2091 2120 * `parent-1`: add from a delta to the first parent
2092 2121 * `parent-2`: add from a delta to the second parent if it exists
2093 2122 (use a delta from the first parent otherwise)
2094 2123 * `parent-smallest`: add from the smallest delta (either p1 or p2)
2095 2124 * `storage`: add from the existing precomputed deltas
2096 2125
2097 2126 Note: This performance command measures performance in a custom way. As a
2098 2127 result some of the global configuration of the 'perf' command does not
2099 2128 apply to it:
2100 2129
2101 2130 * ``pre-run``: disabled
2102 2131
2103 2132 * ``profile-benchmark``: disabled
2104 2133
2105 2134 * ``run-limits``: disabled use --count instead
2106 2135 """
2107 2136 opts = _byteskwargs(opts)
2108 2137
2109 2138 rl = cmdutil.openrevlog(repo, b'perfrevlogwrite', file_, opts)
2110 2139 rllen = getlen(ui)(rl)
2111 2140 if startrev < 0:
2112 2141 startrev = rllen + startrev
2113 2142 if stoprev < 0:
2114 2143 stoprev = rllen + stoprev
2115 2144
2116 2145 lazydeltabase = opts['lazydeltabase']
2117 2146 source = opts['source']
2118 2147 clearcaches = opts['clear_caches']
2119 2148 validsource = (b'full', b'parent-1', b'parent-2', b'parent-smallest',
2120 2149 b'storage')
2121 2150 if source not in validsource:
2122 2151 raise error.Abort('invalid source type: %s' % source)
2123 2152
2124 2153 ### actually gather results
2125 2154 count = opts['count']
2126 2155 if count <= 0:
2127 2156 raise error.Abort('invalide run count: %d' % count)
2128 2157 allresults = []
2129 2158 for c in range(count):
2130 2159 timing = _timeonewrite(ui, rl, source, startrev, stoprev, c + 1,
2131 2160 lazydeltabase=lazydeltabase,
2132 2161 clearcaches=clearcaches)
2133 2162 allresults.append(timing)
2134 2163
2135 2164 ### consolidate the results in a single list
2136 2165 results = []
2137 2166 for idx, (rev, t) in enumerate(allresults[0]):
2138 2167 ts = [t]
2139 2168 for other in allresults[1:]:
2140 2169 orev, ot = other[idx]
2141 2170 assert orev == rev
2142 2171 ts.append(ot)
2143 2172 results.append((rev, ts))
2144 2173 resultcount = len(results)
2145 2174
2146 2175 ### Compute and display relevant statistics
2147 2176
2148 2177 # get a formatter
2149 2178 fm = ui.formatter(b'perf', opts)
2150 2179 displayall = ui.configbool(b"perf", b"all-timing", False)
2151 2180
2152 2181 # print individual details if requested
2153 2182 if opts['details']:
2154 2183 for idx, item in enumerate(results, 1):
2155 2184 rev, data = item
2156 2185 title = 'revisions #%d of %d, rev %d' % (idx, resultcount, rev)
2157 2186 formatone(fm, data, title=title, displayall=displayall)
2158 2187
2159 2188 # sorts results by median time
2160 2189 results.sort(key=lambda x: sorted(x[1])[len(x[1]) // 2])
2161 2190 # list of (name, index) to display)
2162 2191 relevants = [
2163 2192 ("min", 0),
2164 2193 ("10%", resultcount * 10 // 100),
2165 2194 ("25%", resultcount * 25 // 100),
2166 2195 ("50%", resultcount * 70 // 100),
2167 2196 ("75%", resultcount * 75 // 100),
2168 2197 ("90%", resultcount * 90 // 100),
2169 2198 ("95%", resultcount * 95 // 100),
2170 2199 ("99%", resultcount * 99 // 100),
2171 2200 ("99.9%", resultcount * 999 // 1000),
2172 2201 ("99.99%", resultcount * 9999 // 10000),
2173 2202 ("99.999%", resultcount * 99999 // 100000),
2174 2203 ("max", -1),
2175 2204 ]
2176 2205 if not ui.quiet:
2177 2206 for name, idx in relevants:
2178 2207 data = results[idx]
2179 2208 title = '%s of %d, rev %d' % (name, resultcount, data[0])
2180 2209 formatone(fm, data[1], title=title, displayall=displayall)
2181 2210
2182 2211 # XXX summing that many float will not be very precise, we ignore this fact
2183 2212 # for now
2184 2213 totaltime = []
2185 2214 for item in allresults:
2186 2215 totaltime.append((sum(x[1][0] for x in item),
2187 2216 sum(x[1][1] for x in item),
2188 2217 sum(x[1][2] for x in item),)
2189 2218 )
2190 2219 formatone(fm, totaltime, title="total time (%d revs)" % resultcount,
2191 2220 displayall=displayall)
2192 2221 fm.end()
2193 2222
2194 2223 class _faketr(object):
2195 2224 def add(s, x, y, z=None):
2196 2225 return None
2197 2226
2198 2227 def _timeonewrite(ui, orig, source, startrev, stoprev, runidx=None,
2199 2228 lazydeltabase=True, clearcaches=True):
2200 2229 timings = []
2201 2230 tr = _faketr()
2202 2231 with _temprevlog(ui, orig, startrev) as dest:
2203 2232 dest._lazydeltabase = lazydeltabase
2204 2233 revs = list(orig.revs(startrev, stoprev))
2205 2234 total = len(revs)
2206 2235 topic = 'adding'
2207 2236 if runidx is not None:
2208 2237 topic += ' (run #%d)' % runidx
2209 2238 # Support both old and new progress API
2210 2239 if util.safehasattr(ui, 'makeprogress'):
2211 2240 progress = ui.makeprogress(topic, unit='revs', total=total)
2212 2241 def updateprogress(pos):
2213 2242 progress.update(pos)
2214 2243 def completeprogress():
2215 2244 progress.complete()
2216 2245 else:
2217 2246 def updateprogress(pos):
2218 2247 ui.progress(topic, pos, unit='revs', total=total)
2219 2248 def completeprogress():
2220 2249 ui.progress(topic, None, unit='revs', total=total)
2221 2250
2222 2251 for idx, rev in enumerate(revs):
2223 2252 updateprogress(idx)
2224 2253 addargs, addkwargs = _getrevisionseed(orig, rev, tr, source)
2225 2254 if clearcaches:
2226 2255 dest.index.clearcaches()
2227 2256 dest.clearcaches()
2228 2257 with timeone() as r:
2229 2258 dest.addrawrevision(*addargs, **addkwargs)
2230 2259 timings.append((rev, r[0]))
2231 2260 updateprogress(total)
2232 2261 completeprogress()
2233 2262 return timings
2234 2263
2235 2264 def _getrevisionseed(orig, rev, tr, source):
2236 2265 from mercurial.node import nullid
2237 2266
2238 2267 linkrev = orig.linkrev(rev)
2239 2268 node = orig.node(rev)
2240 2269 p1, p2 = orig.parents(node)
2241 2270 flags = orig.flags(rev)
2242 2271 cachedelta = None
2243 2272 text = None
2244 2273
2245 2274 if source == b'full':
2246 2275 text = orig.revision(rev)
2247 2276 elif source == b'parent-1':
2248 2277 baserev = orig.rev(p1)
2249 2278 cachedelta = (baserev, orig.revdiff(p1, rev))
2250 2279 elif source == b'parent-2':
2251 2280 parent = p2
2252 2281 if p2 == nullid:
2253 2282 parent = p1
2254 2283 baserev = orig.rev(parent)
2255 2284 cachedelta = (baserev, orig.revdiff(parent, rev))
2256 2285 elif source == b'parent-smallest':
2257 2286 p1diff = orig.revdiff(p1, rev)
2258 2287 parent = p1
2259 2288 diff = p1diff
2260 2289 if p2 != nullid:
2261 2290 p2diff = orig.revdiff(p2, rev)
2262 2291 if len(p1diff) > len(p2diff):
2263 2292 parent = p2
2264 2293 diff = p2diff
2265 2294 baserev = orig.rev(parent)
2266 2295 cachedelta = (baserev, diff)
2267 2296 elif source == b'storage':
2268 2297 baserev = orig.deltaparent(rev)
2269 2298 cachedelta = (baserev, orig.revdiff(orig.node(baserev), rev))
2270 2299
2271 2300 return ((text, tr, linkrev, p1, p2),
2272 2301 {'node': node, 'flags': flags, 'cachedelta': cachedelta})
2273 2302
2274 2303 @contextlib.contextmanager
2275 2304 def _temprevlog(ui, orig, truncaterev):
2276 2305 from mercurial import vfs as vfsmod
2277 2306
2278 2307 if orig._inline:
2279 2308 raise error.Abort('not supporting inline revlog (yet)')
2280 2309 revlogkwargs = {}
2281 2310 k = 'upperboundcomp'
2282 2311 if util.safehasattr(orig, k):
2283 2312 revlogkwargs[k] = getattr(orig, k)
2284 2313
2285 2314 origindexpath = orig.opener.join(orig.indexfile)
2286 2315 origdatapath = orig.opener.join(orig.datafile)
2287 2316 indexname = 'revlog.i'
2288 2317 dataname = 'revlog.d'
2289 2318
2290 2319 tmpdir = tempfile.mkdtemp(prefix='tmp-hgperf-')
2291 2320 try:
2292 2321 # copy the data file in a temporary directory
2293 2322 ui.debug('copying data in %s\n' % tmpdir)
2294 2323 destindexpath = os.path.join(tmpdir, 'revlog.i')
2295 2324 destdatapath = os.path.join(tmpdir, 'revlog.d')
2296 2325 shutil.copyfile(origindexpath, destindexpath)
2297 2326 shutil.copyfile(origdatapath, destdatapath)
2298 2327
2299 2328 # remove the data we want to add again
2300 2329 ui.debug('truncating data to be rewritten\n')
2301 2330 with open(destindexpath, 'ab') as index:
2302 2331 index.seek(0)
2303 2332 index.truncate(truncaterev * orig._io.size)
2304 2333 with open(destdatapath, 'ab') as data:
2305 2334 data.seek(0)
2306 2335 data.truncate(orig.start(truncaterev))
2307 2336
2308 2337 # instantiate a new revlog from the temporary copy
2309 2338 ui.debug('truncating adding to be rewritten\n')
2310 2339 vfs = vfsmod.vfs(tmpdir)
2311 2340 vfs.options = getattr(orig.opener, 'options', None)
2312 2341
2313 2342 dest = revlog.revlog(vfs,
2314 2343 indexfile=indexname,
2315 2344 datafile=dataname, **revlogkwargs)
2316 2345 if dest._inline:
2317 2346 raise error.Abort('not supporting inline revlog (yet)')
2318 2347 # make sure internals are initialized
2319 2348 dest.revision(len(dest) - 1)
2320 2349 yield dest
2321 2350 del dest, vfs
2322 2351 finally:
2323 2352 shutil.rmtree(tmpdir, True)
2324 2353
2325 2354 @command(b'perfrevlogchunks', revlogopts + formatteropts +
2326 2355 [(b'e', b'engines', b'', b'compression engines to use'),
2327 2356 (b's', b'startrev', 0, b'revision to start at')],
2328 2357 b'-c|-m|FILE')
2329 2358 def perfrevlogchunks(ui, repo, file_=None, engines=None, startrev=0, **opts):
2330 2359 """Benchmark operations on revlog chunks.
2331 2360
2332 2361 Logically, each revlog is a collection of fulltext revisions. However,
2333 2362 stored within each revlog are "chunks" of possibly compressed data. This
2334 2363 data needs to be read and decompressed or compressed and written.
2335 2364
2336 2365 This command measures the time it takes to read+decompress and recompress
2337 2366 chunks in a revlog. It effectively isolates I/O and compression performance.
2338 2367 For measurements of higher-level operations like resolving revisions,
2339 2368 see ``perfrevlogrevisions`` and ``perfrevlogrevision``.
2340 2369 """
2341 2370 opts = _byteskwargs(opts)
2342 2371
2343 2372 rl = cmdutil.openrevlog(repo, b'perfrevlogchunks', file_, opts)
2344 2373
2345 2374 # _chunkraw was renamed to _getsegmentforrevs.
2346 2375 try:
2347 2376 segmentforrevs = rl._getsegmentforrevs
2348 2377 except AttributeError:
2349 2378 segmentforrevs = rl._chunkraw
2350 2379
2351 2380 # Verify engines argument.
2352 2381 if engines:
2353 2382 engines = set(e.strip() for e in engines.split(b','))
2354 2383 for engine in engines:
2355 2384 try:
2356 2385 util.compressionengines[engine]
2357 2386 except KeyError:
2358 2387 raise error.Abort(b'unknown compression engine: %s' % engine)
2359 2388 else:
2360 2389 engines = []
2361 2390 for e in util.compengines:
2362 2391 engine = util.compengines[e]
2363 2392 try:
2364 2393 if engine.available():
2365 2394 engine.revlogcompressor().compress(b'dummy')
2366 2395 engines.append(e)
2367 2396 except NotImplementedError:
2368 2397 pass
2369 2398
2370 2399 revs = list(rl.revs(startrev, len(rl) - 1))
2371 2400
2372 2401 def rlfh(rl):
2373 2402 if rl._inline:
2374 2403 return getsvfs(repo)(rl.indexfile)
2375 2404 else:
2376 2405 return getsvfs(repo)(rl.datafile)
2377 2406
2378 2407 def doread():
2379 2408 rl.clearcaches()
2380 2409 for rev in revs:
2381 2410 segmentforrevs(rev, rev)
2382 2411
2383 2412 def doreadcachedfh():
2384 2413 rl.clearcaches()
2385 2414 fh = rlfh(rl)
2386 2415 for rev in revs:
2387 2416 segmentforrevs(rev, rev, df=fh)
2388 2417
2389 2418 def doreadbatch():
2390 2419 rl.clearcaches()
2391 2420 segmentforrevs(revs[0], revs[-1])
2392 2421
2393 2422 def doreadbatchcachedfh():
2394 2423 rl.clearcaches()
2395 2424 fh = rlfh(rl)
2396 2425 segmentforrevs(revs[0], revs[-1], df=fh)
2397 2426
2398 2427 def dochunk():
2399 2428 rl.clearcaches()
2400 2429 fh = rlfh(rl)
2401 2430 for rev in revs:
2402 2431 rl._chunk(rev, df=fh)
2403 2432
2404 2433 chunks = [None]
2405 2434
2406 2435 def dochunkbatch():
2407 2436 rl.clearcaches()
2408 2437 fh = rlfh(rl)
2409 2438 # Save chunks as a side-effect.
2410 2439 chunks[0] = rl._chunks(revs, df=fh)
2411 2440
2412 2441 def docompress(compressor):
2413 2442 rl.clearcaches()
2414 2443
2415 2444 try:
2416 2445 # Swap in the requested compression engine.
2417 2446 oldcompressor = rl._compressor
2418 2447 rl._compressor = compressor
2419 2448 for chunk in chunks[0]:
2420 2449 rl.compress(chunk)
2421 2450 finally:
2422 2451 rl._compressor = oldcompressor
2423 2452
2424 2453 benches = [
2425 2454 (lambda: doread(), b'read'),
2426 2455 (lambda: doreadcachedfh(), b'read w/ reused fd'),
2427 2456 (lambda: doreadbatch(), b'read batch'),
2428 2457 (lambda: doreadbatchcachedfh(), b'read batch w/ reused fd'),
2429 2458 (lambda: dochunk(), b'chunk'),
2430 2459 (lambda: dochunkbatch(), b'chunk batch'),
2431 2460 ]
2432 2461
2433 2462 for engine in sorted(engines):
2434 2463 compressor = util.compengines[engine].revlogcompressor()
2435 2464 benches.append((functools.partial(docompress, compressor),
2436 2465 b'compress w/ %s' % engine))
2437 2466
2438 2467 for fn, title in benches:
2439 2468 timer, fm = gettimer(ui, opts)
2440 2469 timer(fn, title=title)
2441 2470 fm.end()
2442 2471
2443 2472 @command(b'perfrevlogrevision', revlogopts + formatteropts +
2444 2473 [(b'', b'cache', False, b'use caches instead of clearing')],
2445 2474 b'-c|-m|FILE REV')
2446 2475 def perfrevlogrevision(ui, repo, file_, rev=None, cache=None, **opts):
2447 2476 """Benchmark obtaining a revlog revision.
2448 2477
2449 2478 Obtaining a revlog revision consists of roughly the following steps:
2450 2479
2451 2480 1. Compute the delta chain
2452 2481 2. Slice the delta chain if applicable
2453 2482 3. Obtain the raw chunks for that delta chain
2454 2483 4. Decompress each raw chunk
2455 2484 5. Apply binary patches to obtain fulltext
2456 2485 6. Verify hash of fulltext
2457 2486
2458 2487 This command measures the time spent in each of these phases.
2459 2488 """
2460 2489 opts = _byteskwargs(opts)
2461 2490
2462 2491 if opts.get(b'changelog') or opts.get(b'manifest'):
2463 2492 file_, rev = None, file_
2464 2493 elif rev is None:
2465 2494 raise error.CommandError(b'perfrevlogrevision', b'invalid arguments')
2466 2495
2467 2496 r = cmdutil.openrevlog(repo, b'perfrevlogrevision', file_, opts)
2468 2497
2469 2498 # _chunkraw was renamed to _getsegmentforrevs.
2470 2499 try:
2471 2500 segmentforrevs = r._getsegmentforrevs
2472 2501 except AttributeError:
2473 2502 segmentforrevs = r._chunkraw
2474 2503
2475 2504 node = r.lookup(rev)
2476 2505 rev = r.rev(node)
2477 2506
2478 2507 def getrawchunks(data, chain):
2479 2508 start = r.start
2480 2509 length = r.length
2481 2510 inline = r._inline
2482 2511 iosize = r._io.size
2483 2512 buffer = util.buffer
2484 2513
2485 2514 chunks = []
2486 2515 ladd = chunks.append
2487 2516 for idx, item in enumerate(chain):
2488 2517 offset = start(item[0])
2489 2518 bits = data[idx]
2490 2519 for rev in item:
2491 2520 chunkstart = start(rev)
2492 2521 if inline:
2493 2522 chunkstart += (rev + 1) * iosize
2494 2523 chunklength = length(rev)
2495 2524 ladd(buffer(bits, chunkstart - offset, chunklength))
2496 2525
2497 2526 return chunks
2498 2527
2499 2528 def dodeltachain(rev):
2500 2529 if not cache:
2501 2530 r.clearcaches()
2502 2531 r._deltachain(rev)
2503 2532
2504 2533 def doread(chain):
2505 2534 if not cache:
2506 2535 r.clearcaches()
2507 2536 for item in slicedchain:
2508 2537 segmentforrevs(item[0], item[-1])
2509 2538
2510 2539 def doslice(r, chain, size):
2511 2540 for s in slicechunk(r, chain, targetsize=size):
2512 2541 pass
2513 2542
2514 2543 def dorawchunks(data, chain):
2515 2544 if not cache:
2516 2545 r.clearcaches()
2517 2546 getrawchunks(data, chain)
2518 2547
2519 2548 def dodecompress(chunks):
2520 2549 decomp = r.decompress
2521 2550 for chunk in chunks:
2522 2551 decomp(chunk)
2523 2552
2524 2553 def dopatch(text, bins):
2525 2554 if not cache:
2526 2555 r.clearcaches()
2527 2556 mdiff.patches(text, bins)
2528 2557
2529 2558 def dohash(text):
2530 2559 if not cache:
2531 2560 r.clearcaches()
2532 2561 r.checkhash(text, node, rev=rev)
2533 2562
2534 2563 def dorevision():
2535 2564 if not cache:
2536 2565 r.clearcaches()
2537 2566 r.revision(node)
2538 2567
2539 2568 try:
2540 2569 from mercurial.revlogutils.deltas import slicechunk
2541 2570 except ImportError:
2542 2571 slicechunk = getattr(revlog, '_slicechunk', None)
2543 2572
2544 2573 size = r.length(rev)
2545 2574 chain = r._deltachain(rev)[0]
2546 2575 if not getattr(r, '_withsparseread', False):
2547 2576 slicedchain = (chain,)
2548 2577 else:
2549 2578 slicedchain = tuple(slicechunk(r, chain, targetsize=size))
2550 2579 data = [segmentforrevs(seg[0], seg[-1])[1] for seg in slicedchain]
2551 2580 rawchunks = getrawchunks(data, slicedchain)
2552 2581 bins = r._chunks(chain)
2553 2582 text = bytes(bins[0])
2554 2583 bins = bins[1:]
2555 2584 text = mdiff.patches(text, bins)
2556 2585
2557 2586 benches = [
2558 2587 (lambda: dorevision(), b'full'),
2559 2588 (lambda: dodeltachain(rev), b'deltachain'),
2560 2589 (lambda: doread(chain), b'read'),
2561 2590 ]
2562 2591
2563 2592 if getattr(r, '_withsparseread', False):
2564 2593 slicing = (lambda: doslice(r, chain, size), b'slice-sparse-chain')
2565 2594 benches.append(slicing)
2566 2595
2567 2596 benches.extend([
2568 2597 (lambda: dorawchunks(data, slicedchain), b'rawchunks'),
2569 2598 (lambda: dodecompress(rawchunks), b'decompress'),
2570 2599 (lambda: dopatch(text, bins), b'patch'),
2571 2600 (lambda: dohash(text), b'hash'),
2572 2601 ])
2573 2602
2574 2603 timer, fm = gettimer(ui, opts)
2575 2604 for fn, title in benches:
2576 2605 timer(fn, title=title)
2577 2606 fm.end()
2578 2607
2579 2608 @command(b'perfrevset',
2580 2609 [(b'C', b'clear', False, b'clear volatile cache between each call.'),
2581 2610 (b'', b'contexts', False, b'obtain changectx for each revision')]
2582 2611 + formatteropts, b"REVSET")
2583 2612 def perfrevset(ui, repo, expr, clear=False, contexts=False, **opts):
2584 2613 """benchmark the execution time of a revset
2585 2614
2586 2615 Use the --clean option if need to evaluate the impact of build volatile
2587 2616 revisions set cache on the revset execution. Volatile cache hold filtered
2588 2617 and obsolete related cache."""
2589 2618 opts = _byteskwargs(opts)
2590 2619
2591 2620 timer, fm = gettimer(ui, opts)
2592 2621 def d():
2593 2622 if clear:
2594 2623 repo.invalidatevolatilesets()
2595 2624 if contexts:
2596 2625 for ctx in repo.set(expr): pass
2597 2626 else:
2598 2627 for r in repo.revs(expr): pass
2599 2628 timer(d)
2600 2629 fm.end()
2601 2630
2602 2631 @command(b'perfvolatilesets',
2603 2632 [(b'', b'clear-obsstore', False, b'drop obsstore between each call.'),
2604 2633 ] + formatteropts)
2605 2634 def perfvolatilesets(ui, repo, *names, **opts):
2606 2635 """benchmark the computation of various volatile set
2607 2636
2608 2637 Volatile set computes element related to filtering and obsolescence."""
2609 2638 opts = _byteskwargs(opts)
2610 2639 timer, fm = gettimer(ui, opts)
2611 2640 repo = repo.unfiltered()
2612 2641
2613 2642 def getobs(name):
2614 2643 def d():
2615 2644 repo.invalidatevolatilesets()
2616 2645 if opts[b'clear_obsstore']:
2617 2646 clearfilecache(repo, b'obsstore')
2618 2647 obsolete.getrevs(repo, name)
2619 2648 return d
2620 2649
2621 2650 allobs = sorted(obsolete.cachefuncs)
2622 2651 if names:
2623 2652 allobs = [n for n in allobs if n in names]
2624 2653
2625 2654 for name in allobs:
2626 2655 timer(getobs(name), title=name)
2627 2656
2628 2657 def getfiltered(name):
2629 2658 def d():
2630 2659 repo.invalidatevolatilesets()
2631 2660 if opts[b'clear_obsstore']:
2632 2661 clearfilecache(repo, b'obsstore')
2633 2662 repoview.filterrevs(repo, name)
2634 2663 return d
2635 2664
2636 2665 allfilter = sorted(repoview.filtertable)
2637 2666 if names:
2638 2667 allfilter = [n for n in allfilter if n in names]
2639 2668
2640 2669 for name in allfilter:
2641 2670 timer(getfiltered(name), title=name)
2642 2671 fm.end()
2643 2672
2644 2673 @command(b'perfbranchmap',
2645 2674 [(b'f', b'full', False,
2646 2675 b'Includes build time of subset'),
2647 2676 (b'', b'clear-revbranch', False,
2648 2677 b'purge the revbranch cache between computation'),
2649 2678 ] + formatteropts)
2650 2679 def perfbranchmap(ui, repo, *filternames, **opts):
2651 2680 """benchmark the update of a branchmap
2652 2681
2653 2682 This benchmarks the full repo.branchmap() call with read and write disabled
2654 2683 """
2655 2684 opts = _byteskwargs(opts)
2656 2685 full = opts.get(b"full", False)
2657 2686 clear_revbranch = opts.get(b"clear_revbranch", False)
2658 2687 timer, fm = gettimer(ui, opts)
2659 2688 def getbranchmap(filtername):
2660 2689 """generate a benchmark function for the filtername"""
2661 2690 if filtername is None:
2662 2691 view = repo
2663 2692 else:
2664 2693 view = repo.filtered(filtername)
2665 2694 if util.safehasattr(view._branchcaches, '_per_filter'):
2666 2695 filtered = view._branchcaches._per_filter
2667 2696 else:
2668 2697 # older versions
2669 2698 filtered = view._branchcaches
2670 2699 def d():
2671 2700 if clear_revbranch:
2672 2701 repo.revbranchcache()._clear()
2673 2702 if full:
2674 2703 view._branchcaches.clear()
2675 2704 else:
2676 2705 filtered.pop(filtername, None)
2677 2706 view.branchmap()
2678 2707 return d
2679 2708 # add filter in smaller subset to bigger subset
2680 2709 possiblefilters = set(repoview.filtertable)
2681 2710 if filternames:
2682 2711 possiblefilters &= set(filternames)
2683 2712 subsettable = getbranchmapsubsettable()
2684 2713 allfilters = []
2685 2714 while possiblefilters:
2686 2715 for name in possiblefilters:
2687 2716 subset = subsettable.get(name)
2688 2717 if subset not in possiblefilters:
2689 2718 break
2690 2719 else:
2691 2720 assert False, b'subset cycle %s!' % possiblefilters
2692 2721 allfilters.append(name)
2693 2722 possiblefilters.remove(name)
2694 2723
2695 2724 # warm the cache
2696 2725 if not full:
2697 2726 for name in allfilters:
2698 2727 repo.filtered(name).branchmap()
2699 2728 if not filternames or b'unfiltered' in filternames:
2700 2729 # add unfiltered
2701 2730 allfilters.append(None)
2702 2731
2703 2732 if util.safehasattr(branchmap.branchcache, 'fromfile'):
2704 2733 branchcacheread = safeattrsetter(branchmap.branchcache, b'fromfile')
2705 2734 branchcacheread.set(classmethod(lambda *args: None))
2706 2735 else:
2707 2736 # older versions
2708 2737 branchcacheread = safeattrsetter(branchmap, b'read')
2709 2738 branchcacheread.set(lambda *args: None)
2710 2739 branchcachewrite = safeattrsetter(branchmap.branchcache, b'write')
2711 2740 branchcachewrite.set(lambda *args: None)
2712 2741 try:
2713 2742 for name in allfilters:
2714 2743 printname = name
2715 2744 if name is None:
2716 2745 printname = b'unfiltered'
2717 2746 timer(getbranchmap(name), title=str(printname))
2718 2747 finally:
2719 2748 branchcacheread.restore()
2720 2749 branchcachewrite.restore()
2721 2750 fm.end()
2722 2751
2723 2752 @command(b'perfbranchmapupdate', [
2724 2753 (b'', b'base', [], b'subset of revision to start from'),
2725 2754 (b'', b'target', [], b'subset of revision to end with'),
2726 2755 (b'', b'clear-caches', False, b'clear cache between each runs')
2727 2756 ] + formatteropts)
2728 2757 def perfbranchmapupdate(ui, repo, base=(), target=(), **opts):
2729 2758 """benchmark branchmap update from for <base> revs to <target> revs
2730 2759
2731 2760 If `--clear-caches` is passed, the following items will be reset before
2732 2761 each update:
2733 2762 * the changelog instance and associated indexes
2734 2763 * the rev-branch-cache instance
2735 2764
2736 2765 Examples:
2737 2766
2738 2767 # update for the one last revision
2739 2768 $ hg perfbranchmapupdate --base 'not tip' --target 'tip'
2740 2769
2741 2770 $ update for change coming with a new branch
2742 2771 $ hg perfbranchmapupdate --base 'stable' --target 'default'
2743 2772 """
2744 2773 from mercurial import branchmap
2745 2774 from mercurial import repoview
2746 2775 opts = _byteskwargs(opts)
2747 2776 timer, fm = gettimer(ui, opts)
2748 2777 clearcaches = opts[b'clear_caches']
2749 2778 unfi = repo.unfiltered()
2750 2779 x = [None] # used to pass data between closure
2751 2780
2752 2781 # we use a `list` here to avoid possible side effect from smartset
2753 2782 baserevs = list(scmutil.revrange(repo, base))
2754 2783 targetrevs = list(scmutil.revrange(repo, target))
2755 2784 if not baserevs:
2756 2785 raise error.Abort(b'no revisions selected for --base')
2757 2786 if not targetrevs:
2758 2787 raise error.Abort(b'no revisions selected for --target')
2759 2788
2760 2789 # make sure the target branchmap also contains the one in the base
2761 2790 targetrevs = list(set(baserevs) | set(targetrevs))
2762 2791 targetrevs.sort()
2763 2792
2764 2793 cl = repo.changelog
2765 2794 allbaserevs = list(cl.ancestors(baserevs, inclusive=True))
2766 2795 allbaserevs.sort()
2767 2796 alltargetrevs = frozenset(cl.ancestors(targetrevs, inclusive=True))
2768 2797
2769 2798 newrevs = list(alltargetrevs.difference(allbaserevs))
2770 2799 newrevs.sort()
2771 2800
2772 2801 allrevs = frozenset(unfi.changelog.revs())
2773 2802 basefilterrevs = frozenset(allrevs.difference(allbaserevs))
2774 2803 targetfilterrevs = frozenset(allrevs.difference(alltargetrevs))
2775 2804
2776 2805 def basefilter(repo, visibilityexceptions=None):
2777 2806 return basefilterrevs
2778 2807
2779 2808 def targetfilter(repo, visibilityexceptions=None):
2780 2809 return targetfilterrevs
2781 2810
2782 2811 msg = b'benchmark of branchmap with %d revisions with %d new ones\n'
2783 2812 ui.status(msg % (len(allbaserevs), len(newrevs)))
2784 2813 if targetfilterrevs:
2785 2814 msg = b'(%d revisions still filtered)\n'
2786 2815 ui.status(msg % len(targetfilterrevs))
2787 2816
2788 2817 try:
2789 2818 repoview.filtertable[b'__perf_branchmap_update_base'] = basefilter
2790 2819 repoview.filtertable[b'__perf_branchmap_update_target'] = targetfilter
2791 2820
2792 2821 baserepo = repo.filtered(b'__perf_branchmap_update_base')
2793 2822 targetrepo = repo.filtered(b'__perf_branchmap_update_target')
2794 2823
2795 2824 # try to find an existing branchmap to reuse
2796 2825 subsettable = getbranchmapsubsettable()
2797 2826 candidatefilter = subsettable.get(None)
2798 2827 while candidatefilter is not None:
2799 2828 candidatebm = repo.filtered(candidatefilter).branchmap()
2800 2829 if candidatebm.validfor(baserepo):
2801 2830 filtered = repoview.filterrevs(repo, candidatefilter)
2802 2831 missing = [r for r in allbaserevs if r in filtered]
2803 2832 base = candidatebm.copy()
2804 2833 base.update(baserepo, missing)
2805 2834 break
2806 2835 candidatefilter = subsettable.get(candidatefilter)
2807 2836 else:
2808 2837 # no suitable subset where found
2809 2838 base = branchmap.branchcache()
2810 2839 base.update(baserepo, allbaserevs)
2811 2840
2812 2841 def setup():
2813 2842 x[0] = base.copy()
2814 2843 if clearcaches:
2815 2844 unfi._revbranchcache = None
2816 2845 clearchangelog(repo)
2817 2846
2818 2847 def bench():
2819 2848 x[0].update(targetrepo, newrevs)
2820 2849
2821 2850 timer(bench, setup=setup)
2822 2851 fm.end()
2823 2852 finally:
2824 2853 repoview.filtertable.pop(b'__perf_branchmap_update_base', None)
2825 2854 repoview.filtertable.pop(b'__perf_branchmap_update_target', None)
2826 2855
2827 2856 @command(b'perfbranchmapload', [
2828 2857 (b'f', b'filter', b'', b'Specify repoview filter'),
2829 2858 (b'', b'list', False, b'List brachmap filter caches'),
2830 2859 (b'', b'clear-revlogs', False, b'refresh changelog and manifest'),
2831 2860
2832 2861 ] + formatteropts)
2833 2862 def perfbranchmapload(ui, repo, filter=b'', list=False, **opts):
2834 2863 """benchmark reading the branchmap"""
2835 2864 opts = _byteskwargs(opts)
2836 2865 clearrevlogs = opts[b'clear_revlogs']
2837 2866
2838 2867 if list:
2839 2868 for name, kind, st in repo.cachevfs.readdir(stat=True):
2840 2869 if name.startswith(b'branch2'):
2841 2870 filtername = name.partition(b'-')[2] or b'unfiltered'
2842 2871 ui.status(b'%s - %s\n'
2843 2872 % (filtername, util.bytecount(st.st_size)))
2844 2873 return
2845 2874 if not filter:
2846 2875 filter = None
2847 2876 subsettable = getbranchmapsubsettable()
2848 2877 if filter is None:
2849 2878 repo = repo.unfiltered()
2850 2879 else:
2851 2880 repo = repoview.repoview(repo, filter)
2852 2881
2853 2882 repo.branchmap() # make sure we have a relevant, up to date branchmap
2854 2883
2855 2884 try:
2856 2885 fromfile = branchmap.branchcache.fromfile
2857 2886 except AttributeError:
2858 2887 # older versions
2859 2888 fromfile = branchmap.read
2860 2889
2861 2890 currentfilter = filter
2862 2891 # try once without timer, the filter may not be cached
2863 2892 while fromfile(repo) is None:
2864 2893 currentfilter = subsettable.get(currentfilter)
2865 2894 if currentfilter is None:
2866 2895 raise error.Abort(b'No branchmap cached for %s repo'
2867 2896 % (filter or b'unfiltered'))
2868 2897 repo = repo.filtered(currentfilter)
2869 2898 timer, fm = gettimer(ui, opts)
2870 2899 def setup():
2871 2900 if clearrevlogs:
2872 2901 clearchangelog(repo)
2873 2902 def bench():
2874 2903 fromfile(repo)
2875 2904 timer(bench, setup=setup)
2876 2905 fm.end()
2877 2906
2878 2907 @command(b'perfloadmarkers')
2879 2908 def perfloadmarkers(ui, repo):
2880 2909 """benchmark the time to parse the on-disk markers for a repo
2881 2910
2882 2911 Result is the number of markers in the repo."""
2883 2912 timer, fm = gettimer(ui)
2884 2913 svfs = getsvfs(repo)
2885 2914 timer(lambda: len(obsolete.obsstore(svfs)))
2886 2915 fm.end()
2887 2916
2888 2917 @command(b'perflrucachedict', formatteropts +
2889 2918 [(b'', b'costlimit', 0, b'maximum total cost of items in cache'),
2890 2919 (b'', b'mincost', 0, b'smallest cost of items in cache'),
2891 2920 (b'', b'maxcost', 100, b'maximum cost of items in cache'),
2892 2921 (b'', b'size', 4, b'size of cache'),
2893 2922 (b'', b'gets', 10000, b'number of key lookups'),
2894 2923 (b'', b'sets', 10000, b'number of key sets'),
2895 2924 (b'', b'mixed', 10000, b'number of mixed mode operations'),
2896 2925 (b'', b'mixedgetfreq', 50, b'frequency of get vs set ops in mixed mode')],
2897 2926 norepo=True)
2898 2927 def perflrucache(ui, mincost=0, maxcost=100, costlimit=0, size=4,
2899 2928 gets=10000, sets=10000, mixed=10000, mixedgetfreq=50, **opts):
2900 2929 opts = _byteskwargs(opts)
2901 2930
2902 2931 def doinit():
2903 2932 for i in _xrange(10000):
2904 2933 util.lrucachedict(size)
2905 2934
2906 2935 costrange = list(range(mincost, maxcost + 1))
2907 2936
2908 2937 values = []
2909 2938 for i in _xrange(size):
2910 2939 values.append(random.randint(0, _maxint))
2911 2940
2912 2941 # Get mode fills the cache and tests raw lookup performance with no
2913 2942 # eviction.
2914 2943 getseq = []
2915 2944 for i in _xrange(gets):
2916 2945 getseq.append(random.choice(values))
2917 2946
2918 2947 def dogets():
2919 2948 d = util.lrucachedict(size)
2920 2949 for v in values:
2921 2950 d[v] = v
2922 2951 for key in getseq:
2923 2952 value = d[key]
2924 2953 value # silence pyflakes warning
2925 2954
2926 2955 def dogetscost():
2927 2956 d = util.lrucachedict(size, maxcost=costlimit)
2928 2957 for i, v in enumerate(values):
2929 2958 d.insert(v, v, cost=costs[i])
2930 2959 for key in getseq:
2931 2960 try:
2932 2961 value = d[key]
2933 2962 value # silence pyflakes warning
2934 2963 except KeyError:
2935 2964 pass
2936 2965
2937 2966 # Set mode tests insertion speed with cache eviction.
2938 2967 setseq = []
2939 2968 costs = []
2940 2969 for i in _xrange(sets):
2941 2970 setseq.append(random.randint(0, _maxint))
2942 2971 costs.append(random.choice(costrange))
2943 2972
2944 2973 def doinserts():
2945 2974 d = util.lrucachedict(size)
2946 2975 for v in setseq:
2947 2976 d.insert(v, v)
2948 2977
2949 2978 def doinsertscost():
2950 2979 d = util.lrucachedict(size, maxcost=costlimit)
2951 2980 for i, v in enumerate(setseq):
2952 2981 d.insert(v, v, cost=costs[i])
2953 2982
2954 2983 def dosets():
2955 2984 d = util.lrucachedict(size)
2956 2985 for v in setseq:
2957 2986 d[v] = v
2958 2987
2959 2988 # Mixed mode randomly performs gets and sets with eviction.
2960 2989 mixedops = []
2961 2990 for i in _xrange(mixed):
2962 2991 r = random.randint(0, 100)
2963 2992 if r < mixedgetfreq:
2964 2993 op = 0
2965 2994 else:
2966 2995 op = 1
2967 2996
2968 2997 mixedops.append((op,
2969 2998 random.randint(0, size * 2),
2970 2999 random.choice(costrange)))
2971 3000
2972 3001 def domixed():
2973 3002 d = util.lrucachedict(size)
2974 3003
2975 3004 for op, v, cost in mixedops:
2976 3005 if op == 0:
2977 3006 try:
2978 3007 d[v]
2979 3008 except KeyError:
2980 3009 pass
2981 3010 else:
2982 3011 d[v] = v
2983 3012
2984 3013 def domixedcost():
2985 3014 d = util.lrucachedict(size, maxcost=costlimit)
2986 3015
2987 3016 for op, v, cost in mixedops:
2988 3017 if op == 0:
2989 3018 try:
2990 3019 d[v]
2991 3020 except KeyError:
2992 3021 pass
2993 3022 else:
2994 3023 d.insert(v, v, cost=cost)
2995 3024
2996 3025 benches = [
2997 3026 (doinit, b'init'),
2998 3027 ]
2999 3028
3000 3029 if costlimit:
3001 3030 benches.extend([
3002 3031 (dogetscost, b'gets w/ cost limit'),
3003 3032 (doinsertscost, b'inserts w/ cost limit'),
3004 3033 (domixedcost, b'mixed w/ cost limit'),
3005 3034 ])
3006 3035 else:
3007 3036 benches.extend([
3008 3037 (dogets, b'gets'),
3009 3038 (doinserts, b'inserts'),
3010 3039 (dosets, b'sets'),
3011 3040 (domixed, b'mixed')
3012 3041 ])
3013 3042
3014 3043 for fn, title in benches:
3015 3044 timer, fm = gettimer(ui, opts)
3016 3045 timer(fn, title=title)
3017 3046 fm.end()
3018 3047
3019 3048 @command(b'perfwrite', formatteropts)
3020 3049 def perfwrite(ui, repo, **opts):
3021 3050 """microbenchmark ui.write
3022 3051 """
3023 3052 opts = _byteskwargs(opts)
3024 3053
3025 3054 timer, fm = gettimer(ui, opts)
3026 3055 def write():
3027 3056 for i in range(100000):
3028 3057 ui.write((b'Testing write performance\n'))
3029 3058 timer(write)
3030 3059 fm.end()
3031 3060
3032 3061 def uisetup(ui):
3033 3062 if (util.safehasattr(cmdutil, b'openrevlog') and
3034 3063 not util.safehasattr(commands, b'debugrevlogopts')):
3035 3064 # for "historical portability":
3036 3065 # In this case, Mercurial should be 1.9 (or a79fea6b3e77) -
3037 3066 # 3.7 (or 5606f7d0d063). Therefore, '--dir' option for
3038 3067 # openrevlog() should cause failure, because it has been
3039 3068 # available since 3.5 (or 49c583ca48c4).
3040 3069 def openrevlog(orig, repo, cmd, file_, opts):
3041 3070 if opts.get(b'dir') and not util.safehasattr(repo, b'dirlog'):
3042 3071 raise error.Abort(b"This version doesn't support --dir option",
3043 3072 hint=b"use 3.5 or later")
3044 3073 return orig(repo, cmd, file_, opts)
3045 3074 extensions.wrapfunction(cmdutil, b'openrevlog', openrevlog)
3046 3075
3047 3076 @command(b'perfprogress', formatteropts + [
3048 3077 (b'', b'topic', b'topic', b'topic for progress messages'),
3049 3078 (b'c', b'total', 1000000, b'total value we are progressing to'),
3050 3079 ], norepo=True)
3051 3080 def perfprogress(ui, topic=None, total=None, **opts):
3052 3081 """printing of progress bars"""
3053 3082 opts = _byteskwargs(opts)
3054 3083
3055 3084 timer, fm = gettimer(ui, opts)
3056 3085
3057 3086 def doprogress():
3058 3087 with ui.makeprogress(topic, total=total) as progress:
3059 3088 for i in pycompat.xrange(total):
3060 3089 progress.increment()
3061 3090
3062 3091 timer(doprogress)
3063 3092 fm.end()
@@ -1,850 +1,851 b''
1 1 # __init__.py - fsmonitor initialization and overrides
2 2 #
3 3 # Copyright 2013-2016 Facebook, Inc.
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 '''Faster status operations with the Watchman file monitor (EXPERIMENTAL)
9 9
10 10 Integrates the file-watching program Watchman with Mercurial to produce faster
11 11 status results.
12 12
13 13 On a particular Linux system, for a real-world repository with over 400,000
14 14 files hosted on ext4, vanilla `hg status` takes 1.3 seconds. On the same
15 15 system, with fsmonitor it takes about 0.3 seconds.
16 16
17 17 fsmonitor requires no configuration -- it will tell Watchman about your
18 18 repository as necessary. You'll need to install Watchman from
19 19 https://facebook.github.io/watchman/ and make sure it is in your PATH.
20 20
21 21 fsmonitor is incompatible with the largefiles and eol extensions, and
22 22 will disable itself if any of those are active.
23 23
24 24 The following configuration options exist:
25 25
26 26 ::
27 27
28 28 [fsmonitor]
29 29 mode = {off, on, paranoid}
30 30
31 31 When `mode = off`, fsmonitor will disable itself (similar to not loading the
32 32 extension at all). When `mode = on`, fsmonitor will be enabled (the default).
33 33 When `mode = paranoid`, fsmonitor will query both Watchman and the filesystem,
34 34 and ensure that the results are consistent.
35 35
36 36 ::
37 37
38 38 [fsmonitor]
39 39 timeout = (float)
40 40
41 41 A value, in seconds, that determines how long fsmonitor will wait for Watchman
42 42 to return results. Defaults to `2.0`.
43 43
44 44 ::
45 45
46 46 [fsmonitor]
47 47 blacklistusers = (list of userids)
48 48
49 49 A list of usernames for which fsmonitor will disable itself altogether.
50 50
51 51 ::
52 52
53 53 [fsmonitor]
54 54 walk_on_invalidate = (boolean)
55 55
56 56 Whether or not to walk the whole repo ourselves when our cached state has been
57 57 invalidated, for example when Watchman has been restarted or .hgignore rules
58 58 have been changed. Walking the repo in that case can result in competing for
59 59 I/O with Watchman. For large repos it is recommended to set this value to
60 60 false. You may wish to set this to true if you have a very fast filesystem
61 61 that can outpace the IPC overhead of getting the result data for the full repo
62 62 from Watchman. Defaults to false.
63 63
64 64 ::
65 65
66 66 [fsmonitor]
67 67 warn_when_unused = (boolean)
68 68
69 69 Whether to print a warning during certain operations when fsmonitor would be
70 70 beneficial to performance but isn't enabled.
71 71
72 72 ::
73 73
74 74 [fsmonitor]
75 75 warn_update_file_count = (integer)
76 76
77 77 If ``warn_when_unused`` is set and fsmonitor isn't enabled, a warning will
78 78 be printed during working directory updates if this many files will be
79 79 created.
80 80 '''
81 81
82 82 # Platforms Supported
83 83 # ===================
84 84 #
85 85 # **Linux:** *Stable*. Watchman and fsmonitor are both known to work reliably,
86 86 # even under severe loads.
87 87 #
88 88 # **Mac OS X:** *Stable*. The Mercurial test suite passes with fsmonitor
89 89 # turned on, on case-insensitive HFS+. There has been a reasonable amount of
90 90 # user testing under normal loads.
91 91 #
92 92 # **Solaris, BSD:** *Alpha*. watchman and fsmonitor are believed to work, but
93 93 # very little testing has been done.
94 94 #
95 95 # **Windows:** *Alpha*. Not in a release version of watchman or fsmonitor yet.
96 96 #
97 97 # Known Issues
98 98 # ============
99 99 #
100 100 # * fsmonitor will disable itself if any of the following extensions are
101 101 # enabled: largefiles, inotify, eol; or if the repository has subrepos.
102 102 # * fsmonitor will produce incorrect results if nested repos that are not
103 103 # subrepos exist. *Workaround*: add nested repo paths to your `.hgignore`.
104 104 #
105 105 # The issues related to nested repos and subrepos are probably not fundamental
106 106 # ones. Patches to fix them are welcome.
107 107
108 108 from __future__ import absolute_import
109 109
110 110 import codecs
111 111 import hashlib
112 112 import os
113 113 import stat
114 114 import sys
115 115 import tempfile
116 116 import weakref
117 117
118 118 from mercurial.i18n import _
119 119 from mercurial.node import (
120 120 hex,
121 121 )
122 122
123 123 from mercurial import (
124 124 context,
125 125 encoding,
126 126 error,
127 127 extensions,
128 128 localrepo,
129 129 merge,
130 130 pathutil,
131 131 pycompat,
132 132 registrar,
133 133 scmutil,
134 134 util,
135 135 )
136 136 from mercurial import match as matchmod
137 137
138 138 from . import (
139 139 pywatchman,
140 140 state,
141 141 watchmanclient,
142 142 )
143 143
144 144 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
145 145 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
146 146 # be specifying the version(s) of Mercurial they are tested with, or
147 147 # leave the attribute unspecified.
148 148 testedwith = 'ships-with-hg-core'
149 149
150 150 configtable = {}
151 151 configitem = registrar.configitem(configtable)
152 152
153 153 configitem('fsmonitor', 'mode',
154 154 default='on',
155 155 )
156 156 configitem('fsmonitor', 'walk_on_invalidate',
157 157 default=False,
158 158 )
159 159 configitem('fsmonitor', 'timeout',
160 160 default='2',
161 161 )
162 162 configitem('fsmonitor', 'blacklistusers',
163 163 default=list,
164 164 )
165 165 configitem('fsmonitor', 'watchman_exe',
166 166 default='watchman',
167 167 )
168 168 configitem('fsmonitor', 'verbose',
169 169 default=True,
170 experimental=True,
170 171 )
171 172 configitem('experimental', 'fsmonitor.transaction_notify',
172 173 default=False,
173 174 )
174 175
175 176 # This extension is incompatible with the following blacklisted extensions
176 177 # and will disable itself when encountering one of these:
177 178 _blacklist = ['largefiles', 'eol']
178 179
179 180 def debuginstall(ui, fm):
180 181 fm.write("fsmonitor-watchman",
181 182 _("fsmonitor checking for watchman binary... (%s)\n"),
182 183 ui.configpath("fsmonitor", "watchman_exe"))
183 184 root = tempfile.mkdtemp()
184 185 c = watchmanclient.client(ui, root)
185 186 err = None
186 187 try:
187 188 v = c.command("version")
188 189 fm.write("fsmonitor-watchman-version",
189 190 _(" watchman binary version %s\n"), v["version"])
190 191 except watchmanclient.Unavailable as e:
191 192 err = str(e)
192 193 fm.condwrite(err, "fsmonitor-watchman-error",
193 194 _(" watchman binary missing or broken: %s\n"), err)
194 195 return 1 if err else 0
195 196
196 197 def _handleunavailable(ui, state, ex):
197 198 """Exception handler for Watchman interaction exceptions"""
198 199 if isinstance(ex, watchmanclient.Unavailable):
199 200 # experimental config: fsmonitor.verbose
200 201 if ex.warn and ui.configbool('fsmonitor', 'verbose'):
201 202 if 'illegal_fstypes' not in str(ex):
202 203 ui.warn(str(ex) + '\n')
203 204 if ex.invalidate:
204 205 state.invalidate()
205 206 # experimental config: fsmonitor.verbose
206 207 if ui.configbool('fsmonitor', 'verbose'):
207 208 ui.log('fsmonitor', 'Watchman unavailable: %s\n', ex.msg)
208 209 else:
209 210 ui.log('fsmonitor', 'Watchman exception: %s\n', ex)
210 211
211 212 def _hashignore(ignore):
212 213 """Calculate hash for ignore patterns and filenames
213 214
214 215 If this information changes between Mercurial invocations, we can't
215 216 rely on Watchman information anymore and have to re-scan the working
216 217 copy.
217 218
218 219 """
219 220 sha1 = hashlib.sha1()
220 221 sha1.update(repr(ignore))
221 222 return sha1.hexdigest()
222 223
223 224 _watchmanencoding = pywatchman.encoding.get_local_encoding()
224 225 _fsencoding = sys.getfilesystemencoding() or sys.getdefaultencoding()
225 226 _fixencoding = codecs.lookup(_watchmanencoding) != codecs.lookup(_fsencoding)
226 227
227 228 def _watchmantofsencoding(path):
228 229 """Fix path to match watchman and local filesystem encoding
229 230
230 231 watchman's paths encoding can differ from filesystem encoding. For example,
231 232 on Windows, it's always utf-8.
232 233 """
233 234 try:
234 235 decoded = path.decode(_watchmanencoding)
235 236 except UnicodeDecodeError as e:
236 237 raise error.Abort(str(e), hint='watchman encoding error')
237 238
238 239 try:
239 240 encoded = decoded.encode(_fsencoding, 'strict')
240 241 except UnicodeEncodeError as e:
241 242 raise error.Abort(str(e))
242 243
243 244 return encoded
244 245
245 246 def overridewalk(orig, self, match, subrepos, unknown, ignored, full=True):
246 247 '''Replacement for dirstate.walk, hooking into Watchman.
247 248
248 249 Whenever full is False, ignored is False, and the Watchman client is
249 250 available, use Watchman combined with saved state to possibly return only a
250 251 subset of files.'''
251 252 def bail(reason):
252 253 self._ui.debug('fsmonitor: fallback to core status, %s\n' % reason)
253 254 return orig(match, subrepos, unknown, ignored, full=True)
254 255
255 256 if full:
256 257 return bail('full rewalk requested')
257 258 if ignored:
258 259 return bail('listing ignored files')
259 260 if not self._watchmanclient.available():
260 261 return bail('client unavailable')
261 262 state = self._fsmonitorstate
262 263 clock, ignorehash, notefiles = state.get()
263 264 if not clock:
264 265 if state.walk_on_invalidate:
265 266 return bail('no clock')
266 267 # Initial NULL clock value, see
267 268 # https://facebook.github.io/watchman/docs/clockspec.html
268 269 clock = 'c:0:0'
269 270 notefiles = []
270 271
271 272 ignore = self._ignore
272 273 dirignore = self._dirignore
273 274 if unknown:
274 275 if _hashignore(ignore) != ignorehash and clock != 'c:0:0':
275 276 # ignore list changed -- can't rely on Watchman state any more
276 277 if state.walk_on_invalidate:
277 278 return bail('ignore rules changed')
278 279 notefiles = []
279 280 clock = 'c:0:0'
280 281 else:
281 282 # always ignore
282 283 ignore = util.always
283 284 dirignore = util.always
284 285
285 286 matchfn = match.matchfn
286 287 matchalways = match.always()
287 288 dmap = self._map
288 289 if util.safehasattr(dmap, '_map'):
289 290 # for better performance, directly access the inner dirstate map if the
290 291 # standard dirstate implementation is in use.
291 292 dmap = dmap._map
292 293 nonnormalset = self._map.nonnormalset
293 294
294 295 copymap = self._map.copymap
295 296 getkind = stat.S_IFMT
296 297 dirkind = stat.S_IFDIR
297 298 regkind = stat.S_IFREG
298 299 lnkkind = stat.S_IFLNK
299 300 join = self._join
300 301 normcase = util.normcase
301 302 fresh_instance = False
302 303
303 304 exact = skipstep3 = False
304 305 if match.isexact(): # match.exact
305 306 exact = True
306 307 dirignore = util.always # skip step 2
307 308 elif match.prefix(): # match.match, no patterns
308 309 skipstep3 = True
309 310
310 311 if not exact and self._checkcase:
311 312 # note that even though we could receive directory entries, we're only
312 313 # interested in checking if a file with the same name exists. So only
313 314 # normalize files if possible.
314 315 normalize = self._normalizefile
315 316 skipstep3 = False
316 317 else:
317 318 normalize = None
318 319
319 320 # step 1: find all explicit files
320 321 results, work, dirsnotfound = self._walkexplicit(match, subrepos)
321 322
322 323 skipstep3 = skipstep3 and not (work or dirsnotfound)
323 324 work = [d for d in work if not dirignore(d[0])]
324 325
325 326 if not work and (exact or skipstep3):
326 327 for s in subrepos:
327 328 del results[s]
328 329 del results['.hg']
329 330 return results
330 331
331 332 # step 2: query Watchman
332 333 try:
333 334 # Use the user-configured timeout for the query.
334 335 # Add a little slack over the top of the user query to allow for
335 336 # overheads while transferring the data
336 337 self._watchmanclient.settimeout(state.timeout + 0.1)
337 338 result = self._watchmanclient.command('query', {
338 339 'fields': ['mode', 'mtime', 'size', 'exists', 'name'],
339 340 'since': clock,
340 341 'expression': [
341 342 'not', [
342 343 'anyof', ['dirname', '.hg'],
343 344 ['name', '.hg', 'wholename']
344 345 ]
345 346 ],
346 347 'sync_timeout': int(state.timeout * 1000),
347 348 'empty_on_fresh_instance': state.walk_on_invalidate,
348 349 })
349 350 except Exception as ex:
350 351 _handleunavailable(self._ui, state, ex)
351 352 self._watchmanclient.clearconnection()
352 353 return bail('exception during run')
353 354 else:
354 355 # We need to propagate the last observed clock up so that we
355 356 # can use it for our next query
356 357 state.setlastclock(result['clock'])
357 358 if result['is_fresh_instance']:
358 359 if state.walk_on_invalidate:
359 360 state.invalidate()
360 361 return bail('fresh instance')
361 362 fresh_instance = True
362 363 # Ignore any prior noteable files from the state info
363 364 notefiles = []
364 365
365 366 # for file paths which require normalization and we encounter a case
366 367 # collision, we store our own foldmap
367 368 if normalize:
368 369 foldmap = dict((normcase(k), k) for k in results)
369 370
370 371 switch_slashes = pycompat.ossep == '\\'
371 372 # The order of the results is, strictly speaking, undefined.
372 373 # For case changes on a case insensitive filesystem we may receive
373 374 # two entries, one with exists=True and another with exists=False.
374 375 # The exists=True entries in the same response should be interpreted
375 376 # as being happens-after the exists=False entries due to the way that
376 377 # Watchman tracks files. We use this property to reconcile deletes
377 378 # for name case changes.
378 379 for entry in result['files']:
379 380 fname = entry['name']
380 381 if _fixencoding:
381 382 fname = _watchmantofsencoding(fname)
382 383 if switch_slashes:
383 384 fname = fname.replace('\\', '/')
384 385 if normalize:
385 386 normed = normcase(fname)
386 387 fname = normalize(fname, True, True)
387 388 foldmap[normed] = fname
388 389 fmode = entry['mode']
389 390 fexists = entry['exists']
390 391 kind = getkind(fmode)
391 392
392 393 if '/.hg/' in fname or fname.endswith('/.hg'):
393 394 return bail('nested-repo-detected')
394 395
395 396 if not fexists:
396 397 # if marked as deleted and we don't already have a change
397 398 # record, mark it as deleted. If we already have an entry
398 399 # for fname then it was either part of walkexplicit or was
399 400 # an earlier result that was a case change
400 401 if fname not in results and fname in dmap and (
401 402 matchalways or matchfn(fname)):
402 403 results[fname] = None
403 404 elif kind == dirkind:
404 405 if fname in dmap and (matchalways or matchfn(fname)):
405 406 results[fname] = None
406 407 elif kind == regkind or kind == lnkkind:
407 408 if fname in dmap:
408 409 if matchalways or matchfn(fname):
409 410 results[fname] = entry
410 411 elif (matchalways or matchfn(fname)) and not ignore(fname):
411 412 results[fname] = entry
412 413 elif fname in dmap and (matchalways or matchfn(fname)):
413 414 results[fname] = None
414 415
415 416 # step 3: query notable files we don't already know about
416 417 # XXX try not to iterate over the entire dmap
417 418 if normalize:
418 419 # any notable files that have changed case will already be handled
419 420 # above, so just check membership in the foldmap
420 421 notefiles = set((normalize(f, True, True) for f in notefiles
421 422 if normcase(f) not in foldmap))
422 423 visit = set((f for f in notefiles if (f not in results and matchfn(f)
423 424 and (f in dmap or not ignore(f)))))
424 425
425 426 if not fresh_instance:
426 427 if matchalways:
427 428 visit.update(f for f in nonnormalset if f not in results)
428 429 visit.update(f for f in copymap if f not in results)
429 430 else:
430 431 visit.update(f for f in nonnormalset
431 432 if f not in results and matchfn(f))
432 433 visit.update(f for f in copymap
433 434 if f not in results and matchfn(f))
434 435 else:
435 436 if matchalways:
436 437 visit.update(f for f, st in dmap.iteritems() if f not in results)
437 438 visit.update(f for f in copymap if f not in results)
438 439 else:
439 440 visit.update(f for f, st in dmap.iteritems()
440 441 if f not in results and matchfn(f))
441 442 visit.update(f for f in copymap
442 443 if f not in results and matchfn(f))
443 444
444 445 audit = pathutil.pathauditor(self._root, cached=True).check
445 446 auditpass = [f for f in visit if audit(f)]
446 447 auditpass.sort()
447 448 auditfail = visit.difference(auditpass)
448 449 for f in auditfail:
449 450 results[f] = None
450 451
451 452 nf = iter(auditpass).next
452 453 for st in util.statfiles([join(f) for f in auditpass]):
453 454 f = nf()
454 455 if st or f in dmap:
455 456 results[f] = st
456 457
457 458 for s in subrepos:
458 459 del results[s]
459 460 del results['.hg']
460 461 return results
461 462
462 463 def overridestatus(
463 464 orig, self, node1='.', node2=None, match=None, ignored=False,
464 465 clean=False, unknown=False, listsubrepos=False):
465 466 listignored = ignored
466 467 listclean = clean
467 468 listunknown = unknown
468 469
469 470 def _cmpsets(l1, l2):
470 471 try:
471 472 if 'FSMONITOR_LOG_FILE' in encoding.environ:
472 473 fn = encoding.environ['FSMONITOR_LOG_FILE']
473 474 f = open(fn, 'wb')
474 475 else:
475 476 fn = 'fsmonitorfail.log'
476 477 f = self.vfs.open(fn, 'wb')
477 478 except (IOError, OSError):
478 479 self.ui.warn(_('warning: unable to write to %s\n') % fn)
479 480 return
480 481
481 482 try:
482 483 for i, (s1, s2) in enumerate(zip(l1, l2)):
483 484 if set(s1) != set(s2):
484 485 f.write('sets at position %d are unequal\n' % i)
485 486 f.write('watchman returned: %s\n' % s1)
486 487 f.write('stat returned: %s\n' % s2)
487 488 finally:
488 489 f.close()
489 490
490 491 if isinstance(node1, context.changectx):
491 492 ctx1 = node1
492 493 else:
493 494 ctx1 = self[node1]
494 495 if isinstance(node2, context.changectx):
495 496 ctx2 = node2
496 497 else:
497 498 ctx2 = self[node2]
498 499
499 500 working = ctx2.rev() is None
500 501 parentworking = working and ctx1 == self['.']
501 502 match = match or matchmod.always()
502 503
503 504 # Maybe we can use this opportunity to update Watchman's state.
504 505 # Mercurial uses workingcommitctx and/or memctx to represent the part of
505 506 # the workingctx that is to be committed. So don't update the state in
506 507 # that case.
507 508 # HG_PENDING is set in the environment when the dirstate is being updated
508 509 # in the middle of a transaction; we must not update our state in that
509 510 # case, or we risk forgetting about changes in the working copy.
510 511 updatestate = (parentworking and match.always() and
511 512 not isinstance(ctx2, (context.workingcommitctx,
512 513 context.memctx)) and
513 514 'HG_PENDING' not in encoding.environ)
514 515
515 516 try:
516 517 if self._fsmonitorstate.walk_on_invalidate:
517 518 # Use a short timeout to query the current clock. If that
518 519 # takes too long then we assume that the service will be slow
519 520 # to answer our query.
520 521 # walk_on_invalidate indicates that we prefer to walk the
521 522 # tree ourselves because we can ignore portions that Watchman
522 523 # cannot and we tend to be faster in the warmer buffer cache
523 524 # cases.
524 525 self._watchmanclient.settimeout(0.1)
525 526 else:
526 527 # Give Watchman more time to potentially complete its walk
527 528 # and return the initial clock. In this mode we assume that
528 529 # the filesystem will be slower than parsing a potentially
529 530 # very large Watchman result set.
530 531 self._watchmanclient.settimeout(
531 532 self._fsmonitorstate.timeout + 0.1)
532 533 startclock = self._watchmanclient.getcurrentclock()
533 534 except Exception as ex:
534 535 self._watchmanclient.clearconnection()
535 536 _handleunavailable(self.ui, self._fsmonitorstate, ex)
536 537 # boo, Watchman failed. bail
537 538 return orig(node1, node2, match, listignored, listclean,
538 539 listunknown, listsubrepos)
539 540
540 541 if updatestate:
541 542 # We need info about unknown files. This may make things slower the
542 543 # first time, but whatever.
543 544 stateunknown = True
544 545 else:
545 546 stateunknown = listunknown
546 547
547 548 if updatestate:
548 549 ps = poststatus(startclock)
549 550 self.addpostdsstatus(ps)
550 551
551 552 r = orig(node1, node2, match, listignored, listclean, stateunknown,
552 553 listsubrepos)
553 554 modified, added, removed, deleted, unknown, ignored, clean = r
554 555
555 556 if not listunknown:
556 557 unknown = []
557 558
558 559 # don't do paranoid checks if we're not going to query Watchman anyway
559 560 full = listclean or match.traversedir is not None
560 561 if self._fsmonitorstate.mode == 'paranoid' and not full:
561 562 # run status again and fall back to the old walk this time
562 563 self.dirstate._fsmonitordisable = True
563 564
564 565 # shut the UI up
565 566 quiet = self.ui.quiet
566 567 self.ui.quiet = True
567 568 fout, ferr = self.ui.fout, self.ui.ferr
568 569 self.ui.fout = self.ui.ferr = open(os.devnull, 'wb')
569 570
570 571 try:
571 572 rv2 = orig(
572 573 node1, node2, match, listignored, listclean, listunknown,
573 574 listsubrepos)
574 575 finally:
575 576 self.dirstate._fsmonitordisable = False
576 577 self.ui.quiet = quiet
577 578 self.ui.fout, self.ui.ferr = fout, ferr
578 579
579 580 # clean isn't tested since it's set to True above
580 581 with self.wlock():
581 582 _cmpsets(
582 583 [modified, added, removed, deleted, unknown, ignored, clean],
583 584 rv2)
584 585 modified, added, removed, deleted, unknown, ignored, clean = rv2
585 586
586 587 return scmutil.status(
587 588 modified, added, removed, deleted, unknown, ignored, clean)
588 589
589 590 class poststatus(object):
590 591 def __init__(self, startclock):
591 592 self._startclock = startclock
592 593
593 594 def __call__(self, wctx, status):
594 595 clock = wctx.repo()._fsmonitorstate.getlastclock() or self._startclock
595 596 hashignore = _hashignore(wctx.repo().dirstate._ignore)
596 597 notefiles = (status.modified + status.added + status.removed +
597 598 status.deleted + status.unknown)
598 599 wctx.repo()._fsmonitorstate.set(clock, hashignore, notefiles)
599 600
600 601 def makedirstate(repo, dirstate):
601 602 class fsmonitordirstate(dirstate.__class__):
602 603 def _fsmonitorinit(self, repo):
603 604 # _fsmonitordisable is used in paranoid mode
604 605 self._fsmonitordisable = False
605 606 self._fsmonitorstate = repo._fsmonitorstate
606 607 self._watchmanclient = repo._watchmanclient
607 608 self._repo = weakref.proxy(repo)
608 609
609 610 def walk(self, *args, **kwargs):
610 611 orig = super(fsmonitordirstate, self).walk
611 612 if self._fsmonitordisable:
612 613 return orig(*args, **kwargs)
613 614 return overridewalk(orig, self, *args, **kwargs)
614 615
615 616 def rebuild(self, *args, **kwargs):
616 617 self._fsmonitorstate.invalidate()
617 618 return super(fsmonitordirstate, self).rebuild(*args, **kwargs)
618 619
619 620 def invalidate(self, *args, **kwargs):
620 621 self._fsmonitorstate.invalidate()
621 622 return super(fsmonitordirstate, self).invalidate(*args, **kwargs)
622 623
623 624 dirstate.__class__ = fsmonitordirstate
624 625 dirstate._fsmonitorinit(repo)
625 626
626 627 def wrapdirstate(orig, self):
627 628 ds = orig(self)
628 629 # only override the dirstate when Watchman is available for the repo
629 630 if util.safehasattr(self, '_fsmonitorstate'):
630 631 makedirstate(self, ds)
631 632 return ds
632 633
633 634 def extsetup(ui):
634 635 extensions.wrapfilecache(
635 636 localrepo.localrepository, 'dirstate', wrapdirstate)
636 637 if pycompat.isdarwin:
637 638 # An assist for avoiding the dangling-symlink fsevents bug
638 639 extensions.wrapfunction(os, 'symlink', wrapsymlink)
639 640
640 641 extensions.wrapfunction(merge, 'update', wrapupdate)
641 642
642 643 def wrapsymlink(orig, source, link_name):
643 644 ''' if we create a dangling symlink, also touch the parent dir
644 645 to encourage fsevents notifications to work more correctly '''
645 646 try:
646 647 return orig(source, link_name)
647 648 finally:
648 649 try:
649 650 os.utime(os.path.dirname(link_name), None)
650 651 except OSError:
651 652 pass
652 653
653 654 class state_update(object):
654 655 ''' This context manager is responsible for dispatching the state-enter
655 656 and state-leave signals to the watchman service. The enter and leave
656 657 methods can be invoked manually (for scenarios where context manager
657 658 semantics are not possible). If parameters oldnode and newnode are None,
658 659 they will be populated based on current working copy in enter and
659 660 leave, respectively. Similarly, if the distance is none, it will be
660 661 calculated based on the oldnode and newnode in the leave method.'''
661 662
662 663 def __init__(self, repo, name, oldnode=None, newnode=None, distance=None,
663 664 partial=False):
664 665 self.repo = repo.unfiltered()
665 666 self.name = name
666 667 self.oldnode = oldnode
667 668 self.newnode = newnode
668 669 self.distance = distance
669 670 self.partial = partial
670 671 self._lock = None
671 672 self.need_leave = False
672 673
673 674 def __enter__(self):
674 675 self.enter()
675 676
676 677 def enter(self):
677 678 # Make sure we have a wlock prior to sending notifications to watchman.
678 679 # We don't want to race with other actors. In the update case,
679 680 # merge.update is going to take the wlock almost immediately. We are
680 681 # effectively extending the lock around several short sanity checks.
681 682 if self.oldnode is None:
682 683 self.oldnode = self.repo['.'].node()
683 684
684 685 if self.repo.currentwlock() is None:
685 686 if util.safehasattr(self.repo, 'wlocknostateupdate'):
686 687 self._lock = self.repo.wlocknostateupdate()
687 688 else:
688 689 self._lock = self.repo.wlock()
689 690 self.need_leave = self._state(
690 691 'state-enter',
691 692 hex(self.oldnode))
692 693 return self
693 694
694 695 def __exit__(self, type_, value, tb):
695 696 abort = True if type_ else False
696 697 self.exit(abort=abort)
697 698
698 699 def exit(self, abort=False):
699 700 try:
700 701 if self.need_leave:
701 702 status = 'failed' if abort else 'ok'
702 703 if self.newnode is None:
703 704 self.newnode = self.repo['.'].node()
704 705 if self.distance is None:
705 706 self.distance = calcdistance(
706 707 self.repo, self.oldnode, self.newnode)
707 708 self._state(
708 709 'state-leave',
709 710 hex(self.newnode),
710 711 status=status)
711 712 finally:
712 713 self.need_leave = False
713 714 if self._lock:
714 715 self._lock.release()
715 716
716 717 def _state(self, cmd, commithash, status='ok'):
717 718 if not util.safehasattr(self.repo, '_watchmanclient'):
718 719 return False
719 720 try:
720 721 self.repo._watchmanclient.command(cmd, {
721 722 'name': self.name,
722 723 'metadata': {
723 724 # the target revision
724 725 'rev': commithash,
725 726 # approximate number of commits between current and target
726 727 'distance': self.distance if self.distance else 0,
727 728 # success/failure (only really meaningful for state-leave)
728 729 'status': status,
729 730 # whether the working copy parent is changing
730 731 'partial': self.partial,
731 732 }})
732 733 return True
733 734 except Exception as e:
734 735 # Swallow any errors; fire and forget
735 736 self.repo.ui.log(
736 737 'watchman', 'Exception %s while running %s\n', e, cmd)
737 738 return False
738 739
739 740 # Estimate the distance between two nodes
740 741 def calcdistance(repo, oldnode, newnode):
741 742 anc = repo.changelog.ancestor(oldnode, newnode)
742 743 ancrev = repo[anc].rev()
743 744 distance = (abs(repo[oldnode].rev() - ancrev)
744 745 + abs(repo[newnode].rev() - ancrev))
745 746 return distance
746 747
747 748 # Bracket working copy updates with calls to the watchman state-enter
748 749 # and state-leave commands. This allows clients to perform more intelligent
749 750 # settling during bulk file change scenarios
750 751 # https://facebook.github.io/watchman/docs/cmd/subscribe.html#advanced-settling
751 752 def wrapupdate(orig, repo, node, branchmerge, force, ancestor=None,
752 753 mergeancestor=False, labels=None, matcher=None, **kwargs):
753 754
754 755 distance = 0
755 756 partial = True
756 757 oldnode = repo['.'].node()
757 758 newnode = repo[node].node()
758 759 if matcher is None or matcher.always():
759 760 partial = False
760 761 distance = calcdistance(repo.unfiltered(), oldnode, newnode)
761 762
762 763 with state_update(repo, name="hg.update", oldnode=oldnode, newnode=newnode,
763 764 distance=distance, partial=partial):
764 765 return orig(
765 766 repo, node, branchmerge, force, ancestor, mergeancestor,
766 767 labels, matcher, **kwargs)
767 768
768 769 def repo_has_depth_one_nested_repo(repo):
769 770 for f in repo.wvfs.listdir():
770 771 if os.path.isdir(os.path.join(repo.root, f, '.hg')):
771 772 msg = 'fsmonitor: sub-repository %r detected, fsmonitor disabled\n'
772 773 repo.ui.debug(msg % f)
773 774 return True
774 775 return False
775 776
776 777 def reposetup(ui, repo):
777 778 # We don't work with largefiles or inotify
778 779 exts = extensions.enabled()
779 780 for ext in _blacklist:
780 781 if ext in exts:
781 782 ui.warn(_('The fsmonitor extension is incompatible with the %s '
782 783 'extension and has been disabled.\n') % ext)
783 784 return
784 785
785 786 if repo.local():
786 787 # We don't work with subrepos either.
787 788 #
788 789 # if repo[None].substate can cause a dirstate parse, which is too
789 790 # slow. Instead, look for a file called hgsubstate,
790 791 if repo.wvfs.exists('.hgsubstate') or repo.wvfs.exists('.hgsub'):
791 792 return
792 793
793 794 if repo_has_depth_one_nested_repo(repo):
794 795 return
795 796
796 797 fsmonitorstate = state.state(repo)
797 798 if fsmonitorstate.mode == 'off':
798 799 return
799 800
800 801 try:
801 802 client = watchmanclient.client(repo.ui, repo._root)
802 803 except Exception as ex:
803 804 _handleunavailable(ui, fsmonitorstate, ex)
804 805 return
805 806
806 807 repo._fsmonitorstate = fsmonitorstate
807 808 repo._watchmanclient = client
808 809
809 810 dirstate, cached = localrepo.isfilecached(repo, 'dirstate')
810 811 if cached:
811 812 # at this point since fsmonitorstate wasn't present,
812 813 # repo.dirstate is not a fsmonitordirstate
813 814 makedirstate(repo, dirstate)
814 815
815 816 class fsmonitorrepo(repo.__class__):
816 817 def status(self, *args, **kwargs):
817 818 orig = super(fsmonitorrepo, self).status
818 819 return overridestatus(orig, self, *args, **kwargs)
819 820
820 821 def wlocknostateupdate(self, *args, **kwargs):
821 822 return super(fsmonitorrepo, self).wlock(*args, **kwargs)
822 823
823 824 def wlock(self, *args, **kwargs):
824 825 l = super(fsmonitorrepo, self).wlock(*args, **kwargs)
825 826 if not ui.configbool(
826 827 "experimental", "fsmonitor.transaction_notify"):
827 828 return l
828 829 if l.held != 1:
829 830 return l
830 831 origrelease = l.releasefn
831 832
832 833 def staterelease():
833 834 if origrelease:
834 835 origrelease()
835 836 if l.stateupdate:
836 837 l.stateupdate.exit()
837 838 l.stateupdate = None
838 839
839 840 try:
840 841 l.stateupdate = None
841 842 l.stateupdate = state_update(self, name="hg.transaction")
842 843 l.stateupdate.enter()
843 844 l.releasefn = staterelease
844 845 except Exception as e:
845 846 # Swallow any errors; fire and forget
846 847 self.ui.log(
847 848 'watchman', 'Exception in state update %s\n', e)
848 849 return l
849 850
850 851 repo.__class__ = fsmonitorrepo
@@ -1,1113 +1,1113 b''
1 1 # __init__.py - remotefilelog extension
2 2 #
3 3 # Copyright 2013 Facebook, Inc.
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 """remotefilelog causes Mercurial to lazilly fetch file contents (EXPERIMENTAL)
8 8
9 9 This extension is HIGHLY EXPERIMENTAL. There are NO BACKWARDS COMPATIBILITY
10 10 GUARANTEES. This means that repositories created with this extension may
11 11 only be usable with the exact version of this extension/Mercurial that was
12 12 used. The extension attempts to enforce this in order to prevent repository
13 13 corruption.
14 14
15 15 remotefilelog works by fetching file contents lazily and storing them
16 16 in a cache on the client rather than in revlogs. This allows enormous
17 17 histories to be transferred only partially, making them easier to
18 18 operate on.
19 19
20 20 Configs:
21 21
22 22 ``packs.maxchainlen`` specifies the maximum delta chain length in pack files
23 23
24 24 ``packs.maxpacksize`` specifies the maximum pack file size
25 25
26 26 ``packs.maxpackfilecount`` specifies the maximum number of packs in the
27 27 shared cache (trees only for now)
28 28
29 29 ``remotefilelog.backgroundprefetch`` runs prefetch in background when True
30 30
31 31 ``remotefilelog.bgprefetchrevs`` specifies revisions to fetch on commit and
32 32 update, and on other commands that use them. Different from pullprefetch.
33 33
34 34 ``remotefilelog.gcrepack`` does garbage collection during repack when True
35 35
36 36 ``remotefilelog.nodettl`` specifies maximum TTL of a node in seconds before
37 37 it is garbage collected
38 38
39 39 ``remotefilelog.repackonhggc`` runs repack on hg gc when True
40 40
41 41 ``remotefilelog.prefetchdays`` specifies the maximum age of a commit in
42 42 days after which it is no longer prefetched.
43 43
44 44 ``remotefilelog.prefetchdelay`` specifies delay between background
45 45 prefetches in seconds after operations that change the working copy parent
46 46
47 47 ``remotefilelog.data.gencountlimit`` constraints the minimum number of data
48 48 pack files required to be considered part of a generation. In particular,
49 49 minimum number of packs files > gencountlimit.
50 50
51 51 ``remotefilelog.data.generations`` list for specifying the lower bound of
52 52 each generation of the data pack files. For example, list ['100MB','1MB']
53 53 or ['1MB', '100MB'] will lead to three generations: [0, 1MB), [
54 54 1MB, 100MB) and [100MB, infinity).
55 55
56 56 ``remotefilelog.data.maxrepackpacks`` the maximum number of pack files to
57 57 include in an incremental data repack.
58 58
59 59 ``remotefilelog.data.repackmaxpacksize`` the maximum size of a pack file for
60 60 it to be considered for an incremental data repack.
61 61
62 62 ``remotefilelog.data.repacksizelimit`` the maximum total size of pack files
63 63 to include in an incremental data repack.
64 64
65 65 ``remotefilelog.history.gencountlimit`` constraints the minimum number of
66 66 history pack files required to be considered part of a generation. In
67 67 particular, minimum number of packs files > gencountlimit.
68 68
69 69 ``remotefilelog.history.generations`` list for specifying the lower bound of
70 70 each generation of the history pack files. For example, list [
71 71 '100MB', '1MB'] or ['1MB', '100MB'] will lead to three generations: [
72 72 0, 1MB), [1MB, 100MB) and [100MB, infinity).
73 73
74 74 ``remotefilelog.history.maxrepackpacks`` the maximum number of pack files to
75 75 include in an incremental history repack.
76 76
77 77 ``remotefilelog.history.repackmaxpacksize`` the maximum size of a pack file
78 78 for it to be considered for an incremental history repack.
79 79
80 80 ``remotefilelog.history.repacksizelimit`` the maximum total size of pack
81 81 files to include in an incremental history repack.
82 82
83 83 ``remotefilelog.backgroundrepack`` automatically consolidate packs in the
84 84 background
85 85
86 86 ``remotefilelog.cachepath`` path to cache
87 87
88 88 ``remotefilelog.cachegroup`` if set, make cache directory sgid to this
89 89 group
90 90
91 91 ``remotefilelog.cacheprocess`` binary to invoke for fetching file data
92 92
93 93 ``remotefilelog.debug`` turn on remotefilelog-specific debug output
94 94
95 95 ``remotefilelog.excludepattern`` pattern of files to exclude from pulls
96 96
97 97 ``remotefilelog.includepattern`` pattern of files to include in pulls
98 98
99 99 ``remotefilelog.fetchwarning``: message to print when too many
100 100 single-file fetches occur
101 101
102 102 ``remotefilelog.getfilesstep`` number of files to request in a single RPC
103 103
104 104 ``remotefilelog.getfilestype`` if set to 'threaded' use threads to fetch
105 105 files, otherwise use optimistic fetching
106 106
107 107 ``remotefilelog.pullprefetch`` revset for selecting files that should be
108 108 eagerly downloaded rather than lazily
109 109
110 110 ``remotefilelog.reponame`` name of the repo. If set, used to partition
111 111 data from other repos in a shared store.
112 112
113 113 ``remotefilelog.server`` if true, enable server-side functionality
114 114
115 115 ``remotefilelog.servercachepath`` path for caching blobs on the server
116 116
117 117 ``remotefilelog.serverexpiration`` number of days to keep cached server
118 118 blobs
119 119
120 120 ``remotefilelog.validatecache`` if set, check cache entries for corruption
121 121 before returning blobs
122 122
123 123 ``remotefilelog.validatecachelog`` if set, check cache entries for
124 124 corruption before returning metadata
125 125
126 126 """
127 127 from __future__ import absolute_import
128 128
129 129 import os
130 130 import time
131 131 import traceback
132 132
133 133 from mercurial.node import hex
134 134 from mercurial.i18n import _
135 135 from mercurial import (
136 136 changegroup,
137 137 changelog,
138 138 cmdutil,
139 139 commands,
140 140 configitems,
141 141 context,
142 142 copies,
143 143 debugcommands as hgdebugcommands,
144 144 dispatch,
145 145 error,
146 146 exchange,
147 147 extensions,
148 148 hg,
149 149 localrepo,
150 150 match,
151 151 merge,
152 152 node as nodemod,
153 153 patch,
154 154 pycompat,
155 155 registrar,
156 156 repair,
157 157 repoview,
158 158 revset,
159 159 scmutil,
160 160 smartset,
161 161 streamclone,
162 162 util,
163 163 )
164 164 from . import (
165 165 constants,
166 166 debugcommands,
167 167 fileserverclient,
168 168 remotefilectx,
169 169 remotefilelog,
170 170 remotefilelogserver,
171 171 repack as repackmod,
172 172 shallowbundle,
173 173 shallowrepo,
174 174 shallowstore,
175 175 shallowutil,
176 176 shallowverifier,
177 177 )
178 178
179 179 # ensures debug commands are registered
180 180 hgdebugcommands.command
181 181
182 182 cmdtable = {}
183 183 command = registrar.command(cmdtable)
184 184
185 185 configtable = {}
186 186 configitem = registrar.configitem(configtable)
187 187
188 188 configitem('remotefilelog', 'debug', default=False)
189 189
190 190 configitem('remotefilelog', 'reponame', default='')
191 191 configitem('remotefilelog', 'cachepath', default=None)
192 192 configitem('remotefilelog', 'cachegroup', default=None)
193 193 configitem('remotefilelog', 'cacheprocess', default=None)
194 194 configitem('remotefilelog', 'cacheprocess.includepath', default=None)
195 195 configitem("remotefilelog", "cachelimit", default="1000 GB")
196 196
197 197 configitem('remotefilelog', 'fallbackpath', default=configitems.dynamicdefault,
198 198 alias=[('remotefilelog', 'fallbackrepo')])
199 199
200 200 configitem('remotefilelog', 'validatecachelog', default=None)
201 201 configitem('remotefilelog', 'validatecache', default='on')
202 202 configitem('remotefilelog', 'server', default=None)
203 203 configitem('remotefilelog', 'servercachepath', default=None)
204 204 configitem("remotefilelog", "serverexpiration", default=30)
205 205 configitem('remotefilelog', 'backgroundrepack', default=False)
206 206 configitem('remotefilelog', 'bgprefetchrevs', default=None)
207 207 configitem('remotefilelog', 'pullprefetch', default=None)
208 208 configitem('remotefilelog', 'backgroundprefetch', default=False)
209 209 configitem('remotefilelog', 'prefetchdelay', default=120)
210 210 configitem('remotefilelog', 'prefetchdays', default=14)
211 211
212 212 configitem('remotefilelog', 'getfilesstep', default=10000)
213 213 configitem('remotefilelog', 'getfilestype', default='optimistic')
214 214 configitem('remotefilelog', 'batchsize', configitems.dynamicdefault)
215 215 configitem('remotefilelog', 'fetchwarning', default='')
216 216
217 217 configitem('remotefilelog', 'includepattern', default=None)
218 218 configitem('remotefilelog', 'excludepattern', default=None)
219 219
220 220 configitem('remotefilelog', 'gcrepack', default=False)
221 221 configitem('remotefilelog', 'repackonhggc', default=False)
222 configitem('repack', 'chainorphansbysize', default=True)
222 configitem('repack', 'chainorphansbysize', default=True, experimental=True)
223 223
224 224 configitem('packs', 'maxpacksize', default=0)
225 225 configitem('packs', 'maxchainlen', default=1000)
226 226
227 227 # default TTL limit is 30 days
228 228 _defaultlimit = 60 * 60 * 24 * 30
229 229 configitem('remotefilelog', 'nodettl', default=_defaultlimit)
230 230
231 231 configitem('remotefilelog', 'data.gencountlimit', default=2),
232 232 configitem('remotefilelog', 'data.generations',
233 233 default=['1GB', '100MB', '1MB'])
234 234 configitem('remotefilelog', 'data.maxrepackpacks', default=50)
235 235 configitem('remotefilelog', 'data.repackmaxpacksize', default='4GB')
236 236 configitem('remotefilelog', 'data.repacksizelimit', default='100MB')
237 237
238 238 configitem('remotefilelog', 'history.gencountlimit', default=2),
239 239 configitem('remotefilelog', 'history.generations', default=['100MB'])
240 240 configitem('remotefilelog', 'history.maxrepackpacks', default=50)
241 241 configitem('remotefilelog', 'history.repackmaxpacksize', default='400MB')
242 242 configitem('remotefilelog', 'history.repacksizelimit', default='100MB')
243 243
244 244 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
245 245 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
246 246 # be specifying the version(s) of Mercurial they are tested with, or
247 247 # leave the attribute unspecified.
248 248 testedwith = 'ships-with-hg-core'
249 249
250 250 repoclass = localrepo.localrepository
251 251 repoclass._basesupported.add(constants.SHALLOWREPO_REQUIREMENT)
252 252
253 253 isenabled = shallowutil.isenabled
254 254
255 255 def uisetup(ui):
256 256 """Wraps user facing Mercurial commands to swap them out with shallow
257 257 versions.
258 258 """
259 259 hg.wirepeersetupfuncs.append(fileserverclient.peersetup)
260 260
261 261 entry = extensions.wrapcommand(commands.table, 'clone', cloneshallow)
262 262 entry[1].append(('', 'shallow', None,
263 263 _("create a shallow clone which uses remote file "
264 264 "history")))
265 265
266 266 extensions.wrapcommand(commands.table, 'debugindex',
267 267 debugcommands.debugindex)
268 268 extensions.wrapcommand(commands.table, 'debugindexdot',
269 269 debugcommands.debugindexdot)
270 270 extensions.wrapcommand(commands.table, 'log', log)
271 271 extensions.wrapcommand(commands.table, 'pull', pull)
272 272
273 273 # Prevent 'hg manifest --all'
274 274 def _manifest(orig, ui, repo, *args, **opts):
275 275 if (isenabled(repo) and opts.get(r'all')):
276 276 raise error.Abort(_("--all is not supported in a shallow repo"))
277 277
278 278 return orig(ui, repo, *args, **opts)
279 279 extensions.wrapcommand(commands.table, "manifest", _manifest)
280 280
281 281 # Wrap remotefilelog with lfs code
282 282 def _lfsloaded(loaded=False):
283 283 lfsmod = None
284 284 try:
285 285 lfsmod = extensions.find('lfs')
286 286 except KeyError:
287 287 pass
288 288 if lfsmod:
289 289 lfsmod.wrapfilelog(remotefilelog.remotefilelog)
290 290 fileserverclient._lfsmod = lfsmod
291 291 extensions.afterloaded('lfs', _lfsloaded)
292 292
293 293 # debugdata needs remotefilelog.len to work
294 294 extensions.wrapcommand(commands.table, 'debugdata', debugdatashallow)
295 295
296 296 changegroup.cgpacker = shallowbundle.shallowcg1packer
297 297
298 298 extensions.wrapfunction(changegroup, '_addchangegroupfiles',
299 299 shallowbundle.addchangegroupfiles)
300 300 extensions.wrapfunction(
301 301 changegroup, 'makechangegroup', shallowbundle.makechangegroup)
302 302 extensions.wrapfunction(localrepo, 'makestore', storewrapper)
303 303 extensions.wrapfunction(exchange, 'pull', exchangepull)
304 304 extensions.wrapfunction(merge, 'applyupdates', applyupdates)
305 305 extensions.wrapfunction(merge, '_checkunknownfiles', checkunknownfiles)
306 306 extensions.wrapfunction(context.workingctx, '_checklookup', checklookup)
307 307 extensions.wrapfunction(scmutil, '_findrenames', findrenames)
308 308 extensions.wrapfunction(copies, '_computeforwardmissing',
309 309 computeforwardmissing)
310 310 extensions.wrapfunction(dispatch, 'runcommand', runcommand)
311 311 extensions.wrapfunction(repair, '_collectbrokencsets', _collectbrokencsets)
312 312 extensions.wrapfunction(context.changectx, 'filectx', filectx)
313 313 extensions.wrapfunction(context.workingctx, 'filectx', workingfilectx)
314 314 extensions.wrapfunction(patch, 'trydiff', trydiff)
315 315 extensions.wrapfunction(hg, 'verify', _verify)
316 316 scmutil.fileprefetchhooks.add('remotefilelog', _fileprefetchhook)
317 317
318 318 # disappointing hacks below
319 319 extensions.wrapfunction(scmutil, 'getrenamedfn', getrenamedfn)
320 320 extensions.wrapfunction(revset, 'filelog', filelogrevset)
321 321 revset.symbols['filelog'] = revset.filelog
322 322 extensions.wrapfunction(cmdutil, 'walkfilerevs', walkfilerevs)
323 323
324 324
325 325 def cloneshallow(orig, ui, repo, *args, **opts):
326 326 if opts.get(r'shallow'):
327 327 repos = []
328 328 def pull_shallow(orig, self, *args, **kwargs):
329 329 if not isenabled(self):
330 330 repos.append(self.unfiltered())
331 331 # set up the client hooks so the post-clone update works
332 332 setupclient(self.ui, self.unfiltered())
333 333
334 334 # setupclient fixed the class on the repo itself
335 335 # but we also need to fix it on the repoview
336 336 if isinstance(self, repoview.repoview):
337 337 self.__class__.__bases__ = (self.__class__.__bases__[0],
338 338 self.unfiltered().__class__)
339 339 self.requirements.add(constants.SHALLOWREPO_REQUIREMENT)
340 340 self._writerequirements()
341 341
342 342 # Since setupclient hadn't been called, exchange.pull was not
343 343 # wrapped. So we need to manually invoke our version of it.
344 344 return exchangepull(orig, self, *args, **kwargs)
345 345 else:
346 346 return orig(self, *args, **kwargs)
347 347 extensions.wrapfunction(exchange, 'pull', pull_shallow)
348 348
349 349 # Wrap the stream logic to add requirements and to pass include/exclude
350 350 # patterns around.
351 351 def setup_streamout(repo, remote):
352 352 # Replace remote.stream_out with a version that sends file
353 353 # patterns.
354 354 def stream_out_shallow(orig):
355 355 caps = remote.capabilities()
356 356 if constants.NETWORK_CAP_LEGACY_SSH_GETFILES in caps:
357 357 opts = {}
358 358 if repo.includepattern:
359 359 opts[r'includepattern'] = '\0'.join(repo.includepattern)
360 360 if repo.excludepattern:
361 361 opts[r'excludepattern'] = '\0'.join(repo.excludepattern)
362 362 return remote._callstream('stream_out_shallow', **opts)
363 363 else:
364 364 return orig()
365 365 extensions.wrapfunction(remote, 'stream_out', stream_out_shallow)
366 366 def stream_wrap(orig, op):
367 367 setup_streamout(op.repo, op.remote)
368 368 return orig(op)
369 369 extensions.wrapfunction(
370 370 streamclone, 'maybeperformlegacystreamclone', stream_wrap)
371 371
372 372 def canperformstreamclone(orig, pullop, bundle2=False):
373 373 # remotefilelog is currently incompatible with the
374 374 # bundle2 flavor of streamclones, so force us to use
375 375 # v1 instead.
376 376 if 'v2' in pullop.remotebundle2caps.get('stream', []):
377 377 pullop.remotebundle2caps['stream'] = [
378 378 c for c in pullop.remotebundle2caps['stream']
379 379 if c != 'v2']
380 380 if bundle2:
381 381 return False, None
382 382 supported, requirements = orig(pullop, bundle2=bundle2)
383 383 if requirements is not None:
384 384 requirements.add(constants.SHALLOWREPO_REQUIREMENT)
385 385 return supported, requirements
386 386 extensions.wrapfunction(
387 387 streamclone, 'canperformstreamclone', canperformstreamclone)
388 388
389 389 try:
390 390 orig(ui, repo, *args, **opts)
391 391 finally:
392 392 if opts.get(r'shallow'):
393 393 for r in repos:
394 394 if util.safehasattr(r, 'fileservice'):
395 395 r.fileservice.close()
396 396
397 397 def debugdatashallow(orig, *args, **kwds):
398 398 oldlen = remotefilelog.remotefilelog.__len__
399 399 try:
400 400 remotefilelog.remotefilelog.__len__ = lambda x: 1
401 401 return orig(*args, **kwds)
402 402 finally:
403 403 remotefilelog.remotefilelog.__len__ = oldlen
404 404
405 405 def reposetup(ui, repo):
406 406 if not repo.local():
407 407 return
408 408
409 409 # put here intentionally bc doesnt work in uisetup
410 410 ui.setconfig('hooks', 'update.prefetch', wcpprefetch)
411 411 ui.setconfig('hooks', 'commit.prefetch', wcpprefetch)
412 412
413 413 isserverenabled = ui.configbool('remotefilelog', 'server')
414 414 isshallowclient = isenabled(repo)
415 415
416 416 if isserverenabled and isshallowclient:
417 417 raise RuntimeError("Cannot be both a server and shallow client.")
418 418
419 419 if isshallowclient:
420 420 setupclient(ui, repo)
421 421
422 422 if isserverenabled:
423 423 remotefilelogserver.setupserver(ui, repo)
424 424
425 425 def setupclient(ui, repo):
426 426 if not isinstance(repo, localrepo.localrepository):
427 427 return
428 428
429 429 # Even clients get the server setup since they need to have the
430 430 # wireprotocol endpoints registered.
431 431 remotefilelogserver.onetimesetup(ui)
432 432 onetimeclientsetup(ui)
433 433
434 434 shallowrepo.wraprepo(repo)
435 435 repo.store = shallowstore.wrapstore(repo.store)
436 436
437 437 def storewrapper(orig, requirements, path, vfstype):
438 438 s = orig(requirements, path, vfstype)
439 439 if constants.SHALLOWREPO_REQUIREMENT in requirements:
440 440 s = shallowstore.wrapstore(s)
441 441
442 442 return s
443 443
444 444 # prefetch files before update
445 445 def applyupdates(orig, repo, actions, wctx, mctx, overwrite, wantfiledata,
446 446 labels=None):
447 447 if isenabled(repo):
448 448 manifest = mctx.manifest()
449 449 files = []
450 450 for f, args, msg in actions['g']:
451 451 files.append((f, hex(manifest[f])))
452 452 # batch fetch the needed files from the server
453 453 repo.fileservice.prefetch(files)
454 454 return orig(repo, actions, wctx, mctx, overwrite, wantfiledata,
455 455 labels=labels)
456 456
457 457 # Prefetch merge checkunknownfiles
458 458 def checkunknownfiles(orig, repo, wctx, mctx, force, actions,
459 459 *args, **kwargs):
460 460 if isenabled(repo):
461 461 files = []
462 462 sparsematch = repo.maybesparsematch(mctx.rev())
463 463 for f, (m, actionargs, msg) in actions.iteritems():
464 464 if sparsematch and not sparsematch(f):
465 465 continue
466 466 if m in ('c', 'dc', 'cm'):
467 467 files.append((f, hex(mctx.filenode(f))))
468 468 elif m == 'dg':
469 469 f2 = actionargs[0]
470 470 files.append((f2, hex(mctx.filenode(f2))))
471 471 # batch fetch the needed files from the server
472 472 repo.fileservice.prefetch(files)
473 473 return orig(repo, wctx, mctx, force, actions, *args, **kwargs)
474 474
475 475 # Prefetch files before status attempts to look at their size and contents
476 476 def checklookup(orig, self, files):
477 477 repo = self._repo
478 478 if isenabled(repo):
479 479 prefetchfiles = []
480 480 for parent in self._parents:
481 481 for f in files:
482 482 if f in parent:
483 483 prefetchfiles.append((f, hex(parent.filenode(f))))
484 484 # batch fetch the needed files from the server
485 485 repo.fileservice.prefetch(prefetchfiles)
486 486 return orig(self, files)
487 487
488 488 # Prefetch the logic that compares added and removed files for renames
489 489 def findrenames(orig, repo, matcher, added, removed, *args, **kwargs):
490 490 if isenabled(repo):
491 491 files = []
492 492 pmf = repo['.'].manifest()
493 493 for f in removed:
494 494 if f in pmf:
495 495 files.append((f, hex(pmf[f])))
496 496 # batch fetch the needed files from the server
497 497 repo.fileservice.prefetch(files)
498 498 return orig(repo, matcher, added, removed, *args, **kwargs)
499 499
500 500 # prefetch files before pathcopies check
501 501 def computeforwardmissing(orig, a, b, match=None):
502 502 missing = orig(a, b, match=match)
503 503 repo = a._repo
504 504 if isenabled(repo):
505 505 mb = b.manifest()
506 506
507 507 files = []
508 508 sparsematch = repo.maybesparsematch(b.rev())
509 509 if sparsematch:
510 510 sparsemissing = set()
511 511 for f in missing:
512 512 if sparsematch(f):
513 513 files.append((f, hex(mb[f])))
514 514 sparsemissing.add(f)
515 515 missing = sparsemissing
516 516
517 517 # batch fetch the needed files from the server
518 518 repo.fileservice.prefetch(files)
519 519 return missing
520 520
521 521 # close cache miss server connection after the command has finished
522 522 def runcommand(orig, lui, repo, *args, **kwargs):
523 523 fileservice = None
524 524 # repo can be None when running in chg:
525 525 # - at startup, reposetup was called because serve is not norepo
526 526 # - a norepo command like "help" is called
527 527 if repo and isenabled(repo):
528 528 fileservice = repo.fileservice
529 529 try:
530 530 return orig(lui, repo, *args, **kwargs)
531 531 finally:
532 532 if fileservice:
533 533 fileservice.close()
534 534
535 535 # prevent strip from stripping remotefilelogs
536 536 def _collectbrokencsets(orig, repo, files, striprev):
537 537 if isenabled(repo):
538 538 files = list([f for f in files if not repo.shallowmatch(f)])
539 539 return orig(repo, files, striprev)
540 540
541 541 # changectx wrappers
542 542 def filectx(orig, self, path, fileid=None, filelog=None):
543 543 if fileid is None:
544 544 fileid = self.filenode(path)
545 545 if (isenabled(self._repo) and self._repo.shallowmatch(path)):
546 546 return remotefilectx.remotefilectx(self._repo, path, fileid=fileid,
547 547 changectx=self, filelog=filelog)
548 548 return orig(self, path, fileid=fileid, filelog=filelog)
549 549
550 550 def workingfilectx(orig, self, path, filelog=None):
551 551 if (isenabled(self._repo) and self._repo.shallowmatch(path)):
552 552 return remotefilectx.remoteworkingfilectx(self._repo, path,
553 553 workingctx=self,
554 554 filelog=filelog)
555 555 return orig(self, path, filelog=filelog)
556 556
557 557 # prefetch required revisions before a diff
558 558 def trydiff(orig, repo, revs, ctx1, ctx2, modified, added, removed,
559 559 copy, getfilectx, *args, **kwargs):
560 560 if isenabled(repo):
561 561 prefetch = []
562 562 mf1 = ctx1.manifest()
563 563 for fname in modified + added + removed:
564 564 if fname in mf1:
565 565 fnode = getfilectx(fname, ctx1).filenode()
566 566 # fnode can be None if it's a edited working ctx file
567 567 if fnode:
568 568 prefetch.append((fname, hex(fnode)))
569 569 if fname not in removed:
570 570 fnode = getfilectx(fname, ctx2).filenode()
571 571 if fnode:
572 572 prefetch.append((fname, hex(fnode)))
573 573
574 574 repo.fileservice.prefetch(prefetch)
575 575
576 576 return orig(repo, revs, ctx1, ctx2, modified, added, removed, copy,
577 577 getfilectx, *args, **kwargs)
578 578
579 579 # Prevent verify from processing files
580 580 # a stub for mercurial.hg.verify()
581 581 def _verify(orig, repo, level=None):
582 582 lock = repo.lock()
583 583 try:
584 584 return shallowverifier.shallowverifier(repo).verify()
585 585 finally:
586 586 lock.release()
587 587
588 588
589 589 clientonetime = False
590 590 def onetimeclientsetup(ui):
591 591 global clientonetime
592 592 if clientonetime:
593 593 return
594 594 clientonetime = True
595 595
596 596 # Don't commit filelogs until we know the commit hash, since the hash
597 597 # is present in the filelog blob.
598 598 # This violates Mercurial's filelog->manifest->changelog write order,
599 599 # but is generally fine for client repos.
600 600 pendingfilecommits = []
601 601 def addrawrevision(orig, self, rawtext, transaction, link, p1, p2, node,
602 602 flags, cachedelta=None, _metatuple=None):
603 603 if isinstance(link, int):
604 604 pendingfilecommits.append(
605 605 (self, rawtext, transaction, link, p1, p2, node, flags,
606 606 cachedelta, _metatuple))
607 607 return node
608 608 else:
609 609 return orig(self, rawtext, transaction, link, p1, p2, node, flags,
610 610 cachedelta, _metatuple=_metatuple)
611 611 extensions.wrapfunction(
612 612 remotefilelog.remotefilelog, 'addrawrevision', addrawrevision)
613 613
614 614 def changelogadd(orig, self, *args):
615 615 oldlen = len(self)
616 616 node = orig(self, *args)
617 617 newlen = len(self)
618 618 if oldlen != newlen:
619 619 for oldargs in pendingfilecommits:
620 620 log, rt, tr, link, p1, p2, n, fl, c, m = oldargs
621 621 linknode = self.node(link)
622 622 if linknode == node:
623 623 log.addrawrevision(rt, tr, linknode, p1, p2, n, fl, c, m)
624 624 else:
625 625 raise error.ProgrammingError(
626 626 'pending multiple integer revisions are not supported')
627 627 else:
628 628 # "link" is actually wrong here (it is set to len(changelog))
629 629 # if changelog remains unchanged, skip writing file revisions
630 630 # but still do a sanity check about pending multiple revisions
631 631 if len(set(x[3] for x in pendingfilecommits)) > 1:
632 632 raise error.ProgrammingError(
633 633 'pending multiple integer revisions are not supported')
634 634 del pendingfilecommits[:]
635 635 return node
636 636 extensions.wrapfunction(changelog.changelog, 'add', changelogadd)
637 637
638 638 def getrenamedfn(orig, repo, endrev=None):
639 639 if not isenabled(repo) or copies.usechangesetcentricalgo(repo):
640 640 return orig(repo, endrev)
641 641
642 642 rcache = {}
643 643
644 644 def getrenamed(fn, rev):
645 645 '''looks up all renames for a file (up to endrev) the first
646 646 time the file is given. It indexes on the changerev and only
647 647 parses the manifest if linkrev != changerev.
648 648 Returns rename info for fn at changerev rev.'''
649 649 if rev in rcache.setdefault(fn, {}):
650 650 return rcache[fn][rev]
651 651
652 652 try:
653 653 fctx = repo[rev].filectx(fn)
654 654 for ancestor in fctx.ancestors():
655 655 if ancestor.path() == fn:
656 656 renamed = ancestor.renamed()
657 657 rcache[fn][ancestor.rev()] = renamed and renamed[0]
658 658
659 659 renamed = fctx.renamed()
660 660 return renamed and renamed[0]
661 661 except error.LookupError:
662 662 return None
663 663
664 664 return getrenamed
665 665
666 666 def walkfilerevs(orig, repo, match, follow, revs, fncache):
667 667 if not isenabled(repo):
668 668 return orig(repo, match, follow, revs, fncache)
669 669
670 670 # remotefilelog's can't be walked in rev order, so throw.
671 671 # The caller will see the exception and walk the commit tree instead.
672 672 if not follow:
673 673 raise cmdutil.FileWalkError("Cannot walk via filelog")
674 674
675 675 wanted = set()
676 676 minrev, maxrev = min(revs), max(revs)
677 677
678 678 pctx = repo['.']
679 679 for filename in match.files():
680 680 if filename not in pctx:
681 681 raise error.Abort(_('cannot follow file not in parent '
682 682 'revision: "%s"') % filename)
683 683 fctx = pctx[filename]
684 684
685 685 linkrev = fctx.linkrev()
686 686 if linkrev >= minrev and linkrev <= maxrev:
687 687 fncache.setdefault(linkrev, []).append(filename)
688 688 wanted.add(linkrev)
689 689
690 690 for ancestor in fctx.ancestors():
691 691 linkrev = ancestor.linkrev()
692 692 if linkrev >= minrev and linkrev <= maxrev:
693 693 fncache.setdefault(linkrev, []).append(ancestor.path())
694 694 wanted.add(linkrev)
695 695
696 696 return wanted
697 697
698 698 def filelogrevset(orig, repo, subset, x):
699 699 """``filelog(pattern)``
700 700 Changesets connected to the specified filelog.
701 701
702 702 For performance reasons, ``filelog()`` does not show every changeset
703 703 that affects the requested file(s). See :hg:`help log` for details. For
704 704 a slower, more accurate result, use ``file()``.
705 705 """
706 706
707 707 if not isenabled(repo):
708 708 return orig(repo, subset, x)
709 709
710 710 # i18n: "filelog" is a keyword
711 711 pat = revset.getstring(x, _("filelog requires a pattern"))
712 712 m = match.match(repo.root, repo.getcwd(), [pat], default='relpath',
713 713 ctx=repo[None])
714 714 s = set()
715 715
716 716 if not match.patkind(pat):
717 717 # slow
718 718 for r in subset:
719 719 ctx = repo[r]
720 720 cfiles = ctx.files()
721 721 for f in m.files():
722 722 if f in cfiles:
723 723 s.add(ctx.rev())
724 724 break
725 725 else:
726 726 # partial
727 727 files = (f for f in repo[None] if m(f))
728 728 for f in files:
729 729 fctx = repo[None].filectx(f)
730 730 s.add(fctx.linkrev())
731 731 for actx in fctx.ancestors():
732 732 s.add(actx.linkrev())
733 733
734 734 return smartset.baseset([r for r in subset if r in s])
735 735
736 736 @command('gc', [], _('hg gc [REPO...]'), norepo=True)
737 737 def gc(ui, *args, **opts):
738 738 '''garbage collect the client and server filelog caches
739 739 '''
740 740 cachepaths = set()
741 741
742 742 # get the system client cache
743 743 systemcache = shallowutil.getcachepath(ui, allowempty=True)
744 744 if systemcache:
745 745 cachepaths.add(systemcache)
746 746
747 747 # get repo client and server cache
748 748 repopaths = []
749 749 pwd = ui.environ.get('PWD')
750 750 if pwd:
751 751 repopaths.append(pwd)
752 752
753 753 repopaths.extend(args)
754 754 repos = []
755 755 for repopath in repopaths:
756 756 try:
757 757 repo = hg.peer(ui, {}, repopath)
758 758 repos.append(repo)
759 759
760 760 repocache = shallowutil.getcachepath(repo.ui, allowempty=True)
761 761 if repocache:
762 762 cachepaths.add(repocache)
763 763 except error.RepoError:
764 764 pass
765 765
766 766 # gc client cache
767 767 for cachepath in cachepaths:
768 768 gcclient(ui, cachepath)
769 769
770 770 # gc server cache
771 771 for repo in repos:
772 772 remotefilelogserver.gcserver(ui, repo._repo)
773 773
774 774 def gcclient(ui, cachepath):
775 775 # get list of repos that use this cache
776 776 repospath = os.path.join(cachepath, 'repos')
777 777 if not os.path.exists(repospath):
778 778 ui.warn(_("no known cache at %s\n") % cachepath)
779 779 return
780 780
781 781 reposfile = open(repospath, 'rb')
782 782 repos = {r[:-1] for r in reposfile.readlines()}
783 783 reposfile.close()
784 784
785 785 # build list of useful files
786 786 validrepos = []
787 787 keepkeys = set()
788 788
789 789 sharedcache = None
790 790 filesrepacked = False
791 791
792 792 count = 0
793 793 progress = ui.makeprogress(_("analyzing repositories"), unit="repos",
794 794 total=len(repos))
795 795 for path in repos:
796 796 progress.update(count)
797 797 count += 1
798 798 try:
799 799 path = ui.expandpath(os.path.normpath(path))
800 800 except TypeError as e:
801 801 ui.warn(_("warning: malformed path: %r:%s\n") % (path, e))
802 802 traceback.print_exc()
803 803 continue
804 804 try:
805 805 peer = hg.peer(ui, {}, path)
806 806 repo = peer._repo
807 807 except error.RepoError:
808 808 continue
809 809
810 810 validrepos.append(path)
811 811
812 812 # Protect against any repo or config changes that have happened since
813 813 # this repo was added to the repos file. We'd rather this loop succeed
814 814 # and too much be deleted, than the loop fail and nothing gets deleted.
815 815 if not isenabled(repo):
816 816 continue
817 817
818 818 if not util.safehasattr(repo, 'name'):
819 819 ui.warn(_("repo %s is a misconfigured remotefilelog repo\n") % path)
820 820 continue
821 821
822 822 # If garbage collection on repack and repack on hg gc are enabled
823 823 # then loose files are repacked and garbage collected.
824 824 # Otherwise regular garbage collection is performed.
825 825 repackonhggc = repo.ui.configbool('remotefilelog', 'repackonhggc')
826 826 gcrepack = repo.ui.configbool('remotefilelog', 'gcrepack')
827 827 if repackonhggc and gcrepack:
828 828 try:
829 829 repackmod.incrementalrepack(repo)
830 830 filesrepacked = True
831 831 continue
832 832 except (IOError, repackmod.RepackAlreadyRunning):
833 833 # If repack cannot be performed due to not enough disk space
834 834 # continue doing garbage collection of loose files w/o repack
835 835 pass
836 836
837 837 reponame = repo.name
838 838 if not sharedcache:
839 839 sharedcache = repo.sharedstore
840 840
841 841 # Compute a keepset which is not garbage collected
842 842 def keyfn(fname, fnode):
843 843 return fileserverclient.getcachekey(reponame, fname, hex(fnode))
844 844 keepkeys = repackmod.keepset(repo, keyfn=keyfn, lastkeepkeys=keepkeys)
845 845
846 846 progress.complete()
847 847
848 848 # write list of valid repos back
849 849 oldumask = os.umask(0o002)
850 850 try:
851 851 reposfile = open(repospath, 'wb')
852 852 reposfile.writelines([("%s\n" % r) for r in validrepos])
853 853 reposfile.close()
854 854 finally:
855 855 os.umask(oldumask)
856 856
857 857 # prune cache
858 858 if sharedcache is not None:
859 859 sharedcache.gc(keepkeys)
860 860 elif not filesrepacked:
861 861 ui.warn(_("warning: no valid repos in repofile\n"))
862 862
863 863 def log(orig, ui, repo, *pats, **opts):
864 864 if not isenabled(repo):
865 865 return orig(ui, repo, *pats, **opts)
866 866
867 867 follow = opts.get(r'follow')
868 868 revs = opts.get(r'rev')
869 869 if pats:
870 870 # Force slowpath for non-follow patterns and follows that start from
871 871 # non-working-copy-parent revs.
872 872 if not follow or revs:
873 873 # This forces the slowpath
874 874 opts[r'removed'] = True
875 875
876 876 # If this is a non-follow log without any revs specified, recommend that
877 877 # the user add -f to speed it up.
878 878 if not follow and not revs:
879 879 match = scmutil.match(repo['.'], pats, pycompat.byteskwargs(opts))
880 880 isfile = not match.anypats()
881 881 if isfile:
882 882 for file in match.files():
883 883 if not os.path.isfile(repo.wjoin(file)):
884 884 isfile = False
885 885 break
886 886
887 887 if isfile:
888 888 ui.warn(_("warning: file log can be slow on large repos - " +
889 889 "use -f to speed it up\n"))
890 890
891 891 return orig(ui, repo, *pats, **opts)
892 892
893 893 def revdatelimit(ui, revset):
894 894 """Update revset so that only changesets no older than 'prefetchdays' days
895 895 are included. The default value is set to 14 days. If 'prefetchdays' is set
896 896 to zero or negative value then date restriction is not applied.
897 897 """
898 898 days = ui.configint('remotefilelog', 'prefetchdays')
899 899 if days > 0:
900 900 revset = '(%s) & date(-%s)' % (revset, days)
901 901 return revset
902 902
903 903 def readytofetch(repo):
904 904 """Check that enough time has passed since the last background prefetch.
905 905 This only relates to prefetches after operations that change the working
906 906 copy parent. Default delay between background prefetches is 2 minutes.
907 907 """
908 908 timeout = repo.ui.configint('remotefilelog', 'prefetchdelay')
909 909 fname = repo.vfs.join('lastprefetch')
910 910
911 911 ready = False
912 912 with open(fname, 'a'):
913 913 # the with construct above is used to avoid race conditions
914 914 modtime = os.path.getmtime(fname)
915 915 if (time.time() - modtime) > timeout:
916 916 os.utime(fname, None)
917 917 ready = True
918 918
919 919 return ready
920 920
921 921 def wcpprefetch(ui, repo, **kwargs):
922 922 """Prefetches in background revisions specified by bgprefetchrevs revset.
923 923 Does background repack if backgroundrepack flag is set in config.
924 924 """
925 925 shallow = isenabled(repo)
926 926 bgprefetchrevs = ui.config('remotefilelog', 'bgprefetchrevs')
927 927 isready = readytofetch(repo)
928 928
929 929 if not (shallow and bgprefetchrevs and isready):
930 930 return
931 931
932 932 bgrepack = repo.ui.configbool('remotefilelog', 'backgroundrepack')
933 933 # update a revset with a date limit
934 934 bgprefetchrevs = revdatelimit(ui, bgprefetchrevs)
935 935
936 936 def anon():
937 937 if util.safehasattr(repo, 'ranprefetch') and repo.ranprefetch:
938 938 return
939 939 repo.ranprefetch = True
940 940 repo.backgroundprefetch(bgprefetchrevs, repack=bgrepack)
941 941
942 942 repo._afterlock(anon)
943 943
944 944 def pull(orig, ui, repo, *pats, **opts):
945 945 result = orig(ui, repo, *pats, **opts)
946 946
947 947 if isenabled(repo):
948 948 # prefetch if it's configured
949 949 prefetchrevset = ui.config('remotefilelog', 'pullprefetch')
950 950 bgrepack = repo.ui.configbool('remotefilelog', 'backgroundrepack')
951 951 bgprefetch = repo.ui.configbool('remotefilelog', 'backgroundprefetch')
952 952
953 953 if prefetchrevset:
954 954 ui.status(_("prefetching file contents\n"))
955 955 revs = scmutil.revrange(repo, [prefetchrevset])
956 956 base = repo['.'].rev()
957 957 if bgprefetch:
958 958 repo.backgroundprefetch(prefetchrevset, repack=bgrepack)
959 959 else:
960 960 repo.prefetch(revs, base=base)
961 961 if bgrepack:
962 962 repackmod.backgroundrepack(repo, incremental=True)
963 963 elif bgrepack:
964 964 repackmod.backgroundrepack(repo, incremental=True)
965 965
966 966 return result
967 967
968 968 def exchangepull(orig, repo, remote, *args, **kwargs):
969 969 # Hook into the callstream/getbundle to insert bundle capabilities
970 970 # during a pull.
971 971 def localgetbundle(orig, source, heads=None, common=None, bundlecaps=None,
972 972 **kwargs):
973 973 if not bundlecaps:
974 974 bundlecaps = set()
975 975 bundlecaps.add(constants.BUNDLE2_CAPABLITY)
976 976 return orig(source, heads=heads, common=common, bundlecaps=bundlecaps,
977 977 **kwargs)
978 978
979 979 if util.safehasattr(remote, '_callstream'):
980 980 remote._localrepo = repo
981 981 elif util.safehasattr(remote, 'getbundle'):
982 982 extensions.wrapfunction(remote, 'getbundle', localgetbundle)
983 983
984 984 return orig(repo, remote, *args, **kwargs)
985 985
986 986 def _fileprefetchhook(repo, revs, match):
987 987 if isenabled(repo):
988 988 allfiles = []
989 989 for rev in revs:
990 990 if rev == nodemod.wdirrev or rev is None:
991 991 continue
992 992 ctx = repo[rev]
993 993 mf = ctx.manifest()
994 994 sparsematch = repo.maybesparsematch(ctx.rev())
995 995 for path in ctx.walk(match):
996 996 if (not sparsematch or sparsematch(path)) and path in mf:
997 997 allfiles.append((path, hex(mf[path])))
998 998 repo.fileservice.prefetch(allfiles)
999 999
1000 1000 @command('debugremotefilelog', [
1001 1001 ('d', 'decompress', None, _('decompress the filelog first')),
1002 1002 ], _('hg debugremotefilelog <path>'), norepo=True)
1003 1003 def debugremotefilelog(ui, path, **opts):
1004 1004 return debugcommands.debugremotefilelog(ui, path, **opts)
1005 1005
1006 1006 @command('verifyremotefilelog', [
1007 1007 ('d', 'decompress', None, _('decompress the filelogs first')),
1008 1008 ], _('hg verifyremotefilelogs <directory>'), norepo=True)
1009 1009 def verifyremotefilelog(ui, path, **opts):
1010 1010 return debugcommands.verifyremotefilelog(ui, path, **opts)
1011 1011
1012 1012 @command('debugdatapack', [
1013 1013 ('', 'long', None, _('print the long hashes')),
1014 1014 ('', 'node', '', _('dump the contents of node'), 'NODE'),
1015 1015 ], _('hg debugdatapack <paths>'), norepo=True)
1016 1016 def debugdatapack(ui, *paths, **opts):
1017 1017 return debugcommands.debugdatapack(ui, *paths, **opts)
1018 1018
1019 1019 @command('debughistorypack', [
1020 1020 ], _('hg debughistorypack <path>'), norepo=True)
1021 1021 def debughistorypack(ui, path, **opts):
1022 1022 return debugcommands.debughistorypack(ui, path)
1023 1023
1024 1024 @command('debugkeepset', [
1025 1025 ], _('hg debugkeepset'))
1026 1026 def debugkeepset(ui, repo, **opts):
1027 1027 # The command is used to measure keepset computation time
1028 1028 def keyfn(fname, fnode):
1029 1029 return fileserverclient.getcachekey(repo.name, fname, hex(fnode))
1030 1030 repackmod.keepset(repo, keyfn)
1031 1031 return
1032 1032
1033 1033 @command('debugwaitonrepack', [
1034 1034 ], _('hg debugwaitonrepack'))
1035 1035 def debugwaitonrepack(ui, repo, **opts):
1036 1036 return debugcommands.debugwaitonrepack(repo)
1037 1037
1038 1038 @command('debugwaitonprefetch', [
1039 1039 ], _('hg debugwaitonprefetch'))
1040 1040 def debugwaitonprefetch(ui, repo, **opts):
1041 1041 return debugcommands.debugwaitonprefetch(repo)
1042 1042
1043 1043 def resolveprefetchopts(ui, opts):
1044 1044 if not opts.get('rev'):
1045 1045 revset = ['.', 'draft()']
1046 1046
1047 1047 prefetchrevset = ui.config('remotefilelog', 'pullprefetch', None)
1048 1048 if prefetchrevset:
1049 1049 revset.append('(%s)' % prefetchrevset)
1050 1050 bgprefetchrevs = ui.config('remotefilelog', 'bgprefetchrevs', None)
1051 1051 if bgprefetchrevs:
1052 1052 revset.append('(%s)' % bgprefetchrevs)
1053 1053 revset = '+'.join(revset)
1054 1054
1055 1055 # update a revset with a date limit
1056 1056 revset = revdatelimit(ui, revset)
1057 1057
1058 1058 opts['rev'] = [revset]
1059 1059
1060 1060 if not opts.get('base'):
1061 1061 opts['base'] = None
1062 1062
1063 1063 return opts
1064 1064
1065 1065 @command('prefetch', [
1066 1066 ('r', 'rev', [], _('prefetch the specified revisions'), _('REV')),
1067 1067 ('', 'repack', False, _('run repack after prefetch')),
1068 1068 ('b', 'base', '', _("rev that is assumed to already be local")),
1069 1069 ] + commands.walkopts, _('hg prefetch [OPTIONS] [FILE...]'))
1070 1070 def prefetch(ui, repo, *pats, **opts):
1071 1071 """prefetch file revisions from the server
1072 1072
1073 1073 Prefetchs file revisions for the specified revs and stores them in the
1074 1074 local remotefilelog cache. If no rev is specified, the default rev is
1075 1075 used which is the union of dot, draft, pullprefetch and bgprefetchrev.
1076 1076 File names or patterns can be used to limit which files are downloaded.
1077 1077
1078 1078 Return 0 on success.
1079 1079 """
1080 1080 opts = pycompat.byteskwargs(opts)
1081 1081 if not isenabled(repo):
1082 1082 raise error.Abort(_("repo is not shallow"))
1083 1083
1084 1084 opts = resolveprefetchopts(ui, opts)
1085 1085 revs = scmutil.revrange(repo, opts.get('rev'))
1086 1086 repo.prefetch(revs, opts.get('base'), pats, opts)
1087 1087
1088 1088 # Run repack in background
1089 1089 if opts.get('repack'):
1090 1090 repackmod.backgroundrepack(repo, incremental=True)
1091 1091
1092 1092 @command('repack', [
1093 1093 ('', 'background', None, _('run in a background process'), None),
1094 1094 ('', 'incremental', None, _('do an incremental repack'), None),
1095 1095 ('', 'packsonly', None, _('only repack packs (skip loose objects)'), None),
1096 1096 ], _('hg repack [OPTIONS]'))
1097 1097 def repack_(ui, repo, *pats, **opts):
1098 1098 if opts.get(r'background'):
1099 1099 repackmod.backgroundrepack(repo, incremental=opts.get(r'incremental'),
1100 1100 packsonly=opts.get(r'packsonly', False))
1101 1101 return
1102 1102
1103 1103 options = {'packsonly': opts.get(r'packsonly')}
1104 1104
1105 1105 try:
1106 1106 if opts.get(r'incremental'):
1107 1107 repackmod.incrementalrepack(repo, options=options)
1108 1108 else:
1109 1109 repackmod.fullrepack(repo, options=options)
1110 1110 except repackmod.RepackAlreadyRunning as ex:
1111 1111 # Don't propogate the exception if the repack is already in
1112 1112 # progress, since we want the command to exit 0.
1113 1113 repo.ui.warn('%s\n' % ex)
@@ -1,1174 +1,1175 b''
1 1 # sqlitestore.py - Storage backend that uses SQLite
2 2 #
3 3 # Copyright 2018 Gregory Szorc <gregory.szorc@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 """store repository data in SQLite (EXPERIMENTAL)
9 9
10 10 The sqlitestore extension enables the storage of repository data in SQLite.
11 11
12 12 This extension is HIGHLY EXPERIMENTAL. There are NO BACKWARDS COMPATIBILITY
13 13 GUARANTEES. This means that repositories created with this extension may
14 14 only be usable with the exact version of this extension/Mercurial that was
15 15 used. The extension attempts to enforce this in order to prevent repository
16 16 corruption.
17 17
18 18 In addition, several features are not yet supported or have known bugs:
19 19
20 20 * Only some data is stored in SQLite. Changeset, manifest, and other repository
21 21 data is not yet stored in SQLite.
22 22 * Transactions are not robust. If the process is aborted at the right time
23 23 during transaction close/rollback, the repository could be in an inconsistent
24 24 state. This problem will diminish once all repository data is tracked by
25 25 SQLite.
26 26 * Bundle repositories do not work (the ability to use e.g.
27 27 `hg -R <bundle-file> log` to automatically overlay a bundle on top of the
28 28 existing repository).
29 29 * Various other features don't work.
30 30
31 31 This extension should work for basic clone/pull, update, and commit workflows.
32 32 Some history rewriting operations may fail due to lack of support for bundle
33 33 repositories.
34 34
35 35 To use, activate the extension and set the ``storage.new-repo-backend`` config
36 36 option to ``sqlite`` to enable new repositories to use SQLite for storage.
37 37 """
38 38
39 39 # To run the test suite with repos using SQLite by default, execute the
40 40 # following:
41 41 #
42 42 # HGREPOFEATURES="sqlitestore" run-tests.py \
43 43 # --extra-config-opt extensions.sqlitestore= \
44 44 # --extra-config-opt storage.new-repo-backend=sqlite
45 45
46 46 from __future__ import absolute_import
47 47
48 48 import hashlib
49 49 import sqlite3
50 50 import struct
51 51 import threading
52 52 import zlib
53 53
54 54 from mercurial.i18n import _
55 55 from mercurial.node import (
56 56 nullid,
57 57 nullrev,
58 58 short,
59 59 )
60 60 from mercurial.thirdparty import (
61 61 attr,
62 62 )
63 63 from mercurial import (
64 64 ancestor,
65 65 dagop,
66 66 encoding,
67 67 error,
68 68 extensions,
69 69 localrepo,
70 70 mdiff,
71 71 pycompat,
72 72 registrar,
73 73 repository,
74 74 util,
75 75 verify,
76 76 )
77 77 from mercurial.utils import (
78 78 interfaceutil,
79 79 storageutil,
80 80 )
81 81
82 82 try:
83 83 from mercurial import zstd
84 84 zstd.__version__
85 85 except ImportError:
86 86 zstd = None
87 87
88 88 configtable = {}
89 89 configitem = registrar.configitem(configtable)
90 90
91 91 # experimental config: storage.sqlite.compression
92 92 configitem('storage', 'sqlite.compression',
93 default='zstd' if zstd else 'zlib')
93 default='zstd' if zstd else 'zlib',
94 experimental=True)
94 95
95 96 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
96 97 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
97 98 # be specifying the version(s) of Mercurial they are tested with, or
98 99 # leave the attribute unspecified.
99 100 testedwith = 'ships-with-hg-core'
100 101
101 102 REQUIREMENT = b'exp-sqlite-001'
102 103 REQUIREMENT_ZSTD = b'exp-sqlite-comp-001=zstd'
103 104 REQUIREMENT_ZLIB = b'exp-sqlite-comp-001=zlib'
104 105 REQUIREMENT_NONE = b'exp-sqlite-comp-001=none'
105 106 REQUIREMENT_SHALLOW_FILES = b'exp-sqlite-shallow-files'
106 107
107 108 CURRENT_SCHEMA_VERSION = 1
108 109
109 110 COMPRESSION_NONE = 1
110 111 COMPRESSION_ZSTD = 2
111 112 COMPRESSION_ZLIB = 3
112 113
113 114 FLAG_CENSORED = 1
114 115 FLAG_MISSING_P1 = 2
115 116 FLAG_MISSING_P2 = 4
116 117
117 118 CREATE_SCHEMA = [
118 119 # Deltas are stored as content-indexed blobs.
119 120 # compression column holds COMPRESSION_* constant for how the
120 121 # delta is encoded.
121 122
122 123 r'CREATE TABLE delta ('
123 124 r' id INTEGER PRIMARY KEY, '
124 125 r' compression INTEGER NOT NULL, '
125 126 r' hash BLOB UNIQUE ON CONFLICT ABORT, '
126 127 r' delta BLOB NOT NULL '
127 128 r')',
128 129
129 130 # Tracked paths are denormalized to integers to avoid redundant
130 131 # storage of the path name.
131 132 r'CREATE TABLE filepath ('
132 133 r' id INTEGER PRIMARY KEY, '
133 134 r' path BLOB NOT NULL '
134 135 r')',
135 136
136 137 r'CREATE UNIQUE INDEX filepath_path '
137 138 r' ON filepath (path)',
138 139
139 140 # We have a single table for all file revision data.
140 141 # Each file revision is uniquely described by a (path, rev) and
141 142 # (path, node).
142 143 #
143 144 # Revision data is stored as a pointer to the delta producing this
144 145 # revision and the file revision whose delta should be applied before
145 146 # that one. One can reconstruct the delta chain by recursively following
146 147 # the delta base revision pointers until one encounters NULL.
147 148 #
148 149 # flags column holds bitwise integer flags controlling storage options.
149 150 # These flags are defined by the FLAG_* constants.
150 151 r'CREATE TABLE fileindex ('
151 152 r' id INTEGER PRIMARY KEY, '
152 153 r' pathid INTEGER REFERENCES filepath(id), '
153 154 r' revnum INTEGER NOT NULL, '
154 155 r' p1rev INTEGER NOT NULL, '
155 156 r' p2rev INTEGER NOT NULL, '
156 157 r' linkrev INTEGER NOT NULL, '
157 158 r' flags INTEGER NOT NULL, '
158 159 r' deltaid INTEGER REFERENCES delta(id), '
159 160 r' deltabaseid INTEGER REFERENCES fileindex(id), '
160 161 r' node BLOB NOT NULL '
161 162 r')',
162 163
163 164 r'CREATE UNIQUE INDEX fileindex_pathrevnum '
164 165 r' ON fileindex (pathid, revnum)',
165 166
166 167 r'CREATE UNIQUE INDEX fileindex_pathnode '
167 168 r' ON fileindex (pathid, node)',
168 169
169 170 # Provide a view over all file data for convenience.
170 171 r'CREATE VIEW filedata AS '
171 172 r'SELECT '
172 173 r' fileindex.id AS id, '
173 174 r' filepath.id AS pathid, '
174 175 r' filepath.path AS path, '
175 176 r' fileindex.revnum AS revnum, '
176 177 r' fileindex.node AS node, '
177 178 r' fileindex.p1rev AS p1rev, '
178 179 r' fileindex.p2rev AS p2rev, '
179 180 r' fileindex.linkrev AS linkrev, '
180 181 r' fileindex.flags AS flags, '
181 182 r' fileindex.deltaid AS deltaid, '
182 183 r' fileindex.deltabaseid AS deltabaseid '
183 184 r'FROM filepath, fileindex '
184 185 r'WHERE fileindex.pathid=filepath.id',
185 186
186 187 r'PRAGMA user_version=%d' % CURRENT_SCHEMA_VERSION,
187 188 ]
188 189
189 190 def resolvedeltachain(db, pathid, node, revisioncache,
190 191 stoprids, zstddctx=None):
191 192 """Resolve a delta chain for a file node."""
192 193
193 194 # TODO the "not in ({stops})" here is possibly slowing down the query
194 195 # because it needs to perform the lookup on every recursive invocation.
195 196 # This could possibly be faster if we created a temporary query with
196 197 # baseid "poisoned" to null and limited the recursive filter to
197 198 # "is not null".
198 199 res = db.execute(
199 200 r'WITH RECURSIVE '
200 201 r' deltachain(deltaid, baseid) AS ('
201 202 r' SELECT deltaid, deltabaseid FROM fileindex '
202 203 r' WHERE pathid=? AND node=? '
203 204 r' UNION ALL '
204 205 r' SELECT fileindex.deltaid, deltabaseid '
205 206 r' FROM fileindex, deltachain '
206 207 r' WHERE '
207 208 r' fileindex.id=deltachain.baseid '
208 209 r' AND deltachain.baseid IS NOT NULL '
209 210 r' AND fileindex.id NOT IN ({stops}) '
210 211 r' ) '
211 212 r'SELECT deltachain.baseid, compression, delta '
212 213 r'FROM deltachain, delta '
213 214 r'WHERE delta.id=deltachain.deltaid'.format(
214 215 stops=r','.join([r'?'] * len(stoprids))),
215 216 tuple([pathid, node] + list(stoprids.keys())))
216 217
217 218 deltas = []
218 219 lastdeltabaseid = None
219 220
220 221 for deltabaseid, compression, delta in res:
221 222 lastdeltabaseid = deltabaseid
222 223
223 224 if compression == COMPRESSION_ZSTD:
224 225 delta = zstddctx.decompress(delta)
225 226 elif compression == COMPRESSION_NONE:
226 227 delta = delta
227 228 elif compression == COMPRESSION_ZLIB:
228 229 delta = zlib.decompress(delta)
229 230 else:
230 231 raise SQLiteStoreError('unhandled compression type: %d' %
231 232 compression)
232 233
233 234 deltas.append(delta)
234 235
235 236 if lastdeltabaseid in stoprids:
236 237 basetext = revisioncache[stoprids[lastdeltabaseid]]
237 238 else:
238 239 basetext = deltas.pop()
239 240
240 241 deltas.reverse()
241 242 fulltext = mdiff.patches(basetext, deltas)
242 243
243 244 # SQLite returns buffer instances for blob columns on Python 2. This
244 245 # type can propagate through the delta application layer. Because
245 246 # downstream callers assume revisions are bytes, cast as needed.
246 247 if not isinstance(fulltext, bytes):
247 248 fulltext = bytes(delta)
248 249
249 250 return fulltext
250 251
251 252 def insertdelta(db, compression, hash, delta):
252 253 try:
253 254 return db.execute(
254 255 r'INSERT INTO delta (compression, hash, delta) '
255 256 r'VALUES (?, ?, ?)',
256 257 (compression, hash, delta)).lastrowid
257 258 except sqlite3.IntegrityError:
258 259 return db.execute(
259 260 r'SELECT id FROM delta WHERE hash=?',
260 261 (hash,)).fetchone()[0]
261 262
262 263 class SQLiteStoreError(error.StorageError):
263 264 pass
264 265
265 266 @attr.s
266 267 class revisionentry(object):
267 268 rid = attr.ib()
268 269 rev = attr.ib()
269 270 node = attr.ib()
270 271 p1rev = attr.ib()
271 272 p2rev = attr.ib()
272 273 p1node = attr.ib()
273 274 p2node = attr.ib()
274 275 linkrev = attr.ib()
275 276 flags = attr.ib()
276 277
277 278 @interfaceutil.implementer(repository.irevisiondelta)
278 279 @attr.s(slots=True)
279 280 class sqliterevisiondelta(object):
280 281 node = attr.ib()
281 282 p1node = attr.ib()
282 283 p2node = attr.ib()
283 284 basenode = attr.ib()
284 285 flags = attr.ib()
285 286 baserevisionsize = attr.ib()
286 287 revision = attr.ib()
287 288 delta = attr.ib()
288 289 linknode = attr.ib(default=None)
289 290
290 291 @interfaceutil.implementer(repository.iverifyproblem)
291 292 @attr.s(frozen=True)
292 293 class sqliteproblem(object):
293 294 warning = attr.ib(default=None)
294 295 error = attr.ib(default=None)
295 296 node = attr.ib(default=None)
296 297
297 298 @interfaceutil.implementer(repository.ifilestorage)
298 299 class sqlitefilestore(object):
299 300 """Implements storage for an individual tracked path."""
300 301
301 302 def __init__(self, db, path, compression):
302 303 self._db = db
303 304 self._path = path
304 305
305 306 self._pathid = None
306 307
307 308 # revnum -> node
308 309 self._revtonode = {}
309 310 # node -> revnum
310 311 self._nodetorev = {}
311 312 # node -> data structure
312 313 self._revisions = {}
313 314
314 315 self._revisioncache = util.lrucachedict(10)
315 316
316 317 self._compengine = compression
317 318
318 319 if compression == 'zstd':
319 320 self._cctx = zstd.ZstdCompressor(level=3)
320 321 self._dctx = zstd.ZstdDecompressor()
321 322 else:
322 323 self._cctx = None
323 324 self._dctx = None
324 325
325 326 self._refreshindex()
326 327
327 328 def _refreshindex(self):
328 329 self._revtonode = {}
329 330 self._nodetorev = {}
330 331 self._revisions = {}
331 332
332 333 res = list(self._db.execute(
333 334 r'SELECT id FROM filepath WHERE path=?', (self._path,)))
334 335
335 336 if not res:
336 337 self._pathid = None
337 338 return
338 339
339 340 self._pathid = res[0][0]
340 341
341 342 res = self._db.execute(
342 343 r'SELECT id, revnum, node, p1rev, p2rev, linkrev, flags '
343 344 r'FROM fileindex '
344 345 r'WHERE pathid=? '
345 346 r'ORDER BY revnum ASC',
346 347 (self._pathid,))
347 348
348 349 for i, row in enumerate(res):
349 350 rid, rev, node, p1rev, p2rev, linkrev, flags = row
350 351
351 352 if i != rev:
352 353 raise SQLiteStoreError(_('sqlite database has inconsistent '
353 354 'revision numbers'))
354 355
355 356 if p1rev == nullrev:
356 357 p1node = nullid
357 358 else:
358 359 p1node = self._revtonode[p1rev]
359 360
360 361 if p2rev == nullrev:
361 362 p2node = nullid
362 363 else:
363 364 p2node = self._revtonode[p2rev]
364 365
365 366 entry = revisionentry(
366 367 rid=rid,
367 368 rev=rev,
368 369 node=node,
369 370 p1rev=p1rev,
370 371 p2rev=p2rev,
371 372 p1node=p1node,
372 373 p2node=p2node,
373 374 linkrev=linkrev,
374 375 flags=flags)
375 376
376 377 self._revtonode[rev] = node
377 378 self._nodetorev[node] = rev
378 379 self._revisions[node] = entry
379 380
380 381 # Start of ifileindex interface.
381 382
382 383 def __len__(self):
383 384 return len(self._revisions)
384 385
385 386 def __iter__(self):
386 387 return iter(pycompat.xrange(len(self._revisions)))
387 388
388 389 def hasnode(self, node):
389 390 if node == nullid:
390 391 return False
391 392
392 393 return node in self._nodetorev
393 394
394 395 def revs(self, start=0, stop=None):
395 396 return storageutil.iterrevs(len(self._revisions), start=start,
396 397 stop=stop)
397 398
398 399 def parents(self, node):
399 400 if node == nullid:
400 401 return nullid, nullid
401 402
402 403 if node not in self._revisions:
403 404 raise error.LookupError(node, self._path, _('no node'))
404 405
405 406 entry = self._revisions[node]
406 407 return entry.p1node, entry.p2node
407 408
408 409 def parentrevs(self, rev):
409 410 if rev == nullrev:
410 411 return nullrev, nullrev
411 412
412 413 if rev not in self._revtonode:
413 414 raise IndexError(rev)
414 415
415 416 entry = self._revisions[self._revtonode[rev]]
416 417 return entry.p1rev, entry.p2rev
417 418
418 419 def rev(self, node):
419 420 if node == nullid:
420 421 return nullrev
421 422
422 423 if node not in self._nodetorev:
423 424 raise error.LookupError(node, self._path, _('no node'))
424 425
425 426 return self._nodetorev[node]
426 427
427 428 def node(self, rev):
428 429 if rev == nullrev:
429 430 return nullid
430 431
431 432 if rev not in self._revtonode:
432 433 raise IndexError(rev)
433 434
434 435 return self._revtonode[rev]
435 436
436 437 def lookup(self, node):
437 438 return storageutil.fileidlookup(self, node, self._path)
438 439
439 440 def linkrev(self, rev):
440 441 if rev == nullrev:
441 442 return nullrev
442 443
443 444 if rev not in self._revtonode:
444 445 raise IndexError(rev)
445 446
446 447 entry = self._revisions[self._revtonode[rev]]
447 448 return entry.linkrev
448 449
449 450 def iscensored(self, rev):
450 451 if rev == nullrev:
451 452 return False
452 453
453 454 if rev not in self._revtonode:
454 455 raise IndexError(rev)
455 456
456 457 return self._revisions[self._revtonode[rev]].flags & FLAG_CENSORED
457 458
458 459 def commonancestorsheads(self, node1, node2):
459 460 rev1 = self.rev(node1)
460 461 rev2 = self.rev(node2)
461 462
462 463 ancestors = ancestor.commonancestorsheads(self.parentrevs, rev1, rev2)
463 464 return pycompat.maplist(self.node, ancestors)
464 465
465 466 def descendants(self, revs):
466 467 # TODO we could implement this using a recursive SQL query, which
467 468 # might be faster.
468 469 return dagop.descendantrevs(revs, self.revs, self.parentrevs)
469 470
470 471 def heads(self, start=None, stop=None):
471 472 if start is None and stop is None:
472 473 if not len(self):
473 474 return [nullid]
474 475
475 476 startrev = self.rev(start) if start is not None else nullrev
476 477 stoprevs = {self.rev(n) for n in stop or []}
477 478
478 479 revs = dagop.headrevssubset(self.revs, self.parentrevs,
479 480 startrev=startrev, stoprevs=stoprevs)
480 481
481 482 return [self.node(rev) for rev in revs]
482 483
483 484 def children(self, node):
484 485 rev = self.rev(node)
485 486
486 487 res = self._db.execute(
487 488 r'SELECT'
488 489 r' node '
489 490 r' FROM filedata '
490 491 r' WHERE path=? AND (p1rev=? OR p2rev=?) '
491 492 r' ORDER BY revnum ASC',
492 493 (self._path, rev, rev))
493 494
494 495 return [row[0] for row in res]
495 496
496 497 # End of ifileindex interface.
497 498
498 499 # Start of ifiledata interface.
499 500
500 501 def size(self, rev):
501 502 if rev == nullrev:
502 503 return 0
503 504
504 505 if rev not in self._revtonode:
505 506 raise IndexError(rev)
506 507
507 508 node = self._revtonode[rev]
508 509
509 510 if self.renamed(node):
510 511 return len(self.read(node))
511 512
512 513 return len(self.revision(node))
513 514
514 515 def revision(self, node, raw=False, _verifyhash=True):
515 516 if node in (nullid, nullrev):
516 517 return b''
517 518
518 519 if isinstance(node, int):
519 520 node = self.node(node)
520 521
521 522 if node not in self._nodetorev:
522 523 raise error.LookupError(node, self._path, _('no node'))
523 524
524 525 if node in self._revisioncache:
525 526 return self._revisioncache[node]
526 527
527 528 # Because we have a fulltext revision cache, we are able to
528 529 # short-circuit delta chain traversal and decompression as soon as
529 530 # we encounter a revision in the cache.
530 531
531 532 stoprids = {self._revisions[n].rid: n
532 533 for n in self._revisioncache}
533 534
534 535 if not stoprids:
535 536 stoprids[-1] = None
536 537
537 538 fulltext = resolvedeltachain(self._db, self._pathid, node,
538 539 self._revisioncache, stoprids,
539 540 zstddctx=self._dctx)
540 541
541 542 # Don't verify hashes if parent nodes were rewritten, as the hash
542 543 # wouldn't verify.
543 544 if self._revisions[node].flags & (FLAG_MISSING_P1 | FLAG_MISSING_P2):
544 545 _verifyhash = False
545 546
546 547 if _verifyhash:
547 548 self._checkhash(fulltext, node)
548 549 self._revisioncache[node] = fulltext
549 550
550 551 return fulltext
551 552
552 553 def rawdata(self, *args, **kwargs):
553 554 return self.revision(*args, **kwargs)
554 555
555 556 def read(self, node):
556 557 return storageutil.filtermetadata(self.revision(node))
557 558
558 559 def renamed(self, node):
559 560 return storageutil.filerevisioncopied(self, node)
560 561
561 562 def cmp(self, node, fulltext):
562 563 return not storageutil.filedataequivalent(self, node, fulltext)
563 564
564 565 def emitrevisions(self, nodes, nodesorder=None, revisiondata=False,
565 566 assumehaveparentrevisions=False,
566 567 deltamode=repository.CG_DELTAMODE_STD):
567 568 if nodesorder not in ('nodes', 'storage', 'linear', None):
568 569 raise error.ProgrammingError('unhandled value for nodesorder: %s' %
569 570 nodesorder)
570 571
571 572 nodes = [n for n in nodes if n != nullid]
572 573
573 574 if not nodes:
574 575 return
575 576
576 577 # TODO perform in a single query.
577 578 res = self._db.execute(
578 579 r'SELECT revnum, deltaid FROM fileindex '
579 580 r'WHERE pathid=? '
580 581 r' AND node in (%s)' % (r','.join([r'?'] * len(nodes))),
581 582 tuple([self._pathid] + nodes))
582 583
583 584 deltabases = {}
584 585
585 586 for rev, deltaid in res:
586 587 res = self._db.execute(
587 588 r'SELECT revnum from fileindex WHERE pathid=? AND deltaid=?',
588 589 (self._pathid, deltaid))
589 590 deltabases[rev] = res.fetchone()[0]
590 591
591 592 # TODO define revdifffn so we can use delta from storage.
592 593 for delta in storageutil.emitrevisions(
593 594 self, nodes, nodesorder, sqliterevisiondelta,
594 595 deltaparentfn=deltabases.__getitem__,
595 596 revisiondata=revisiondata,
596 597 assumehaveparentrevisions=assumehaveparentrevisions,
597 598 deltamode=deltamode):
598 599
599 600 yield delta
600 601
601 602 # End of ifiledata interface.
602 603
603 604 # Start of ifilemutation interface.
604 605
605 606 def add(self, filedata, meta, transaction, linkrev, p1, p2):
606 607 if meta or filedata.startswith(b'\x01\n'):
607 608 filedata = storageutil.packmeta(meta, filedata)
608 609
609 610 return self.addrevision(filedata, transaction, linkrev, p1, p2)
610 611
611 612 def addrevision(self, revisiondata, transaction, linkrev, p1, p2, node=None,
612 613 flags=0, cachedelta=None):
613 614 if flags:
614 615 raise SQLiteStoreError(_('flags not supported on revisions'))
615 616
616 617 validatehash = node is not None
617 618 node = node or storageutil.hashrevisionsha1(revisiondata, p1, p2)
618 619
619 620 if validatehash:
620 621 self._checkhash(revisiondata, node, p1, p2)
621 622
622 623 if node in self._nodetorev:
623 624 return node
624 625
625 626 node = self._addrawrevision(node, revisiondata, transaction, linkrev,
626 627 p1, p2)
627 628
628 629 self._revisioncache[node] = revisiondata
629 630 return node
630 631
631 632 def addgroup(self, deltas, linkmapper, transaction, addrevisioncb=None,
632 633 maybemissingparents=False):
633 634 nodes = []
634 635
635 636 for node, p1, p2, linknode, deltabase, delta, wireflags in deltas:
636 637 storeflags = 0
637 638
638 639 if wireflags & repository.REVISION_FLAG_CENSORED:
639 640 storeflags |= FLAG_CENSORED
640 641
641 642 if wireflags & ~repository.REVISION_FLAG_CENSORED:
642 643 raise SQLiteStoreError('unhandled revision flag')
643 644
644 645 if maybemissingparents:
645 646 if p1 != nullid and not self.hasnode(p1):
646 647 p1 = nullid
647 648 storeflags |= FLAG_MISSING_P1
648 649
649 650 if p2 != nullid and not self.hasnode(p2):
650 651 p2 = nullid
651 652 storeflags |= FLAG_MISSING_P2
652 653
653 654 baserev = self.rev(deltabase)
654 655
655 656 # If base is censored, delta must be full replacement in a single
656 657 # patch operation.
657 658 if baserev != nullrev and self.iscensored(baserev):
658 659 hlen = struct.calcsize('>lll')
659 660 oldlen = len(self.revision(deltabase, raw=True,
660 661 _verifyhash=False))
661 662 newlen = len(delta) - hlen
662 663
663 664 if delta[:hlen] != mdiff.replacediffheader(oldlen, newlen):
664 665 raise error.CensoredBaseError(self._path,
665 666 deltabase)
666 667
667 668 if (not (storeflags & FLAG_CENSORED)
668 669 and storageutil.deltaiscensored(
669 670 delta, baserev, lambda x: len(self.revision(x, raw=True)))):
670 671 storeflags |= FLAG_CENSORED
671 672
672 673 linkrev = linkmapper(linknode)
673 674
674 675 nodes.append(node)
675 676
676 677 if node in self._revisions:
677 678 # Possibly reset parents to make them proper.
678 679 entry = self._revisions[node]
679 680
680 681 if entry.flags & FLAG_MISSING_P1 and p1 != nullid:
681 682 entry.p1node = p1
682 683 entry.p1rev = self._nodetorev[p1]
683 684 entry.flags &= ~FLAG_MISSING_P1
684 685
685 686 self._db.execute(
686 687 r'UPDATE fileindex SET p1rev=?, flags=? '
687 688 r'WHERE id=?',
688 689 (self._nodetorev[p1], entry.flags, entry.rid))
689 690
690 691 if entry.flags & FLAG_MISSING_P2 and p2 != nullid:
691 692 entry.p2node = p2
692 693 entry.p2rev = self._nodetorev[p2]
693 694 entry.flags &= ~FLAG_MISSING_P2
694 695
695 696 self._db.execute(
696 697 r'UPDATE fileindex SET p2rev=?, flags=? '
697 698 r'WHERE id=?',
698 699 (self._nodetorev[p1], entry.flags, entry.rid))
699 700
700 701 continue
701 702
702 703 if deltabase == nullid:
703 704 text = mdiff.patch(b'', delta)
704 705 storedelta = None
705 706 else:
706 707 text = None
707 708 storedelta = (deltabase, delta)
708 709
709 710 self._addrawrevision(node, text, transaction, linkrev, p1, p2,
710 711 storedelta=storedelta, flags=storeflags)
711 712
712 713 if addrevisioncb:
713 714 addrevisioncb(self, node)
714 715
715 716 return nodes
716 717
717 718 def censorrevision(self, tr, censornode, tombstone=b''):
718 719 tombstone = storageutil.packmeta({b'censored': tombstone}, b'')
719 720
720 721 # This restriction is cargo culted from revlogs and makes no sense for
721 722 # SQLite, since columns can be resized at will.
722 723 if len(tombstone) > len(self.revision(censornode, raw=True)):
723 724 raise error.Abort(_('censor tombstone must be no longer than '
724 725 'censored data'))
725 726
726 727 # We need to replace the censored revision's data with the tombstone.
727 728 # But replacing that data will have implications for delta chains that
728 729 # reference it.
729 730 #
730 731 # While "better," more complex strategies are possible, we do something
731 732 # simple: we find delta chain children of the censored revision and we
732 733 # replace those incremental deltas with fulltexts of their corresponding
733 734 # revision. Then we delete the now-unreferenced delta and original
734 735 # revision and insert a replacement.
735 736
736 737 # Find the delta to be censored.
737 738 censoreddeltaid = self._db.execute(
738 739 r'SELECT deltaid FROM fileindex WHERE id=?',
739 740 (self._revisions[censornode].rid,)).fetchone()[0]
740 741
741 742 # Find all its delta chain children.
742 743 # TODO once we support storing deltas for !files, we'll need to look
743 744 # for those delta chains too.
744 745 rows = list(self._db.execute(
745 746 r'SELECT id, pathid, node FROM fileindex '
746 747 r'WHERE deltabaseid=? OR deltaid=?',
747 748 (censoreddeltaid, censoreddeltaid)))
748 749
749 750 for row in rows:
750 751 rid, pathid, node = row
751 752
752 753 fulltext = resolvedeltachain(self._db, pathid, node, {}, {-1: None},
753 754 zstddctx=self._dctx)
754 755
755 756 deltahash = hashlib.sha1(fulltext).digest()
756 757
757 758 if self._compengine == 'zstd':
758 759 deltablob = self._cctx.compress(fulltext)
759 760 compression = COMPRESSION_ZSTD
760 761 elif self._compengine == 'zlib':
761 762 deltablob = zlib.compress(fulltext)
762 763 compression = COMPRESSION_ZLIB
763 764 elif self._compengine == 'none':
764 765 deltablob = fulltext
765 766 compression = COMPRESSION_NONE
766 767 else:
767 768 raise error.ProgrammingError('unhandled compression engine: %s'
768 769 % self._compengine)
769 770
770 771 if len(deltablob) >= len(fulltext):
771 772 deltablob = fulltext
772 773 compression = COMPRESSION_NONE
773 774
774 775 deltaid = insertdelta(self._db, compression, deltahash, deltablob)
775 776
776 777 self._db.execute(
777 778 r'UPDATE fileindex SET deltaid=?, deltabaseid=NULL '
778 779 r'WHERE id=?', (deltaid, rid))
779 780
780 781 # Now create the tombstone delta and replace the delta on the censored
781 782 # node.
782 783 deltahash = hashlib.sha1(tombstone).digest()
783 784 tombstonedeltaid = insertdelta(self._db, COMPRESSION_NONE,
784 785 deltahash, tombstone)
785 786
786 787 flags = self._revisions[censornode].flags
787 788 flags |= FLAG_CENSORED
788 789
789 790 self._db.execute(
790 791 r'UPDATE fileindex SET flags=?, deltaid=?, deltabaseid=NULL '
791 792 r'WHERE pathid=? AND node=?',
792 793 (flags, tombstonedeltaid, self._pathid, censornode))
793 794
794 795 self._db.execute(
795 796 r'DELETE FROM delta WHERE id=?', (censoreddeltaid,))
796 797
797 798 self._refreshindex()
798 799 self._revisioncache.clear()
799 800
800 801 def getstrippoint(self, minlink):
801 802 return storageutil.resolvestripinfo(minlink, len(self) - 1,
802 803 [self.rev(n) for n in self.heads()],
803 804 self.linkrev,
804 805 self.parentrevs)
805 806
806 807 def strip(self, minlink, transaction):
807 808 if not len(self):
808 809 return
809 810
810 811 rev, _ignored = self.getstrippoint(minlink)
811 812
812 813 if rev == len(self):
813 814 return
814 815
815 816 for rev in self.revs(rev):
816 817 self._db.execute(
817 818 r'DELETE FROM fileindex WHERE pathid=? AND node=?',
818 819 (self._pathid, self.node(rev)))
819 820
820 821 # TODO how should we garbage collect data in delta table?
821 822
822 823 self._refreshindex()
823 824
824 825 # End of ifilemutation interface.
825 826
826 827 # Start of ifilestorage interface.
827 828
828 829 def files(self):
829 830 return []
830 831
831 832 def storageinfo(self, exclusivefiles=False, sharedfiles=False,
832 833 revisionscount=False, trackedsize=False,
833 834 storedsize=False):
834 835 d = {}
835 836
836 837 if exclusivefiles:
837 838 d['exclusivefiles'] = []
838 839
839 840 if sharedfiles:
840 841 # TODO list sqlite file(s) here.
841 842 d['sharedfiles'] = []
842 843
843 844 if revisionscount:
844 845 d['revisionscount'] = len(self)
845 846
846 847 if trackedsize:
847 848 d['trackedsize'] = sum(len(self.revision(node))
848 849 for node in self._nodetorev)
849 850
850 851 if storedsize:
851 852 # TODO implement this?
852 853 d['storedsize'] = None
853 854
854 855 return d
855 856
856 857 def verifyintegrity(self, state):
857 858 state['skipread'] = set()
858 859
859 860 for rev in self:
860 861 node = self.node(rev)
861 862
862 863 try:
863 864 self.revision(node)
864 865 except Exception as e:
865 866 yield sqliteproblem(
866 867 error=_('unpacking %s: %s') % (short(node), e),
867 868 node=node)
868 869
869 870 state['skipread'].add(node)
870 871
871 872 # End of ifilestorage interface.
872 873
873 874 def _checkhash(self, fulltext, node, p1=None, p2=None):
874 875 if p1 is None and p2 is None:
875 876 p1, p2 = self.parents(node)
876 877
877 878 if node == storageutil.hashrevisionsha1(fulltext, p1, p2):
878 879 return
879 880
880 881 try:
881 882 del self._revisioncache[node]
882 883 except KeyError:
883 884 pass
884 885
885 886 if storageutil.iscensoredtext(fulltext):
886 887 raise error.CensoredNodeError(self._path, node, fulltext)
887 888
888 889 raise SQLiteStoreError(_('integrity check failed on %s') %
889 890 self._path)
890 891
891 892 def _addrawrevision(self, node, revisiondata, transaction, linkrev,
892 893 p1, p2, storedelta=None, flags=0):
893 894 if self._pathid is None:
894 895 res = self._db.execute(
895 896 r'INSERT INTO filepath (path) VALUES (?)', (self._path,))
896 897 self._pathid = res.lastrowid
897 898
898 899 # For simplicity, always store a delta against p1.
899 900 # TODO we need a lot more logic here to make behavior reasonable.
900 901
901 902 if storedelta:
902 903 deltabase, delta = storedelta
903 904
904 905 if isinstance(deltabase, int):
905 906 deltabase = self.node(deltabase)
906 907
907 908 else:
908 909 assert revisiondata is not None
909 910 deltabase = p1
910 911
911 912 if deltabase == nullid:
912 913 delta = revisiondata
913 914 else:
914 915 delta = mdiff.textdiff(self.revision(self.rev(deltabase)),
915 916 revisiondata)
916 917
917 918 # File index stores a pointer to its delta and the parent delta.
918 919 # The parent delta is stored via a pointer to the fileindex PK.
919 920 if deltabase == nullid:
920 921 baseid = None
921 922 else:
922 923 baseid = self._revisions[deltabase].rid
923 924
924 925 # Deltas are stored with a hash of their content. This allows
925 926 # us to de-duplicate. The table is configured to ignore conflicts
926 927 # and it is faster to just insert and silently noop than to look
927 928 # first.
928 929 deltahash = hashlib.sha1(delta).digest()
929 930
930 931 if self._compengine == 'zstd':
931 932 deltablob = self._cctx.compress(delta)
932 933 compression = COMPRESSION_ZSTD
933 934 elif self._compengine == 'zlib':
934 935 deltablob = zlib.compress(delta)
935 936 compression = COMPRESSION_ZLIB
936 937 elif self._compengine == 'none':
937 938 deltablob = delta
938 939 compression = COMPRESSION_NONE
939 940 else:
940 941 raise error.ProgrammingError('unhandled compression engine: %s' %
941 942 self._compengine)
942 943
943 944 # Don't store compressed data if it isn't practical.
944 945 if len(deltablob) >= len(delta):
945 946 deltablob = delta
946 947 compression = COMPRESSION_NONE
947 948
948 949 deltaid = insertdelta(self._db, compression, deltahash, deltablob)
949 950
950 951 rev = len(self)
951 952
952 953 if p1 == nullid:
953 954 p1rev = nullrev
954 955 else:
955 956 p1rev = self._nodetorev[p1]
956 957
957 958 if p2 == nullid:
958 959 p2rev = nullrev
959 960 else:
960 961 p2rev = self._nodetorev[p2]
961 962
962 963 rid = self._db.execute(
963 964 r'INSERT INTO fileindex ('
964 965 r' pathid, revnum, node, p1rev, p2rev, linkrev, flags, '
965 966 r' deltaid, deltabaseid) '
966 967 r' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
967 968 (self._pathid, rev, node, p1rev, p2rev, linkrev, flags,
968 969 deltaid, baseid)
969 970 ).lastrowid
970 971
971 972 entry = revisionentry(
972 973 rid=rid,
973 974 rev=rev,
974 975 node=node,
975 976 p1rev=p1rev,
976 977 p2rev=p2rev,
977 978 p1node=p1,
978 979 p2node=p2,
979 980 linkrev=linkrev,
980 981 flags=flags)
981 982
982 983 self._nodetorev[node] = rev
983 984 self._revtonode[rev] = node
984 985 self._revisions[node] = entry
985 986
986 987 return node
987 988
988 989 class sqliterepository(localrepo.localrepository):
989 990 def cancopy(self):
990 991 return False
991 992
992 993 def transaction(self, *args, **kwargs):
993 994 current = self.currenttransaction()
994 995
995 996 tr = super(sqliterepository, self).transaction(*args, **kwargs)
996 997
997 998 if current:
998 999 return tr
999 1000
1000 1001 self._dbconn.execute(r'BEGIN TRANSACTION')
1001 1002
1002 1003 def committransaction(_):
1003 1004 self._dbconn.commit()
1004 1005
1005 1006 tr.addfinalize('sqlitestore', committransaction)
1006 1007
1007 1008 return tr
1008 1009
1009 1010 @property
1010 1011 def _dbconn(self):
1011 1012 # SQLite connections can only be used on the thread that created
1012 1013 # them. In most cases, this "just works." However, hgweb uses
1013 1014 # multiple threads.
1014 1015 tid = threading.current_thread().ident
1015 1016
1016 1017 if self._db:
1017 1018 if self._db[0] == tid:
1018 1019 return self._db[1]
1019 1020
1020 1021 db = makedb(self.svfs.join('db.sqlite'))
1021 1022 self._db = (tid, db)
1022 1023
1023 1024 return db
1024 1025
1025 1026 def makedb(path):
1026 1027 """Construct a database handle for a database at path."""
1027 1028
1028 1029 db = sqlite3.connect(encoding.strfromlocal(path))
1029 1030 db.text_factory = bytes
1030 1031
1031 1032 res = db.execute(r'PRAGMA user_version').fetchone()[0]
1032 1033
1033 1034 # New database.
1034 1035 if res == 0:
1035 1036 for statement in CREATE_SCHEMA:
1036 1037 db.execute(statement)
1037 1038
1038 1039 db.commit()
1039 1040
1040 1041 elif res == CURRENT_SCHEMA_VERSION:
1041 1042 pass
1042 1043
1043 1044 else:
1044 1045 raise error.Abort(_('sqlite database has unrecognized version'))
1045 1046
1046 1047 db.execute(r'PRAGMA journal_mode=WAL')
1047 1048
1048 1049 return db
1049 1050
1050 1051 def featuresetup(ui, supported):
1051 1052 supported.add(REQUIREMENT)
1052 1053
1053 1054 if zstd:
1054 1055 supported.add(REQUIREMENT_ZSTD)
1055 1056
1056 1057 supported.add(REQUIREMENT_ZLIB)
1057 1058 supported.add(REQUIREMENT_NONE)
1058 1059 supported.add(REQUIREMENT_SHALLOW_FILES)
1059 1060 supported.add(repository.NARROW_REQUIREMENT)
1060 1061
1061 1062 def newreporequirements(orig, ui, createopts):
1062 1063 if createopts['backend'] != 'sqlite':
1063 1064 return orig(ui, createopts)
1064 1065
1065 1066 # This restriction can be lifted once we have more confidence.
1066 1067 if 'sharedrepo' in createopts:
1067 1068 raise error.Abort(_('shared repositories not supported with SQLite '
1068 1069 'store'))
1069 1070
1070 1071 # This filtering is out of an abundance of caution: we want to ensure
1071 1072 # we honor creation options and we do that by annotating exactly the
1072 1073 # creation options we recognize.
1073 1074 known = {
1074 1075 'narrowfiles',
1075 1076 'backend',
1076 1077 'shallowfilestore',
1077 1078 }
1078 1079
1079 1080 unsupported = set(createopts) - known
1080 1081 if unsupported:
1081 1082 raise error.Abort(_('SQLite store does not support repo creation '
1082 1083 'option: %s') % ', '.join(sorted(unsupported)))
1083 1084
1084 1085 # Since we're a hybrid store that still relies on revlogs, we fall back
1085 1086 # to using the revlogv1 backend's storage requirements then adding our
1086 1087 # own requirement.
1087 1088 createopts['backend'] = 'revlogv1'
1088 1089 requirements = orig(ui, createopts)
1089 1090 requirements.add(REQUIREMENT)
1090 1091
1091 1092 compression = ui.config('storage', 'sqlite.compression')
1092 1093
1093 1094 if compression == 'zstd' and not zstd:
1094 1095 raise error.Abort(_('storage.sqlite.compression set to "zstd" but '
1095 1096 'zstandard compression not available to this '
1096 1097 'Mercurial install'))
1097 1098
1098 1099 if compression == 'zstd':
1099 1100 requirements.add(REQUIREMENT_ZSTD)
1100 1101 elif compression == 'zlib':
1101 1102 requirements.add(REQUIREMENT_ZLIB)
1102 1103 elif compression == 'none':
1103 1104 requirements.add(REQUIREMENT_NONE)
1104 1105 else:
1105 1106 raise error.Abort(_('unknown compression engine defined in '
1106 1107 'storage.sqlite.compression: %s') % compression)
1107 1108
1108 1109 if createopts.get('shallowfilestore'):
1109 1110 requirements.add(REQUIREMENT_SHALLOW_FILES)
1110 1111
1111 1112 return requirements
1112 1113
1113 1114 @interfaceutil.implementer(repository.ilocalrepositoryfilestorage)
1114 1115 class sqlitefilestorage(object):
1115 1116 """Repository file storage backed by SQLite."""
1116 1117 def file(self, path):
1117 1118 if path[0] == b'/':
1118 1119 path = path[1:]
1119 1120
1120 1121 if REQUIREMENT_ZSTD in self.requirements:
1121 1122 compression = 'zstd'
1122 1123 elif REQUIREMENT_ZLIB in self.requirements:
1123 1124 compression = 'zlib'
1124 1125 elif REQUIREMENT_NONE in self.requirements:
1125 1126 compression = 'none'
1126 1127 else:
1127 1128 raise error.Abort(_('unable to determine what compression engine '
1128 1129 'to use for SQLite storage'))
1129 1130
1130 1131 return sqlitefilestore(self._dbconn, path, compression)
1131 1132
1132 1133 def makefilestorage(orig, requirements, features, **kwargs):
1133 1134 """Produce a type conforming to ``ilocalrepositoryfilestorage``."""
1134 1135 if REQUIREMENT in requirements:
1135 1136 if REQUIREMENT_SHALLOW_FILES in requirements:
1136 1137 features.add(repository.REPO_FEATURE_SHALLOW_FILE_STORAGE)
1137 1138
1138 1139 return sqlitefilestorage
1139 1140 else:
1140 1141 return orig(requirements=requirements, features=features, **kwargs)
1141 1142
1142 1143 def makemain(orig, ui, requirements, **kwargs):
1143 1144 if REQUIREMENT in requirements:
1144 1145 if REQUIREMENT_ZSTD in requirements and not zstd:
1145 1146 raise error.Abort(_('repository uses zstandard compression, which '
1146 1147 'is not available to this Mercurial install'))
1147 1148
1148 1149 return sqliterepository
1149 1150
1150 1151 return orig(requirements=requirements, **kwargs)
1151 1152
1152 1153 def verifierinit(orig, self, *args, **kwargs):
1153 1154 orig(self, *args, **kwargs)
1154 1155
1155 1156 # We don't care that files in the store don't align with what is
1156 1157 # advertised. So suppress these warnings.
1157 1158 self.warnorphanstorefiles = False
1158 1159
1159 1160 def extsetup(ui):
1160 1161 localrepo.featuresetupfuncs.add(featuresetup)
1161 1162 extensions.wrapfunction(localrepo, 'newreporequirements',
1162 1163 newreporequirements)
1163 1164 extensions.wrapfunction(localrepo, 'makefilestorage',
1164 1165 makefilestorage)
1165 1166 extensions.wrapfunction(localrepo, 'makemain',
1166 1167 makemain)
1167 1168 extensions.wrapfunction(verify.verifier, '__init__',
1168 1169 verifierinit)
1169 1170
1170 1171 def reposetup(ui, repo):
1171 1172 if isinstance(repo, sqliterepository):
1172 1173 repo._db = None
1173 1174
1174 1175 # TODO check for bundlerepository?
@@ -1,1505 +1,1521 b''
1 1 # configitems.py - centralized declaration of configuration option
2 2 #
3 3 # Copyright 2017 Pierre-Yves David <pierre-yves.david@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 from __future__ import absolute_import
9 9
10 10 import functools
11 11 import re
12 12
13 13 from . import (
14 14 encoding,
15 15 error,
16 16 )
17 17
18 18 def loadconfigtable(ui, extname, configtable):
19 19 """update config item known to the ui with the extension ones"""
20 20 for section, items in sorted(configtable.items()):
21 21 knownitems = ui._knownconfig.setdefault(section, itemregister())
22 22 knownkeys = set(knownitems)
23 23 newkeys = set(items)
24 24 for key in sorted(knownkeys & newkeys):
25 25 msg = "extension '%s' overwrite config item '%s.%s'"
26 26 msg %= (extname, section, key)
27 27 ui.develwarn(msg, config='warn-config')
28 28
29 29 knownitems.update(items)
30 30
31 31 class configitem(object):
32 32 """represent a known config item
33 33
34 34 :section: the official config section where to find this item,
35 35 :name: the official name within the section,
36 36 :default: default value for this item,
37 37 :alias: optional list of tuples as alternatives,
38 38 :generic: this is a generic definition, match name using regular expression.
39 39 """
40 40
41 41 def __init__(self, section, name, default=None, alias=(),
42 generic=False, priority=0):
42 generic=False, priority=0, experimental=False):
43 43 self.section = section
44 44 self.name = name
45 45 self.default = default
46 46 self.alias = list(alias)
47 47 self.generic = generic
48 48 self.priority = priority
49 self.experimental = experimental
49 50 self._re = None
50 51 if generic:
51 52 self._re = re.compile(self.name)
52 53
53 54 class itemregister(dict):
54 55 """A specialized dictionary that can handle wild-card selection"""
55 56
56 57 def __init__(self):
57 58 super(itemregister, self).__init__()
58 59 self._generics = set()
59 60
60 61 def update(self, other):
61 62 super(itemregister, self).update(other)
62 63 self._generics.update(other._generics)
63 64
64 65 def __setitem__(self, key, item):
65 66 super(itemregister, self).__setitem__(key, item)
66 67 if item.generic:
67 68 self._generics.add(item)
68 69
69 70 def get(self, key):
70 71 baseitem = super(itemregister, self).get(key)
71 72 if baseitem is not None and not baseitem.generic:
72 73 return baseitem
73 74
74 75 # search for a matching generic item
75 76 generics = sorted(self._generics, key=(lambda x: (x.priority, x.name)))
76 77 for item in generics:
77 78 # we use 'match' instead of 'search' to make the matching simpler
78 79 # for people unfamiliar with regular expression. Having the match
79 80 # rooted to the start of the string will produce less surprising
80 81 # result for user writing simple regex for sub-attribute.
81 82 #
82 83 # For example using "color\..*" match produces an unsurprising
83 84 # result, while using search could suddenly match apparently
84 85 # unrelated configuration that happens to contains "color."
85 86 # anywhere. This is a tradeoff where we favor requiring ".*" on
86 87 # some match to avoid the need to prefix most pattern with "^".
87 88 # The "^" seems more error prone.
88 89 if item._re.match(key):
89 90 return item
90 91
91 92 return None
92 93
93 94 coreitems = {}
94 95
95 96 def _register(configtable, *args, **kwargs):
96 97 item = configitem(*args, **kwargs)
97 98 section = configtable.setdefault(item.section, itemregister())
98 99 if item.name in section:
99 100 msg = "duplicated config item registration for '%s.%s'"
100 101 raise error.ProgrammingError(msg % (item.section, item.name))
101 102 section[item.name] = item
102 103
103 104 # special value for case where the default is derived from other values
104 105 dynamicdefault = object()
105 106
106 107 # Registering actual config items
107 108
108 109 def getitemregister(configtable):
109 110 f = functools.partial(_register, configtable)
110 111 # export pseudo enum as configitem.*
111 112 f.dynamicdefault = dynamicdefault
112 113 return f
113 114
114 115 coreconfigitem = getitemregister(coreitems)
115 116
116 117 def _registerdiffopts(section, configprefix=''):
117 118 coreconfigitem(section, configprefix + 'nodates',
118 119 default=False,
119 120 )
120 121 coreconfigitem(section, configprefix + 'showfunc',
121 122 default=False,
122 123 )
123 124 coreconfigitem(section, configprefix + 'unified',
124 125 default=None,
125 126 )
126 127 coreconfigitem(section, configprefix + 'git',
127 128 default=False,
128 129 )
129 130 coreconfigitem(section, configprefix + 'ignorews',
130 131 default=False,
131 132 )
132 133 coreconfigitem(section, configprefix + 'ignorewsamount',
133 134 default=False,
134 135 )
135 136 coreconfigitem(section, configprefix + 'ignoreblanklines',
136 137 default=False,
137 138 )
138 139 coreconfigitem(section, configprefix + 'ignorewseol',
139 140 default=False,
140 141 )
141 142 coreconfigitem(section, configprefix + 'nobinary',
142 143 default=False,
143 144 )
144 145 coreconfigitem(section, configprefix + 'noprefix',
145 146 default=False,
146 147 )
147 148 coreconfigitem(section, configprefix + 'word-diff',
148 149 default=False,
149 150 )
150 151
151 152 coreconfigitem('alias', '.*',
152 153 default=dynamicdefault,
153 154 generic=True,
154 155 )
155 156 coreconfigitem('auth', 'cookiefile',
156 157 default=None,
157 158 )
158 159 _registerdiffopts(section='annotate')
159 160 # bookmarks.pushing: internal hack for discovery
160 161 coreconfigitem('bookmarks', 'pushing',
161 162 default=list,
162 163 )
163 164 # bundle.mainreporoot: internal hack for bundlerepo
164 165 coreconfigitem('bundle', 'mainreporoot',
165 166 default='',
166 167 )
167 168 coreconfigitem('censor', 'policy',
168 169 default='abort',
170 experimental=True,
169 171 )
170 172 coreconfigitem('chgserver', 'idletimeout',
171 173 default=3600,
172 174 )
173 175 coreconfigitem('chgserver', 'skiphash',
174 176 default=False,
175 177 )
176 178 coreconfigitem('cmdserver', 'log',
177 179 default=None,
178 180 )
179 181 coreconfigitem('cmdserver', 'max-log-files',
180 182 default=7,
181 183 )
182 184 coreconfigitem('cmdserver', 'max-log-size',
183 185 default='1 MB',
184 186 )
185 187 coreconfigitem('cmdserver', 'max-repo-cache',
186 188 default=0,
189 experimental=True,
187 190 )
188 191 coreconfigitem('cmdserver', 'message-encodings',
189 192 default=list,
193 experimental=True,
190 194 )
191 195 coreconfigitem('cmdserver', 'track-log',
192 196 default=lambda: ['chgserver', 'cmdserver', 'repocache'],
193 197 )
194 198 coreconfigitem('color', '.*',
195 199 default=None,
196 200 generic=True,
197 201 )
198 202 coreconfigitem('color', 'mode',
199 203 default='auto',
200 204 )
201 205 coreconfigitem('color', 'pagermode',
202 206 default=dynamicdefault,
203 207 )
204 208 _registerdiffopts(section='commands', configprefix='commit.interactive.')
205 209 coreconfigitem('commands', 'commit.post-status',
206 210 default=False,
207 211 )
208 212 coreconfigitem('commands', 'grep.all-files',
209 213 default=False,
214 experimental=True,
210 215 )
211 216 coreconfigitem('commands', 'resolve.confirm',
212 217 default=False,
213 218 )
214 219 coreconfigitem('commands', 'resolve.explicit-re-merge',
215 220 default=False,
216 221 )
217 222 coreconfigitem('commands', 'resolve.mark-check',
218 223 default='none',
219 224 )
220 225 _registerdiffopts(section='commands', configprefix='revert.interactive.')
221 226 coreconfigitem('commands', 'show.aliasprefix',
222 227 default=list,
223 228 )
224 229 coreconfigitem('commands', 'status.relative',
225 230 default=False,
226 231 )
227 232 coreconfigitem('commands', 'status.skipstates',
228 233 default=[],
234 experimental=True,
229 235 )
230 236 coreconfigitem('commands', 'status.terse',
231 237 default='',
232 238 )
233 239 coreconfigitem('commands', 'status.verbose',
234 240 default=False,
235 241 )
236 242 coreconfigitem('commands', 'update.check',
237 243 default=None,
238 244 )
239 245 coreconfigitem('commands', 'update.requiredest',
240 246 default=False,
241 247 )
242 248 coreconfigitem('committemplate', '.*',
243 249 default=None,
244 250 generic=True,
245 251 )
246 252 coreconfigitem('convert', 'bzr.saverev',
247 253 default=True,
248 254 )
249 255 coreconfigitem('convert', 'cvsps.cache',
250 256 default=True,
251 257 )
252 258 coreconfigitem('convert', 'cvsps.fuzz',
253 259 default=60,
254 260 )
255 261 coreconfigitem('convert', 'cvsps.logencoding',
256 262 default=None,
257 263 )
258 264 coreconfigitem('convert', 'cvsps.mergefrom',
259 265 default=None,
260 266 )
261 267 coreconfigitem('convert', 'cvsps.mergeto',
262 268 default=None,
263 269 )
264 270 coreconfigitem('convert', 'git.committeractions',
265 271 default=lambda: ['messagedifferent'],
266 272 )
267 273 coreconfigitem('convert', 'git.extrakeys',
268 274 default=list,
269 275 )
270 276 coreconfigitem('convert', 'git.findcopiesharder',
271 277 default=False,
272 278 )
273 279 coreconfigitem('convert', 'git.remoteprefix',
274 280 default='remote',
275 281 )
276 282 coreconfigitem('convert', 'git.renamelimit',
277 283 default=400,
278 284 )
279 285 coreconfigitem('convert', 'git.saverev',
280 286 default=True,
281 287 )
282 288 coreconfigitem('convert', 'git.similarity',
283 289 default=50,
284 290 )
285 291 coreconfigitem('convert', 'git.skipsubmodules',
286 292 default=False,
287 293 )
288 294 coreconfigitem('convert', 'hg.clonebranches',
289 295 default=False,
290 296 )
291 297 coreconfigitem('convert', 'hg.ignoreerrors',
292 298 default=False,
293 299 )
294 300 coreconfigitem('convert', 'hg.preserve-hash',
295 301 default=False,
296 302 )
297 303 coreconfigitem('convert', 'hg.revs',
298 304 default=None,
299 305 )
300 306 coreconfigitem('convert', 'hg.saverev',
301 307 default=False,
302 308 )
303 309 coreconfigitem('convert', 'hg.sourcename',
304 310 default=None,
305 311 )
306 312 coreconfigitem('convert', 'hg.startrev',
307 313 default=None,
308 314 )
309 315 coreconfigitem('convert', 'hg.tagsbranch',
310 316 default='default',
311 317 )
312 318 coreconfigitem('convert', 'hg.usebranchnames',
313 319 default=True,
314 320 )
315 321 coreconfigitem('convert', 'ignoreancestorcheck',
316 322 default=False,
323 experimental=True,
317 324 )
318 325 coreconfigitem('convert', 'localtimezone',
319 326 default=False,
320 327 )
321 328 coreconfigitem('convert', 'p4.encoding',
322 329 default=dynamicdefault,
323 330 )
324 331 coreconfigitem('convert', 'p4.startrev',
325 332 default=0,
326 333 )
327 334 coreconfigitem('convert', 'skiptags',
328 335 default=False,
329 336 )
330 337 coreconfigitem('convert', 'svn.debugsvnlog',
331 338 default=True,
332 339 )
333 340 coreconfigitem('convert', 'svn.trunk',
334 341 default=None,
335 342 )
336 343 coreconfigitem('convert', 'svn.tags',
337 344 default=None,
338 345 )
339 346 coreconfigitem('convert', 'svn.branches',
340 347 default=None,
341 348 )
342 349 coreconfigitem('convert', 'svn.startrev',
343 350 default=0,
344 351 )
345 352 coreconfigitem('debug', 'dirstate.delaywrite',
346 353 default=0,
347 354 )
348 355 coreconfigitem('defaults', '.*',
349 356 default=None,
350 357 generic=True,
351 358 )
352 359 coreconfigitem('devel', 'all-warnings',
353 360 default=False,
354 361 )
355 362 coreconfigitem('devel', 'bundle2.debug',
356 363 default=False,
357 364 )
358 365 coreconfigitem('devel', 'bundle.delta',
359 366 default='',
360 367 )
361 368 coreconfigitem('devel', 'cache-vfs',
362 369 default=None,
363 370 )
364 371 coreconfigitem('devel', 'check-locks',
365 372 default=False,
366 373 )
367 374 coreconfigitem('devel', 'check-relroot',
368 375 default=False,
369 376 )
370 377 coreconfigitem('devel', 'default-date',
371 378 default=None,
372 379 )
373 380 coreconfigitem('devel', 'deprec-warn',
374 381 default=False,
375 382 )
376 383 coreconfigitem('devel', 'disableloaddefaultcerts',
377 384 default=False,
378 385 )
379 386 coreconfigitem('devel', 'warn-empty-changegroup',
380 387 default=False,
381 388 )
382 389 coreconfigitem('devel', 'legacy.exchange',
383 390 default=list,
384 391 )
385 392 coreconfigitem('devel', 'servercafile',
386 393 default='',
387 394 )
388 395 coreconfigitem('devel', 'serverexactprotocol',
389 396 default='',
390 397 )
391 398 coreconfigitem('devel', 'serverrequirecert',
392 399 default=False,
393 400 )
394 401 coreconfigitem('devel', 'strip-obsmarkers',
395 402 default=True,
396 403 )
397 404 coreconfigitem('devel', 'warn-config',
398 405 default=None,
399 406 )
400 407 coreconfigitem('devel', 'warn-config-default',
401 408 default=None,
402 409 )
403 410 coreconfigitem('devel', 'user.obsmarker',
404 411 default=None,
405 412 )
406 413 coreconfigitem('devel', 'warn-config-unknown',
407 414 default=None,
408 415 )
409 416 coreconfigitem('devel', 'debug.copies',
410 417 default=False,
411 418 )
412 419 coreconfigitem('devel', 'debug.extensions',
413 420 default=False,
414 421 )
415 422 coreconfigitem('devel', 'debug.peer-request',
416 423 default=False,
417 424 )
418 425 coreconfigitem('devel', 'discovery.randomize',
419 426 default=True,
420 427 )
421 428 _registerdiffopts(section='diff')
422 429 coreconfigitem('email', 'bcc',
423 430 default=None,
424 431 )
425 432 coreconfigitem('email', 'cc',
426 433 default=None,
427 434 )
428 435 coreconfigitem('email', 'charsets',
429 436 default=list,
430 437 )
431 438 coreconfigitem('email', 'from',
432 439 default=None,
433 440 )
434 441 coreconfigitem('email', 'method',
435 442 default='smtp',
436 443 )
437 444 coreconfigitem('email', 'reply-to',
438 445 default=None,
439 446 )
440 447 coreconfigitem('email', 'to',
441 448 default=None,
442 449 )
443 450 coreconfigitem('experimental', 'archivemetatemplate',
444 451 default=dynamicdefault,
445 452 )
446 453 coreconfigitem('experimental', 'auto-publish',
447 454 default='publish',
448 455 )
449 456 coreconfigitem('experimental', 'bundle-phases',
450 457 default=False,
451 458 )
452 459 coreconfigitem('experimental', 'bundle2-advertise',
453 460 default=True,
454 461 )
455 462 coreconfigitem('experimental', 'bundle2-output-capture',
456 463 default=False,
457 464 )
458 465 coreconfigitem('experimental', 'bundle2.pushback',
459 466 default=False,
460 467 )
461 468 coreconfigitem('experimental', 'bundle2lazylocking',
462 469 default=False,
463 470 )
464 471 coreconfigitem('experimental', 'bundlecomplevel',
465 472 default=None,
466 473 )
467 474 coreconfigitem('experimental', 'bundlecomplevel.bzip2',
468 475 default=None,
469 476 )
470 477 coreconfigitem('experimental', 'bundlecomplevel.gzip',
471 478 default=None,
472 479 )
473 480 coreconfigitem('experimental', 'bundlecomplevel.none',
474 481 default=None,
475 482 )
476 483 coreconfigitem('experimental', 'bundlecomplevel.zstd',
477 484 default=None,
478 485 )
479 486 coreconfigitem('experimental', 'changegroup3',
480 487 default=False,
481 488 )
482 489 coreconfigitem('experimental', 'cleanup-as-archived',
483 490 default=False,
484 491 )
485 492 coreconfigitem('experimental', 'clientcompressionengines',
486 493 default=list,
487 494 )
488 495 coreconfigitem('experimental', 'copytrace',
489 496 default='on',
490 497 )
491 498 coreconfigitem('experimental', 'copytrace.movecandidateslimit',
492 499 default=100,
493 500 )
494 501 coreconfigitem('experimental', 'copytrace.sourcecommitlimit',
495 502 default=100,
496 503 )
497 504 coreconfigitem('experimental', 'copies.read-from',
498 505 default="filelog-only",
499 506 )
500 507 coreconfigitem('experimental', 'copies.write-to',
501 508 default='filelog-only',
502 509 )
503 510 coreconfigitem('experimental', 'crecordtest',
504 511 default=None,
505 512 )
506 513 coreconfigitem('experimental', 'directaccess',
507 514 default=False,
508 515 )
509 516 coreconfigitem('experimental', 'directaccess.revnums',
510 517 default=False,
511 518 )
512 519 coreconfigitem('experimental', 'editortmpinhg',
513 520 default=False,
514 521 )
515 522 coreconfigitem('experimental', 'evolution',
516 523 default=list,
517 524 )
518 525 coreconfigitem('experimental', 'evolution.allowdivergence',
519 526 default=False,
520 527 alias=[('experimental', 'allowdivergence')]
521 528 )
522 529 coreconfigitem('experimental', 'evolution.allowunstable',
523 530 default=None,
524 531 )
525 532 coreconfigitem('experimental', 'evolution.createmarkers',
526 533 default=None,
527 534 )
528 535 coreconfigitem('experimental', 'evolution.effect-flags',
529 536 default=True,
530 537 alias=[('experimental', 'effect-flags')]
531 538 )
532 539 coreconfigitem('experimental', 'evolution.exchange',
533 540 default=None,
534 541 )
535 542 coreconfigitem('experimental', 'evolution.bundle-obsmarker',
536 543 default=False,
537 544 )
538 545 coreconfigitem('experimental', 'log.topo',
539 546 default=False,
540 547 )
541 548 coreconfigitem('experimental', 'evolution.report-instabilities',
542 549 default=True,
543 550 )
544 551 coreconfigitem('experimental', 'evolution.track-operation',
545 552 default=True,
546 553 )
547 554 # repo-level config to exclude a revset visibility
548 555 #
549 556 # The target use case is to use `share` to expose different subset of the same
550 557 # repository, especially server side. See also `server.view`.
551 558 coreconfigitem('experimental', 'extra-filter-revs',
552 559 default=None,
553 560 )
554 561 coreconfigitem('experimental', 'maxdeltachainspan',
555 562 default=-1,
556 563 )
557 564 coreconfigitem('experimental', 'mergetempdirprefix',
558 565 default=None,
559 566 )
560 567 coreconfigitem('experimental', 'mmapindexthreshold',
561 568 default=None,
562 569 )
563 570 coreconfigitem('experimental', 'narrow',
564 571 default=False,
565 572 )
566 573 coreconfigitem('experimental', 'nonnormalparanoidcheck',
567 574 default=False,
568 575 )
569 576 coreconfigitem('experimental', 'exportableenviron',
570 577 default=list,
571 578 )
572 579 coreconfigitem('experimental', 'extendedheader.index',
573 580 default=None,
574 581 )
575 582 coreconfigitem('experimental', 'extendedheader.similarity',
576 583 default=False,
577 584 )
578 585 coreconfigitem('experimental', 'graphshorten',
579 586 default=False,
580 587 )
581 588 coreconfigitem('experimental', 'graphstyle.parent',
582 589 default=dynamicdefault,
583 590 )
584 591 coreconfigitem('experimental', 'graphstyle.missing',
585 592 default=dynamicdefault,
586 593 )
587 594 coreconfigitem('experimental', 'graphstyle.grandparent',
588 595 default=dynamicdefault,
589 596 )
590 597 coreconfigitem('experimental', 'hook-track-tags',
591 598 default=False,
592 599 )
593 600 coreconfigitem('experimental', 'httppeer.advertise-v2',
594 601 default=False,
595 602 )
596 603 coreconfigitem('experimental', 'httppeer.v2-encoder-order',
597 604 default=None,
598 605 )
599 606 coreconfigitem('experimental', 'httppostargs',
600 607 default=False,
601 608 )
602 609 coreconfigitem('experimental', 'mergedriver',
603 610 default=None,
604 611 )
605 612 coreconfigitem('experimental', 'nointerrupt', default=False)
606 613 coreconfigitem('experimental', 'nointerrupt-interactiveonly', default=True)
607 614
608 615 coreconfigitem('experimental', 'obsmarkers-exchange-debug',
609 616 default=False,
610 617 )
611 618 coreconfigitem('experimental', 'remotenames',
612 619 default=False,
613 620 )
614 621 coreconfigitem('experimental', 'removeemptydirs',
615 622 default=True,
616 623 )
617 624 coreconfigitem('experimental', 'revert.interactive.select-to-keep',
618 625 default=False,
619 626 )
620 627 coreconfigitem('experimental', 'revisions.prefixhexnode',
621 628 default=False,
622 629 )
623 630 coreconfigitem('experimental', 'revlogv2',
624 631 default=None,
625 632 )
626 633 coreconfigitem('experimental', 'revisions.disambiguatewithin',
627 634 default=None,
628 635 )
629 636 coreconfigitem('experimental', 'server.filesdata.recommended-batch-size',
630 637 default=50000,
631 638 )
632 639 coreconfigitem('experimental', 'server.manifestdata.recommended-batch-size',
633 640 default=100000,
634 641 )
635 642 coreconfigitem('experimental', 'server.stream-narrow-clones',
636 643 default=False,
637 644 )
638 645 coreconfigitem('experimental', 'single-head-per-branch',
639 646 default=False,
640 647 )
641 648 coreconfigitem('experimental', 'sshserver.support-v2',
642 649 default=False,
643 650 )
644 651 coreconfigitem('experimental', 'sparse-read',
645 652 default=False,
646 653 )
647 654 coreconfigitem('experimental', 'sparse-read.density-threshold',
648 655 default=0.50,
649 656 )
650 657 coreconfigitem('experimental', 'sparse-read.min-gap-size',
651 658 default='65K',
652 659 )
653 660 coreconfigitem('experimental', 'treemanifest',
654 661 default=False,
655 662 )
656 663 coreconfigitem('experimental', 'update.atomic-file',
657 664 default=False,
658 665 )
659 666 coreconfigitem('experimental', 'sshpeer.advertise-v2',
660 667 default=False,
661 668 )
662 669 coreconfigitem('experimental', 'web.apiserver',
663 670 default=False,
664 671 )
665 672 coreconfigitem('experimental', 'web.api.http-v2',
666 673 default=False,
667 674 )
668 675 coreconfigitem('experimental', 'web.api.debugreflect',
669 676 default=False,
670 677 )
671 678 coreconfigitem('experimental', 'worker.wdir-get-thread-safe',
672 679 default=False,
673 680 )
674 681 coreconfigitem('experimental', 'xdiff',
675 682 default=False,
676 683 )
677 684 coreconfigitem('extensions', '.*',
678 685 default=None,
679 686 generic=True,
680 687 )
681 688 coreconfigitem('extdata', '.*',
682 689 default=None,
683 690 generic=True,
684 691 )
685 692 coreconfigitem('format', 'bookmarks-in-store',
686 693 default=False,
687 694 )
688 695 coreconfigitem('format', 'chunkcachesize',
689 696 default=None,
697 experimental=True,
690 698 )
691 699 coreconfigitem('format', 'dotencode',
692 700 default=True,
693 701 )
694 702 coreconfigitem('format', 'generaldelta',
695 703 default=False,
704 experimental=True,
696 705 )
697 706 coreconfigitem('format', 'manifestcachesize',
698 707 default=None,
708 experimental=True,
699 709 )
700 710 coreconfigitem('format', 'maxchainlen',
701 711 default=dynamicdefault,
712 experimental=True,
702 713 )
703 714 coreconfigitem('format', 'obsstore-version',
704 715 default=None,
705 716 )
706 717 coreconfigitem('format', 'sparse-revlog',
707 718 default=True,
708 719 )
709 720 coreconfigitem('format', 'revlog-compression',
710 721 default='zlib',
711 722 alias=[('experimental', 'format.compression')]
712 723 )
713 724 coreconfigitem('format', 'usefncache',
714 725 default=True,
715 726 )
716 727 coreconfigitem('format', 'usegeneraldelta',
717 728 default=True,
718 729 )
719 730 coreconfigitem('format', 'usestore',
720 731 default=True,
721 732 )
722 733 coreconfigitem('format', 'internal-phase',
723 734 default=False,
735 experimental=True,
724 736 )
725 737 coreconfigitem('fsmonitor', 'warn_when_unused',
726 738 default=True,
727 739 )
728 740 coreconfigitem('fsmonitor', 'warn_update_file_count',
729 741 default=50000,
730 742 )
731 743 coreconfigitem('help', br'hidden-command\..*',
732 744 default=False,
733 745 generic=True,
734 746 )
735 747 coreconfigitem('help', br'hidden-topic\..*',
736 748 default=False,
737 749 generic=True,
738 750 )
739 751 coreconfigitem('hooks', '.*',
740 752 default=dynamicdefault,
741 753 generic=True,
742 754 )
743 755 coreconfigitem('hgweb-paths', '.*',
744 756 default=list,
745 757 generic=True,
746 758 )
747 759 coreconfigitem('hostfingerprints', '.*',
748 760 default=list,
749 761 generic=True,
750 762 )
751 763 coreconfigitem('hostsecurity', 'ciphers',
752 764 default=None,
753 765 )
754 766 coreconfigitem('hostsecurity', 'disabletls10warning',
755 767 default=False,
756 768 )
757 769 coreconfigitem('hostsecurity', 'minimumprotocol',
758 770 default=dynamicdefault,
759 771 )
760 772 coreconfigitem('hostsecurity', '.*:minimumprotocol$',
761 773 default=dynamicdefault,
762 774 generic=True,
763 775 )
764 776 coreconfigitem('hostsecurity', '.*:ciphers$',
765 777 default=dynamicdefault,
766 778 generic=True,
767 779 )
768 780 coreconfigitem('hostsecurity', '.*:fingerprints$',
769 781 default=list,
770 782 generic=True,
771 783 )
772 784 coreconfigitem('hostsecurity', '.*:verifycertsfile$',
773 785 default=None,
774 786 generic=True,
775 787 )
776 788
777 789 coreconfigitem('http_proxy', 'always',
778 790 default=False,
779 791 )
780 792 coreconfigitem('http_proxy', 'host',
781 793 default=None,
782 794 )
783 795 coreconfigitem('http_proxy', 'no',
784 796 default=list,
785 797 )
786 798 coreconfigitem('http_proxy', 'passwd',
787 799 default=None,
788 800 )
789 801 coreconfigitem('http_proxy', 'user',
790 802 default=None,
791 803 )
792 804
793 805 coreconfigitem('http', 'timeout',
794 806 default=None,
795 807 )
796 808
797 809 coreconfigitem('logtoprocess', 'commandexception',
798 810 default=None,
799 811 )
800 812 coreconfigitem('logtoprocess', 'commandfinish',
801 813 default=None,
802 814 )
803 815 coreconfigitem('logtoprocess', 'command',
804 816 default=None,
805 817 )
806 818 coreconfigitem('logtoprocess', 'develwarn',
807 819 default=None,
808 820 )
809 821 coreconfigitem('logtoprocess', 'uiblocked',
810 822 default=None,
811 823 )
812 824 coreconfigitem('merge', 'checkunknown',
813 825 default='abort',
814 826 )
815 827 coreconfigitem('merge', 'checkignored',
816 828 default='abort',
817 829 )
818 830 coreconfigitem('experimental', 'merge.checkpathconflicts',
819 831 default=False,
820 832 )
821 833 coreconfigitem('merge', 'followcopies',
822 834 default=True,
823 835 )
824 836 coreconfigitem('merge', 'on-failure',
825 837 default='continue',
826 838 )
827 839 coreconfigitem('merge', 'preferancestor',
828 840 default=lambda: ['*'],
841 experimental=True,
829 842 )
830 843 coreconfigitem('merge', 'strict-capability-check',
831 844 default=False,
832 845 )
833 846 coreconfigitem('merge-tools', '.*',
834 847 default=None,
835 848 generic=True,
836 849 )
837 850 coreconfigitem('merge-tools', br'.*\.args$',
838 851 default="$local $base $other",
839 852 generic=True,
840 853 priority=-1,
841 854 )
842 855 coreconfigitem('merge-tools', br'.*\.binary$',
843 856 default=False,
844 857 generic=True,
845 858 priority=-1,
846 859 )
847 860 coreconfigitem('merge-tools', br'.*\.check$',
848 861 default=list,
849 862 generic=True,
850 863 priority=-1,
851 864 )
852 865 coreconfigitem('merge-tools', br'.*\.checkchanged$',
853 866 default=False,
854 867 generic=True,
855 868 priority=-1,
856 869 )
857 870 coreconfigitem('merge-tools', br'.*\.executable$',
858 871 default=dynamicdefault,
859 872 generic=True,
860 873 priority=-1,
861 874 )
862 875 coreconfigitem('merge-tools', br'.*\.fixeol$',
863 876 default=False,
864 877 generic=True,
865 878 priority=-1,
866 879 )
867 880 coreconfigitem('merge-tools', br'.*\.gui$',
868 881 default=False,
869 882 generic=True,
870 883 priority=-1,
871 884 )
872 885 coreconfigitem('merge-tools', br'.*\.mergemarkers$',
873 886 default='basic',
874 887 generic=True,
875 888 priority=-1,
876 889 )
877 890 coreconfigitem('merge-tools', br'.*\.mergemarkertemplate$',
878 891 default=dynamicdefault, # take from ui.mergemarkertemplate
879 892 generic=True,
880 893 priority=-1,
881 894 )
882 895 coreconfigitem('merge-tools', br'.*\.priority$',
883 896 default=0,
884 897 generic=True,
885 898 priority=-1,
886 899 )
887 900 coreconfigitem('merge-tools', br'.*\.premerge$',
888 901 default=dynamicdefault,
889 902 generic=True,
890 903 priority=-1,
891 904 )
892 905 coreconfigitem('merge-tools', br'.*\.symlink$',
893 906 default=False,
894 907 generic=True,
895 908 priority=-1,
896 909 )
897 910 coreconfigitem('pager', 'attend-.*',
898 911 default=dynamicdefault,
899 912 generic=True,
900 913 )
901 914 coreconfigitem('pager', 'ignore',
902 915 default=list,
903 916 )
904 917 coreconfigitem('pager', 'pager',
905 918 default=dynamicdefault,
906 919 )
907 920 coreconfigitem('patch', 'eol',
908 921 default='strict',
909 922 )
910 923 coreconfigitem('patch', 'fuzz',
911 924 default=2,
912 925 )
913 926 coreconfigitem('paths', 'default',
914 927 default=None,
915 928 )
916 929 coreconfigitem('paths', 'default-push',
917 930 default=None,
918 931 )
919 932 coreconfigitem('paths', '.*',
920 933 default=None,
921 934 generic=True,
922 935 )
923 936 coreconfigitem('phases', 'checksubrepos',
924 937 default='follow',
925 938 )
926 939 coreconfigitem('phases', 'new-commit',
927 940 default='draft',
928 941 )
929 942 coreconfigitem('phases', 'publish',
930 943 default=True,
931 944 )
932 945 coreconfigitem('profiling', 'enabled',
933 946 default=False,
934 947 )
935 948 coreconfigitem('profiling', 'format',
936 949 default='text',
937 950 )
938 951 coreconfigitem('profiling', 'freq',
939 952 default=1000,
940 953 )
941 954 coreconfigitem('profiling', 'limit',
942 955 default=30,
943 956 )
944 957 coreconfigitem('profiling', 'nested',
945 958 default=0,
946 959 )
947 960 coreconfigitem('profiling', 'output',
948 961 default=None,
949 962 )
950 963 coreconfigitem('profiling', 'showmax',
951 964 default=0.999,
952 965 )
953 966 coreconfigitem('profiling', 'showmin',
954 967 default=dynamicdefault,
955 968 )
956 969 coreconfigitem('profiling', 'showtime',
957 970 default=True,
958 971 )
959 972 coreconfigitem('profiling', 'sort',
960 973 default='inlinetime',
961 974 )
962 975 coreconfigitem('profiling', 'statformat',
963 976 default='hotpath',
964 977 )
965 978 coreconfigitem('profiling', 'time-track',
966 979 default=dynamicdefault,
967 980 )
968 981 coreconfigitem('profiling', 'type',
969 982 default='stat',
970 983 )
971 984 coreconfigitem('progress', 'assume-tty',
972 985 default=False,
973 986 )
974 987 coreconfigitem('progress', 'changedelay',
975 988 default=1,
976 989 )
977 990 coreconfigitem('progress', 'clear-complete',
978 991 default=True,
979 992 )
980 993 coreconfigitem('progress', 'debug',
981 994 default=False,
982 995 )
983 996 coreconfigitem('progress', 'delay',
984 997 default=3,
985 998 )
986 999 coreconfigitem('progress', 'disable',
987 1000 default=False,
988 1001 )
989 1002 coreconfigitem('progress', 'estimateinterval',
990 1003 default=60.0,
991 1004 )
992 1005 coreconfigitem('progress', 'format',
993 1006 default=lambda: ['topic', 'bar', 'number', 'estimate'],
994 1007 )
995 1008 coreconfigitem('progress', 'refresh',
996 1009 default=0.1,
997 1010 )
998 1011 coreconfigitem('progress', 'width',
999 1012 default=dynamicdefault,
1000 1013 )
1001 1014 coreconfigitem('push', 'pushvars.server',
1002 1015 default=False,
1003 1016 )
1004 1017 coreconfigitem('rewrite', 'backup-bundle',
1005 1018 default=True,
1006 1019 alias=[('ui', 'history-editing-backup')],
1007 1020 )
1008 1021 coreconfigitem('rewrite', 'update-timestamp',
1009 1022 default=False,
1010 1023 )
1011 1024 coreconfigitem('storage', 'new-repo-backend',
1012 1025 default='revlogv1',
1026 experimental=True,
1013 1027 )
1014 1028 coreconfigitem('storage', 'revlog.optimize-delta-parent-choice',
1015 1029 default=True,
1016 1030 alias=[('format', 'aggressivemergedeltas')],
1017 1031 )
1018 1032 coreconfigitem('storage', 'revlog.reuse-external-delta',
1019 1033 default=True,
1020 1034 )
1021 1035 coreconfigitem('storage', 'revlog.reuse-external-delta-parent',
1022 1036 default=None,
1023 1037 )
1024 1038 coreconfigitem('storage', 'revlog.zlib.level',
1025 1039 default=None,
1026 1040 )
1027 1041 coreconfigitem('storage', 'revlog.zstd.level',
1028 1042 default=None,
1029 1043 )
1030 1044 coreconfigitem('server', 'bookmarks-pushkey-compat',
1031 1045 default=True,
1032 1046 )
1033 1047 coreconfigitem('server', 'bundle1',
1034 1048 default=True,
1035 1049 )
1036 1050 coreconfigitem('server', 'bundle1gd',
1037 1051 default=None,
1038 1052 )
1039 1053 coreconfigitem('server', 'bundle1.pull',
1040 1054 default=None,
1041 1055 )
1042 1056 coreconfigitem('server', 'bundle1gd.pull',
1043 1057 default=None,
1044 1058 )
1045 1059 coreconfigitem('server', 'bundle1.push',
1046 1060 default=None,
1047 1061 )
1048 1062 coreconfigitem('server', 'bundle1gd.push',
1049 1063 default=None,
1050 1064 )
1051 1065 coreconfigitem('server', 'bundle2.stream',
1052 1066 default=True,
1053 1067 alias=[('experimental', 'bundle2.stream')]
1054 1068 )
1055 1069 coreconfigitem('server', 'compressionengines',
1056 1070 default=list,
1057 1071 )
1058 1072 coreconfigitem('server', 'concurrent-push-mode',
1059 1073 default='strict',
1060 1074 )
1061 1075 coreconfigitem('server', 'disablefullbundle',
1062 1076 default=False,
1063 1077 )
1064 1078 coreconfigitem('server', 'maxhttpheaderlen',
1065 1079 default=1024,
1066 1080 )
1067 1081 coreconfigitem('server', 'pullbundle',
1068 1082 default=False,
1069 1083 )
1070 1084 coreconfigitem('server', 'preferuncompressed',
1071 1085 default=False,
1072 1086 )
1073 1087 coreconfigitem('server', 'streamunbundle',
1074 1088 default=False,
1075 1089 )
1076 1090 coreconfigitem('server', 'uncompressed',
1077 1091 default=True,
1078 1092 )
1079 1093 coreconfigitem('server', 'uncompressedallowsecret',
1080 1094 default=False,
1081 1095 )
1082 1096 coreconfigitem('server', 'view',
1083 1097 default='served',
1084 1098 )
1085 1099 coreconfigitem('server', 'validate',
1086 1100 default=False,
1087 1101 )
1088 1102 coreconfigitem('server', 'zliblevel',
1089 1103 default=-1,
1090 1104 )
1091 1105 coreconfigitem('server', 'zstdlevel',
1092 1106 default=3,
1093 1107 )
1094 1108 coreconfigitem('share', 'pool',
1095 1109 default=None,
1096 1110 )
1097 1111 coreconfigitem('share', 'poolnaming',
1098 1112 default='identity',
1099 1113 )
1100 1114 coreconfigitem('shelve','maxbackups',
1101 1115 default=10,
1102 1116 )
1103 1117 coreconfigitem('smtp', 'host',
1104 1118 default=None,
1105 1119 )
1106 1120 coreconfigitem('smtp', 'local_hostname',
1107 1121 default=None,
1108 1122 )
1109 1123 coreconfigitem('smtp', 'password',
1110 1124 default=None,
1111 1125 )
1112 1126 coreconfigitem('smtp', 'port',
1113 1127 default=dynamicdefault,
1114 1128 )
1115 1129 coreconfigitem('smtp', 'tls',
1116 1130 default='none',
1117 1131 )
1118 1132 coreconfigitem('smtp', 'username',
1119 1133 default=None,
1120 1134 )
1121 1135 coreconfigitem('sparse', 'missingwarning',
1122 1136 default=True,
1137 experimental=True,
1123 1138 )
1124 1139 coreconfigitem('subrepos', 'allowed',
1125 1140 default=dynamicdefault, # to make backporting simpler
1126 1141 )
1127 1142 coreconfigitem('subrepos', 'hg:allowed',
1128 1143 default=dynamicdefault,
1129 1144 )
1130 1145 coreconfigitem('subrepos', 'git:allowed',
1131 1146 default=dynamicdefault,
1132 1147 )
1133 1148 coreconfigitem('subrepos', 'svn:allowed',
1134 1149 default=dynamicdefault,
1135 1150 )
1136 1151 coreconfigitem('templates', '.*',
1137 1152 default=None,
1138 1153 generic=True,
1139 1154 )
1140 1155 coreconfigitem('templateconfig', '.*',
1141 1156 default=dynamicdefault,
1142 1157 generic=True,
1143 1158 )
1144 1159 coreconfigitem('trusted', 'groups',
1145 1160 default=list,
1146 1161 )
1147 1162 coreconfigitem('trusted', 'users',
1148 1163 default=list,
1149 1164 )
1150 1165 coreconfigitem('ui', '_usedassubrepo',
1151 1166 default=False,
1152 1167 )
1153 1168 coreconfigitem('ui', 'allowemptycommit',
1154 1169 default=False,
1155 1170 )
1156 1171 coreconfigitem('ui', 'archivemeta',
1157 1172 default=True,
1158 1173 )
1159 1174 coreconfigitem('ui', 'askusername',
1160 1175 default=False,
1161 1176 )
1162 1177 coreconfigitem('ui', 'clonebundlefallback',
1163 1178 default=False,
1164 1179 )
1165 1180 coreconfigitem('ui', 'clonebundleprefers',
1166 1181 default=list,
1167 1182 )
1168 1183 coreconfigitem('ui', 'clonebundles',
1169 1184 default=True,
1170 1185 )
1171 1186 coreconfigitem('ui', 'color',
1172 1187 default='auto',
1173 1188 )
1174 1189 coreconfigitem('ui', 'commitsubrepos',
1175 1190 default=False,
1176 1191 )
1177 1192 coreconfigitem('ui', 'debug',
1178 1193 default=False,
1179 1194 )
1180 1195 coreconfigitem('ui', 'debugger',
1181 1196 default=None,
1182 1197 )
1183 1198 coreconfigitem('ui', 'editor',
1184 1199 default=dynamicdefault,
1185 1200 )
1186 1201 coreconfigitem('ui', 'fallbackencoding',
1187 1202 default=None,
1188 1203 )
1189 1204 coreconfigitem('ui', 'forcecwd',
1190 1205 default=None,
1191 1206 )
1192 1207 coreconfigitem('ui', 'forcemerge',
1193 1208 default=None,
1194 1209 )
1195 1210 coreconfigitem('ui', 'formatdebug',
1196 1211 default=False,
1197 1212 )
1198 1213 coreconfigitem('ui', 'formatjson',
1199 1214 default=False,
1200 1215 )
1201 1216 coreconfigitem('ui', 'formatted',
1202 1217 default=None,
1203 1218 )
1204 1219 coreconfigitem('ui', 'graphnodetemplate',
1205 1220 default=None,
1206 1221 )
1207 1222 coreconfigitem('ui', 'interactive',
1208 1223 default=None,
1209 1224 )
1210 1225 coreconfigitem('ui', 'interface',
1211 1226 default=None,
1212 1227 )
1213 1228 coreconfigitem('ui', 'interface.chunkselector',
1214 1229 default=None,
1215 1230 )
1216 1231 coreconfigitem('ui', 'large-file-limit',
1217 1232 default=10000000,
1218 1233 )
1219 1234 coreconfigitem('ui', 'logblockedtimes',
1220 1235 default=False,
1221 1236 )
1222 1237 coreconfigitem('ui', 'logtemplate',
1223 1238 default=None,
1224 1239 )
1225 1240 coreconfigitem('ui', 'merge',
1226 1241 default=None,
1227 1242 )
1228 1243 coreconfigitem('ui', 'mergemarkers',
1229 1244 default='basic',
1230 1245 )
1231 1246 coreconfigitem('ui', 'mergemarkertemplate',
1232 1247 default=('{node|short} '
1233 1248 '{ifeq(tags, "tip", "", '
1234 1249 'ifeq(tags, "", "", "{tags} "))}'
1235 1250 '{if(bookmarks, "{bookmarks} ")}'
1236 1251 '{ifeq(branch, "default", "", "{branch} ")}'
1237 1252 '- {author|user}: {desc|firstline}')
1238 1253 )
1239 1254 coreconfigitem('ui', 'message-output',
1240 1255 default='stdio',
1241 1256 )
1242 1257 coreconfigitem('ui', 'nontty',
1243 1258 default=False,
1244 1259 )
1245 1260 coreconfigitem('ui', 'origbackuppath',
1246 1261 default=None,
1247 1262 )
1248 1263 coreconfigitem('ui', 'paginate',
1249 1264 default=True,
1250 1265 )
1251 1266 coreconfigitem('ui', 'patch',
1252 1267 default=None,
1253 1268 )
1254 1269 coreconfigitem('ui', 'pre-merge-tool-output-template',
1255 1270 default=None,
1256 1271 )
1257 1272 coreconfigitem('ui', 'portablefilenames',
1258 1273 default='warn',
1259 1274 )
1260 1275 coreconfigitem('ui', 'promptecho',
1261 1276 default=False,
1262 1277 )
1263 1278 coreconfigitem('ui', 'quiet',
1264 1279 default=False,
1265 1280 )
1266 1281 coreconfigitem('ui', 'quietbookmarkmove',
1267 1282 default=False,
1268 1283 )
1269 1284 coreconfigitem('ui', 'relative-paths',
1270 1285 default='legacy',
1271 1286 )
1272 1287 coreconfigitem('ui', 'remotecmd',
1273 1288 default='hg',
1274 1289 )
1275 1290 coreconfigitem('ui', 'report_untrusted',
1276 1291 default=True,
1277 1292 )
1278 1293 coreconfigitem('ui', 'rollback',
1279 1294 default=True,
1280 1295 )
1281 1296 coreconfigitem('ui', 'signal-safe-lock',
1282 1297 default=True,
1283 1298 )
1284 1299 coreconfigitem('ui', 'slash',
1285 1300 default=False,
1286 1301 )
1287 1302 coreconfigitem('ui', 'ssh',
1288 1303 default='ssh',
1289 1304 )
1290 1305 coreconfigitem('ui', 'ssherrorhint',
1291 1306 default=None,
1292 1307 )
1293 1308 coreconfigitem('ui', 'statuscopies',
1294 1309 default=False,
1295 1310 )
1296 1311 coreconfigitem('ui', 'strict',
1297 1312 default=False,
1298 1313 )
1299 1314 coreconfigitem('ui', 'style',
1300 1315 default='',
1301 1316 )
1302 1317 coreconfigitem('ui', 'supportcontact',
1303 1318 default=None,
1304 1319 )
1305 1320 coreconfigitem('ui', 'textwidth',
1306 1321 default=78,
1307 1322 )
1308 1323 coreconfigitem('ui', 'timeout',
1309 1324 default='600',
1310 1325 )
1311 1326 coreconfigitem('ui', 'timeout.warn',
1312 1327 default=0,
1313 1328 )
1314 1329 coreconfigitem('ui', 'traceback',
1315 1330 default=False,
1316 1331 )
1317 1332 coreconfigitem('ui', 'tweakdefaults',
1318 1333 default=False,
1319 1334 )
1320 1335 coreconfigitem('ui', 'username',
1321 1336 alias=[('ui', 'user')]
1322 1337 )
1323 1338 coreconfigitem('ui', 'verbose',
1324 1339 default=False,
1325 1340 )
1326 1341 coreconfigitem('verify', 'skipflags',
1327 1342 default=None,
1328 1343 )
1329 1344 coreconfigitem('web', 'allowbz2',
1330 1345 default=False,
1331 1346 )
1332 1347 coreconfigitem('web', 'allowgz',
1333 1348 default=False,
1334 1349 )
1335 1350 coreconfigitem('web', 'allow-pull',
1336 1351 alias=[('web', 'allowpull')],
1337 1352 default=True,
1338 1353 )
1339 1354 coreconfigitem('web', 'allow-push',
1340 1355 alias=[('web', 'allow_push')],
1341 1356 default=list,
1342 1357 )
1343 1358 coreconfigitem('web', 'allowzip',
1344 1359 default=False,
1345 1360 )
1346 1361 coreconfigitem('web', 'archivesubrepos',
1347 1362 default=False,
1348 1363 )
1349 1364 coreconfigitem('web', 'cache',
1350 1365 default=True,
1351 1366 )
1352 1367 coreconfigitem('web', 'comparisoncontext',
1353 1368 default=5,
1354 1369 )
1355 1370 coreconfigitem('web', 'contact',
1356 1371 default=None,
1357 1372 )
1358 1373 coreconfigitem('web', 'deny_push',
1359 1374 default=list,
1360 1375 )
1361 1376 coreconfigitem('web', 'guessmime',
1362 1377 default=False,
1363 1378 )
1364 1379 coreconfigitem('web', 'hidden',
1365 1380 default=False,
1366 1381 )
1367 1382 coreconfigitem('web', 'labels',
1368 1383 default=list,
1369 1384 )
1370 1385 coreconfigitem('web', 'logoimg',
1371 1386 default='hglogo.png',
1372 1387 )
1373 1388 coreconfigitem('web', 'logourl',
1374 1389 default='https://mercurial-scm.org/',
1375 1390 )
1376 1391 coreconfigitem('web', 'accesslog',
1377 1392 default='-',
1378 1393 )
1379 1394 coreconfigitem('web', 'address',
1380 1395 default='',
1381 1396 )
1382 1397 coreconfigitem('web', 'allow-archive',
1383 1398 alias=[('web', 'allow_archive')],
1384 1399 default=list,
1385 1400 )
1386 1401 coreconfigitem('web', 'allow_read',
1387 1402 default=list,
1388 1403 )
1389 1404 coreconfigitem('web', 'baseurl',
1390 1405 default=None,
1391 1406 )
1392 1407 coreconfigitem('web', 'cacerts',
1393 1408 default=None,
1394 1409 )
1395 1410 coreconfigitem('web', 'certificate',
1396 1411 default=None,
1397 1412 )
1398 1413 coreconfigitem('web', 'collapse',
1399 1414 default=False,
1400 1415 )
1401 1416 coreconfigitem('web', 'csp',
1402 1417 default=None,
1403 1418 )
1404 1419 coreconfigitem('web', 'deny_read',
1405 1420 default=list,
1406 1421 )
1407 1422 coreconfigitem('web', 'descend',
1408 1423 default=True,
1409 1424 )
1410 1425 coreconfigitem('web', 'description',
1411 1426 default="",
1412 1427 )
1413 1428 coreconfigitem('web', 'encoding',
1414 1429 default=lambda: encoding.encoding,
1415 1430 )
1416 1431 coreconfigitem('web', 'errorlog',
1417 1432 default='-',
1418 1433 )
1419 1434 coreconfigitem('web', 'ipv6',
1420 1435 default=False,
1421 1436 )
1422 1437 coreconfigitem('web', 'maxchanges',
1423 1438 default=10,
1424 1439 )
1425 1440 coreconfigitem('web', 'maxfiles',
1426 1441 default=10,
1427 1442 )
1428 1443 coreconfigitem('web', 'maxshortchanges',
1429 1444 default=60,
1430 1445 )
1431 1446 coreconfigitem('web', 'motd',
1432 1447 default='',
1433 1448 )
1434 1449 coreconfigitem('web', 'name',
1435 1450 default=dynamicdefault,
1436 1451 )
1437 1452 coreconfigitem('web', 'port',
1438 1453 default=8000,
1439 1454 )
1440 1455 coreconfigitem('web', 'prefix',
1441 1456 default='',
1442 1457 )
1443 1458 coreconfigitem('web', 'push_ssl',
1444 1459 default=True,
1445 1460 )
1446 1461 coreconfigitem('web', 'refreshinterval',
1447 1462 default=20,
1448 1463 )
1449 1464 coreconfigitem('web', 'server-header',
1450 1465 default=None,
1451 1466 )
1452 1467 coreconfigitem('web', 'static',
1453 1468 default=None,
1454 1469 )
1455 1470 coreconfigitem('web', 'staticurl',
1456 1471 default=None,
1457 1472 )
1458 1473 coreconfigitem('web', 'stripes',
1459 1474 default=1,
1460 1475 )
1461 1476 coreconfigitem('web', 'style',
1462 1477 default='paper',
1463 1478 )
1464 1479 coreconfigitem('web', 'templates',
1465 1480 default=None,
1466 1481 )
1467 1482 coreconfigitem('web', 'view',
1468 1483 default='served',
1484 experimental=True,
1469 1485 )
1470 1486 coreconfigitem('worker', 'backgroundclose',
1471 1487 default=dynamicdefault,
1472 1488 )
1473 1489 # Windows defaults to a limit of 512 open files. A buffer of 128
1474 1490 # should give us enough headway.
1475 1491 coreconfigitem('worker', 'backgroundclosemaxqueue',
1476 1492 default=384,
1477 1493 )
1478 1494 coreconfigitem('worker', 'backgroundcloseminfilecount',
1479 1495 default=2048,
1480 1496 )
1481 1497 coreconfigitem('worker', 'backgroundclosethreadcount',
1482 1498 default=4,
1483 1499 )
1484 1500 coreconfigitem('worker', 'enabled',
1485 1501 default=True,
1486 1502 )
1487 1503 coreconfigitem('worker', 'numcpus',
1488 1504 default=None,
1489 1505 )
1490 1506
1491 1507 # Rebase related configuration moved to core because other extension are doing
1492 1508 # strange things. For example, shelve import the extensions to reuse some bit
1493 1509 # without formally loading it.
1494 1510 coreconfigitem('commands', 'rebase.requiredest',
1495 1511 default=False,
1496 1512 )
1497 1513 coreconfigitem('experimental', 'rebaseskipobsolete',
1498 1514 default=True,
1499 1515 )
1500 1516 coreconfigitem('rebase', 'singletransaction',
1501 1517 default=False,
1502 1518 )
1503 1519 coreconfigitem('rebase', 'experimental.inmemory',
1504 1520 default=False,
1505 1521 )
General Comments 0
You need to be logged in to leave comments. Login now