##// END OF EJS Templates
revlog: use a "radix" to address revlog...
marmoute -
r47921:8d3c2f9d default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -1,55 +1,60 b''
1 1 #!/usr/bin/env python3
2 2 # Dump revlogs as raw data stream
3 3 # $ find .hg/store/ -name "*.i" | xargs dumprevlog > repo.dump
4 4
5 5 from __future__ import absolute_import, print_function
6 6
7 7 import sys
8 8 from mercurial.node import hex
9 9 from mercurial import (
10 10 encoding,
11 11 pycompat,
12 12 revlog,
13 13 )
14 14 from mercurial.utils import procutil
15 15
16 16 from mercurial.revlogutils import (
17 17 constants as revlog_constants,
18 18 )
19 19
20 20 for fp in (sys.stdin, sys.stdout, sys.stderr):
21 21 procutil.setbinary(fp)
22 22
23 23
24 24 def binopen(path, mode=b'rb'):
25 25 if b'b' not in mode:
26 26 mode = mode + b'b'
27 27 return open(path, pycompat.sysstr(mode))
28 28
29 29
30 30 binopen.options = {}
31 31
32 32
33 33 def printb(data, end=b'\n'):
34 34 sys.stdout.flush()
35 35 procutil.stdout.write(data + end)
36 36
37 37
38 38 for f in sys.argv[1:]:
39 localf = encoding.strtolocal(f)
40 if not localf.endswith(b'.i'):
41 print("file:", f, file=sys.stderr)
42 print(" invalida filename", file=sys.stderr)
43
39 44 r = revlog.revlog(
40 45 binopen,
41 46 target=(revlog_constants.KIND_OTHER, b'dump-revlog'),
42 indexfile=encoding.strtolocal(f),
47 radix=localf[:-2],
43 48 )
44 49 print("file:", f)
45 50 for i in r:
46 51 n = r.node(i)
47 52 p = r.parents(n)
48 53 d = r.revision(n)
49 54 printb(b"node: %s" % hex(n))
50 55 printb(b"linkrev: %d" % r.linkrev(i))
51 56 printb(b"parents: %s %s" % (hex(p[0]), hex(p[1])))
52 57 printb(b"length: %d" % len(d))
53 58 printb(b"-start-")
54 59 printb(d)
55 60 printb(b"-end-")
@@ -1,3959 +1,3971 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
70 70 import mercurial.revlog
71 71 from mercurial import (
72 72 changegroup,
73 73 cmdutil,
74 74 commands,
75 75 copies,
76 76 error,
77 77 extensions,
78 78 hg,
79 79 mdiff,
80 80 merge,
81 81 util,
82 82 )
83 83
84 84 # for "historical portability":
85 85 # try to import modules separately (in dict order), and ignore
86 86 # failure, because these aren't available with early Mercurial
87 87 try:
88 88 from mercurial import branchmap # since 2.5 (or bcee63733aad)
89 89 except ImportError:
90 90 pass
91 91 try:
92 92 from mercurial import obsolete # since 2.3 (or ad0d6c2b3279)
93 93 except ImportError:
94 94 pass
95 95 try:
96 96 from mercurial import registrar # since 3.7 (or 37d50250b696)
97 97
98 98 dir(registrar) # forcibly load it
99 99 except ImportError:
100 100 registrar = None
101 101 try:
102 102 from mercurial import repoview # since 2.5 (or 3a6ddacb7198)
103 103 except ImportError:
104 104 pass
105 105 try:
106 106 from mercurial.utils import repoviewutil # since 5.0
107 107 except ImportError:
108 108 repoviewutil = None
109 109 try:
110 110 from mercurial import scmutil # since 1.9 (or 8b252e826c68)
111 111 except ImportError:
112 112 pass
113 113 try:
114 114 from mercurial import setdiscovery # since 1.9 (or cb98fed52495)
115 115 except ImportError:
116 116 pass
117 117
118 118 try:
119 119 from mercurial import profiling
120 120 except ImportError:
121 121 profiling = None
122 122
123 123 try:
124 124 from mercurial.revlogutils import constants as revlog_constants
125 125
126 126 perf_rl_kind = (revlog_constants.KIND_OTHER, b'created-by-perf')
127 127
128 128 def revlog(opener, *args, **kwargs):
129 129 return mercurial.revlog.revlog(opener, perf_rl_kind, *args, **kwargs)
130 130
131 131
132 132 except (ImportError, AttributeError):
133 133 perf_rl_kind = None
134 134
135 135 def revlog(opener, *args, **kwargs):
136 136 return mercurial.revlog.revlog(opener, *args, **kwargs)
137 137
138 138
139 139 def identity(a):
140 140 return a
141 141
142 142
143 143 try:
144 144 from mercurial import pycompat
145 145
146 146 getargspec = pycompat.getargspec # added to module after 4.5
147 147 _byteskwargs = pycompat.byteskwargs # since 4.1 (or fbc3f73dc802)
148 148 _sysstr = pycompat.sysstr # since 4.0 (or 2219f4f82ede)
149 149 _bytestr = pycompat.bytestr # since 4.2 (or b70407bd84d5)
150 150 _xrange = pycompat.xrange # since 4.8 (or 7eba8f83129b)
151 151 fsencode = pycompat.fsencode # since 3.9 (or f4a5e0e86a7e)
152 152 if pycompat.ispy3:
153 153 _maxint = sys.maxsize # per py3 docs for replacing maxint
154 154 else:
155 155 _maxint = sys.maxint
156 156 except (NameError, ImportError, AttributeError):
157 157 import inspect
158 158
159 159 getargspec = inspect.getargspec
160 160 _byteskwargs = identity
161 161 _bytestr = str
162 162 fsencode = identity # no py3 support
163 163 _maxint = sys.maxint # no py3 support
164 164 _sysstr = lambda x: x # no py3 support
165 165 _xrange = xrange
166 166
167 167 try:
168 168 # 4.7+
169 169 queue = pycompat.queue.Queue
170 170 except (NameError, AttributeError, ImportError):
171 171 # <4.7.
172 172 try:
173 173 queue = pycompat.queue
174 174 except (NameError, AttributeError, ImportError):
175 175 import Queue as queue
176 176
177 177 try:
178 178 from mercurial import logcmdutil
179 179
180 180 makelogtemplater = logcmdutil.maketemplater
181 181 except (AttributeError, ImportError):
182 182 try:
183 183 makelogtemplater = cmdutil.makelogtemplater
184 184 except (AttributeError, ImportError):
185 185 makelogtemplater = None
186 186
187 187 # for "historical portability":
188 188 # define util.safehasattr forcibly, because util.safehasattr has been
189 189 # available since 1.9.3 (or 94b200a11cf7)
190 190 _undefined = object()
191 191
192 192
193 193 def safehasattr(thing, attr):
194 194 return getattr(thing, _sysstr(attr), _undefined) is not _undefined
195 195
196 196
197 197 setattr(util, 'safehasattr', safehasattr)
198 198
199 199 # for "historical portability":
200 200 # define util.timer forcibly, because util.timer has been available
201 201 # since ae5d60bb70c9
202 202 if safehasattr(time, 'perf_counter'):
203 203 util.timer = time.perf_counter
204 204 elif os.name == b'nt':
205 205 util.timer = time.clock
206 206 else:
207 207 util.timer = time.time
208 208
209 209 # for "historical portability":
210 210 # use locally defined empty option list, if formatteropts isn't
211 211 # available, because commands.formatteropts has been available since
212 212 # 3.2 (or 7a7eed5176a4), even though formatting itself has been
213 213 # available since 2.2 (or ae5f92e154d3)
214 214 formatteropts = getattr(
215 215 cmdutil, "formatteropts", getattr(commands, "formatteropts", [])
216 216 )
217 217
218 218 # for "historical portability":
219 219 # use locally defined option list, if debugrevlogopts isn't available,
220 220 # because commands.debugrevlogopts has been available since 3.7 (or
221 221 # 5606f7d0d063), even though cmdutil.openrevlog() has been available
222 222 # since 1.9 (or a79fea6b3e77).
223 223 revlogopts = getattr(
224 224 cmdutil,
225 225 "debugrevlogopts",
226 226 getattr(
227 227 commands,
228 228 "debugrevlogopts",
229 229 [
230 230 (b'c', b'changelog', False, b'open changelog'),
231 231 (b'm', b'manifest', False, b'open manifest'),
232 232 (b'', b'dir', False, b'open directory manifest'),
233 233 ],
234 234 ),
235 235 )
236 236
237 237 cmdtable = {}
238 238
239 239 # for "historical portability":
240 240 # define parsealiases locally, because cmdutil.parsealiases has been
241 241 # available since 1.5 (or 6252852b4332)
242 242 def parsealiases(cmd):
243 243 return cmd.split(b"|")
244 244
245 245
246 246 if safehasattr(registrar, 'command'):
247 247 command = registrar.command(cmdtable)
248 248 elif safehasattr(cmdutil, 'command'):
249 249 command = cmdutil.command(cmdtable)
250 250 if 'norepo' not in getargspec(command).args:
251 251 # for "historical portability":
252 252 # wrap original cmdutil.command, because "norepo" option has
253 253 # been available since 3.1 (or 75a96326cecb)
254 254 _command = command
255 255
256 256 def command(name, options=(), synopsis=None, norepo=False):
257 257 if norepo:
258 258 commands.norepo += b' %s' % b' '.join(parsealiases(name))
259 259 return _command(name, list(options), synopsis)
260 260
261 261
262 262 else:
263 263 # for "historical portability":
264 264 # define "@command" annotation locally, because cmdutil.command
265 265 # has been available since 1.9 (or 2daa5179e73f)
266 266 def command(name, options=(), synopsis=None, norepo=False):
267 267 def decorator(func):
268 268 if synopsis:
269 269 cmdtable[name] = func, list(options), synopsis
270 270 else:
271 271 cmdtable[name] = func, list(options)
272 272 if norepo:
273 273 commands.norepo += b' %s' % b' '.join(parsealiases(name))
274 274 return func
275 275
276 276 return decorator
277 277
278 278
279 279 try:
280 280 import mercurial.registrar
281 281 import mercurial.configitems
282 282
283 283 configtable = {}
284 284 configitem = mercurial.registrar.configitem(configtable)
285 285 configitem(
286 286 b'perf',
287 287 b'presleep',
288 288 default=mercurial.configitems.dynamicdefault,
289 289 experimental=True,
290 290 )
291 291 configitem(
292 292 b'perf',
293 293 b'stub',
294 294 default=mercurial.configitems.dynamicdefault,
295 295 experimental=True,
296 296 )
297 297 configitem(
298 298 b'perf',
299 299 b'parentscount',
300 300 default=mercurial.configitems.dynamicdefault,
301 301 experimental=True,
302 302 )
303 303 configitem(
304 304 b'perf',
305 305 b'all-timing',
306 306 default=mercurial.configitems.dynamicdefault,
307 307 experimental=True,
308 308 )
309 309 configitem(
310 310 b'perf',
311 311 b'pre-run',
312 312 default=mercurial.configitems.dynamicdefault,
313 313 )
314 314 configitem(
315 315 b'perf',
316 316 b'profile-benchmark',
317 317 default=mercurial.configitems.dynamicdefault,
318 318 )
319 319 configitem(
320 320 b'perf',
321 321 b'run-limits',
322 322 default=mercurial.configitems.dynamicdefault,
323 323 experimental=True,
324 324 )
325 325 except (ImportError, AttributeError):
326 326 pass
327 327 except TypeError:
328 328 # compatibility fix for a11fd395e83f
329 329 # hg version: 5.2
330 330 configitem(
331 331 b'perf',
332 332 b'presleep',
333 333 default=mercurial.configitems.dynamicdefault,
334 334 )
335 335 configitem(
336 336 b'perf',
337 337 b'stub',
338 338 default=mercurial.configitems.dynamicdefault,
339 339 )
340 340 configitem(
341 341 b'perf',
342 342 b'parentscount',
343 343 default=mercurial.configitems.dynamicdefault,
344 344 )
345 345 configitem(
346 346 b'perf',
347 347 b'all-timing',
348 348 default=mercurial.configitems.dynamicdefault,
349 349 )
350 350 configitem(
351 351 b'perf',
352 352 b'pre-run',
353 353 default=mercurial.configitems.dynamicdefault,
354 354 )
355 355 configitem(
356 356 b'perf',
357 357 b'profile-benchmark',
358 358 default=mercurial.configitems.dynamicdefault,
359 359 )
360 360 configitem(
361 361 b'perf',
362 362 b'run-limits',
363 363 default=mercurial.configitems.dynamicdefault,
364 364 )
365 365
366 366
367 367 def getlen(ui):
368 368 if ui.configbool(b"perf", b"stub", False):
369 369 return lambda x: 1
370 370 return len
371 371
372 372
373 373 class noop(object):
374 374 """dummy context manager"""
375 375
376 376 def __enter__(self):
377 377 pass
378 378
379 379 def __exit__(self, *args):
380 380 pass
381 381
382 382
383 383 NOOPCTX = noop()
384 384
385 385
386 386 def gettimer(ui, opts=None):
387 387 """return a timer function and formatter: (timer, formatter)
388 388
389 389 This function exists to gather the creation of formatter in a single
390 390 place instead of duplicating it in all performance commands."""
391 391
392 392 # enforce an idle period before execution to counteract power management
393 393 # experimental config: perf.presleep
394 394 time.sleep(getint(ui, b"perf", b"presleep", 1))
395 395
396 396 if opts is None:
397 397 opts = {}
398 398 # redirect all to stderr unless buffer api is in use
399 399 if not ui._buffers:
400 400 ui = ui.copy()
401 401 uifout = safeattrsetter(ui, b'fout', ignoremissing=True)
402 402 if uifout:
403 403 # for "historical portability":
404 404 # ui.fout/ferr have been available since 1.9 (or 4e1ccd4c2b6d)
405 405 uifout.set(ui.ferr)
406 406
407 407 # get a formatter
408 408 uiformatter = getattr(ui, 'formatter', None)
409 409 if uiformatter:
410 410 fm = uiformatter(b'perf', opts)
411 411 else:
412 412 # for "historical portability":
413 413 # define formatter locally, because ui.formatter has been
414 414 # available since 2.2 (or ae5f92e154d3)
415 415 from mercurial import node
416 416
417 417 class defaultformatter(object):
418 418 """Minimized composition of baseformatter and plainformatter"""
419 419
420 420 def __init__(self, ui, topic, opts):
421 421 self._ui = ui
422 422 if ui.debugflag:
423 423 self.hexfunc = node.hex
424 424 else:
425 425 self.hexfunc = node.short
426 426
427 427 def __nonzero__(self):
428 428 return False
429 429
430 430 __bool__ = __nonzero__
431 431
432 432 def startitem(self):
433 433 pass
434 434
435 435 def data(self, **data):
436 436 pass
437 437
438 438 def write(self, fields, deftext, *fielddata, **opts):
439 439 self._ui.write(deftext % fielddata, **opts)
440 440
441 441 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
442 442 if cond:
443 443 self._ui.write(deftext % fielddata, **opts)
444 444
445 445 def plain(self, text, **opts):
446 446 self._ui.write(text, **opts)
447 447
448 448 def end(self):
449 449 pass
450 450
451 451 fm = defaultformatter(ui, b'perf', opts)
452 452
453 453 # stub function, runs code only once instead of in a loop
454 454 # experimental config: perf.stub
455 455 if ui.configbool(b"perf", b"stub", False):
456 456 return functools.partial(stub_timer, fm), fm
457 457
458 458 # experimental config: perf.all-timing
459 459 displayall = ui.configbool(b"perf", b"all-timing", False)
460 460
461 461 # experimental config: perf.run-limits
462 462 limitspec = ui.configlist(b"perf", b"run-limits", [])
463 463 limits = []
464 464 for item in limitspec:
465 465 parts = item.split(b'-', 1)
466 466 if len(parts) < 2:
467 467 ui.warn((b'malformatted run limit entry, missing "-": %s\n' % item))
468 468 continue
469 469 try:
470 470 time_limit = float(_sysstr(parts[0]))
471 471 except ValueError as e:
472 472 ui.warn(
473 473 (
474 474 b'malformatted run limit entry, %s: %s\n'
475 475 % (_bytestr(e), item)
476 476 )
477 477 )
478 478 continue
479 479 try:
480 480 run_limit = int(_sysstr(parts[1]))
481 481 except ValueError as e:
482 482 ui.warn(
483 483 (
484 484 b'malformatted run limit entry, %s: %s\n'
485 485 % (_bytestr(e), item)
486 486 )
487 487 )
488 488 continue
489 489 limits.append((time_limit, run_limit))
490 490 if not limits:
491 491 limits = DEFAULTLIMITS
492 492
493 493 profiler = None
494 494 if profiling is not None:
495 495 if ui.configbool(b"perf", b"profile-benchmark", False):
496 496 profiler = profiling.profile(ui)
497 497
498 498 prerun = getint(ui, b"perf", b"pre-run", 0)
499 499 t = functools.partial(
500 500 _timer,
501 501 fm,
502 502 displayall=displayall,
503 503 limits=limits,
504 504 prerun=prerun,
505 505 profiler=profiler,
506 506 )
507 507 return t, fm
508 508
509 509
510 510 def stub_timer(fm, func, setup=None, title=None):
511 511 if setup is not None:
512 512 setup()
513 513 func()
514 514
515 515
516 516 @contextlib.contextmanager
517 517 def timeone():
518 518 r = []
519 519 ostart = os.times()
520 520 cstart = util.timer()
521 521 yield r
522 522 cstop = util.timer()
523 523 ostop = os.times()
524 524 a, b = ostart, ostop
525 525 r.append((cstop - cstart, b[0] - a[0], b[1] - a[1]))
526 526
527 527
528 528 # list of stop condition (elapsed time, minimal run count)
529 529 DEFAULTLIMITS = (
530 530 (3.0, 100),
531 531 (10.0, 3),
532 532 )
533 533
534 534
535 535 def _timer(
536 536 fm,
537 537 func,
538 538 setup=None,
539 539 title=None,
540 540 displayall=False,
541 541 limits=DEFAULTLIMITS,
542 542 prerun=0,
543 543 profiler=None,
544 544 ):
545 545 gc.collect()
546 546 results = []
547 547 begin = util.timer()
548 548 count = 0
549 549 if profiler is None:
550 550 profiler = NOOPCTX
551 551 for i in range(prerun):
552 552 if setup is not None:
553 553 setup()
554 554 func()
555 555 keepgoing = True
556 556 while keepgoing:
557 557 if setup is not None:
558 558 setup()
559 559 with profiler:
560 560 with timeone() as item:
561 561 r = func()
562 562 profiler = NOOPCTX
563 563 count += 1
564 564 results.append(item[0])
565 565 cstop = util.timer()
566 566 # Look for a stop condition.
567 567 elapsed = cstop - begin
568 568 for t, mincount in limits:
569 569 if elapsed >= t and count >= mincount:
570 570 keepgoing = False
571 571 break
572 572
573 573 formatone(fm, results, title=title, result=r, displayall=displayall)
574 574
575 575
576 576 def formatone(fm, timings, title=None, result=None, displayall=False):
577 577
578 578 count = len(timings)
579 579
580 580 fm.startitem()
581 581
582 582 if title:
583 583 fm.write(b'title', b'! %s\n', title)
584 584 if result:
585 585 fm.write(b'result', b'! result: %s\n', result)
586 586
587 587 def display(role, entry):
588 588 prefix = b''
589 589 if role != b'best':
590 590 prefix = b'%s.' % role
591 591 fm.plain(b'!')
592 592 fm.write(prefix + b'wall', b' wall %f', entry[0])
593 593 fm.write(prefix + b'comb', b' comb %f', entry[1] + entry[2])
594 594 fm.write(prefix + b'user', b' user %f', entry[1])
595 595 fm.write(prefix + b'sys', b' sys %f', entry[2])
596 596 fm.write(prefix + b'count', b' (%s of %%d)' % role, count)
597 597 fm.plain(b'\n')
598 598
599 599 timings.sort()
600 600 min_val = timings[0]
601 601 display(b'best', min_val)
602 602 if displayall:
603 603 max_val = timings[-1]
604 604 display(b'max', max_val)
605 605 avg = tuple([sum(x) / count for x in zip(*timings)])
606 606 display(b'avg', avg)
607 607 median = timings[len(timings) // 2]
608 608 display(b'median', median)
609 609
610 610
611 611 # utilities for historical portability
612 612
613 613
614 614 def getint(ui, section, name, default):
615 615 # for "historical portability":
616 616 # ui.configint has been available since 1.9 (or fa2b596db182)
617 617 v = ui.config(section, name, None)
618 618 if v is None:
619 619 return default
620 620 try:
621 621 return int(v)
622 622 except ValueError:
623 623 raise error.ConfigError(
624 624 b"%s.%s is not an integer ('%s')" % (section, name, v)
625 625 )
626 626
627 627
628 628 def safeattrsetter(obj, name, ignoremissing=False):
629 629 """Ensure that 'obj' has 'name' attribute before subsequent setattr
630 630
631 631 This function is aborted, if 'obj' doesn't have 'name' attribute
632 632 at runtime. This avoids overlooking removal of an attribute, which
633 633 breaks assumption of performance measurement, in the future.
634 634
635 635 This function returns the object to (1) assign a new value, and
636 636 (2) restore an original value to the attribute.
637 637
638 638 If 'ignoremissing' is true, missing 'name' attribute doesn't cause
639 639 abortion, and this function returns None. This is useful to
640 640 examine an attribute, which isn't ensured in all Mercurial
641 641 versions.
642 642 """
643 643 if not util.safehasattr(obj, name):
644 644 if ignoremissing:
645 645 return None
646 646 raise error.Abort(
647 647 (
648 648 b"missing attribute %s of %s might break assumption"
649 649 b" of performance measurement"
650 650 )
651 651 % (name, obj)
652 652 )
653 653
654 654 origvalue = getattr(obj, _sysstr(name))
655 655
656 656 class attrutil(object):
657 657 def set(self, newvalue):
658 658 setattr(obj, _sysstr(name), newvalue)
659 659
660 660 def restore(self):
661 661 setattr(obj, _sysstr(name), origvalue)
662 662
663 663 return attrutil()
664 664
665 665
666 666 # utilities to examine each internal API changes
667 667
668 668
669 669 def getbranchmapsubsettable():
670 670 # for "historical portability":
671 671 # subsettable is defined in:
672 672 # - branchmap since 2.9 (or 175c6fd8cacc)
673 673 # - repoview since 2.5 (or 59a9f18d4587)
674 674 # - repoviewutil since 5.0
675 675 for mod in (branchmap, repoview, repoviewutil):
676 676 subsettable = getattr(mod, 'subsettable', None)
677 677 if subsettable:
678 678 return subsettable
679 679
680 680 # bisecting in bcee63733aad::59a9f18d4587 can reach here (both
681 681 # branchmap and repoview modules exist, but subsettable attribute
682 682 # doesn't)
683 683 raise error.Abort(
684 684 b"perfbranchmap not available with this Mercurial",
685 685 hint=b"use 2.5 or later",
686 686 )
687 687
688 688
689 689 def getsvfs(repo):
690 690 """Return appropriate object to access files under .hg/store"""
691 691 # for "historical portability":
692 692 # repo.svfs has been available since 2.3 (or 7034365089bf)
693 693 svfs = getattr(repo, 'svfs', None)
694 694 if svfs:
695 695 return svfs
696 696 else:
697 697 return getattr(repo, 'sopener')
698 698
699 699
700 700 def getvfs(repo):
701 701 """Return appropriate object to access files under .hg"""
702 702 # for "historical portability":
703 703 # repo.vfs has been available since 2.3 (or 7034365089bf)
704 704 vfs = getattr(repo, 'vfs', None)
705 705 if vfs:
706 706 return vfs
707 707 else:
708 708 return getattr(repo, 'opener')
709 709
710 710
711 711 def repocleartagscachefunc(repo):
712 712 """Return the function to clear tags cache according to repo internal API"""
713 713 if util.safehasattr(repo, b'_tagscache'): # since 2.0 (or 9dca7653b525)
714 714 # in this case, setattr(repo, '_tagscache', None) or so isn't
715 715 # correct way to clear tags cache, because existing code paths
716 716 # expect _tagscache to be a structured object.
717 717 def clearcache():
718 718 # _tagscache has been filteredpropertycache since 2.5 (or
719 719 # 98c867ac1330), and delattr() can't work in such case
720 720 if '_tagscache' in vars(repo):
721 721 del repo.__dict__['_tagscache']
722 722
723 723 return clearcache
724 724
725 725 repotags = safeattrsetter(repo, b'_tags', ignoremissing=True)
726 726 if repotags: # since 1.4 (or 5614a628d173)
727 727 return lambda: repotags.set(None)
728 728
729 729 repotagscache = safeattrsetter(repo, b'tagscache', ignoremissing=True)
730 730 if repotagscache: # since 0.6 (or d7df759d0e97)
731 731 return lambda: repotagscache.set(None)
732 732
733 733 # Mercurial earlier than 0.6 (or d7df759d0e97) logically reaches
734 734 # this point, but it isn't so problematic, because:
735 735 # - repo.tags of such Mercurial isn't "callable", and repo.tags()
736 736 # in perftags() causes failure soon
737 737 # - perf.py itself has been available since 1.1 (or eb240755386d)
738 738 raise error.Abort(b"tags API of this hg command is unknown")
739 739
740 740
741 741 # utilities to clear cache
742 742
743 743
744 744 def clearfilecache(obj, attrname):
745 745 unfiltered = getattr(obj, 'unfiltered', None)
746 746 if unfiltered is not None:
747 747 obj = obj.unfiltered()
748 748 if attrname in vars(obj):
749 749 delattr(obj, attrname)
750 750 obj._filecache.pop(attrname, None)
751 751
752 752
753 753 def clearchangelog(repo):
754 754 if repo is not repo.unfiltered():
755 755 object.__setattr__(repo, '_clcachekey', None)
756 756 object.__setattr__(repo, '_clcache', None)
757 757 clearfilecache(repo.unfiltered(), 'changelog')
758 758
759 759
760 760 # perf commands
761 761
762 762
763 763 @command(b'perf::walk|perfwalk', formatteropts)
764 764 def perfwalk(ui, repo, *pats, **opts):
765 765 opts = _byteskwargs(opts)
766 766 timer, fm = gettimer(ui, opts)
767 767 m = scmutil.match(repo[None], pats, {})
768 768 timer(
769 769 lambda: len(
770 770 list(
771 771 repo.dirstate.walk(m, subrepos=[], unknown=True, ignored=False)
772 772 )
773 773 )
774 774 )
775 775 fm.end()
776 776
777 777
778 778 @command(b'perf::annotate|perfannotate', formatteropts)
779 779 def perfannotate(ui, repo, f, **opts):
780 780 opts = _byteskwargs(opts)
781 781 timer, fm = gettimer(ui, opts)
782 782 fc = repo[b'.'][f]
783 783 timer(lambda: len(fc.annotate(True)))
784 784 fm.end()
785 785
786 786
787 787 @command(
788 788 b'perf::status|perfstatus',
789 789 [
790 790 (b'u', b'unknown', False, b'ask status to look for unknown files'),
791 791 (b'', b'dirstate', False, b'benchmark the internal dirstate call'),
792 792 ]
793 793 + formatteropts,
794 794 )
795 795 def perfstatus(ui, repo, **opts):
796 796 """benchmark the performance of a single status call
797 797
798 798 The repository data are preserved between each call.
799 799
800 800 By default, only the status of the tracked file are requested. If
801 801 `--unknown` is passed, the "unknown" files are also tracked.
802 802 """
803 803 opts = _byteskwargs(opts)
804 804 # m = match.always(repo.root, repo.getcwd())
805 805 # timer(lambda: sum(map(len, repo.dirstate.status(m, [], False, False,
806 806 # False))))
807 807 timer, fm = gettimer(ui, opts)
808 808 if opts[b'dirstate']:
809 809 dirstate = repo.dirstate
810 810 m = scmutil.matchall(repo)
811 811 unknown = opts[b'unknown']
812 812
813 813 def status_dirstate():
814 814 s = dirstate.status(
815 815 m, subrepos=[], ignored=False, clean=False, unknown=unknown
816 816 )
817 817 sum(map(bool, s))
818 818
819 819 timer(status_dirstate)
820 820 else:
821 821 timer(lambda: sum(map(len, repo.status(unknown=opts[b'unknown']))))
822 822 fm.end()
823 823
824 824
825 825 @command(b'perf::addremove|perfaddremove', formatteropts)
826 826 def perfaddremove(ui, repo, **opts):
827 827 opts = _byteskwargs(opts)
828 828 timer, fm = gettimer(ui, opts)
829 829 try:
830 830 oldquiet = repo.ui.quiet
831 831 repo.ui.quiet = True
832 832 matcher = scmutil.match(repo[None])
833 833 opts[b'dry_run'] = True
834 834 if 'uipathfn' in getargspec(scmutil.addremove).args:
835 835 uipathfn = scmutil.getuipathfn(repo)
836 836 timer(lambda: scmutil.addremove(repo, matcher, b"", uipathfn, opts))
837 837 else:
838 838 timer(lambda: scmutil.addremove(repo, matcher, b"", opts))
839 839 finally:
840 840 repo.ui.quiet = oldquiet
841 841 fm.end()
842 842
843 843
844 844 def clearcaches(cl):
845 845 # behave somewhat consistently across internal API changes
846 846 if util.safehasattr(cl, b'clearcaches'):
847 847 cl.clearcaches()
848 848 elif util.safehasattr(cl, b'_nodecache'):
849 849 # <= hg-5.2
850 850 from mercurial.node import nullid, nullrev
851 851
852 852 cl._nodecache = {nullid: nullrev}
853 853 cl._nodepos = None
854 854
855 855
856 856 @command(b'perf::heads|perfheads', formatteropts)
857 857 def perfheads(ui, repo, **opts):
858 858 """benchmark the computation of a changelog heads"""
859 859 opts = _byteskwargs(opts)
860 860 timer, fm = gettimer(ui, opts)
861 861 cl = repo.changelog
862 862
863 863 def s():
864 864 clearcaches(cl)
865 865
866 866 def d():
867 867 len(cl.headrevs())
868 868
869 869 timer(d, setup=s)
870 870 fm.end()
871 871
872 872
873 873 @command(
874 874 b'perf::tags|perftags',
875 875 formatteropts
876 876 + [
877 877 (b'', b'clear-revlogs', False, b'refresh changelog and manifest'),
878 878 ],
879 879 )
880 880 def perftags(ui, repo, **opts):
881 881 opts = _byteskwargs(opts)
882 882 timer, fm = gettimer(ui, opts)
883 883 repocleartagscache = repocleartagscachefunc(repo)
884 884 clearrevlogs = opts[b'clear_revlogs']
885 885
886 886 def s():
887 887 if clearrevlogs:
888 888 clearchangelog(repo)
889 889 clearfilecache(repo.unfiltered(), 'manifest')
890 890 repocleartagscache()
891 891
892 892 def t():
893 893 return len(repo.tags())
894 894
895 895 timer(t, setup=s)
896 896 fm.end()
897 897
898 898
899 899 @command(b'perf::ancestors|perfancestors', formatteropts)
900 900 def perfancestors(ui, repo, **opts):
901 901 opts = _byteskwargs(opts)
902 902 timer, fm = gettimer(ui, opts)
903 903 heads = repo.changelog.headrevs()
904 904
905 905 def d():
906 906 for a in repo.changelog.ancestors(heads):
907 907 pass
908 908
909 909 timer(d)
910 910 fm.end()
911 911
912 912
913 913 @command(b'perf::ancestorset|perfancestorset', formatteropts)
914 914 def perfancestorset(ui, repo, revset, **opts):
915 915 opts = _byteskwargs(opts)
916 916 timer, fm = gettimer(ui, opts)
917 917 revs = repo.revs(revset)
918 918 heads = repo.changelog.headrevs()
919 919
920 920 def d():
921 921 s = repo.changelog.ancestors(heads)
922 922 for rev in revs:
923 923 rev in s
924 924
925 925 timer(d)
926 926 fm.end()
927 927
928 928
929 929 @command(b'perf::discovery|perfdiscovery', formatteropts, b'PATH')
930 930 def perfdiscovery(ui, repo, path, **opts):
931 931 """benchmark discovery between local repo and the peer at given path"""
932 932 repos = [repo, None]
933 933 timer, fm = gettimer(ui, opts)
934 934
935 935 try:
936 936 from mercurial.utils.urlutil import get_unique_pull_path
937 937
938 938 path = get_unique_pull_path(b'perfdiscovery', repo, ui, path)[0]
939 939 except ImportError:
940 940 path = ui.expandpath(path)
941 941
942 942 def s():
943 943 repos[1] = hg.peer(ui, opts, path)
944 944
945 945 def d():
946 946 setdiscovery.findcommonheads(ui, *repos)
947 947
948 948 timer(d, setup=s)
949 949 fm.end()
950 950
951 951
952 952 @command(
953 953 b'perf::bookmarks|perfbookmarks',
954 954 formatteropts
955 955 + [
956 956 (b'', b'clear-revlogs', False, b'refresh changelog and manifest'),
957 957 ],
958 958 )
959 959 def perfbookmarks(ui, repo, **opts):
960 960 """benchmark parsing bookmarks from disk to memory"""
961 961 opts = _byteskwargs(opts)
962 962 timer, fm = gettimer(ui, opts)
963 963
964 964 clearrevlogs = opts[b'clear_revlogs']
965 965
966 966 def s():
967 967 if clearrevlogs:
968 968 clearchangelog(repo)
969 969 clearfilecache(repo, b'_bookmarks')
970 970
971 971 def d():
972 972 repo._bookmarks
973 973
974 974 timer(d, setup=s)
975 975 fm.end()
976 976
977 977
978 978 @command(b'perf::bundleread|perfbundleread', formatteropts, b'BUNDLE')
979 979 def perfbundleread(ui, repo, bundlepath, **opts):
980 980 """Benchmark reading of bundle files.
981 981
982 982 This command is meant to isolate the I/O part of bundle reading as
983 983 much as possible.
984 984 """
985 985 from mercurial import (
986 986 bundle2,
987 987 exchange,
988 988 streamclone,
989 989 )
990 990
991 991 opts = _byteskwargs(opts)
992 992
993 993 def makebench(fn):
994 994 def run():
995 995 with open(bundlepath, b'rb') as fh:
996 996 bundle = exchange.readbundle(ui, fh, bundlepath)
997 997 fn(bundle)
998 998
999 999 return run
1000 1000
1001 1001 def makereadnbytes(size):
1002 1002 def run():
1003 1003 with open(bundlepath, b'rb') as fh:
1004 1004 bundle = exchange.readbundle(ui, fh, bundlepath)
1005 1005 while bundle.read(size):
1006 1006 pass
1007 1007
1008 1008 return run
1009 1009
1010 1010 def makestdioread(size):
1011 1011 def run():
1012 1012 with open(bundlepath, b'rb') as fh:
1013 1013 while fh.read(size):
1014 1014 pass
1015 1015
1016 1016 return run
1017 1017
1018 1018 # bundle1
1019 1019
1020 1020 def deltaiter(bundle):
1021 1021 for delta in bundle.deltaiter():
1022 1022 pass
1023 1023
1024 1024 def iterchunks(bundle):
1025 1025 for chunk in bundle.getchunks():
1026 1026 pass
1027 1027
1028 1028 # bundle2
1029 1029
1030 1030 def forwardchunks(bundle):
1031 1031 for chunk in bundle._forwardchunks():
1032 1032 pass
1033 1033
1034 1034 def iterparts(bundle):
1035 1035 for part in bundle.iterparts():
1036 1036 pass
1037 1037
1038 1038 def iterpartsseekable(bundle):
1039 1039 for part in bundle.iterparts(seekable=True):
1040 1040 pass
1041 1041
1042 1042 def seek(bundle):
1043 1043 for part in bundle.iterparts(seekable=True):
1044 1044 part.seek(0, os.SEEK_END)
1045 1045
1046 1046 def makepartreadnbytes(size):
1047 1047 def run():
1048 1048 with open(bundlepath, b'rb') as fh:
1049 1049 bundle = exchange.readbundle(ui, fh, bundlepath)
1050 1050 for part in bundle.iterparts():
1051 1051 while part.read(size):
1052 1052 pass
1053 1053
1054 1054 return run
1055 1055
1056 1056 benches = [
1057 1057 (makestdioread(8192), b'read(8k)'),
1058 1058 (makestdioread(16384), b'read(16k)'),
1059 1059 (makestdioread(32768), b'read(32k)'),
1060 1060 (makestdioread(131072), b'read(128k)'),
1061 1061 ]
1062 1062
1063 1063 with open(bundlepath, b'rb') as fh:
1064 1064 bundle = exchange.readbundle(ui, fh, bundlepath)
1065 1065
1066 1066 if isinstance(bundle, changegroup.cg1unpacker):
1067 1067 benches.extend(
1068 1068 [
1069 1069 (makebench(deltaiter), b'cg1 deltaiter()'),
1070 1070 (makebench(iterchunks), b'cg1 getchunks()'),
1071 1071 (makereadnbytes(8192), b'cg1 read(8k)'),
1072 1072 (makereadnbytes(16384), b'cg1 read(16k)'),
1073 1073 (makereadnbytes(32768), b'cg1 read(32k)'),
1074 1074 (makereadnbytes(131072), b'cg1 read(128k)'),
1075 1075 ]
1076 1076 )
1077 1077 elif isinstance(bundle, bundle2.unbundle20):
1078 1078 benches.extend(
1079 1079 [
1080 1080 (makebench(forwardchunks), b'bundle2 forwardchunks()'),
1081 1081 (makebench(iterparts), b'bundle2 iterparts()'),
1082 1082 (
1083 1083 makebench(iterpartsseekable),
1084 1084 b'bundle2 iterparts() seekable',
1085 1085 ),
1086 1086 (makebench(seek), b'bundle2 part seek()'),
1087 1087 (makepartreadnbytes(8192), b'bundle2 part read(8k)'),
1088 1088 (makepartreadnbytes(16384), b'bundle2 part read(16k)'),
1089 1089 (makepartreadnbytes(32768), b'bundle2 part read(32k)'),
1090 1090 (makepartreadnbytes(131072), b'bundle2 part read(128k)'),
1091 1091 ]
1092 1092 )
1093 1093 elif isinstance(bundle, streamclone.streamcloneapplier):
1094 1094 raise error.Abort(b'stream clone bundles not supported')
1095 1095 else:
1096 1096 raise error.Abort(b'unhandled bundle type: %s' % type(bundle))
1097 1097
1098 1098 for fn, title in benches:
1099 1099 timer, fm = gettimer(ui, opts)
1100 1100 timer(fn, title=title)
1101 1101 fm.end()
1102 1102
1103 1103
1104 1104 @command(
1105 1105 b'perf::changegroupchangelog|perfchangegroupchangelog',
1106 1106 formatteropts
1107 1107 + [
1108 1108 (b'', b'cgversion', b'02', b'changegroup version'),
1109 1109 (b'r', b'rev', b'', b'revisions to add to changegroup'),
1110 1110 ],
1111 1111 )
1112 1112 def perfchangegroupchangelog(ui, repo, cgversion=b'02', rev=None, **opts):
1113 1113 """Benchmark producing a changelog group for a changegroup.
1114 1114
1115 1115 This measures the time spent processing the changelog during a
1116 1116 bundle operation. This occurs during `hg bundle` and on a server
1117 1117 processing a `getbundle` wire protocol request (handles clones
1118 1118 and pull requests).
1119 1119
1120 1120 By default, all revisions are added to the changegroup.
1121 1121 """
1122 1122 opts = _byteskwargs(opts)
1123 1123 cl = repo.changelog
1124 1124 nodes = [cl.lookup(r) for r in repo.revs(rev or b'all()')]
1125 1125 bundler = changegroup.getbundler(cgversion, repo)
1126 1126
1127 1127 def d():
1128 1128 state, chunks = bundler._generatechangelog(cl, nodes)
1129 1129 for chunk in chunks:
1130 1130 pass
1131 1131
1132 1132 timer, fm = gettimer(ui, opts)
1133 1133
1134 1134 # Terminal printing can interfere with timing. So disable it.
1135 1135 with ui.configoverride({(b'progress', b'disable'): True}):
1136 1136 timer(d)
1137 1137
1138 1138 fm.end()
1139 1139
1140 1140
1141 1141 @command(b'perf::dirs|perfdirs', formatteropts)
1142 1142 def perfdirs(ui, repo, **opts):
1143 1143 opts = _byteskwargs(opts)
1144 1144 timer, fm = gettimer(ui, opts)
1145 1145 dirstate = repo.dirstate
1146 1146 b'a' in dirstate
1147 1147
1148 1148 def d():
1149 1149 dirstate.hasdir(b'a')
1150 1150 del dirstate._map._dirs
1151 1151
1152 1152 timer(d)
1153 1153 fm.end()
1154 1154
1155 1155
1156 1156 @command(
1157 1157 b'perf::dirstate|perfdirstate',
1158 1158 [
1159 1159 (
1160 1160 b'',
1161 1161 b'iteration',
1162 1162 None,
1163 1163 b'benchmark a full iteration for the dirstate',
1164 1164 ),
1165 1165 (
1166 1166 b'',
1167 1167 b'contains',
1168 1168 None,
1169 1169 b'benchmark a large amount of `nf in dirstate` calls',
1170 1170 ),
1171 1171 ]
1172 1172 + formatteropts,
1173 1173 )
1174 1174 def perfdirstate(ui, repo, **opts):
1175 1175 """benchmap the time of various distate operations
1176 1176
1177 1177 By default benchmark the time necessary to load a dirstate from scratch.
1178 1178 The dirstate is loaded to the point were a "contains" request can be
1179 1179 answered.
1180 1180 """
1181 1181 opts = _byteskwargs(opts)
1182 1182 timer, fm = gettimer(ui, opts)
1183 1183 b"a" in repo.dirstate
1184 1184
1185 1185 if opts[b'iteration'] and opts[b'contains']:
1186 1186 msg = b'only specify one of --iteration or --contains'
1187 1187 raise error.Abort(msg)
1188 1188
1189 1189 if opts[b'iteration']:
1190 1190 setup = None
1191 1191 dirstate = repo.dirstate
1192 1192
1193 1193 def d():
1194 1194 for f in dirstate:
1195 1195 pass
1196 1196
1197 1197 elif opts[b'contains']:
1198 1198 setup = None
1199 1199 dirstate = repo.dirstate
1200 1200 allfiles = list(dirstate)
1201 1201 # also add file path that will be "missing" from the dirstate
1202 1202 allfiles.extend([f[::-1] for f in allfiles])
1203 1203
1204 1204 def d():
1205 1205 for f in allfiles:
1206 1206 f in dirstate
1207 1207
1208 1208 else:
1209 1209
1210 1210 def setup():
1211 1211 repo.dirstate.invalidate()
1212 1212
1213 1213 def d():
1214 1214 b"a" in repo.dirstate
1215 1215
1216 1216 timer(d, setup=setup)
1217 1217 fm.end()
1218 1218
1219 1219
1220 1220 @command(b'perf::dirstatedirs|perfdirstatedirs', formatteropts)
1221 1221 def perfdirstatedirs(ui, repo, **opts):
1222 1222 """benchmap a 'dirstate.hasdir' call from an empty `dirs` cache"""
1223 1223 opts = _byteskwargs(opts)
1224 1224 timer, fm = gettimer(ui, opts)
1225 1225 repo.dirstate.hasdir(b"a")
1226 1226
1227 1227 def setup():
1228 1228 del repo.dirstate._map._dirs
1229 1229
1230 1230 def d():
1231 1231 repo.dirstate.hasdir(b"a")
1232 1232
1233 1233 timer(d, setup=setup)
1234 1234 fm.end()
1235 1235
1236 1236
1237 1237 @command(b'perf::dirstatefoldmap|perfdirstatefoldmap', formatteropts)
1238 1238 def perfdirstatefoldmap(ui, repo, **opts):
1239 1239 """benchmap a `dirstate._map.filefoldmap.get()` request
1240 1240
1241 1241 The dirstate filefoldmap cache is dropped between every request.
1242 1242 """
1243 1243 opts = _byteskwargs(opts)
1244 1244 timer, fm = gettimer(ui, opts)
1245 1245 dirstate = repo.dirstate
1246 1246 dirstate._map.filefoldmap.get(b'a')
1247 1247
1248 1248 def setup():
1249 1249 del dirstate._map.filefoldmap
1250 1250
1251 1251 def d():
1252 1252 dirstate._map.filefoldmap.get(b'a')
1253 1253
1254 1254 timer(d, setup=setup)
1255 1255 fm.end()
1256 1256
1257 1257
1258 1258 @command(b'perf::dirfoldmap|perfdirfoldmap', formatteropts)
1259 1259 def perfdirfoldmap(ui, repo, **opts):
1260 1260 """benchmap a `dirstate._map.dirfoldmap.get()` request
1261 1261
1262 1262 The dirstate dirfoldmap cache is dropped between every request.
1263 1263 """
1264 1264 opts = _byteskwargs(opts)
1265 1265 timer, fm = gettimer(ui, opts)
1266 1266 dirstate = repo.dirstate
1267 1267 dirstate._map.dirfoldmap.get(b'a')
1268 1268
1269 1269 def setup():
1270 1270 del dirstate._map.dirfoldmap
1271 1271 del dirstate._map._dirs
1272 1272
1273 1273 def d():
1274 1274 dirstate._map.dirfoldmap.get(b'a')
1275 1275
1276 1276 timer(d, setup=setup)
1277 1277 fm.end()
1278 1278
1279 1279
1280 1280 @command(b'perf::dirstatewrite|perfdirstatewrite', formatteropts)
1281 1281 def perfdirstatewrite(ui, repo, **opts):
1282 1282 """benchmap the time it take to write a dirstate on disk"""
1283 1283 opts = _byteskwargs(opts)
1284 1284 timer, fm = gettimer(ui, opts)
1285 1285 ds = repo.dirstate
1286 1286 b"a" in ds
1287 1287
1288 1288 def setup():
1289 1289 ds._dirty = True
1290 1290
1291 1291 def d():
1292 1292 ds.write(repo.currenttransaction())
1293 1293
1294 1294 timer(d, setup=setup)
1295 1295 fm.end()
1296 1296
1297 1297
1298 1298 def _getmergerevs(repo, opts):
1299 1299 """parse command argument to return rev involved in merge
1300 1300
1301 1301 input: options dictionnary with `rev`, `from` and `bse`
1302 1302 output: (localctx, otherctx, basectx)
1303 1303 """
1304 1304 if opts[b'from']:
1305 1305 fromrev = scmutil.revsingle(repo, opts[b'from'])
1306 1306 wctx = repo[fromrev]
1307 1307 else:
1308 1308 wctx = repo[None]
1309 1309 # we don't want working dir files to be stat'd in the benchmark, so
1310 1310 # prime that cache
1311 1311 wctx.dirty()
1312 1312 rctx = scmutil.revsingle(repo, opts[b'rev'], opts[b'rev'])
1313 1313 if opts[b'base']:
1314 1314 fromrev = scmutil.revsingle(repo, opts[b'base'])
1315 1315 ancestor = repo[fromrev]
1316 1316 else:
1317 1317 ancestor = wctx.ancestor(rctx)
1318 1318 return (wctx, rctx, ancestor)
1319 1319
1320 1320
1321 1321 @command(
1322 1322 b'perf::mergecalculate|perfmergecalculate',
1323 1323 [
1324 1324 (b'r', b'rev', b'.', b'rev to merge against'),
1325 1325 (b'', b'from', b'', b'rev to merge from'),
1326 1326 (b'', b'base', b'', b'the revision to use as base'),
1327 1327 ]
1328 1328 + formatteropts,
1329 1329 )
1330 1330 def perfmergecalculate(ui, repo, **opts):
1331 1331 opts = _byteskwargs(opts)
1332 1332 timer, fm = gettimer(ui, opts)
1333 1333
1334 1334 wctx, rctx, ancestor = _getmergerevs(repo, opts)
1335 1335
1336 1336 def d():
1337 1337 # acceptremote is True because we don't want prompts in the middle of
1338 1338 # our benchmark
1339 1339 merge.calculateupdates(
1340 1340 repo,
1341 1341 wctx,
1342 1342 rctx,
1343 1343 [ancestor],
1344 1344 branchmerge=False,
1345 1345 force=False,
1346 1346 acceptremote=True,
1347 1347 followcopies=True,
1348 1348 )
1349 1349
1350 1350 timer(d)
1351 1351 fm.end()
1352 1352
1353 1353
1354 1354 @command(
1355 1355 b'perf::mergecopies|perfmergecopies',
1356 1356 [
1357 1357 (b'r', b'rev', b'.', b'rev to merge against'),
1358 1358 (b'', b'from', b'', b'rev to merge from'),
1359 1359 (b'', b'base', b'', b'the revision to use as base'),
1360 1360 ]
1361 1361 + formatteropts,
1362 1362 )
1363 1363 def perfmergecopies(ui, repo, **opts):
1364 1364 """measure runtime of `copies.mergecopies`"""
1365 1365 opts = _byteskwargs(opts)
1366 1366 timer, fm = gettimer(ui, opts)
1367 1367 wctx, rctx, ancestor = _getmergerevs(repo, opts)
1368 1368
1369 1369 def d():
1370 1370 # acceptremote is True because we don't want prompts in the middle of
1371 1371 # our benchmark
1372 1372 copies.mergecopies(repo, wctx, rctx, ancestor)
1373 1373
1374 1374 timer(d)
1375 1375 fm.end()
1376 1376
1377 1377
1378 1378 @command(b'perf::pathcopies|perfpathcopies', [], b"REV REV")
1379 1379 def perfpathcopies(ui, repo, rev1, rev2, **opts):
1380 1380 """benchmark the copy tracing logic"""
1381 1381 opts = _byteskwargs(opts)
1382 1382 timer, fm = gettimer(ui, opts)
1383 1383 ctx1 = scmutil.revsingle(repo, rev1, rev1)
1384 1384 ctx2 = scmutil.revsingle(repo, rev2, rev2)
1385 1385
1386 1386 def d():
1387 1387 copies.pathcopies(ctx1, ctx2)
1388 1388
1389 1389 timer(d)
1390 1390 fm.end()
1391 1391
1392 1392
1393 1393 @command(
1394 1394 b'perf::phases|perfphases',
1395 1395 [
1396 1396 (b'', b'full', False, b'include file reading time too'),
1397 1397 ],
1398 1398 b"",
1399 1399 )
1400 1400 def perfphases(ui, repo, **opts):
1401 1401 """benchmark phasesets computation"""
1402 1402 opts = _byteskwargs(opts)
1403 1403 timer, fm = gettimer(ui, opts)
1404 1404 _phases = repo._phasecache
1405 1405 full = opts.get(b'full')
1406 1406
1407 1407 def d():
1408 1408 phases = _phases
1409 1409 if full:
1410 1410 clearfilecache(repo, b'_phasecache')
1411 1411 phases = repo._phasecache
1412 1412 phases.invalidate()
1413 1413 phases.loadphaserevs(repo)
1414 1414
1415 1415 timer(d)
1416 1416 fm.end()
1417 1417
1418 1418
1419 1419 @command(b'perf::phasesremote|perfphasesremote', [], b"[DEST]")
1420 1420 def perfphasesremote(ui, repo, dest=None, **opts):
1421 1421 """benchmark time needed to analyse phases of the remote server"""
1422 1422 from mercurial.node import bin
1423 1423 from mercurial import (
1424 1424 exchange,
1425 1425 hg,
1426 1426 phases,
1427 1427 )
1428 1428
1429 1429 opts = _byteskwargs(opts)
1430 1430 timer, fm = gettimer(ui, opts)
1431 1431
1432 1432 path = ui.getpath(dest, default=(b'default-push', b'default'))
1433 1433 if not path:
1434 1434 raise error.Abort(
1435 1435 b'default repository not configured!',
1436 1436 hint=b"see 'hg help config.paths'",
1437 1437 )
1438 1438 dest = path.pushloc or path.loc
1439 1439 ui.statusnoi18n(b'analysing phase of %s\n' % util.hidepassword(dest))
1440 1440 other = hg.peer(repo, opts, dest)
1441 1441
1442 1442 # easier to perform discovery through the operation
1443 1443 op = exchange.pushoperation(repo, other)
1444 1444 exchange._pushdiscoverychangeset(op)
1445 1445
1446 1446 remotesubset = op.fallbackheads
1447 1447
1448 1448 with other.commandexecutor() as e:
1449 1449 remotephases = e.callcommand(
1450 1450 b'listkeys', {b'namespace': b'phases'}
1451 1451 ).result()
1452 1452 del other
1453 1453 publishing = remotephases.get(b'publishing', False)
1454 1454 if publishing:
1455 1455 ui.statusnoi18n(b'publishing: yes\n')
1456 1456 else:
1457 1457 ui.statusnoi18n(b'publishing: no\n')
1458 1458
1459 1459 has_node = getattr(repo.changelog.index, 'has_node', None)
1460 1460 if has_node is None:
1461 1461 has_node = repo.changelog.nodemap.__contains__
1462 1462 nonpublishroots = 0
1463 1463 for nhex, phase in remotephases.iteritems():
1464 1464 if nhex == b'publishing': # ignore data related to publish option
1465 1465 continue
1466 1466 node = bin(nhex)
1467 1467 if has_node(node) and int(phase):
1468 1468 nonpublishroots += 1
1469 1469 ui.statusnoi18n(b'number of roots: %d\n' % len(remotephases))
1470 1470 ui.statusnoi18n(b'number of known non public roots: %d\n' % nonpublishroots)
1471 1471
1472 1472 def d():
1473 1473 phases.remotephasessummary(repo, remotesubset, remotephases)
1474 1474
1475 1475 timer(d)
1476 1476 fm.end()
1477 1477
1478 1478
1479 1479 @command(
1480 1480 b'perf::manifest|perfmanifest',
1481 1481 [
1482 1482 (b'm', b'manifest-rev', False, b'Look up a manifest node revision'),
1483 1483 (b'', b'clear-disk', False, b'clear on-disk caches too'),
1484 1484 ]
1485 1485 + formatteropts,
1486 1486 b'REV|NODE',
1487 1487 )
1488 1488 def perfmanifest(ui, repo, rev, manifest_rev=False, clear_disk=False, **opts):
1489 1489 """benchmark the time to read a manifest from disk and return a usable
1490 1490 dict-like object
1491 1491
1492 1492 Manifest caches are cleared before retrieval."""
1493 1493 opts = _byteskwargs(opts)
1494 1494 timer, fm = gettimer(ui, opts)
1495 1495 if not manifest_rev:
1496 1496 ctx = scmutil.revsingle(repo, rev, rev)
1497 1497 t = ctx.manifestnode()
1498 1498 else:
1499 1499 from mercurial.node import bin
1500 1500
1501 1501 if len(rev) == 40:
1502 1502 t = bin(rev)
1503 1503 else:
1504 1504 try:
1505 1505 rev = int(rev)
1506 1506
1507 1507 if util.safehasattr(repo.manifestlog, b'getstorage'):
1508 1508 t = repo.manifestlog.getstorage(b'').node(rev)
1509 1509 else:
1510 1510 t = repo.manifestlog._revlog.lookup(rev)
1511 1511 except ValueError:
1512 1512 raise error.Abort(
1513 1513 b'manifest revision must be integer or full node'
1514 1514 )
1515 1515
1516 1516 def d():
1517 1517 repo.manifestlog.clearcaches(clear_persisted_data=clear_disk)
1518 1518 repo.manifestlog[t].read()
1519 1519
1520 1520 timer(d)
1521 1521 fm.end()
1522 1522
1523 1523
1524 1524 @command(b'perf::changeset|perfchangeset', formatteropts)
1525 1525 def perfchangeset(ui, repo, rev, **opts):
1526 1526 opts = _byteskwargs(opts)
1527 1527 timer, fm = gettimer(ui, opts)
1528 1528 n = scmutil.revsingle(repo, rev).node()
1529 1529
1530 1530 def d():
1531 1531 repo.changelog.read(n)
1532 1532 # repo.changelog._cache = None
1533 1533
1534 1534 timer(d)
1535 1535 fm.end()
1536 1536
1537 1537
1538 1538 @command(b'perf::ignore|perfignore', formatteropts)
1539 1539 def perfignore(ui, repo, **opts):
1540 1540 """benchmark operation related to computing ignore"""
1541 1541 opts = _byteskwargs(opts)
1542 1542 timer, fm = gettimer(ui, opts)
1543 1543 dirstate = repo.dirstate
1544 1544
1545 1545 def setupone():
1546 1546 dirstate.invalidate()
1547 1547 clearfilecache(dirstate, b'_ignore')
1548 1548
1549 1549 def runone():
1550 1550 dirstate._ignore
1551 1551
1552 1552 timer(runone, setup=setupone, title=b"load")
1553 1553 fm.end()
1554 1554
1555 1555
1556 1556 @command(
1557 1557 b'perf::index|perfindex',
1558 1558 [
1559 1559 (b'', b'rev', [], b'revision to be looked up (default tip)'),
1560 1560 (b'', b'no-lookup', None, b'do not revision lookup post creation'),
1561 1561 ]
1562 1562 + formatteropts,
1563 1563 )
1564 1564 def perfindex(ui, repo, **opts):
1565 1565 """benchmark index creation time followed by a lookup
1566 1566
1567 1567 The default is to look `tip` up. Depending on the index implementation,
1568 1568 the revision looked up can matters. For example, an implementation
1569 1569 scanning the index will have a faster lookup time for `--rev tip` than for
1570 1570 `--rev 0`. The number of looked up revisions and their order can also
1571 1571 matters.
1572 1572
1573 1573 Example of useful set to test:
1574 1574
1575 1575 * tip
1576 1576 * 0
1577 1577 * -10:
1578 1578 * :10
1579 1579 * -10: + :10
1580 1580 * :10: + -10:
1581 1581 * -10000:
1582 1582 * -10000: + 0
1583 1583
1584 1584 It is not currently possible to check for lookup of a missing node. For
1585 1585 deeper lookup benchmarking, checkout the `perfnodemap` command."""
1586 1586 import mercurial.revlog
1587 1587
1588 1588 opts = _byteskwargs(opts)
1589 1589 timer, fm = gettimer(ui, opts)
1590 1590 mercurial.revlog._prereadsize = 2 ** 24 # disable lazy parser in old hg
1591 1591 if opts[b'no_lookup']:
1592 1592 if opts['rev']:
1593 1593 raise error.Abort('--no-lookup and --rev are mutually exclusive')
1594 1594 nodes = []
1595 1595 elif not opts[b'rev']:
1596 1596 nodes = [repo[b"tip"].node()]
1597 1597 else:
1598 1598 revs = scmutil.revrange(repo, opts[b'rev'])
1599 1599 cl = repo.changelog
1600 1600 nodes = [cl.node(r) for r in revs]
1601 1601
1602 1602 unfi = repo.unfiltered()
1603 1603 # find the filecache func directly
1604 1604 # This avoid polluting the benchmark with the filecache logic
1605 1605 makecl = unfi.__class__.changelog.func
1606 1606
1607 1607 def setup():
1608 1608 # probably not necessary, but for good measure
1609 1609 clearchangelog(unfi)
1610 1610
1611 1611 def d():
1612 1612 cl = makecl(unfi)
1613 1613 for n in nodes:
1614 1614 cl.rev(n)
1615 1615
1616 1616 timer(d, setup=setup)
1617 1617 fm.end()
1618 1618
1619 1619
1620 1620 @command(
1621 1621 b'perf::nodemap|perfnodemap',
1622 1622 [
1623 1623 (b'', b'rev', [], b'revision to be looked up (default tip)'),
1624 1624 (b'', b'clear-caches', True, b'clear revlog cache between calls'),
1625 1625 ]
1626 1626 + formatteropts,
1627 1627 )
1628 1628 def perfnodemap(ui, repo, **opts):
1629 1629 """benchmark the time necessary to look up revision from a cold nodemap
1630 1630
1631 1631 Depending on the implementation, the amount and order of revision we look
1632 1632 up can varies. Example of useful set to test:
1633 1633 * tip
1634 1634 * 0
1635 1635 * -10:
1636 1636 * :10
1637 1637 * -10: + :10
1638 1638 * :10: + -10:
1639 1639 * -10000:
1640 1640 * -10000: + 0
1641 1641
1642 1642 The command currently focus on valid binary lookup. Benchmarking for
1643 1643 hexlookup, prefix lookup and missing lookup would also be valuable.
1644 1644 """
1645 1645 import mercurial.revlog
1646 1646
1647 1647 opts = _byteskwargs(opts)
1648 1648 timer, fm = gettimer(ui, opts)
1649 1649 mercurial.revlog._prereadsize = 2 ** 24 # disable lazy parser in old hg
1650 1650
1651 1651 unfi = repo.unfiltered()
1652 1652 clearcaches = opts[b'clear_caches']
1653 1653 # find the filecache func directly
1654 1654 # This avoid polluting the benchmark with the filecache logic
1655 1655 makecl = unfi.__class__.changelog.func
1656 1656 if not opts[b'rev']:
1657 1657 raise error.Abort(b'use --rev to specify revisions to look up')
1658 1658 revs = scmutil.revrange(repo, opts[b'rev'])
1659 1659 cl = repo.changelog
1660 1660 nodes = [cl.node(r) for r in revs]
1661 1661
1662 1662 # use a list to pass reference to a nodemap from one closure to the next
1663 1663 nodeget = [None]
1664 1664
1665 1665 def setnodeget():
1666 1666 # probably not necessary, but for good measure
1667 1667 clearchangelog(unfi)
1668 1668 cl = makecl(unfi)
1669 1669 if util.safehasattr(cl.index, 'get_rev'):
1670 1670 nodeget[0] = cl.index.get_rev
1671 1671 else:
1672 1672 nodeget[0] = cl.nodemap.get
1673 1673
1674 1674 def d():
1675 1675 get = nodeget[0]
1676 1676 for n in nodes:
1677 1677 get(n)
1678 1678
1679 1679 setup = None
1680 1680 if clearcaches:
1681 1681
1682 1682 def setup():
1683 1683 setnodeget()
1684 1684
1685 1685 else:
1686 1686 setnodeget()
1687 1687 d() # prewarm the data structure
1688 1688 timer(d, setup=setup)
1689 1689 fm.end()
1690 1690
1691 1691
1692 1692 @command(b'perf::startup|perfstartup', formatteropts)
1693 1693 def perfstartup(ui, repo, **opts):
1694 1694 opts = _byteskwargs(opts)
1695 1695 timer, fm = gettimer(ui, opts)
1696 1696
1697 1697 def d():
1698 1698 if os.name != 'nt':
1699 1699 os.system(
1700 1700 b"HGRCPATH= %s version -q > /dev/null" % fsencode(sys.argv[0])
1701 1701 )
1702 1702 else:
1703 1703 os.environ['HGRCPATH'] = r' '
1704 1704 os.system("%s version -q > NUL" % sys.argv[0])
1705 1705
1706 1706 timer(d)
1707 1707 fm.end()
1708 1708
1709 1709
1710 1710 @command(b'perf::parents|perfparents', formatteropts)
1711 1711 def perfparents(ui, repo, **opts):
1712 1712 """benchmark the time necessary to fetch one changeset's parents.
1713 1713
1714 1714 The fetch is done using the `node identifier`, traversing all object layers
1715 1715 from the repository object. The first N revisions will be used for this
1716 1716 benchmark. N is controlled by the ``perf.parentscount`` config option
1717 1717 (default: 1000).
1718 1718 """
1719 1719 opts = _byteskwargs(opts)
1720 1720 timer, fm = gettimer(ui, opts)
1721 1721 # control the number of commits perfparents iterates over
1722 1722 # experimental config: perf.parentscount
1723 1723 count = getint(ui, b"perf", b"parentscount", 1000)
1724 1724 if len(repo.changelog) < count:
1725 1725 raise error.Abort(b"repo needs %d commits for this test" % count)
1726 1726 repo = repo.unfiltered()
1727 1727 nl = [repo.changelog.node(i) for i in _xrange(count)]
1728 1728
1729 1729 def d():
1730 1730 for n in nl:
1731 1731 repo.changelog.parents(n)
1732 1732
1733 1733 timer(d)
1734 1734 fm.end()
1735 1735
1736 1736
1737 1737 @command(b'perf::ctxfiles|perfctxfiles', formatteropts)
1738 1738 def perfctxfiles(ui, repo, x, **opts):
1739 1739 opts = _byteskwargs(opts)
1740 1740 x = int(x)
1741 1741 timer, fm = gettimer(ui, opts)
1742 1742
1743 1743 def d():
1744 1744 len(repo[x].files())
1745 1745
1746 1746 timer(d)
1747 1747 fm.end()
1748 1748
1749 1749
1750 1750 @command(b'perf::rawfiles|perfrawfiles', formatteropts)
1751 1751 def perfrawfiles(ui, repo, x, **opts):
1752 1752 opts = _byteskwargs(opts)
1753 1753 x = int(x)
1754 1754 timer, fm = gettimer(ui, opts)
1755 1755 cl = repo.changelog
1756 1756
1757 1757 def d():
1758 1758 len(cl.read(x)[3])
1759 1759
1760 1760 timer(d)
1761 1761 fm.end()
1762 1762
1763 1763
1764 1764 @command(b'perf::lookup|perflookup', formatteropts)
1765 1765 def perflookup(ui, repo, rev, **opts):
1766 1766 opts = _byteskwargs(opts)
1767 1767 timer, fm = gettimer(ui, opts)
1768 1768 timer(lambda: len(repo.lookup(rev)))
1769 1769 fm.end()
1770 1770
1771 1771
1772 1772 @command(
1773 1773 b'perf::linelogedits|perflinelogedits',
1774 1774 [
1775 1775 (b'n', b'edits', 10000, b'number of edits'),
1776 1776 (b'', b'max-hunk-lines', 10, b'max lines in a hunk'),
1777 1777 ],
1778 1778 norepo=True,
1779 1779 )
1780 1780 def perflinelogedits(ui, **opts):
1781 1781 from mercurial import linelog
1782 1782
1783 1783 opts = _byteskwargs(opts)
1784 1784
1785 1785 edits = opts[b'edits']
1786 1786 maxhunklines = opts[b'max_hunk_lines']
1787 1787
1788 1788 maxb1 = 100000
1789 1789 random.seed(0)
1790 1790 randint = random.randint
1791 1791 currentlines = 0
1792 1792 arglist = []
1793 1793 for rev in _xrange(edits):
1794 1794 a1 = randint(0, currentlines)
1795 1795 a2 = randint(a1, min(currentlines, a1 + maxhunklines))
1796 1796 b1 = randint(0, maxb1)
1797 1797 b2 = randint(b1, b1 + maxhunklines)
1798 1798 currentlines += (b2 - b1) - (a2 - a1)
1799 1799 arglist.append((rev, a1, a2, b1, b2))
1800 1800
1801 1801 def d():
1802 1802 ll = linelog.linelog()
1803 1803 for args in arglist:
1804 1804 ll.replacelines(*args)
1805 1805
1806 1806 timer, fm = gettimer(ui, opts)
1807 1807 timer(d)
1808 1808 fm.end()
1809 1809
1810 1810
1811 1811 @command(b'perf::revrange|perfrevrange', formatteropts)
1812 1812 def perfrevrange(ui, repo, *specs, **opts):
1813 1813 opts = _byteskwargs(opts)
1814 1814 timer, fm = gettimer(ui, opts)
1815 1815 revrange = scmutil.revrange
1816 1816 timer(lambda: len(revrange(repo, specs)))
1817 1817 fm.end()
1818 1818
1819 1819
1820 1820 @command(b'perf::nodelookup|perfnodelookup', formatteropts)
1821 1821 def perfnodelookup(ui, repo, rev, **opts):
1822 1822 opts = _byteskwargs(opts)
1823 1823 timer, fm = gettimer(ui, opts)
1824 1824 import mercurial.revlog
1825 1825
1826 1826 mercurial.revlog._prereadsize = 2 ** 24 # disable lazy parser in old hg
1827 1827 n = scmutil.revsingle(repo, rev).node()
1828 1828
1829 try:
1830 cl = revlog(getsvfs(repo), radix=b"00changelog")
1831 except TypeError:
1829 1832 cl = revlog(getsvfs(repo), indexfile=b"00changelog.i")
1830 1833
1831 1834 def d():
1832 1835 cl.rev(n)
1833 1836 clearcaches(cl)
1834 1837
1835 1838 timer(d)
1836 1839 fm.end()
1837 1840
1838 1841
1839 1842 @command(
1840 1843 b'perf::log|perflog',
1841 1844 [(b'', b'rename', False, b'ask log to follow renames')] + formatteropts,
1842 1845 )
1843 1846 def perflog(ui, repo, rev=None, **opts):
1844 1847 opts = _byteskwargs(opts)
1845 1848 if rev is None:
1846 1849 rev = []
1847 1850 timer, fm = gettimer(ui, opts)
1848 1851 ui.pushbuffer()
1849 1852 timer(
1850 1853 lambda: commands.log(
1851 1854 ui, repo, rev=rev, date=b'', user=b'', copies=opts.get(b'rename')
1852 1855 )
1853 1856 )
1854 1857 ui.popbuffer()
1855 1858 fm.end()
1856 1859
1857 1860
1858 1861 @command(b'perf::moonwalk|perfmoonwalk', formatteropts)
1859 1862 def perfmoonwalk(ui, repo, **opts):
1860 1863 """benchmark walking the changelog backwards
1861 1864
1862 1865 This also loads the changelog data for each revision in the changelog.
1863 1866 """
1864 1867 opts = _byteskwargs(opts)
1865 1868 timer, fm = gettimer(ui, opts)
1866 1869
1867 1870 def moonwalk():
1868 1871 for i in repo.changelog.revs(start=(len(repo) - 1), stop=-1):
1869 1872 ctx = repo[i]
1870 1873 ctx.branch() # read changelog data (in addition to the index)
1871 1874
1872 1875 timer(moonwalk)
1873 1876 fm.end()
1874 1877
1875 1878
1876 1879 @command(
1877 1880 b'perf::templating|perftemplating',
1878 1881 [
1879 1882 (b'r', b'rev', [], b'revisions to run the template on'),
1880 1883 ]
1881 1884 + formatteropts,
1882 1885 )
1883 1886 def perftemplating(ui, repo, testedtemplate=None, **opts):
1884 1887 """test the rendering time of a given template"""
1885 1888 if makelogtemplater is None:
1886 1889 raise error.Abort(
1887 1890 b"perftemplating not available with this Mercurial",
1888 1891 hint=b"use 4.3 or later",
1889 1892 )
1890 1893
1891 1894 opts = _byteskwargs(opts)
1892 1895
1893 1896 nullui = ui.copy()
1894 1897 nullui.fout = open(os.devnull, 'wb')
1895 1898 nullui.disablepager()
1896 1899 revs = opts.get(b'rev')
1897 1900 if not revs:
1898 1901 revs = [b'all()']
1899 1902 revs = list(scmutil.revrange(repo, revs))
1900 1903
1901 1904 defaulttemplate = (
1902 1905 b'{date|shortdate} [{rev}:{node|short}]'
1903 1906 b' {author|person}: {desc|firstline}\n'
1904 1907 )
1905 1908 if testedtemplate is None:
1906 1909 testedtemplate = defaulttemplate
1907 1910 displayer = makelogtemplater(nullui, repo, testedtemplate)
1908 1911
1909 1912 def format():
1910 1913 for r in revs:
1911 1914 ctx = repo[r]
1912 1915 displayer.show(ctx)
1913 1916 displayer.flush(ctx)
1914 1917
1915 1918 timer, fm = gettimer(ui, opts)
1916 1919 timer(format)
1917 1920 fm.end()
1918 1921
1919 1922
1920 1923 def _displaystats(ui, opts, entries, data):
1921 1924 # use a second formatter because the data are quite different, not sure
1922 1925 # how it flies with the templater.
1923 1926 fm = ui.formatter(b'perf-stats', opts)
1924 1927 for key, title in entries:
1925 1928 values = data[key]
1926 1929 nbvalues = len(data)
1927 1930 values.sort()
1928 1931 stats = {
1929 1932 'key': key,
1930 1933 'title': title,
1931 1934 'nbitems': len(values),
1932 1935 'min': values[0][0],
1933 1936 '10%': values[(nbvalues * 10) // 100][0],
1934 1937 '25%': values[(nbvalues * 25) // 100][0],
1935 1938 '50%': values[(nbvalues * 50) // 100][0],
1936 1939 '75%': values[(nbvalues * 75) // 100][0],
1937 1940 '80%': values[(nbvalues * 80) // 100][0],
1938 1941 '85%': values[(nbvalues * 85) // 100][0],
1939 1942 '90%': values[(nbvalues * 90) // 100][0],
1940 1943 '95%': values[(nbvalues * 95) // 100][0],
1941 1944 '99%': values[(nbvalues * 99) // 100][0],
1942 1945 'max': values[-1][0],
1943 1946 }
1944 1947 fm.startitem()
1945 1948 fm.data(**stats)
1946 1949 # make node pretty for the human output
1947 1950 fm.plain('### %s (%d items)\n' % (title, len(values)))
1948 1951 lines = [
1949 1952 'min',
1950 1953 '10%',
1951 1954 '25%',
1952 1955 '50%',
1953 1956 '75%',
1954 1957 '80%',
1955 1958 '85%',
1956 1959 '90%',
1957 1960 '95%',
1958 1961 '99%',
1959 1962 'max',
1960 1963 ]
1961 1964 for l in lines:
1962 1965 fm.plain('%s: %s\n' % (l, stats[l]))
1963 1966 fm.end()
1964 1967
1965 1968
1966 1969 @command(
1967 1970 b'perf::helper-mergecopies|perfhelper-mergecopies',
1968 1971 formatteropts
1969 1972 + [
1970 1973 (b'r', b'revs', [], b'restrict search to these revisions'),
1971 1974 (b'', b'timing', False, b'provides extra data (costly)'),
1972 1975 (b'', b'stats', False, b'provides statistic about the measured data'),
1973 1976 ],
1974 1977 )
1975 1978 def perfhelpermergecopies(ui, repo, revs=[], **opts):
1976 1979 """find statistics about potential parameters for `perfmergecopies`
1977 1980
1978 1981 This command find (base, p1, p2) triplet relevant for copytracing
1979 1982 benchmarking in the context of a merge. It reports values for some of the
1980 1983 parameters that impact merge copy tracing time during merge.
1981 1984
1982 1985 If `--timing` is set, rename detection is run and the associated timing
1983 1986 will be reported. The extra details come at the cost of slower command
1984 1987 execution.
1985 1988
1986 1989 Since rename detection is only run once, other factors might easily
1987 1990 affect the precision of the timing. However it should give a good
1988 1991 approximation of which revision triplets are very costly.
1989 1992 """
1990 1993 opts = _byteskwargs(opts)
1991 1994 fm = ui.formatter(b'perf', opts)
1992 1995 dotiming = opts[b'timing']
1993 1996 dostats = opts[b'stats']
1994 1997
1995 1998 output_template = [
1996 1999 ("base", "%(base)12s"),
1997 2000 ("p1", "%(p1.node)12s"),
1998 2001 ("p2", "%(p2.node)12s"),
1999 2002 ("p1.nb-revs", "%(p1.nbrevs)12d"),
2000 2003 ("p1.nb-files", "%(p1.nbmissingfiles)12d"),
2001 2004 ("p1.renames", "%(p1.renamedfiles)12d"),
2002 2005 ("p1.time", "%(p1.time)12.3f"),
2003 2006 ("p2.nb-revs", "%(p2.nbrevs)12d"),
2004 2007 ("p2.nb-files", "%(p2.nbmissingfiles)12d"),
2005 2008 ("p2.renames", "%(p2.renamedfiles)12d"),
2006 2009 ("p2.time", "%(p2.time)12.3f"),
2007 2010 ("renames", "%(nbrenamedfiles)12d"),
2008 2011 ("total.time", "%(time)12.3f"),
2009 2012 ]
2010 2013 if not dotiming:
2011 2014 output_template = [
2012 2015 i
2013 2016 for i in output_template
2014 2017 if not ('time' in i[0] or 'renames' in i[0])
2015 2018 ]
2016 2019 header_names = [h for (h, v) in output_template]
2017 2020 output = ' '.join([v for (h, v) in output_template]) + '\n'
2018 2021 header = ' '.join(['%12s'] * len(header_names)) + '\n'
2019 2022 fm.plain(header % tuple(header_names))
2020 2023
2021 2024 if not revs:
2022 2025 revs = ['all()']
2023 2026 revs = scmutil.revrange(repo, revs)
2024 2027
2025 2028 if dostats:
2026 2029 alldata = {
2027 2030 'nbrevs': [],
2028 2031 'nbmissingfiles': [],
2029 2032 }
2030 2033 if dotiming:
2031 2034 alldata['parentnbrenames'] = []
2032 2035 alldata['totalnbrenames'] = []
2033 2036 alldata['parenttime'] = []
2034 2037 alldata['totaltime'] = []
2035 2038
2036 2039 roi = repo.revs('merge() and %ld', revs)
2037 2040 for r in roi:
2038 2041 ctx = repo[r]
2039 2042 p1 = ctx.p1()
2040 2043 p2 = ctx.p2()
2041 2044 bases = repo.changelog._commonancestorsheads(p1.rev(), p2.rev())
2042 2045 for b in bases:
2043 2046 b = repo[b]
2044 2047 p1missing = copies._computeforwardmissing(b, p1)
2045 2048 p2missing = copies._computeforwardmissing(b, p2)
2046 2049 data = {
2047 2050 b'base': b.hex(),
2048 2051 b'p1.node': p1.hex(),
2049 2052 b'p1.nbrevs': len(repo.revs('only(%d, %d)', p1.rev(), b.rev())),
2050 2053 b'p1.nbmissingfiles': len(p1missing),
2051 2054 b'p2.node': p2.hex(),
2052 2055 b'p2.nbrevs': len(repo.revs('only(%d, %d)', p2.rev(), b.rev())),
2053 2056 b'p2.nbmissingfiles': len(p2missing),
2054 2057 }
2055 2058 if dostats:
2056 2059 if p1missing:
2057 2060 alldata['nbrevs'].append(
2058 2061 (data['p1.nbrevs'], b.hex(), p1.hex())
2059 2062 )
2060 2063 alldata['nbmissingfiles'].append(
2061 2064 (data['p1.nbmissingfiles'], b.hex(), p1.hex())
2062 2065 )
2063 2066 if p2missing:
2064 2067 alldata['nbrevs'].append(
2065 2068 (data['p2.nbrevs'], b.hex(), p2.hex())
2066 2069 )
2067 2070 alldata['nbmissingfiles'].append(
2068 2071 (data['p2.nbmissingfiles'], b.hex(), p2.hex())
2069 2072 )
2070 2073 if dotiming:
2071 2074 begin = util.timer()
2072 2075 mergedata = copies.mergecopies(repo, p1, p2, b)
2073 2076 end = util.timer()
2074 2077 # not very stable timing since we did only one run
2075 2078 data['time'] = end - begin
2076 2079 # mergedata contains five dicts: "copy", "movewithdir",
2077 2080 # "diverge", "renamedelete" and "dirmove".
2078 2081 # The first 4 are about renamed file so lets count that.
2079 2082 renames = len(mergedata[0])
2080 2083 renames += len(mergedata[1])
2081 2084 renames += len(mergedata[2])
2082 2085 renames += len(mergedata[3])
2083 2086 data['nbrenamedfiles'] = renames
2084 2087 begin = util.timer()
2085 2088 p1renames = copies.pathcopies(b, p1)
2086 2089 end = util.timer()
2087 2090 data['p1.time'] = end - begin
2088 2091 begin = util.timer()
2089 2092 p2renames = copies.pathcopies(b, p2)
2090 2093 end = util.timer()
2091 2094 data['p2.time'] = end - begin
2092 2095 data['p1.renamedfiles'] = len(p1renames)
2093 2096 data['p2.renamedfiles'] = len(p2renames)
2094 2097
2095 2098 if dostats:
2096 2099 if p1missing:
2097 2100 alldata['parentnbrenames'].append(
2098 2101 (data['p1.renamedfiles'], b.hex(), p1.hex())
2099 2102 )
2100 2103 alldata['parenttime'].append(
2101 2104 (data['p1.time'], b.hex(), p1.hex())
2102 2105 )
2103 2106 if p2missing:
2104 2107 alldata['parentnbrenames'].append(
2105 2108 (data['p2.renamedfiles'], b.hex(), p2.hex())
2106 2109 )
2107 2110 alldata['parenttime'].append(
2108 2111 (data['p2.time'], b.hex(), p2.hex())
2109 2112 )
2110 2113 if p1missing or p2missing:
2111 2114 alldata['totalnbrenames'].append(
2112 2115 (
2113 2116 data['nbrenamedfiles'],
2114 2117 b.hex(),
2115 2118 p1.hex(),
2116 2119 p2.hex(),
2117 2120 )
2118 2121 )
2119 2122 alldata['totaltime'].append(
2120 2123 (data['time'], b.hex(), p1.hex(), p2.hex())
2121 2124 )
2122 2125 fm.startitem()
2123 2126 fm.data(**data)
2124 2127 # make node pretty for the human output
2125 2128 out = data.copy()
2126 2129 out['base'] = fm.hexfunc(b.node())
2127 2130 out['p1.node'] = fm.hexfunc(p1.node())
2128 2131 out['p2.node'] = fm.hexfunc(p2.node())
2129 2132 fm.plain(output % out)
2130 2133
2131 2134 fm.end()
2132 2135 if dostats:
2133 2136 # use a second formatter because the data are quite different, not sure
2134 2137 # how it flies with the templater.
2135 2138 entries = [
2136 2139 ('nbrevs', 'number of revision covered'),
2137 2140 ('nbmissingfiles', 'number of missing files at head'),
2138 2141 ]
2139 2142 if dotiming:
2140 2143 entries.append(
2141 2144 ('parentnbrenames', 'rename from one parent to base')
2142 2145 )
2143 2146 entries.append(('totalnbrenames', 'total number of renames'))
2144 2147 entries.append(('parenttime', 'time for one parent'))
2145 2148 entries.append(('totaltime', 'time for both parents'))
2146 2149 _displaystats(ui, opts, entries, alldata)
2147 2150
2148 2151
2149 2152 @command(
2150 2153 b'perf::helper-pathcopies|perfhelper-pathcopies',
2151 2154 formatteropts
2152 2155 + [
2153 2156 (b'r', b'revs', [], b'restrict search to these revisions'),
2154 2157 (b'', b'timing', False, b'provides extra data (costly)'),
2155 2158 (b'', b'stats', False, b'provides statistic about the measured data'),
2156 2159 ],
2157 2160 )
2158 2161 def perfhelperpathcopies(ui, repo, revs=[], **opts):
2159 2162 """find statistic about potential parameters for the `perftracecopies`
2160 2163
2161 2164 This command find source-destination pair relevant for copytracing testing.
2162 2165 It report value for some of the parameters that impact copy tracing time.
2163 2166
2164 2167 If `--timing` is set, rename detection is run and the associated timing
2165 2168 will be reported. The extra details comes at the cost of a slower command
2166 2169 execution.
2167 2170
2168 2171 Since the rename detection is only run once, other factors might easily
2169 2172 affect the precision of the timing. However it should give a good
2170 2173 approximation of which revision pairs are very costly.
2171 2174 """
2172 2175 opts = _byteskwargs(opts)
2173 2176 fm = ui.formatter(b'perf', opts)
2174 2177 dotiming = opts[b'timing']
2175 2178 dostats = opts[b'stats']
2176 2179
2177 2180 if dotiming:
2178 2181 header = '%12s %12s %12s %12s %12s %12s\n'
2179 2182 output = (
2180 2183 "%(source)12s %(destination)12s "
2181 2184 "%(nbrevs)12d %(nbmissingfiles)12d "
2182 2185 "%(nbrenamedfiles)12d %(time)18.5f\n"
2183 2186 )
2184 2187 header_names = (
2185 2188 "source",
2186 2189 "destination",
2187 2190 "nb-revs",
2188 2191 "nb-files",
2189 2192 "nb-renames",
2190 2193 "time",
2191 2194 )
2192 2195 fm.plain(header % header_names)
2193 2196 else:
2194 2197 header = '%12s %12s %12s %12s\n'
2195 2198 output = (
2196 2199 "%(source)12s %(destination)12s "
2197 2200 "%(nbrevs)12d %(nbmissingfiles)12d\n"
2198 2201 )
2199 2202 fm.plain(header % ("source", "destination", "nb-revs", "nb-files"))
2200 2203
2201 2204 if not revs:
2202 2205 revs = ['all()']
2203 2206 revs = scmutil.revrange(repo, revs)
2204 2207
2205 2208 if dostats:
2206 2209 alldata = {
2207 2210 'nbrevs': [],
2208 2211 'nbmissingfiles': [],
2209 2212 }
2210 2213 if dotiming:
2211 2214 alldata['nbrenames'] = []
2212 2215 alldata['time'] = []
2213 2216
2214 2217 roi = repo.revs('merge() and %ld', revs)
2215 2218 for r in roi:
2216 2219 ctx = repo[r]
2217 2220 p1 = ctx.p1().rev()
2218 2221 p2 = ctx.p2().rev()
2219 2222 bases = repo.changelog._commonancestorsheads(p1, p2)
2220 2223 for p in (p1, p2):
2221 2224 for b in bases:
2222 2225 base = repo[b]
2223 2226 parent = repo[p]
2224 2227 missing = copies._computeforwardmissing(base, parent)
2225 2228 if not missing:
2226 2229 continue
2227 2230 data = {
2228 2231 b'source': base.hex(),
2229 2232 b'destination': parent.hex(),
2230 2233 b'nbrevs': len(repo.revs('only(%d, %d)', p, b)),
2231 2234 b'nbmissingfiles': len(missing),
2232 2235 }
2233 2236 if dostats:
2234 2237 alldata['nbrevs'].append(
2235 2238 (
2236 2239 data['nbrevs'],
2237 2240 base.hex(),
2238 2241 parent.hex(),
2239 2242 )
2240 2243 )
2241 2244 alldata['nbmissingfiles'].append(
2242 2245 (
2243 2246 data['nbmissingfiles'],
2244 2247 base.hex(),
2245 2248 parent.hex(),
2246 2249 )
2247 2250 )
2248 2251 if dotiming:
2249 2252 begin = util.timer()
2250 2253 renames = copies.pathcopies(base, parent)
2251 2254 end = util.timer()
2252 2255 # not very stable timing since we did only one run
2253 2256 data['time'] = end - begin
2254 2257 data['nbrenamedfiles'] = len(renames)
2255 2258 if dostats:
2256 2259 alldata['time'].append(
2257 2260 (
2258 2261 data['time'],
2259 2262 base.hex(),
2260 2263 parent.hex(),
2261 2264 )
2262 2265 )
2263 2266 alldata['nbrenames'].append(
2264 2267 (
2265 2268 data['nbrenamedfiles'],
2266 2269 base.hex(),
2267 2270 parent.hex(),
2268 2271 )
2269 2272 )
2270 2273 fm.startitem()
2271 2274 fm.data(**data)
2272 2275 out = data.copy()
2273 2276 out['source'] = fm.hexfunc(base.node())
2274 2277 out['destination'] = fm.hexfunc(parent.node())
2275 2278 fm.plain(output % out)
2276 2279
2277 2280 fm.end()
2278 2281 if dostats:
2279 2282 entries = [
2280 2283 ('nbrevs', 'number of revision covered'),
2281 2284 ('nbmissingfiles', 'number of missing files at head'),
2282 2285 ]
2283 2286 if dotiming:
2284 2287 entries.append(('nbrenames', 'renamed files'))
2285 2288 entries.append(('time', 'time'))
2286 2289 _displaystats(ui, opts, entries, alldata)
2287 2290
2288 2291
2289 2292 @command(b'perf::cca|perfcca', formatteropts)
2290 2293 def perfcca(ui, repo, **opts):
2291 2294 opts = _byteskwargs(opts)
2292 2295 timer, fm = gettimer(ui, opts)
2293 2296 timer(lambda: scmutil.casecollisionauditor(ui, False, repo.dirstate))
2294 2297 fm.end()
2295 2298
2296 2299
2297 2300 @command(b'perf::fncacheload|perffncacheload', formatteropts)
2298 2301 def perffncacheload(ui, repo, **opts):
2299 2302 opts = _byteskwargs(opts)
2300 2303 timer, fm = gettimer(ui, opts)
2301 2304 s = repo.store
2302 2305
2303 2306 def d():
2304 2307 s.fncache._load()
2305 2308
2306 2309 timer(d)
2307 2310 fm.end()
2308 2311
2309 2312
2310 2313 @command(b'perf::fncachewrite|perffncachewrite', formatteropts)
2311 2314 def perffncachewrite(ui, repo, **opts):
2312 2315 opts = _byteskwargs(opts)
2313 2316 timer, fm = gettimer(ui, opts)
2314 2317 s = repo.store
2315 2318 lock = repo.lock()
2316 2319 s.fncache._load()
2317 2320 tr = repo.transaction(b'perffncachewrite')
2318 2321 tr.addbackup(b'fncache')
2319 2322
2320 2323 def d():
2321 2324 s.fncache._dirty = True
2322 2325 s.fncache.write(tr)
2323 2326
2324 2327 timer(d)
2325 2328 tr.close()
2326 2329 lock.release()
2327 2330 fm.end()
2328 2331
2329 2332
2330 2333 @command(b'perf::fncacheencode|perffncacheencode', formatteropts)
2331 2334 def perffncacheencode(ui, repo, **opts):
2332 2335 opts = _byteskwargs(opts)
2333 2336 timer, fm = gettimer(ui, opts)
2334 2337 s = repo.store
2335 2338 s.fncache._load()
2336 2339
2337 2340 def d():
2338 2341 for p in s.fncache.entries:
2339 2342 s.encode(p)
2340 2343
2341 2344 timer(d)
2342 2345 fm.end()
2343 2346
2344 2347
2345 2348 def _bdiffworker(q, blocks, xdiff, ready, done):
2346 2349 while not done.is_set():
2347 2350 pair = q.get()
2348 2351 while pair is not None:
2349 2352 if xdiff:
2350 2353 mdiff.bdiff.xdiffblocks(*pair)
2351 2354 elif blocks:
2352 2355 mdiff.bdiff.blocks(*pair)
2353 2356 else:
2354 2357 mdiff.textdiff(*pair)
2355 2358 q.task_done()
2356 2359 pair = q.get()
2357 2360 q.task_done() # for the None one
2358 2361 with ready:
2359 2362 ready.wait()
2360 2363
2361 2364
2362 2365 def _manifestrevision(repo, mnode):
2363 2366 ml = repo.manifestlog
2364 2367
2365 2368 if util.safehasattr(ml, b'getstorage'):
2366 2369 store = ml.getstorage(b'')
2367 2370 else:
2368 2371 store = ml._revlog
2369 2372
2370 2373 return store.revision(mnode)
2371 2374
2372 2375
2373 2376 @command(
2374 2377 b'perf::bdiff|perfbdiff',
2375 2378 revlogopts
2376 2379 + formatteropts
2377 2380 + [
2378 2381 (
2379 2382 b'',
2380 2383 b'count',
2381 2384 1,
2382 2385 b'number of revisions to test (when using --startrev)',
2383 2386 ),
2384 2387 (b'', b'alldata', False, b'test bdiffs for all associated revisions'),
2385 2388 (b'', b'threads', 0, b'number of thread to use (disable with 0)'),
2386 2389 (b'', b'blocks', False, b'test computing diffs into blocks'),
2387 2390 (b'', b'xdiff', False, b'use xdiff algorithm'),
2388 2391 ],
2389 2392 b'-c|-m|FILE REV',
2390 2393 )
2391 2394 def perfbdiff(ui, repo, file_, rev=None, count=None, threads=0, **opts):
2392 2395 """benchmark a bdiff between revisions
2393 2396
2394 2397 By default, benchmark a bdiff between its delta parent and itself.
2395 2398
2396 2399 With ``--count``, benchmark bdiffs between delta parents and self for N
2397 2400 revisions starting at the specified revision.
2398 2401
2399 2402 With ``--alldata``, assume the requested revision is a changeset and
2400 2403 measure bdiffs for all changes related to that changeset (manifest
2401 2404 and filelogs).
2402 2405 """
2403 2406 opts = _byteskwargs(opts)
2404 2407
2405 2408 if opts[b'xdiff'] and not opts[b'blocks']:
2406 2409 raise error.CommandError(b'perfbdiff', b'--xdiff requires --blocks')
2407 2410
2408 2411 if opts[b'alldata']:
2409 2412 opts[b'changelog'] = True
2410 2413
2411 2414 if opts.get(b'changelog') or opts.get(b'manifest'):
2412 2415 file_, rev = None, file_
2413 2416 elif rev is None:
2414 2417 raise error.CommandError(b'perfbdiff', b'invalid arguments')
2415 2418
2416 2419 blocks = opts[b'blocks']
2417 2420 xdiff = opts[b'xdiff']
2418 2421 textpairs = []
2419 2422
2420 2423 r = cmdutil.openrevlog(repo, b'perfbdiff', file_, opts)
2421 2424
2422 2425 startrev = r.rev(r.lookup(rev))
2423 2426 for rev in range(startrev, min(startrev + count, len(r) - 1)):
2424 2427 if opts[b'alldata']:
2425 2428 # Load revisions associated with changeset.
2426 2429 ctx = repo[rev]
2427 2430 mtext = _manifestrevision(repo, ctx.manifestnode())
2428 2431 for pctx in ctx.parents():
2429 2432 pman = _manifestrevision(repo, pctx.manifestnode())
2430 2433 textpairs.append((pman, mtext))
2431 2434
2432 2435 # Load filelog revisions by iterating manifest delta.
2433 2436 man = ctx.manifest()
2434 2437 pman = ctx.p1().manifest()
2435 2438 for filename, change in pman.diff(man).items():
2436 2439 fctx = repo.file(filename)
2437 2440 f1 = fctx.revision(change[0][0] or -1)
2438 2441 f2 = fctx.revision(change[1][0] or -1)
2439 2442 textpairs.append((f1, f2))
2440 2443 else:
2441 2444 dp = r.deltaparent(rev)
2442 2445 textpairs.append((r.revision(dp), r.revision(rev)))
2443 2446
2444 2447 withthreads = threads > 0
2445 2448 if not withthreads:
2446 2449
2447 2450 def d():
2448 2451 for pair in textpairs:
2449 2452 if xdiff:
2450 2453 mdiff.bdiff.xdiffblocks(*pair)
2451 2454 elif blocks:
2452 2455 mdiff.bdiff.blocks(*pair)
2453 2456 else:
2454 2457 mdiff.textdiff(*pair)
2455 2458
2456 2459 else:
2457 2460 q = queue()
2458 2461 for i in _xrange(threads):
2459 2462 q.put(None)
2460 2463 ready = threading.Condition()
2461 2464 done = threading.Event()
2462 2465 for i in _xrange(threads):
2463 2466 threading.Thread(
2464 2467 target=_bdiffworker, args=(q, blocks, xdiff, ready, done)
2465 2468 ).start()
2466 2469 q.join()
2467 2470
2468 2471 def d():
2469 2472 for pair in textpairs:
2470 2473 q.put(pair)
2471 2474 for i in _xrange(threads):
2472 2475 q.put(None)
2473 2476 with ready:
2474 2477 ready.notify_all()
2475 2478 q.join()
2476 2479
2477 2480 timer, fm = gettimer(ui, opts)
2478 2481 timer(d)
2479 2482 fm.end()
2480 2483
2481 2484 if withthreads:
2482 2485 done.set()
2483 2486 for i in _xrange(threads):
2484 2487 q.put(None)
2485 2488 with ready:
2486 2489 ready.notify_all()
2487 2490
2488 2491
2489 2492 @command(
2490 2493 b'perf::unidiff|perfunidiff',
2491 2494 revlogopts
2492 2495 + formatteropts
2493 2496 + [
2494 2497 (
2495 2498 b'',
2496 2499 b'count',
2497 2500 1,
2498 2501 b'number of revisions to test (when using --startrev)',
2499 2502 ),
2500 2503 (b'', b'alldata', False, b'test unidiffs for all associated revisions'),
2501 2504 ],
2502 2505 b'-c|-m|FILE REV',
2503 2506 )
2504 2507 def perfunidiff(ui, repo, file_, rev=None, count=None, **opts):
2505 2508 """benchmark a unified diff between revisions
2506 2509
2507 2510 This doesn't include any copy tracing - it's just a unified diff
2508 2511 of the texts.
2509 2512
2510 2513 By default, benchmark a diff between its delta parent and itself.
2511 2514
2512 2515 With ``--count``, benchmark diffs between delta parents and self for N
2513 2516 revisions starting at the specified revision.
2514 2517
2515 2518 With ``--alldata``, assume the requested revision is a changeset and
2516 2519 measure diffs for all changes related to that changeset (manifest
2517 2520 and filelogs).
2518 2521 """
2519 2522 opts = _byteskwargs(opts)
2520 2523 if opts[b'alldata']:
2521 2524 opts[b'changelog'] = True
2522 2525
2523 2526 if opts.get(b'changelog') or opts.get(b'manifest'):
2524 2527 file_, rev = None, file_
2525 2528 elif rev is None:
2526 2529 raise error.CommandError(b'perfunidiff', b'invalid arguments')
2527 2530
2528 2531 textpairs = []
2529 2532
2530 2533 r = cmdutil.openrevlog(repo, b'perfunidiff', file_, opts)
2531 2534
2532 2535 startrev = r.rev(r.lookup(rev))
2533 2536 for rev in range(startrev, min(startrev + count, len(r) - 1)):
2534 2537 if opts[b'alldata']:
2535 2538 # Load revisions associated with changeset.
2536 2539 ctx = repo[rev]
2537 2540 mtext = _manifestrevision(repo, ctx.manifestnode())
2538 2541 for pctx in ctx.parents():
2539 2542 pman = _manifestrevision(repo, pctx.manifestnode())
2540 2543 textpairs.append((pman, mtext))
2541 2544
2542 2545 # Load filelog revisions by iterating manifest delta.
2543 2546 man = ctx.manifest()
2544 2547 pman = ctx.p1().manifest()
2545 2548 for filename, change in pman.diff(man).items():
2546 2549 fctx = repo.file(filename)
2547 2550 f1 = fctx.revision(change[0][0] or -1)
2548 2551 f2 = fctx.revision(change[1][0] or -1)
2549 2552 textpairs.append((f1, f2))
2550 2553 else:
2551 2554 dp = r.deltaparent(rev)
2552 2555 textpairs.append((r.revision(dp), r.revision(rev)))
2553 2556
2554 2557 def d():
2555 2558 for left, right in textpairs:
2556 2559 # The date strings don't matter, so we pass empty strings.
2557 2560 headerlines, hunks = mdiff.unidiff(
2558 2561 left, b'', right, b'', b'left', b'right', binary=False
2559 2562 )
2560 2563 # consume iterators in roughly the way patch.py does
2561 2564 b'\n'.join(headerlines)
2562 2565 b''.join(sum((list(hlines) for hrange, hlines in hunks), []))
2563 2566
2564 2567 timer, fm = gettimer(ui, opts)
2565 2568 timer(d)
2566 2569 fm.end()
2567 2570
2568 2571
2569 2572 @command(b'perf::diffwd|perfdiffwd', formatteropts)
2570 2573 def perfdiffwd(ui, repo, **opts):
2571 2574 """Profile diff of working directory changes"""
2572 2575 opts = _byteskwargs(opts)
2573 2576 timer, fm = gettimer(ui, opts)
2574 2577 options = {
2575 2578 'w': 'ignore_all_space',
2576 2579 'b': 'ignore_space_change',
2577 2580 'B': 'ignore_blank_lines',
2578 2581 }
2579 2582
2580 2583 for diffopt in ('', 'w', 'b', 'B', 'wB'):
2581 2584 opts = {options[c]: b'1' for c in diffopt}
2582 2585
2583 2586 def d():
2584 2587 ui.pushbuffer()
2585 2588 commands.diff(ui, repo, **opts)
2586 2589 ui.popbuffer()
2587 2590
2588 2591 diffopt = diffopt.encode('ascii')
2589 2592 title = b'diffopts: %s' % (diffopt and (b'-' + diffopt) or b'none')
2590 2593 timer(d, title=title)
2591 2594 fm.end()
2592 2595
2593 2596
2594 2597 @command(
2595 2598 b'perf::revlogindex|perfrevlogindex',
2596 2599 revlogopts + formatteropts,
2597 2600 b'-c|-m|FILE',
2598 2601 )
2599 2602 def perfrevlogindex(ui, repo, file_=None, **opts):
2600 2603 """Benchmark operations against a revlog index.
2601 2604
2602 2605 This tests constructing a revlog instance, reading index data,
2603 2606 parsing index data, and performing various operations related to
2604 2607 index data.
2605 2608 """
2606 2609
2607 2610 opts = _byteskwargs(opts)
2608 2611
2609 2612 rl = cmdutil.openrevlog(repo, b'perfrevlogindex', file_, opts)
2610 2613
2611 2614 opener = getattr(rl, 'opener') # trick linter
2612 2615 # compat with hg <= 5.8
2616 radix = getattr(rl, 'radix', None)
2613 2617 indexfile = getattr(rl, '_indexfile', None)
2614 2618 if indexfile is None:
2615 2619 # compatibility with <= hg-5.8
2616 2620 indexfile = getattr(rl, 'indexfile')
2617 2621 data = opener.read(indexfile)
2618 2622
2619 2623 header = struct.unpack(b'>I', data[0:4])[0]
2620 2624 version = header & 0xFFFF
2621 2625 if version == 1:
2622 2626 inline = header & (1 << 16)
2623 2627 else:
2624 2628 raise error.Abort(b'unsupported revlog version: %d' % version)
2625 2629
2626 2630 parse_index_v1 = getattr(mercurial.revlog, 'parse_index_v1', None)
2627 2631 if parse_index_v1 is None:
2628 2632 parse_index_v1 = mercurial.revlog.revlogio().parseindex
2629 2633
2630 2634 rllen = len(rl)
2631 2635
2632 2636 node0 = rl.node(0)
2633 2637 node25 = rl.node(rllen // 4)
2634 2638 node50 = rl.node(rllen // 2)
2635 2639 node75 = rl.node(rllen // 4 * 3)
2636 2640 node100 = rl.node(rllen - 1)
2637 2641
2638 2642 allrevs = range(rllen)
2639 2643 allrevsrev = list(reversed(allrevs))
2640 2644 allnodes = [rl.node(rev) for rev in range(rllen)]
2641 2645 allnodesrev = list(reversed(allnodes))
2642 2646
2643 2647 def constructor():
2648 if radix is not None:
2649 revlog(opener, radix=radix)
2650 else:
2651 # hg <= 5.8
2644 2652 revlog(opener, indexfile=indexfile)
2645 2653
2646 2654 def read():
2647 2655 with opener(indexfile) as fh:
2648 2656 fh.read()
2649 2657
2650 2658 def parseindex():
2651 2659 parse_index_v1(data, inline)
2652 2660
2653 2661 def getentry(revornode):
2654 2662 index = parse_index_v1(data, inline)[0]
2655 2663 index[revornode]
2656 2664
2657 2665 def getentries(revs, count=1):
2658 2666 index = parse_index_v1(data, inline)[0]
2659 2667
2660 2668 for i in range(count):
2661 2669 for rev in revs:
2662 2670 index[rev]
2663 2671
2664 2672 def resolvenode(node):
2665 2673 index = parse_index_v1(data, inline)[0]
2666 2674 rev = getattr(index, 'rev', None)
2667 2675 if rev is None:
2668 2676 nodemap = getattr(parse_index_v1(data, inline)[0], 'nodemap', None)
2669 2677 # This only works for the C code.
2670 2678 if nodemap is None:
2671 2679 return
2672 2680 rev = nodemap.__getitem__
2673 2681
2674 2682 try:
2675 2683 rev(node)
2676 2684 except error.RevlogError:
2677 2685 pass
2678 2686
2679 2687 def resolvenodes(nodes, count=1):
2680 2688 index = parse_index_v1(data, inline)[0]
2681 2689 rev = getattr(index, 'rev', None)
2682 2690 if rev is None:
2683 2691 nodemap = getattr(parse_index_v1(data, inline)[0], 'nodemap', None)
2684 2692 # This only works for the C code.
2685 2693 if nodemap is None:
2686 2694 return
2687 2695 rev = nodemap.__getitem__
2688 2696
2689 2697 for i in range(count):
2690 2698 for node in nodes:
2691 2699 try:
2692 2700 rev(node)
2693 2701 except error.RevlogError:
2694 2702 pass
2695 2703
2696 2704 benches = [
2697 2705 (constructor, b'revlog constructor'),
2698 2706 (read, b'read'),
2699 2707 (parseindex, b'create index object'),
2700 2708 (lambda: getentry(0), b'retrieve index entry for rev 0'),
2701 2709 (lambda: resolvenode(b'a' * 20), b'look up missing node'),
2702 2710 (lambda: resolvenode(node0), b'look up node at rev 0'),
2703 2711 (lambda: resolvenode(node25), b'look up node at 1/4 len'),
2704 2712 (lambda: resolvenode(node50), b'look up node at 1/2 len'),
2705 2713 (lambda: resolvenode(node75), b'look up node at 3/4 len'),
2706 2714 (lambda: resolvenode(node100), b'look up node at tip'),
2707 2715 # 2x variation is to measure caching impact.
2708 2716 (lambda: resolvenodes(allnodes), b'look up all nodes (forward)'),
2709 2717 (lambda: resolvenodes(allnodes, 2), b'look up all nodes 2x (forward)'),
2710 2718 (lambda: resolvenodes(allnodesrev), b'look up all nodes (reverse)'),
2711 2719 (
2712 2720 lambda: resolvenodes(allnodesrev, 2),
2713 2721 b'look up all nodes 2x (reverse)',
2714 2722 ),
2715 2723 (lambda: getentries(allrevs), b'retrieve all index entries (forward)'),
2716 2724 (
2717 2725 lambda: getentries(allrevs, 2),
2718 2726 b'retrieve all index entries 2x (forward)',
2719 2727 ),
2720 2728 (
2721 2729 lambda: getentries(allrevsrev),
2722 2730 b'retrieve all index entries (reverse)',
2723 2731 ),
2724 2732 (
2725 2733 lambda: getentries(allrevsrev, 2),
2726 2734 b'retrieve all index entries 2x (reverse)',
2727 2735 ),
2728 2736 ]
2729 2737
2730 2738 for fn, title in benches:
2731 2739 timer, fm = gettimer(ui, opts)
2732 2740 timer(fn, title=title)
2733 2741 fm.end()
2734 2742
2735 2743
2736 2744 @command(
2737 2745 b'perf::revlogrevisions|perfrevlogrevisions',
2738 2746 revlogopts
2739 2747 + formatteropts
2740 2748 + [
2741 2749 (b'd', b'dist', 100, b'distance between the revisions'),
2742 2750 (b's', b'startrev', 0, b'revision to start reading at'),
2743 2751 (b'', b'reverse', False, b'read in reverse'),
2744 2752 ],
2745 2753 b'-c|-m|FILE',
2746 2754 )
2747 2755 def perfrevlogrevisions(
2748 2756 ui, repo, file_=None, startrev=0, reverse=False, **opts
2749 2757 ):
2750 2758 """Benchmark reading a series of revisions from a revlog.
2751 2759
2752 2760 By default, we read every ``-d/--dist`` revision from 0 to tip of
2753 2761 the specified revlog.
2754 2762
2755 2763 The start revision can be defined via ``-s/--startrev``.
2756 2764 """
2757 2765 opts = _byteskwargs(opts)
2758 2766
2759 2767 rl = cmdutil.openrevlog(repo, b'perfrevlogrevisions', file_, opts)
2760 2768 rllen = getlen(ui)(rl)
2761 2769
2762 2770 if startrev < 0:
2763 2771 startrev = rllen + startrev
2764 2772
2765 2773 def d():
2766 2774 rl.clearcaches()
2767 2775
2768 2776 beginrev = startrev
2769 2777 endrev = rllen
2770 2778 dist = opts[b'dist']
2771 2779
2772 2780 if reverse:
2773 2781 beginrev, endrev = endrev - 1, beginrev - 1
2774 2782 dist = -1 * dist
2775 2783
2776 2784 for x in _xrange(beginrev, endrev, dist):
2777 2785 # Old revisions don't support passing int.
2778 2786 n = rl.node(x)
2779 2787 rl.revision(n)
2780 2788
2781 2789 timer, fm = gettimer(ui, opts)
2782 2790 timer(d)
2783 2791 fm.end()
2784 2792
2785 2793
2786 2794 @command(
2787 2795 b'perf::revlogwrite|perfrevlogwrite',
2788 2796 revlogopts
2789 2797 + formatteropts
2790 2798 + [
2791 2799 (b's', b'startrev', 1000, b'revision to start writing at'),
2792 2800 (b'', b'stoprev', -1, b'last revision to write'),
2793 2801 (b'', b'count', 3, b'number of passes to perform'),
2794 2802 (b'', b'details', False, b'print timing for every revisions tested'),
2795 2803 (b'', b'source', b'full', b'the kind of data feed in the revlog'),
2796 2804 (b'', b'lazydeltabase', True, b'try the provided delta first'),
2797 2805 (b'', b'clear-caches', True, b'clear revlog cache between calls'),
2798 2806 ],
2799 2807 b'-c|-m|FILE',
2800 2808 )
2801 2809 def perfrevlogwrite(ui, repo, file_=None, startrev=1000, stoprev=-1, **opts):
2802 2810 """Benchmark writing a series of revisions to a revlog.
2803 2811
2804 2812 Possible source values are:
2805 2813 * `full`: add from a full text (default).
2806 2814 * `parent-1`: add from a delta to the first parent
2807 2815 * `parent-2`: add from a delta to the second parent if it exists
2808 2816 (use a delta from the first parent otherwise)
2809 2817 * `parent-smallest`: add from the smallest delta (either p1 or p2)
2810 2818 * `storage`: add from the existing precomputed deltas
2811 2819
2812 2820 Note: This performance command measures performance in a custom way. As a
2813 2821 result some of the global configuration of the 'perf' command does not
2814 2822 apply to it:
2815 2823
2816 2824 * ``pre-run``: disabled
2817 2825
2818 2826 * ``profile-benchmark``: disabled
2819 2827
2820 2828 * ``run-limits``: disabled use --count instead
2821 2829 """
2822 2830 opts = _byteskwargs(opts)
2823 2831
2824 2832 rl = cmdutil.openrevlog(repo, b'perfrevlogwrite', file_, opts)
2825 2833 rllen = getlen(ui)(rl)
2826 2834 if startrev < 0:
2827 2835 startrev = rllen + startrev
2828 2836 if stoprev < 0:
2829 2837 stoprev = rllen + stoprev
2830 2838
2831 2839 lazydeltabase = opts['lazydeltabase']
2832 2840 source = opts['source']
2833 2841 clearcaches = opts['clear_caches']
2834 2842 validsource = (
2835 2843 b'full',
2836 2844 b'parent-1',
2837 2845 b'parent-2',
2838 2846 b'parent-smallest',
2839 2847 b'storage',
2840 2848 )
2841 2849 if source not in validsource:
2842 2850 raise error.Abort('invalid source type: %s' % source)
2843 2851
2844 2852 ### actually gather results
2845 2853 count = opts['count']
2846 2854 if count <= 0:
2847 2855 raise error.Abort('invalide run count: %d' % count)
2848 2856 allresults = []
2849 2857 for c in range(count):
2850 2858 timing = _timeonewrite(
2851 2859 ui,
2852 2860 rl,
2853 2861 source,
2854 2862 startrev,
2855 2863 stoprev,
2856 2864 c + 1,
2857 2865 lazydeltabase=lazydeltabase,
2858 2866 clearcaches=clearcaches,
2859 2867 )
2860 2868 allresults.append(timing)
2861 2869
2862 2870 ### consolidate the results in a single list
2863 2871 results = []
2864 2872 for idx, (rev, t) in enumerate(allresults[0]):
2865 2873 ts = [t]
2866 2874 for other in allresults[1:]:
2867 2875 orev, ot = other[idx]
2868 2876 assert orev == rev
2869 2877 ts.append(ot)
2870 2878 results.append((rev, ts))
2871 2879 resultcount = len(results)
2872 2880
2873 2881 ### Compute and display relevant statistics
2874 2882
2875 2883 # get a formatter
2876 2884 fm = ui.formatter(b'perf', opts)
2877 2885 displayall = ui.configbool(b"perf", b"all-timing", False)
2878 2886
2879 2887 # print individual details if requested
2880 2888 if opts['details']:
2881 2889 for idx, item in enumerate(results, 1):
2882 2890 rev, data = item
2883 2891 title = 'revisions #%d of %d, rev %d' % (idx, resultcount, rev)
2884 2892 formatone(fm, data, title=title, displayall=displayall)
2885 2893
2886 2894 # sorts results by median time
2887 2895 results.sort(key=lambda x: sorted(x[1])[len(x[1]) // 2])
2888 2896 # list of (name, index) to display)
2889 2897 relevants = [
2890 2898 ("min", 0),
2891 2899 ("10%", resultcount * 10 // 100),
2892 2900 ("25%", resultcount * 25 // 100),
2893 2901 ("50%", resultcount * 70 // 100),
2894 2902 ("75%", resultcount * 75 // 100),
2895 2903 ("90%", resultcount * 90 // 100),
2896 2904 ("95%", resultcount * 95 // 100),
2897 2905 ("99%", resultcount * 99 // 100),
2898 2906 ("99.9%", resultcount * 999 // 1000),
2899 2907 ("99.99%", resultcount * 9999 // 10000),
2900 2908 ("99.999%", resultcount * 99999 // 100000),
2901 2909 ("max", -1),
2902 2910 ]
2903 2911 if not ui.quiet:
2904 2912 for name, idx in relevants:
2905 2913 data = results[idx]
2906 2914 title = '%s of %d, rev %d' % (name, resultcount, data[0])
2907 2915 formatone(fm, data[1], title=title, displayall=displayall)
2908 2916
2909 2917 # XXX summing that many float will not be very precise, we ignore this fact
2910 2918 # for now
2911 2919 totaltime = []
2912 2920 for item in allresults:
2913 2921 totaltime.append(
2914 2922 (
2915 2923 sum(x[1][0] for x in item),
2916 2924 sum(x[1][1] for x in item),
2917 2925 sum(x[1][2] for x in item),
2918 2926 )
2919 2927 )
2920 2928 formatone(
2921 2929 fm,
2922 2930 totaltime,
2923 2931 title="total time (%d revs)" % resultcount,
2924 2932 displayall=displayall,
2925 2933 )
2926 2934 fm.end()
2927 2935
2928 2936
2929 2937 class _faketr(object):
2930 2938 def add(s, x, y, z=None):
2931 2939 return None
2932 2940
2933 2941
2934 2942 def _timeonewrite(
2935 2943 ui,
2936 2944 orig,
2937 2945 source,
2938 2946 startrev,
2939 2947 stoprev,
2940 2948 runidx=None,
2941 2949 lazydeltabase=True,
2942 2950 clearcaches=True,
2943 2951 ):
2944 2952 timings = []
2945 2953 tr = _faketr()
2946 2954 with _temprevlog(ui, orig, startrev) as dest:
2947 2955 dest._lazydeltabase = lazydeltabase
2948 2956 revs = list(orig.revs(startrev, stoprev))
2949 2957 total = len(revs)
2950 2958 topic = 'adding'
2951 2959 if runidx is not None:
2952 2960 topic += ' (run #%d)' % runidx
2953 2961 # Support both old and new progress API
2954 2962 if util.safehasattr(ui, 'makeprogress'):
2955 2963 progress = ui.makeprogress(topic, unit='revs', total=total)
2956 2964
2957 2965 def updateprogress(pos):
2958 2966 progress.update(pos)
2959 2967
2960 2968 def completeprogress():
2961 2969 progress.complete()
2962 2970
2963 2971 else:
2964 2972
2965 2973 def updateprogress(pos):
2966 2974 ui.progress(topic, pos, unit='revs', total=total)
2967 2975
2968 2976 def completeprogress():
2969 2977 ui.progress(topic, None, unit='revs', total=total)
2970 2978
2971 2979 for idx, rev in enumerate(revs):
2972 2980 updateprogress(idx)
2973 2981 addargs, addkwargs = _getrevisionseed(orig, rev, tr, source)
2974 2982 if clearcaches:
2975 2983 dest.index.clearcaches()
2976 2984 dest.clearcaches()
2977 2985 with timeone() as r:
2978 2986 dest.addrawrevision(*addargs, **addkwargs)
2979 2987 timings.append((rev, r[0]))
2980 2988 updateprogress(total)
2981 2989 completeprogress()
2982 2990 return timings
2983 2991
2984 2992
2985 2993 def _getrevisionseed(orig, rev, tr, source):
2986 2994 from mercurial.node import nullid
2987 2995
2988 2996 linkrev = orig.linkrev(rev)
2989 2997 node = orig.node(rev)
2990 2998 p1, p2 = orig.parents(node)
2991 2999 flags = orig.flags(rev)
2992 3000 cachedelta = None
2993 3001 text = None
2994 3002
2995 3003 if source == b'full':
2996 3004 text = orig.revision(rev)
2997 3005 elif source == b'parent-1':
2998 3006 baserev = orig.rev(p1)
2999 3007 cachedelta = (baserev, orig.revdiff(p1, rev))
3000 3008 elif source == b'parent-2':
3001 3009 parent = p2
3002 3010 if p2 == nullid:
3003 3011 parent = p1
3004 3012 baserev = orig.rev(parent)
3005 3013 cachedelta = (baserev, orig.revdiff(parent, rev))
3006 3014 elif source == b'parent-smallest':
3007 3015 p1diff = orig.revdiff(p1, rev)
3008 3016 parent = p1
3009 3017 diff = p1diff
3010 3018 if p2 != nullid:
3011 3019 p2diff = orig.revdiff(p2, rev)
3012 3020 if len(p1diff) > len(p2diff):
3013 3021 parent = p2
3014 3022 diff = p2diff
3015 3023 baserev = orig.rev(parent)
3016 3024 cachedelta = (baserev, diff)
3017 3025 elif source == b'storage':
3018 3026 baserev = orig.deltaparent(rev)
3019 3027 cachedelta = (baserev, orig.revdiff(orig.node(baserev), rev))
3020 3028
3021 3029 return (
3022 3030 (text, tr, linkrev, p1, p2),
3023 3031 {'node': node, 'flags': flags, 'cachedelta': cachedelta},
3024 3032 )
3025 3033
3026 3034
3027 3035 @contextlib.contextmanager
3028 3036 def _temprevlog(ui, orig, truncaterev):
3029 3037 from mercurial import vfs as vfsmod
3030 3038
3031 3039 if orig._inline:
3032 3040 raise error.Abort('not supporting inline revlog (yet)')
3033 3041 revlogkwargs = {}
3034 3042 k = 'upperboundcomp'
3035 3043 if util.safehasattr(orig, k):
3036 3044 revlogkwargs[k] = getattr(orig, k)
3037 3045
3038 3046 indexfile = getattr(orig, '_indexfile', None)
3039 3047 if indexfile is None:
3040 3048 # compatibility with <= hg-5.8
3041 3049 indexfile = getattr(orig, 'indexfile')
3042 3050 origindexpath = orig.opener.join(indexfile)
3043 3051
3044 3052 datafile = getattr(orig, '_datafile', getattr(orig, 'datafile'))
3045 3053 origdatapath = orig.opener.join(datafile)
3046 indexname = 'revlog.i'
3047 dataname = 'revlog.d'
3054 radix = b'revlog'
3055 indexname = b'revlog.i'
3056 dataname = b'revlog.d'
3048 3057
3049 3058 tmpdir = tempfile.mkdtemp(prefix='tmp-hgperf-')
3050 3059 try:
3051 3060 # copy the data file in a temporary directory
3052 3061 ui.debug('copying data in %s\n' % tmpdir)
3053 3062 destindexpath = os.path.join(tmpdir, 'revlog.i')
3054 3063 destdatapath = os.path.join(tmpdir, 'revlog.d')
3055 3064 shutil.copyfile(origindexpath, destindexpath)
3056 3065 shutil.copyfile(origdatapath, destdatapath)
3057 3066
3058 3067 # remove the data we want to add again
3059 3068 ui.debug('truncating data to be rewritten\n')
3060 3069 with open(destindexpath, 'ab') as index:
3061 3070 index.seek(0)
3062 3071 index.truncate(truncaterev * orig._io.size)
3063 3072 with open(destdatapath, 'ab') as data:
3064 3073 data.seek(0)
3065 3074 data.truncate(orig.start(truncaterev))
3066 3075
3067 3076 # instantiate a new revlog from the temporary copy
3068 3077 ui.debug('truncating adding to be rewritten\n')
3069 3078 vfs = vfsmod.vfs(tmpdir)
3070 3079 vfs.options = getattr(orig.opener, 'options', None)
3071 3080
3081 try:
3082 dest = revlog(vfs, radix=radix, **revlogkwargs)
3083 except TypeError:
3072 3084 dest = revlog(
3073 3085 vfs, indexfile=indexname, datafile=dataname, **revlogkwargs
3074 3086 )
3075 3087 if dest._inline:
3076 3088 raise error.Abort('not supporting inline revlog (yet)')
3077 3089 # make sure internals are initialized
3078 3090 dest.revision(len(dest) - 1)
3079 3091 yield dest
3080 3092 del dest, vfs
3081 3093 finally:
3082 3094 shutil.rmtree(tmpdir, True)
3083 3095
3084 3096
3085 3097 @command(
3086 3098 b'perf::revlogchunks|perfrevlogchunks',
3087 3099 revlogopts
3088 3100 + formatteropts
3089 3101 + [
3090 3102 (b'e', b'engines', b'', b'compression engines to use'),
3091 3103 (b's', b'startrev', 0, b'revision to start at'),
3092 3104 ],
3093 3105 b'-c|-m|FILE',
3094 3106 )
3095 3107 def perfrevlogchunks(ui, repo, file_=None, engines=None, startrev=0, **opts):
3096 3108 """Benchmark operations on revlog chunks.
3097 3109
3098 3110 Logically, each revlog is a collection of fulltext revisions. However,
3099 3111 stored within each revlog are "chunks" of possibly compressed data. This
3100 3112 data needs to be read and decompressed or compressed and written.
3101 3113
3102 3114 This command measures the time it takes to read+decompress and recompress
3103 3115 chunks in a revlog. It effectively isolates I/O and compression performance.
3104 3116 For measurements of higher-level operations like resolving revisions,
3105 3117 see ``perfrevlogrevisions`` and ``perfrevlogrevision``.
3106 3118 """
3107 3119 opts = _byteskwargs(opts)
3108 3120
3109 3121 rl = cmdutil.openrevlog(repo, b'perfrevlogchunks', file_, opts)
3110 3122
3111 3123 # _chunkraw was renamed to _getsegmentforrevs.
3112 3124 try:
3113 3125 segmentforrevs = rl._getsegmentforrevs
3114 3126 except AttributeError:
3115 3127 segmentforrevs = rl._chunkraw
3116 3128
3117 3129 # Verify engines argument.
3118 3130 if engines:
3119 3131 engines = {e.strip() for e in engines.split(b',')}
3120 3132 for engine in engines:
3121 3133 try:
3122 3134 util.compressionengines[engine]
3123 3135 except KeyError:
3124 3136 raise error.Abort(b'unknown compression engine: %s' % engine)
3125 3137 else:
3126 3138 engines = []
3127 3139 for e in util.compengines:
3128 3140 engine = util.compengines[e]
3129 3141 try:
3130 3142 if engine.available():
3131 3143 engine.revlogcompressor().compress(b'dummy')
3132 3144 engines.append(e)
3133 3145 except NotImplementedError:
3134 3146 pass
3135 3147
3136 3148 revs = list(rl.revs(startrev, len(rl) - 1))
3137 3149
3138 3150 def rlfh(rl):
3139 3151 if rl._inline:
3140 3152 indexfile = getattr(rl, '_indexfile', None)
3141 3153 if indexfile is None:
3142 3154 # compatibility with <= hg-5.8
3143 3155 indexfile = getattr(rl, 'indexfile')
3144 3156 return getsvfs(repo)(indexfile)
3145 3157 else:
3146 3158 datafile = getattr(rl, 'datafile', getattr(rl, 'datafile'))
3147 3159 return getsvfs(repo)(datafile)
3148 3160
3149 3161 def doread():
3150 3162 rl.clearcaches()
3151 3163 for rev in revs:
3152 3164 segmentforrevs(rev, rev)
3153 3165
3154 3166 def doreadcachedfh():
3155 3167 rl.clearcaches()
3156 3168 fh = rlfh(rl)
3157 3169 for rev in revs:
3158 3170 segmentforrevs(rev, rev, df=fh)
3159 3171
3160 3172 def doreadbatch():
3161 3173 rl.clearcaches()
3162 3174 segmentforrevs(revs[0], revs[-1])
3163 3175
3164 3176 def doreadbatchcachedfh():
3165 3177 rl.clearcaches()
3166 3178 fh = rlfh(rl)
3167 3179 segmentforrevs(revs[0], revs[-1], df=fh)
3168 3180
3169 3181 def dochunk():
3170 3182 rl.clearcaches()
3171 3183 fh = rlfh(rl)
3172 3184 for rev in revs:
3173 3185 rl._chunk(rev, df=fh)
3174 3186
3175 3187 chunks = [None]
3176 3188
3177 3189 def dochunkbatch():
3178 3190 rl.clearcaches()
3179 3191 fh = rlfh(rl)
3180 3192 # Save chunks as a side-effect.
3181 3193 chunks[0] = rl._chunks(revs, df=fh)
3182 3194
3183 3195 def docompress(compressor):
3184 3196 rl.clearcaches()
3185 3197
3186 3198 try:
3187 3199 # Swap in the requested compression engine.
3188 3200 oldcompressor = rl._compressor
3189 3201 rl._compressor = compressor
3190 3202 for chunk in chunks[0]:
3191 3203 rl.compress(chunk)
3192 3204 finally:
3193 3205 rl._compressor = oldcompressor
3194 3206
3195 3207 benches = [
3196 3208 (lambda: doread(), b'read'),
3197 3209 (lambda: doreadcachedfh(), b'read w/ reused fd'),
3198 3210 (lambda: doreadbatch(), b'read batch'),
3199 3211 (lambda: doreadbatchcachedfh(), b'read batch w/ reused fd'),
3200 3212 (lambda: dochunk(), b'chunk'),
3201 3213 (lambda: dochunkbatch(), b'chunk batch'),
3202 3214 ]
3203 3215
3204 3216 for engine in sorted(engines):
3205 3217 compressor = util.compengines[engine].revlogcompressor()
3206 3218 benches.append(
3207 3219 (
3208 3220 functools.partial(docompress, compressor),
3209 3221 b'compress w/ %s' % engine,
3210 3222 )
3211 3223 )
3212 3224
3213 3225 for fn, title in benches:
3214 3226 timer, fm = gettimer(ui, opts)
3215 3227 timer(fn, title=title)
3216 3228 fm.end()
3217 3229
3218 3230
3219 3231 @command(
3220 3232 b'perf::revlogrevision|perfrevlogrevision',
3221 3233 revlogopts
3222 3234 + formatteropts
3223 3235 + [(b'', b'cache', False, b'use caches instead of clearing')],
3224 3236 b'-c|-m|FILE REV',
3225 3237 )
3226 3238 def perfrevlogrevision(ui, repo, file_, rev=None, cache=None, **opts):
3227 3239 """Benchmark obtaining a revlog revision.
3228 3240
3229 3241 Obtaining a revlog revision consists of roughly the following steps:
3230 3242
3231 3243 1. Compute the delta chain
3232 3244 2. Slice the delta chain if applicable
3233 3245 3. Obtain the raw chunks for that delta chain
3234 3246 4. Decompress each raw chunk
3235 3247 5. Apply binary patches to obtain fulltext
3236 3248 6. Verify hash of fulltext
3237 3249
3238 3250 This command measures the time spent in each of these phases.
3239 3251 """
3240 3252 opts = _byteskwargs(opts)
3241 3253
3242 3254 if opts.get(b'changelog') or opts.get(b'manifest'):
3243 3255 file_, rev = None, file_
3244 3256 elif rev is None:
3245 3257 raise error.CommandError(b'perfrevlogrevision', b'invalid arguments')
3246 3258
3247 3259 r = cmdutil.openrevlog(repo, b'perfrevlogrevision', file_, opts)
3248 3260
3249 3261 # _chunkraw was renamed to _getsegmentforrevs.
3250 3262 try:
3251 3263 segmentforrevs = r._getsegmentforrevs
3252 3264 except AttributeError:
3253 3265 segmentforrevs = r._chunkraw
3254 3266
3255 3267 node = r.lookup(rev)
3256 3268 rev = r.rev(node)
3257 3269
3258 3270 def getrawchunks(data, chain):
3259 3271 start = r.start
3260 3272 length = r.length
3261 3273 inline = r._inline
3262 3274 try:
3263 3275 iosize = r.index.entry_size
3264 3276 except AttributeError:
3265 3277 iosize = r._io.size
3266 3278 buffer = util.buffer
3267 3279
3268 3280 chunks = []
3269 3281 ladd = chunks.append
3270 3282 for idx, item in enumerate(chain):
3271 3283 offset = start(item[0])
3272 3284 bits = data[idx]
3273 3285 for rev in item:
3274 3286 chunkstart = start(rev)
3275 3287 if inline:
3276 3288 chunkstart += (rev + 1) * iosize
3277 3289 chunklength = length(rev)
3278 3290 ladd(buffer(bits, chunkstart - offset, chunklength))
3279 3291
3280 3292 return chunks
3281 3293
3282 3294 def dodeltachain(rev):
3283 3295 if not cache:
3284 3296 r.clearcaches()
3285 3297 r._deltachain(rev)
3286 3298
3287 3299 def doread(chain):
3288 3300 if not cache:
3289 3301 r.clearcaches()
3290 3302 for item in slicedchain:
3291 3303 segmentforrevs(item[0], item[-1])
3292 3304
3293 3305 def doslice(r, chain, size):
3294 3306 for s in slicechunk(r, chain, targetsize=size):
3295 3307 pass
3296 3308
3297 3309 def dorawchunks(data, chain):
3298 3310 if not cache:
3299 3311 r.clearcaches()
3300 3312 getrawchunks(data, chain)
3301 3313
3302 3314 def dodecompress(chunks):
3303 3315 decomp = r.decompress
3304 3316 for chunk in chunks:
3305 3317 decomp(chunk)
3306 3318
3307 3319 def dopatch(text, bins):
3308 3320 if not cache:
3309 3321 r.clearcaches()
3310 3322 mdiff.patches(text, bins)
3311 3323
3312 3324 def dohash(text):
3313 3325 if not cache:
3314 3326 r.clearcaches()
3315 3327 r.checkhash(text, node, rev=rev)
3316 3328
3317 3329 def dorevision():
3318 3330 if not cache:
3319 3331 r.clearcaches()
3320 3332 r.revision(node)
3321 3333
3322 3334 try:
3323 3335 from mercurial.revlogutils.deltas import slicechunk
3324 3336 except ImportError:
3325 3337 slicechunk = getattr(revlog, '_slicechunk', None)
3326 3338
3327 3339 size = r.length(rev)
3328 3340 chain = r._deltachain(rev)[0]
3329 3341 if not getattr(r, '_withsparseread', False):
3330 3342 slicedchain = (chain,)
3331 3343 else:
3332 3344 slicedchain = tuple(slicechunk(r, chain, targetsize=size))
3333 3345 data = [segmentforrevs(seg[0], seg[-1])[1] for seg in slicedchain]
3334 3346 rawchunks = getrawchunks(data, slicedchain)
3335 3347 bins = r._chunks(chain)
3336 3348 text = bytes(bins[0])
3337 3349 bins = bins[1:]
3338 3350 text = mdiff.patches(text, bins)
3339 3351
3340 3352 benches = [
3341 3353 (lambda: dorevision(), b'full'),
3342 3354 (lambda: dodeltachain(rev), b'deltachain'),
3343 3355 (lambda: doread(chain), b'read'),
3344 3356 ]
3345 3357
3346 3358 if getattr(r, '_withsparseread', False):
3347 3359 slicing = (lambda: doslice(r, chain, size), b'slice-sparse-chain')
3348 3360 benches.append(slicing)
3349 3361
3350 3362 benches.extend(
3351 3363 [
3352 3364 (lambda: dorawchunks(data, slicedchain), b'rawchunks'),
3353 3365 (lambda: dodecompress(rawchunks), b'decompress'),
3354 3366 (lambda: dopatch(text, bins), b'patch'),
3355 3367 (lambda: dohash(text), b'hash'),
3356 3368 ]
3357 3369 )
3358 3370
3359 3371 timer, fm = gettimer(ui, opts)
3360 3372 for fn, title in benches:
3361 3373 timer(fn, title=title)
3362 3374 fm.end()
3363 3375
3364 3376
3365 3377 @command(
3366 3378 b'perf::revset|perfrevset',
3367 3379 [
3368 3380 (b'C', b'clear', False, b'clear volatile cache between each call.'),
3369 3381 (b'', b'contexts', False, b'obtain changectx for each revision'),
3370 3382 ]
3371 3383 + formatteropts,
3372 3384 b"REVSET",
3373 3385 )
3374 3386 def perfrevset(ui, repo, expr, clear=False, contexts=False, **opts):
3375 3387 """benchmark the execution time of a revset
3376 3388
3377 3389 Use the --clean option if need to evaluate the impact of build volatile
3378 3390 revisions set cache on the revset execution. Volatile cache hold filtered
3379 3391 and obsolete related cache."""
3380 3392 opts = _byteskwargs(opts)
3381 3393
3382 3394 timer, fm = gettimer(ui, opts)
3383 3395
3384 3396 def d():
3385 3397 if clear:
3386 3398 repo.invalidatevolatilesets()
3387 3399 if contexts:
3388 3400 for ctx in repo.set(expr):
3389 3401 pass
3390 3402 else:
3391 3403 for r in repo.revs(expr):
3392 3404 pass
3393 3405
3394 3406 timer(d)
3395 3407 fm.end()
3396 3408
3397 3409
3398 3410 @command(
3399 3411 b'perf::volatilesets|perfvolatilesets',
3400 3412 [
3401 3413 (b'', b'clear-obsstore', False, b'drop obsstore between each call.'),
3402 3414 ]
3403 3415 + formatteropts,
3404 3416 )
3405 3417 def perfvolatilesets(ui, repo, *names, **opts):
3406 3418 """benchmark the computation of various volatile set
3407 3419
3408 3420 Volatile set computes element related to filtering and obsolescence."""
3409 3421 opts = _byteskwargs(opts)
3410 3422 timer, fm = gettimer(ui, opts)
3411 3423 repo = repo.unfiltered()
3412 3424
3413 3425 def getobs(name):
3414 3426 def d():
3415 3427 repo.invalidatevolatilesets()
3416 3428 if opts[b'clear_obsstore']:
3417 3429 clearfilecache(repo, b'obsstore')
3418 3430 obsolete.getrevs(repo, name)
3419 3431
3420 3432 return d
3421 3433
3422 3434 allobs = sorted(obsolete.cachefuncs)
3423 3435 if names:
3424 3436 allobs = [n for n in allobs if n in names]
3425 3437
3426 3438 for name in allobs:
3427 3439 timer(getobs(name), title=name)
3428 3440
3429 3441 def getfiltered(name):
3430 3442 def d():
3431 3443 repo.invalidatevolatilesets()
3432 3444 if opts[b'clear_obsstore']:
3433 3445 clearfilecache(repo, b'obsstore')
3434 3446 repoview.filterrevs(repo, name)
3435 3447
3436 3448 return d
3437 3449
3438 3450 allfilter = sorted(repoview.filtertable)
3439 3451 if names:
3440 3452 allfilter = [n for n in allfilter if n in names]
3441 3453
3442 3454 for name in allfilter:
3443 3455 timer(getfiltered(name), title=name)
3444 3456 fm.end()
3445 3457
3446 3458
3447 3459 @command(
3448 3460 b'perf::branchmap|perfbranchmap',
3449 3461 [
3450 3462 (b'f', b'full', False, b'Includes build time of subset'),
3451 3463 (
3452 3464 b'',
3453 3465 b'clear-revbranch',
3454 3466 False,
3455 3467 b'purge the revbranch cache between computation',
3456 3468 ),
3457 3469 ]
3458 3470 + formatteropts,
3459 3471 )
3460 3472 def perfbranchmap(ui, repo, *filternames, **opts):
3461 3473 """benchmark the update of a branchmap
3462 3474
3463 3475 This benchmarks the full repo.branchmap() call with read and write disabled
3464 3476 """
3465 3477 opts = _byteskwargs(opts)
3466 3478 full = opts.get(b"full", False)
3467 3479 clear_revbranch = opts.get(b"clear_revbranch", False)
3468 3480 timer, fm = gettimer(ui, opts)
3469 3481
3470 3482 def getbranchmap(filtername):
3471 3483 """generate a benchmark function for the filtername"""
3472 3484 if filtername is None:
3473 3485 view = repo
3474 3486 else:
3475 3487 view = repo.filtered(filtername)
3476 3488 if util.safehasattr(view._branchcaches, '_per_filter'):
3477 3489 filtered = view._branchcaches._per_filter
3478 3490 else:
3479 3491 # older versions
3480 3492 filtered = view._branchcaches
3481 3493
3482 3494 def d():
3483 3495 if clear_revbranch:
3484 3496 repo.revbranchcache()._clear()
3485 3497 if full:
3486 3498 view._branchcaches.clear()
3487 3499 else:
3488 3500 filtered.pop(filtername, None)
3489 3501 view.branchmap()
3490 3502
3491 3503 return d
3492 3504
3493 3505 # add filter in smaller subset to bigger subset
3494 3506 possiblefilters = set(repoview.filtertable)
3495 3507 if filternames:
3496 3508 possiblefilters &= set(filternames)
3497 3509 subsettable = getbranchmapsubsettable()
3498 3510 allfilters = []
3499 3511 while possiblefilters:
3500 3512 for name in possiblefilters:
3501 3513 subset = subsettable.get(name)
3502 3514 if subset not in possiblefilters:
3503 3515 break
3504 3516 else:
3505 3517 assert False, b'subset cycle %s!' % possiblefilters
3506 3518 allfilters.append(name)
3507 3519 possiblefilters.remove(name)
3508 3520
3509 3521 # warm the cache
3510 3522 if not full:
3511 3523 for name in allfilters:
3512 3524 repo.filtered(name).branchmap()
3513 3525 if not filternames or b'unfiltered' in filternames:
3514 3526 # add unfiltered
3515 3527 allfilters.append(None)
3516 3528
3517 3529 if util.safehasattr(branchmap.branchcache, 'fromfile'):
3518 3530 branchcacheread = safeattrsetter(branchmap.branchcache, b'fromfile')
3519 3531 branchcacheread.set(classmethod(lambda *args: None))
3520 3532 else:
3521 3533 # older versions
3522 3534 branchcacheread = safeattrsetter(branchmap, b'read')
3523 3535 branchcacheread.set(lambda *args: None)
3524 3536 branchcachewrite = safeattrsetter(branchmap.branchcache, b'write')
3525 3537 branchcachewrite.set(lambda *args: None)
3526 3538 try:
3527 3539 for name in allfilters:
3528 3540 printname = name
3529 3541 if name is None:
3530 3542 printname = b'unfiltered'
3531 3543 timer(getbranchmap(name), title=printname)
3532 3544 finally:
3533 3545 branchcacheread.restore()
3534 3546 branchcachewrite.restore()
3535 3547 fm.end()
3536 3548
3537 3549
3538 3550 @command(
3539 3551 b'perf::branchmapupdate|perfbranchmapupdate',
3540 3552 [
3541 3553 (b'', b'base', [], b'subset of revision to start from'),
3542 3554 (b'', b'target', [], b'subset of revision to end with'),
3543 3555 (b'', b'clear-caches', False, b'clear cache between each runs'),
3544 3556 ]
3545 3557 + formatteropts,
3546 3558 )
3547 3559 def perfbranchmapupdate(ui, repo, base=(), target=(), **opts):
3548 3560 """benchmark branchmap update from for <base> revs to <target> revs
3549 3561
3550 3562 If `--clear-caches` is passed, the following items will be reset before
3551 3563 each update:
3552 3564 * the changelog instance and associated indexes
3553 3565 * the rev-branch-cache instance
3554 3566
3555 3567 Examples:
3556 3568
3557 3569 # update for the one last revision
3558 3570 $ hg perfbranchmapupdate --base 'not tip' --target 'tip'
3559 3571
3560 3572 $ update for change coming with a new branch
3561 3573 $ hg perfbranchmapupdate --base 'stable' --target 'default'
3562 3574 """
3563 3575 from mercurial import branchmap
3564 3576 from mercurial import repoview
3565 3577
3566 3578 opts = _byteskwargs(opts)
3567 3579 timer, fm = gettimer(ui, opts)
3568 3580 clearcaches = opts[b'clear_caches']
3569 3581 unfi = repo.unfiltered()
3570 3582 x = [None] # used to pass data between closure
3571 3583
3572 3584 # we use a `list` here to avoid possible side effect from smartset
3573 3585 baserevs = list(scmutil.revrange(repo, base))
3574 3586 targetrevs = list(scmutil.revrange(repo, target))
3575 3587 if not baserevs:
3576 3588 raise error.Abort(b'no revisions selected for --base')
3577 3589 if not targetrevs:
3578 3590 raise error.Abort(b'no revisions selected for --target')
3579 3591
3580 3592 # make sure the target branchmap also contains the one in the base
3581 3593 targetrevs = list(set(baserevs) | set(targetrevs))
3582 3594 targetrevs.sort()
3583 3595
3584 3596 cl = repo.changelog
3585 3597 allbaserevs = list(cl.ancestors(baserevs, inclusive=True))
3586 3598 allbaserevs.sort()
3587 3599 alltargetrevs = frozenset(cl.ancestors(targetrevs, inclusive=True))
3588 3600
3589 3601 newrevs = list(alltargetrevs.difference(allbaserevs))
3590 3602 newrevs.sort()
3591 3603
3592 3604 allrevs = frozenset(unfi.changelog.revs())
3593 3605 basefilterrevs = frozenset(allrevs.difference(allbaserevs))
3594 3606 targetfilterrevs = frozenset(allrevs.difference(alltargetrevs))
3595 3607
3596 3608 def basefilter(repo, visibilityexceptions=None):
3597 3609 return basefilterrevs
3598 3610
3599 3611 def targetfilter(repo, visibilityexceptions=None):
3600 3612 return targetfilterrevs
3601 3613
3602 3614 msg = b'benchmark of branchmap with %d revisions with %d new ones\n'
3603 3615 ui.status(msg % (len(allbaserevs), len(newrevs)))
3604 3616 if targetfilterrevs:
3605 3617 msg = b'(%d revisions still filtered)\n'
3606 3618 ui.status(msg % len(targetfilterrevs))
3607 3619
3608 3620 try:
3609 3621 repoview.filtertable[b'__perf_branchmap_update_base'] = basefilter
3610 3622 repoview.filtertable[b'__perf_branchmap_update_target'] = targetfilter
3611 3623
3612 3624 baserepo = repo.filtered(b'__perf_branchmap_update_base')
3613 3625 targetrepo = repo.filtered(b'__perf_branchmap_update_target')
3614 3626
3615 3627 # try to find an existing branchmap to reuse
3616 3628 subsettable = getbranchmapsubsettable()
3617 3629 candidatefilter = subsettable.get(None)
3618 3630 while candidatefilter is not None:
3619 3631 candidatebm = repo.filtered(candidatefilter).branchmap()
3620 3632 if candidatebm.validfor(baserepo):
3621 3633 filtered = repoview.filterrevs(repo, candidatefilter)
3622 3634 missing = [r for r in allbaserevs if r in filtered]
3623 3635 base = candidatebm.copy()
3624 3636 base.update(baserepo, missing)
3625 3637 break
3626 3638 candidatefilter = subsettable.get(candidatefilter)
3627 3639 else:
3628 3640 # no suitable subset where found
3629 3641 base = branchmap.branchcache()
3630 3642 base.update(baserepo, allbaserevs)
3631 3643
3632 3644 def setup():
3633 3645 x[0] = base.copy()
3634 3646 if clearcaches:
3635 3647 unfi._revbranchcache = None
3636 3648 clearchangelog(repo)
3637 3649
3638 3650 def bench():
3639 3651 x[0].update(targetrepo, newrevs)
3640 3652
3641 3653 timer(bench, setup=setup)
3642 3654 fm.end()
3643 3655 finally:
3644 3656 repoview.filtertable.pop(b'__perf_branchmap_update_base', None)
3645 3657 repoview.filtertable.pop(b'__perf_branchmap_update_target', None)
3646 3658
3647 3659
3648 3660 @command(
3649 3661 b'perf::branchmapload|perfbranchmapload',
3650 3662 [
3651 3663 (b'f', b'filter', b'', b'Specify repoview filter'),
3652 3664 (b'', b'list', False, b'List brachmap filter caches'),
3653 3665 (b'', b'clear-revlogs', False, b'refresh changelog and manifest'),
3654 3666 ]
3655 3667 + formatteropts,
3656 3668 )
3657 3669 def perfbranchmapload(ui, repo, filter=b'', list=False, **opts):
3658 3670 """benchmark reading the branchmap"""
3659 3671 opts = _byteskwargs(opts)
3660 3672 clearrevlogs = opts[b'clear_revlogs']
3661 3673
3662 3674 if list:
3663 3675 for name, kind, st in repo.cachevfs.readdir(stat=True):
3664 3676 if name.startswith(b'branch2'):
3665 3677 filtername = name.partition(b'-')[2] or b'unfiltered'
3666 3678 ui.status(
3667 3679 b'%s - %s\n' % (filtername, util.bytecount(st.st_size))
3668 3680 )
3669 3681 return
3670 3682 if not filter:
3671 3683 filter = None
3672 3684 subsettable = getbranchmapsubsettable()
3673 3685 if filter is None:
3674 3686 repo = repo.unfiltered()
3675 3687 else:
3676 3688 repo = repoview.repoview(repo, filter)
3677 3689
3678 3690 repo.branchmap() # make sure we have a relevant, up to date branchmap
3679 3691
3680 3692 try:
3681 3693 fromfile = branchmap.branchcache.fromfile
3682 3694 except AttributeError:
3683 3695 # older versions
3684 3696 fromfile = branchmap.read
3685 3697
3686 3698 currentfilter = filter
3687 3699 # try once without timer, the filter may not be cached
3688 3700 while fromfile(repo) is None:
3689 3701 currentfilter = subsettable.get(currentfilter)
3690 3702 if currentfilter is None:
3691 3703 raise error.Abort(
3692 3704 b'No branchmap cached for %s repo' % (filter or b'unfiltered')
3693 3705 )
3694 3706 repo = repo.filtered(currentfilter)
3695 3707 timer, fm = gettimer(ui, opts)
3696 3708
3697 3709 def setup():
3698 3710 if clearrevlogs:
3699 3711 clearchangelog(repo)
3700 3712
3701 3713 def bench():
3702 3714 fromfile(repo)
3703 3715
3704 3716 timer(bench, setup=setup)
3705 3717 fm.end()
3706 3718
3707 3719
3708 3720 @command(b'perf::loadmarkers|perfloadmarkers')
3709 3721 def perfloadmarkers(ui, repo):
3710 3722 """benchmark the time to parse the on-disk markers for a repo
3711 3723
3712 3724 Result is the number of markers in the repo."""
3713 3725 timer, fm = gettimer(ui)
3714 3726 svfs = getsvfs(repo)
3715 3727 timer(lambda: len(obsolete.obsstore(repo, svfs)))
3716 3728 fm.end()
3717 3729
3718 3730
3719 3731 @command(
3720 3732 b'perf::lrucachedict|perflrucachedict',
3721 3733 formatteropts
3722 3734 + [
3723 3735 (b'', b'costlimit', 0, b'maximum total cost of items in cache'),
3724 3736 (b'', b'mincost', 0, b'smallest cost of items in cache'),
3725 3737 (b'', b'maxcost', 100, b'maximum cost of items in cache'),
3726 3738 (b'', b'size', 4, b'size of cache'),
3727 3739 (b'', b'gets', 10000, b'number of key lookups'),
3728 3740 (b'', b'sets', 10000, b'number of key sets'),
3729 3741 (b'', b'mixed', 10000, b'number of mixed mode operations'),
3730 3742 (
3731 3743 b'',
3732 3744 b'mixedgetfreq',
3733 3745 50,
3734 3746 b'frequency of get vs set ops in mixed mode',
3735 3747 ),
3736 3748 ],
3737 3749 norepo=True,
3738 3750 )
3739 3751 def perflrucache(
3740 3752 ui,
3741 3753 mincost=0,
3742 3754 maxcost=100,
3743 3755 costlimit=0,
3744 3756 size=4,
3745 3757 gets=10000,
3746 3758 sets=10000,
3747 3759 mixed=10000,
3748 3760 mixedgetfreq=50,
3749 3761 **opts
3750 3762 ):
3751 3763 opts = _byteskwargs(opts)
3752 3764
3753 3765 def doinit():
3754 3766 for i in _xrange(10000):
3755 3767 util.lrucachedict(size)
3756 3768
3757 3769 costrange = list(range(mincost, maxcost + 1))
3758 3770
3759 3771 values = []
3760 3772 for i in _xrange(size):
3761 3773 values.append(random.randint(0, _maxint))
3762 3774
3763 3775 # Get mode fills the cache and tests raw lookup performance with no
3764 3776 # eviction.
3765 3777 getseq = []
3766 3778 for i in _xrange(gets):
3767 3779 getseq.append(random.choice(values))
3768 3780
3769 3781 def dogets():
3770 3782 d = util.lrucachedict(size)
3771 3783 for v in values:
3772 3784 d[v] = v
3773 3785 for key in getseq:
3774 3786 value = d[key]
3775 3787 value # silence pyflakes warning
3776 3788
3777 3789 def dogetscost():
3778 3790 d = util.lrucachedict(size, maxcost=costlimit)
3779 3791 for i, v in enumerate(values):
3780 3792 d.insert(v, v, cost=costs[i])
3781 3793 for key in getseq:
3782 3794 try:
3783 3795 value = d[key]
3784 3796 value # silence pyflakes warning
3785 3797 except KeyError:
3786 3798 pass
3787 3799
3788 3800 # Set mode tests insertion speed with cache eviction.
3789 3801 setseq = []
3790 3802 costs = []
3791 3803 for i in _xrange(sets):
3792 3804 setseq.append(random.randint(0, _maxint))
3793 3805 costs.append(random.choice(costrange))
3794 3806
3795 3807 def doinserts():
3796 3808 d = util.lrucachedict(size)
3797 3809 for v in setseq:
3798 3810 d.insert(v, v)
3799 3811
3800 3812 def doinsertscost():
3801 3813 d = util.lrucachedict(size, maxcost=costlimit)
3802 3814 for i, v in enumerate(setseq):
3803 3815 d.insert(v, v, cost=costs[i])
3804 3816
3805 3817 def dosets():
3806 3818 d = util.lrucachedict(size)
3807 3819 for v in setseq:
3808 3820 d[v] = v
3809 3821
3810 3822 # Mixed mode randomly performs gets and sets with eviction.
3811 3823 mixedops = []
3812 3824 for i in _xrange(mixed):
3813 3825 r = random.randint(0, 100)
3814 3826 if r < mixedgetfreq:
3815 3827 op = 0
3816 3828 else:
3817 3829 op = 1
3818 3830
3819 3831 mixedops.append(
3820 3832 (op, random.randint(0, size * 2), random.choice(costrange))
3821 3833 )
3822 3834
3823 3835 def domixed():
3824 3836 d = util.lrucachedict(size)
3825 3837
3826 3838 for op, v, cost in mixedops:
3827 3839 if op == 0:
3828 3840 try:
3829 3841 d[v]
3830 3842 except KeyError:
3831 3843 pass
3832 3844 else:
3833 3845 d[v] = v
3834 3846
3835 3847 def domixedcost():
3836 3848 d = util.lrucachedict(size, maxcost=costlimit)
3837 3849
3838 3850 for op, v, cost in mixedops:
3839 3851 if op == 0:
3840 3852 try:
3841 3853 d[v]
3842 3854 except KeyError:
3843 3855 pass
3844 3856 else:
3845 3857 d.insert(v, v, cost=cost)
3846 3858
3847 3859 benches = [
3848 3860 (doinit, b'init'),
3849 3861 ]
3850 3862
3851 3863 if costlimit:
3852 3864 benches.extend(
3853 3865 [
3854 3866 (dogetscost, b'gets w/ cost limit'),
3855 3867 (doinsertscost, b'inserts w/ cost limit'),
3856 3868 (domixedcost, b'mixed w/ cost limit'),
3857 3869 ]
3858 3870 )
3859 3871 else:
3860 3872 benches.extend(
3861 3873 [
3862 3874 (dogets, b'gets'),
3863 3875 (doinserts, b'inserts'),
3864 3876 (dosets, b'sets'),
3865 3877 (domixed, b'mixed'),
3866 3878 ]
3867 3879 )
3868 3880
3869 3881 for fn, title in benches:
3870 3882 timer, fm = gettimer(ui, opts)
3871 3883 timer(fn, title=title)
3872 3884 fm.end()
3873 3885
3874 3886
3875 3887 @command(
3876 3888 b'perf::write|perfwrite',
3877 3889 formatteropts
3878 3890 + [
3879 3891 (b'', b'write-method', b'write', b'ui write method'),
3880 3892 (b'', b'nlines', 100, b'number of lines'),
3881 3893 (b'', b'nitems', 100, b'number of items (per line)'),
3882 3894 (b'', b'item', b'x', b'item that is written'),
3883 3895 (b'', b'batch-line', None, b'pass whole line to write method at once'),
3884 3896 (b'', b'flush-line', None, b'flush after each line'),
3885 3897 ],
3886 3898 )
3887 3899 def perfwrite(ui, repo, **opts):
3888 3900 """microbenchmark ui.write (and others)"""
3889 3901 opts = _byteskwargs(opts)
3890 3902
3891 3903 write = getattr(ui, _sysstr(opts[b'write_method']))
3892 3904 nlines = int(opts[b'nlines'])
3893 3905 nitems = int(opts[b'nitems'])
3894 3906 item = opts[b'item']
3895 3907 batch_line = opts.get(b'batch_line')
3896 3908 flush_line = opts.get(b'flush_line')
3897 3909
3898 3910 if batch_line:
3899 3911 line = item * nitems + b'\n'
3900 3912
3901 3913 def benchmark():
3902 3914 for i in pycompat.xrange(nlines):
3903 3915 if batch_line:
3904 3916 write(line)
3905 3917 else:
3906 3918 for i in pycompat.xrange(nitems):
3907 3919 write(item)
3908 3920 write(b'\n')
3909 3921 if flush_line:
3910 3922 ui.flush()
3911 3923 ui.flush()
3912 3924
3913 3925 timer, fm = gettimer(ui, opts)
3914 3926 timer(benchmark)
3915 3927 fm.end()
3916 3928
3917 3929
3918 3930 def uisetup(ui):
3919 3931 if util.safehasattr(cmdutil, b'openrevlog') and not util.safehasattr(
3920 3932 commands, b'debugrevlogopts'
3921 3933 ):
3922 3934 # for "historical portability":
3923 3935 # In this case, Mercurial should be 1.9 (or a79fea6b3e77) -
3924 3936 # 3.7 (or 5606f7d0d063). Therefore, '--dir' option for
3925 3937 # openrevlog() should cause failure, because it has been
3926 3938 # available since 3.5 (or 49c583ca48c4).
3927 3939 def openrevlog(orig, repo, cmd, file_, opts):
3928 3940 if opts.get(b'dir') and not util.safehasattr(repo, b'dirlog'):
3929 3941 raise error.Abort(
3930 3942 b"This version doesn't support --dir option",
3931 3943 hint=b"use 3.5 or later",
3932 3944 )
3933 3945 return orig(repo, cmd, file_, opts)
3934 3946
3935 3947 extensions.wrapfunction(cmdutil, b'openrevlog', openrevlog)
3936 3948
3937 3949
3938 3950 @command(
3939 3951 b'perf::progress|perfprogress',
3940 3952 formatteropts
3941 3953 + [
3942 3954 (b'', b'topic', b'topic', b'topic for progress messages'),
3943 3955 (b'c', b'total', 1000000, b'total value we are progressing to'),
3944 3956 ],
3945 3957 norepo=True,
3946 3958 )
3947 3959 def perfprogress(ui, topic=None, total=None, **opts):
3948 3960 """printing of progress bars"""
3949 3961 opts = _byteskwargs(opts)
3950 3962
3951 3963 timer, fm = gettimer(ui, opts)
3952 3964
3953 3965 def doprogress():
3954 3966 with ui.makeprogress(topic, total=total) as progress:
3955 3967 for i in _xrange(total):
3956 3968 progress.increment()
3957 3969
3958 3970 timer(doprogress)
3959 3971 fm.end()
@@ -1,56 +1,57 b''
1 1 #!/usr/bin/env python3
2 2 # Undump a dump from dumprevlog
3 3 # $ hg init
4 4 # $ undumprevlog < repo.dump
5 5
6 6 from __future__ import absolute_import, print_function
7 7
8 8 import sys
9 9 from mercurial.node import bin
10 10 from mercurial import (
11 11 encoding,
12 12 revlog,
13 13 transaction,
14 14 vfs as vfsmod,
15 15 )
16 16 from mercurial.utils import procutil
17 17
18 18 from mercurial.revlogutils import (
19 19 constants as revlog_constants,
20 20 )
21 21
22 22 for fp in (sys.stdin, sys.stdout, sys.stderr):
23 23 procutil.setbinary(fp)
24 24
25 25 opener = vfsmod.vfs(b'.', False)
26 26 tr = transaction.transaction(
27 27 sys.stderr.write, opener, {b'store': opener}, b"undump.journal"
28 28 )
29 29 while True:
30 30 l = sys.stdin.readline()
31 31 if not l:
32 32 break
33 33 if l.startswith("file:"):
34 34 f = encoding.strtolocal(l[6:-1])
35 assert f.endswith(b'.i')
35 36 r = revlog.revlog(
36 37 opener,
37 38 target=(revlog_constants.KIND_OTHER, b'undump-revlog'),
38 indexfile=f,
39 radix=f[:-2],
39 40 )
40 41 procutil.stdout.write(b'%s\n' % f)
41 42 elif l.startswith("node:"):
42 43 n = bin(l[6:-1])
43 44 elif l.startswith("linkrev:"):
44 45 lr = int(l[9:-1])
45 46 elif l.startswith("parents:"):
46 47 p = l[9:-1].split()
47 48 p1 = bin(p[0])
48 49 p2 = bin(p[1])
49 50 elif l.startswith("length:"):
50 51 length = int(l[8:-1])
51 52 sys.stdin.readline() # start marker
52 53 d = encoding.strtolocal(sys.stdin.read(length))
53 54 sys.stdin.readline() # end marker
54 55 r.addrevision(d, tr, lr, p1, p2)
55 56
56 57 tr.close()
@@ -1,399 +1,399 b''
1 1 from __future__ import absolute_import
2 2
3 3 import threading
4 4
5 5 from mercurial.node import (
6 6 hex,
7 7 sha1nodeconstants,
8 8 )
9 9 from mercurial.pycompat import getattr
10 10 from mercurial import (
11 11 mdiff,
12 12 pycompat,
13 13 revlog,
14 14 )
15 15 from . import (
16 16 basestore,
17 17 constants,
18 18 shallowutil,
19 19 )
20 20
21 21
22 22 class ChainIndicies(object):
23 23 """A static class for easy reference to the delta chain indicies."""
24 24
25 25 # The filename of this revision delta
26 26 NAME = 0
27 27 # The mercurial file node for this revision delta
28 28 NODE = 1
29 29 # The filename of the delta base's revision. This is useful when delta
30 30 # between different files (like in the case of a move or copy, we can delta
31 31 # against the original file content).
32 32 BASENAME = 2
33 33 # The mercurial file node for the delta base revision. This is the nullid if
34 34 # this delta is a full text.
35 35 BASENODE = 3
36 36 # The actual delta or full text data.
37 37 DATA = 4
38 38
39 39
40 40 class unioncontentstore(basestore.baseunionstore):
41 41 def __init__(self, *args, **kwargs):
42 42 super(unioncontentstore, self).__init__(*args, **kwargs)
43 43
44 44 self.stores = args
45 45 self.writestore = kwargs.get('writestore')
46 46
47 47 # If allowincomplete==True then the union store can return partial
48 48 # delta chains, otherwise it will throw a KeyError if a full
49 49 # deltachain can't be found.
50 50 self.allowincomplete = kwargs.get('allowincomplete', False)
51 51
52 52 def get(self, name, node):
53 53 """Fetches the full text revision contents of the given name+node pair.
54 54 If the full text doesn't exist, throws a KeyError.
55 55
56 56 Under the hood, this uses getdeltachain() across all the stores to build
57 57 up a full chain to produce the full text.
58 58 """
59 59 chain = self.getdeltachain(name, node)
60 60
61 61 if chain[-1][ChainIndicies.BASENODE] != sha1nodeconstants.nullid:
62 62 # If we didn't receive a full chain, throw
63 63 raise KeyError((name, hex(node)))
64 64
65 65 # The last entry in the chain is a full text, so we start our delta
66 66 # applies with that.
67 67 fulltext = chain.pop()[ChainIndicies.DATA]
68 68
69 69 text = fulltext
70 70 while chain:
71 71 delta = chain.pop()[ChainIndicies.DATA]
72 72 text = mdiff.patches(text, [delta])
73 73
74 74 return text
75 75
76 76 @basestore.baseunionstore.retriable
77 77 def getdelta(self, name, node):
78 78 """Return the single delta entry for the given name/node pair."""
79 79 for store in self.stores:
80 80 try:
81 81 return store.getdelta(name, node)
82 82 except KeyError:
83 83 pass
84 84
85 85 raise KeyError((name, hex(node)))
86 86
87 87 def getdeltachain(self, name, node):
88 88 """Returns the deltachain for the given name/node pair.
89 89
90 90 Returns an ordered list of:
91 91
92 92 [(name, node, deltabasename, deltabasenode, deltacontent),...]
93 93
94 94 where the chain is terminated by a full text entry with a nullid
95 95 deltabasenode.
96 96 """
97 97 chain = self._getpartialchain(name, node)
98 98 while chain[-1][ChainIndicies.BASENODE] != sha1nodeconstants.nullid:
99 99 x, x, deltabasename, deltabasenode, x = chain[-1]
100 100 try:
101 101 morechain = self._getpartialchain(deltabasename, deltabasenode)
102 102 chain.extend(morechain)
103 103 except KeyError:
104 104 # If we allow incomplete chains, don't throw.
105 105 if not self.allowincomplete:
106 106 raise
107 107 break
108 108
109 109 return chain
110 110
111 111 @basestore.baseunionstore.retriable
112 112 def getmeta(self, name, node):
113 113 """Returns the metadata dict for given node."""
114 114 for store in self.stores:
115 115 try:
116 116 return store.getmeta(name, node)
117 117 except KeyError:
118 118 pass
119 119 raise KeyError((name, hex(node)))
120 120
121 121 def getmetrics(self):
122 122 metrics = [s.getmetrics() for s in self.stores]
123 123 return shallowutil.sumdicts(*metrics)
124 124
125 125 @basestore.baseunionstore.retriable
126 126 def _getpartialchain(self, name, node):
127 127 """Returns a partial delta chain for the given name/node pair.
128 128
129 129 A partial chain is a chain that may not be terminated in a full-text.
130 130 """
131 131 for store in self.stores:
132 132 try:
133 133 return store.getdeltachain(name, node)
134 134 except KeyError:
135 135 pass
136 136
137 137 raise KeyError((name, hex(node)))
138 138
139 139 def add(self, name, node, data):
140 140 raise RuntimeError(
141 141 b"cannot add content only to remotefilelog contentstore"
142 142 )
143 143
144 144 def getmissing(self, keys):
145 145 missing = keys
146 146 for store in self.stores:
147 147 if missing:
148 148 missing = store.getmissing(missing)
149 149 return missing
150 150
151 151 def addremotefilelognode(self, name, node, data):
152 152 if self.writestore:
153 153 self.writestore.addremotefilelognode(name, node, data)
154 154 else:
155 155 raise RuntimeError(b"no writable store configured")
156 156
157 157 def markledger(self, ledger, options=None):
158 158 for store in self.stores:
159 159 store.markledger(ledger, options)
160 160
161 161
162 162 class remotefilelogcontentstore(basestore.basestore):
163 163 def __init__(self, *args, **kwargs):
164 164 super(remotefilelogcontentstore, self).__init__(*args, **kwargs)
165 165 self._threaddata = threading.local()
166 166
167 167 def get(self, name, node):
168 168 # return raw revision text
169 169 data = self._getdata(name, node)
170 170
171 171 offset, size, flags = shallowutil.parsesizeflags(data)
172 172 content = data[offset : offset + size]
173 173
174 174 ancestormap = shallowutil.ancestormap(data)
175 175 p1, p2, linknode, copyfrom = ancestormap[node]
176 176 copyrev = None
177 177 if copyfrom:
178 178 copyrev = hex(p1)
179 179
180 180 self._updatemetacache(node, size, flags)
181 181
182 182 # lfs tracks renames in its own metadata, remove hg copy metadata,
183 183 # because copy metadata will be re-added by lfs flag processor.
184 184 if flags & revlog.REVIDX_EXTSTORED:
185 185 copyrev = copyfrom = None
186 186 revision = shallowutil.createrevlogtext(content, copyfrom, copyrev)
187 187 return revision
188 188
189 189 def getdelta(self, name, node):
190 190 # Since remotefilelog content stores only contain full texts, just
191 191 # return that.
192 192 revision = self.get(name, node)
193 193 return (
194 194 revision,
195 195 name,
196 196 sha1nodeconstants.nullid,
197 197 self.getmeta(name, node),
198 198 )
199 199
200 200 def getdeltachain(self, name, node):
201 201 # Since remotefilelog content stores just contain full texts, we return
202 202 # a fake delta chain that just consists of a single full text revision.
203 203 # The nullid in the deltabasenode slot indicates that the revision is a
204 204 # fulltext.
205 205 revision = self.get(name, node)
206 206 return [(name, node, None, sha1nodeconstants.nullid, revision)]
207 207
208 208 def getmeta(self, name, node):
209 209 self._sanitizemetacache()
210 210 if node != self._threaddata.metacache[0]:
211 211 data = self._getdata(name, node)
212 212 offset, size, flags = shallowutil.parsesizeflags(data)
213 213 self._updatemetacache(node, size, flags)
214 214 return self._threaddata.metacache[1]
215 215
216 216 def add(self, name, node, data):
217 217 raise RuntimeError(
218 218 b"cannot add content only to remotefilelog contentstore"
219 219 )
220 220
221 221 def _sanitizemetacache(self):
222 222 metacache = getattr(self._threaddata, 'metacache', None)
223 223 if metacache is None:
224 224 self._threaddata.metacache = (None, None) # (node, meta)
225 225
226 226 def _updatemetacache(self, node, size, flags):
227 227 self._sanitizemetacache()
228 228 if node == self._threaddata.metacache[0]:
229 229 return
230 230 meta = {constants.METAKEYFLAG: flags, constants.METAKEYSIZE: size}
231 231 self._threaddata.metacache = (node, meta)
232 232
233 233
234 234 class remotecontentstore(object):
235 235 def __init__(self, ui, fileservice, shared):
236 236 self._fileservice = fileservice
237 237 # type(shared) is usually remotefilelogcontentstore
238 238 self._shared = shared
239 239
240 240 def get(self, name, node):
241 241 self._fileservice.prefetch(
242 242 [(name, hex(node))], force=True, fetchdata=True
243 243 )
244 244 return self._shared.get(name, node)
245 245
246 246 def getdelta(self, name, node):
247 247 revision = self.get(name, node)
248 248 return (
249 249 revision,
250 250 name,
251 251 sha1nodeconstants.nullid,
252 252 self._shared.getmeta(name, node),
253 253 )
254 254
255 255 def getdeltachain(self, name, node):
256 256 # Since our remote content stores just contain full texts, we return a
257 257 # fake delta chain that just consists of a single full text revision.
258 258 # The nullid in the deltabasenode slot indicates that the revision is a
259 259 # fulltext.
260 260 revision = self.get(name, node)
261 261 return [(name, node, None, sha1nodeconstants.nullid, revision)]
262 262
263 263 def getmeta(self, name, node):
264 264 self._fileservice.prefetch(
265 265 [(name, hex(node))], force=True, fetchdata=True
266 266 )
267 267 return self._shared.getmeta(name, node)
268 268
269 269 def add(self, name, node, data):
270 270 raise RuntimeError(b"cannot add to a remote store")
271 271
272 272 def getmissing(self, keys):
273 273 return keys
274 274
275 275 def markledger(self, ledger, options=None):
276 276 pass
277 277
278 278
279 279 class manifestrevlogstore(object):
280 280 def __init__(self, repo):
281 281 self._store = repo.store
282 282 self._svfs = repo.svfs
283 283 self._revlogs = dict()
284 self._cl = revlog.revlog(self._svfs, indexfile=b'00changelog.i')
284 self._cl = revlog.revlog(self._svfs, radix=b'00changelog.i')
285 285 self._repackstartlinkrev = 0
286 286
287 287 def get(self, name, node):
288 288 return self._revlog(name).rawdata(node)
289 289
290 290 def getdelta(self, name, node):
291 291 revision = self.get(name, node)
292 292 return revision, name, self._cl.nullid, self.getmeta(name, node)
293 293
294 294 def getdeltachain(self, name, node):
295 295 revision = self.get(name, node)
296 296 return [(name, node, None, self._cl.nullid, revision)]
297 297
298 298 def getmeta(self, name, node):
299 299 rl = self._revlog(name)
300 300 rev = rl.rev(node)
301 301 return {
302 302 constants.METAKEYFLAG: rl.flags(rev),
303 303 constants.METAKEYSIZE: rl.rawsize(rev),
304 304 }
305 305
306 306 def getancestors(self, name, node, known=None):
307 307 if known is None:
308 308 known = set()
309 309 if node in known:
310 310 return []
311 311
312 312 rl = self._revlog(name)
313 313 ancestors = {}
314 314 missing = {node}
315 315 for ancrev in rl.ancestors([rl.rev(node)], inclusive=True):
316 316 ancnode = rl.node(ancrev)
317 317 missing.discard(ancnode)
318 318
319 319 p1, p2 = rl.parents(ancnode)
320 320 if p1 != self._cl.nullid and p1 not in known:
321 321 missing.add(p1)
322 322 if p2 != self._cl.nullid and p2 not in known:
323 323 missing.add(p2)
324 324
325 325 linknode = self._cl.node(rl.linkrev(ancrev))
326 326 ancestors[rl.node(ancrev)] = (p1, p2, linknode, b'')
327 327 if not missing:
328 328 break
329 329 return ancestors
330 330
331 331 def getnodeinfo(self, name, node):
332 332 cl = self._cl
333 333 rl = self._revlog(name)
334 334 parents = rl.parents(node)
335 335 linkrev = rl.linkrev(rl.rev(node))
336 336 return (parents[0], parents[1], cl.node(linkrev), None)
337 337
338 338 def add(self, *args):
339 339 raise RuntimeError(b"cannot add to a revlog store")
340 340
341 341 def _revlog(self, name):
342 342 rl = self._revlogs.get(name)
343 343 if rl is None:
344 revlogname = b'00manifesttree.i'
344 revlogname = b'00manifesttree'
345 345 if name != b'':
346 revlogname = b'meta/%s/00manifest.i' % name
347 rl = revlog.revlog(self._svfs, indexfile=revlogname)
346 revlogname = b'meta/%s/00manifest' % name
347 rl = revlog.revlog(self._svfs, radix=revlogname)
348 348 self._revlogs[name] = rl
349 349 return rl
350 350
351 351 def getmissing(self, keys):
352 352 missing = []
353 353 for name, node in keys:
354 354 mfrevlog = self._revlog(name)
355 355 if node not in mfrevlog.nodemap:
356 356 missing.append((name, node))
357 357
358 358 return missing
359 359
360 360 def setrepacklinkrevrange(self, startrev, endrev):
361 361 self._repackstartlinkrev = startrev
362 362 self._repackendlinkrev = endrev
363 363
364 364 def markledger(self, ledger, options=None):
365 365 if options and options.get(constants.OPTION_PACKSONLY):
366 366 return
367 367 treename = b''
368 rl = revlog.revlog(self._svfs, indexfile=b'00manifesttree.i')
368 rl = revlog.revlog(self._svfs, radix=b'00manifesttree')
369 369 startlinkrev = self._repackstartlinkrev
370 370 endlinkrev = self._repackendlinkrev
371 371 for rev in pycompat.xrange(len(rl) - 1, -1, -1):
372 372 linkrev = rl.linkrev(rev)
373 373 if linkrev < startlinkrev:
374 374 break
375 375 if linkrev > endlinkrev:
376 376 continue
377 377 node = rl.node(rev)
378 378 ledger.markdataentry(self, treename, node)
379 379 ledger.markhistoryentry(self, treename, node)
380 380
381 381 for t, path, encoded, size in self._store.datafiles():
382 382 if path[:5] != b'meta/' or path[-2:] != b'.i':
383 383 continue
384 384
385 treename = path[5 : -len(b'/00manifest.i')]
385 treename = path[5 : -len(b'/00manifest')]
386 386
387 rl = revlog.revlog(self._svfs, indexfile=path)
387 rl = revlog.revlog(self._svfs, indexfile=path[:-2])
388 388 for rev in pycompat.xrange(len(rl) - 1, -1, -1):
389 389 linkrev = rl.linkrev(rev)
390 390 if linkrev < startlinkrev:
391 391 break
392 392 if linkrev > endlinkrev:
393 393 continue
394 394 node = rl.node(rev)
395 395 ledger.markdataentry(self, treename, node)
396 396 ledger.markhistoryentry(self, treename, node)
397 397
398 398 def cleanup(self, ledger):
399 399 pass
@@ -1,714 +1,714 b''
1 1 # bundlerepo.py - repository class for viewing uncompressed bundles
2 2 #
3 3 # Copyright 2006, 2007 Benoit Boissinot <bboissin@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 """Repository class for viewing uncompressed bundles.
9 9
10 10 This provides a read-only repository interface to bundles as if they
11 11 were part of the actual repository.
12 12 """
13 13
14 14 from __future__ import absolute_import
15 15
16 16 import os
17 17 import shutil
18 18
19 19 from .i18n import _
20 20 from .node import (
21 21 hex,
22 22 nullrev,
23 23 )
24 24
25 25 from . import (
26 26 bundle2,
27 27 changegroup,
28 28 changelog,
29 29 cmdutil,
30 30 discovery,
31 31 encoding,
32 32 error,
33 33 exchange,
34 34 filelog,
35 35 localrepo,
36 36 manifest,
37 37 mdiff,
38 38 pathutil,
39 39 phases,
40 40 pycompat,
41 41 revlog,
42 42 util,
43 43 vfs as vfsmod,
44 44 )
45 45 from .utils import (
46 46 urlutil,
47 47 )
48 48
49 49 from .revlogutils import (
50 50 constants as revlog_constants,
51 51 )
52 52
53 53
54 54 class bundlerevlog(revlog.revlog):
55 def __init__(self, opener, target, indexfile, cgunpacker, linkmapper):
55 def __init__(self, opener, target, radix, cgunpacker, linkmapper):
56 56 # How it works:
57 57 # To retrieve a revision, we need to know the offset of the revision in
58 58 # the bundle (an unbundle object). We store this offset in the index
59 59 # (start). The base of the delta is stored in the base field.
60 60 #
61 61 # To differentiate a rev in the bundle from a rev in the revlog, we
62 62 # check revision against repotiprev.
63 63 opener = vfsmod.readonlyvfs(opener)
64 revlog.revlog.__init__(self, opener, target=target, indexfile=indexfile)
64 revlog.revlog.__init__(self, opener, target=target, radix=radix)
65 65 self.bundle = cgunpacker
66 66 n = len(self)
67 67 self.repotiprev = n - 1
68 68 self.bundlerevs = set() # used by 'bundle()' revset expression
69 69 for deltadata in cgunpacker.deltaiter():
70 70 node, p1, p2, cs, deltabase, delta, flags, sidedata = deltadata
71 71
72 72 size = len(delta)
73 73 start = cgunpacker.tell() - size
74 74
75 75 if self.index.has_node(node):
76 76 # this can happen if two branches make the same change
77 77 self.bundlerevs.add(self.index.rev(node))
78 78 continue
79 79 if cs == node:
80 80 linkrev = nullrev
81 81 else:
82 82 linkrev = linkmapper(cs)
83 83
84 84 for p in (p1, p2):
85 85 if not self.index.has_node(p):
86 86 raise error.LookupError(
87 87 p, self._indexfile, _(b"unknown parent")
88 88 )
89 89
90 90 if not self.index.has_node(deltabase):
91 91 raise LookupError(
92 92 deltabase, self._indexfile, _(b'unknown delta base')
93 93 )
94 94
95 95 baserev = self.rev(deltabase)
96 96 # start, size, full unc. size, base (unused), link, p1, p2, node, sidedata_offset (unused), sidedata_size (unused)
97 97 e = (
98 98 revlog.offset_type(start, flags),
99 99 size,
100 100 -1,
101 101 baserev,
102 102 linkrev,
103 103 self.rev(p1),
104 104 self.rev(p2),
105 105 node,
106 106 0,
107 107 0,
108 108 )
109 109 self.index.append(e)
110 110 self.bundlerevs.add(n)
111 111 n += 1
112 112
113 113 def _chunk(self, rev, df=None):
114 114 # Warning: in case of bundle, the diff is against what we stored as
115 115 # delta base, not against rev - 1
116 116 # XXX: could use some caching
117 117 if rev <= self.repotiprev:
118 118 return revlog.revlog._chunk(self, rev)
119 119 self.bundle.seek(self.start(rev))
120 120 return self.bundle.read(self.length(rev))
121 121
122 122 def revdiff(self, rev1, rev2):
123 123 """return or calculate a delta between two revisions"""
124 124 if rev1 > self.repotiprev and rev2 > self.repotiprev:
125 125 # hot path for bundle
126 126 revb = self.index[rev2][3]
127 127 if revb == rev1:
128 128 return self._chunk(rev2)
129 129 elif rev1 <= self.repotiprev and rev2 <= self.repotiprev:
130 130 return revlog.revlog.revdiff(self, rev1, rev2)
131 131
132 132 return mdiff.textdiff(self.rawdata(rev1), self.rawdata(rev2))
133 133
134 134 def _rawtext(self, node, rev, _df=None):
135 135 if rev is None:
136 136 rev = self.rev(node)
137 137 validated = False
138 138 rawtext = None
139 139 chain = []
140 140 iterrev = rev
141 141 # reconstruct the revision if it is from a changegroup
142 142 while iterrev > self.repotiprev:
143 143 if self._revisioncache and self._revisioncache[1] == iterrev:
144 144 rawtext = self._revisioncache[2]
145 145 break
146 146 chain.append(iterrev)
147 147 iterrev = self.index[iterrev][3]
148 148 if iterrev == nullrev:
149 149 rawtext = b''
150 150 elif rawtext is None:
151 151 r = super(bundlerevlog, self)._rawtext(
152 152 self.node(iterrev), iterrev, _df=_df
153 153 )
154 154 __, rawtext, validated = r
155 155 if chain:
156 156 validated = False
157 157 while chain:
158 158 delta = self._chunk(chain.pop())
159 159 rawtext = mdiff.patches(rawtext, [delta])
160 160 return rev, rawtext, validated
161 161
162 162 def addrevision(self, *args, **kwargs):
163 163 raise NotImplementedError
164 164
165 165 def addgroup(self, *args, **kwargs):
166 166 raise NotImplementedError
167 167
168 168 def strip(self, *args, **kwargs):
169 169 raise NotImplementedError
170 170
171 171 def checksize(self):
172 172 raise NotImplementedError
173 173
174 174
175 175 class bundlechangelog(bundlerevlog, changelog.changelog):
176 176 def __init__(self, opener, cgunpacker):
177 177 changelog.changelog.__init__(self, opener)
178 178 linkmapper = lambda x: x
179 179 bundlerevlog.__init__(
180 180 self,
181 181 opener,
182 182 (revlog_constants.KIND_CHANGELOG, None),
183 self._indexfile,
183 self.radix,
184 184 cgunpacker,
185 185 linkmapper,
186 186 )
187 187
188 188
189 189 class bundlemanifest(bundlerevlog, manifest.manifestrevlog):
190 190 def __init__(
191 191 self,
192 192 nodeconstants,
193 193 opener,
194 194 cgunpacker,
195 195 linkmapper,
196 196 dirlogstarts=None,
197 197 dir=b'',
198 198 ):
199 199 manifest.manifestrevlog.__init__(self, nodeconstants, opener, tree=dir)
200 200 bundlerevlog.__init__(
201 201 self,
202 202 opener,
203 203 (revlog_constants.KIND_MANIFESTLOG, dir),
204 self._revlog._indexfile,
204 self._revlog.radix,
205 205 cgunpacker,
206 206 linkmapper,
207 207 )
208 208 if dirlogstarts is None:
209 209 dirlogstarts = {}
210 210 if self.bundle.version == b"03":
211 211 dirlogstarts = _getfilestarts(self.bundle)
212 212 self._dirlogstarts = dirlogstarts
213 213 self._linkmapper = linkmapper
214 214
215 215 def dirlog(self, d):
216 216 if d in self._dirlogstarts:
217 217 self.bundle.seek(self._dirlogstarts[d])
218 218 return bundlemanifest(
219 219 self.nodeconstants,
220 220 self.opener,
221 221 self.bundle,
222 222 self._linkmapper,
223 223 self._dirlogstarts,
224 224 dir=d,
225 225 )
226 226 return super(bundlemanifest, self).dirlog(d)
227 227
228 228
229 229 class bundlefilelog(filelog.filelog):
230 230 def __init__(self, opener, path, cgunpacker, linkmapper):
231 231 filelog.filelog.__init__(self, opener, path)
232 232 self._revlog = bundlerevlog(
233 233 opener,
234 234 # XXX should use the unencoded path
235 235 target=(revlog_constants.KIND_FILELOG, path),
236 indexfile=self._revlog._indexfile,
236 radix=self._revlog.radix,
237 237 cgunpacker=cgunpacker,
238 238 linkmapper=linkmapper,
239 239 )
240 240
241 241
242 242 class bundlepeer(localrepo.localpeer):
243 243 def canpush(self):
244 244 return False
245 245
246 246
247 247 class bundlephasecache(phases.phasecache):
248 248 def __init__(self, *args, **kwargs):
249 249 super(bundlephasecache, self).__init__(*args, **kwargs)
250 250 if util.safehasattr(self, 'opener'):
251 251 self.opener = vfsmod.readonlyvfs(self.opener)
252 252
253 253 def write(self):
254 254 raise NotImplementedError
255 255
256 256 def _write(self, fp):
257 257 raise NotImplementedError
258 258
259 259 def _updateroots(self, phase, newroots, tr):
260 260 self.phaseroots[phase] = newroots
261 261 self.invalidate()
262 262 self.dirty = True
263 263
264 264
265 265 def _getfilestarts(cgunpacker):
266 266 filespos = {}
267 267 for chunkdata in iter(cgunpacker.filelogheader, {}):
268 268 fname = chunkdata[b'filename']
269 269 filespos[fname] = cgunpacker.tell()
270 270 for chunk in iter(lambda: cgunpacker.deltachunk(None), {}):
271 271 pass
272 272 return filespos
273 273
274 274
275 275 class bundlerepository(object):
276 276 """A repository instance that is a union of a local repo and a bundle.
277 277
278 278 Instances represent a read-only repository composed of a local repository
279 279 with the contents of a bundle file applied. The repository instance is
280 280 conceptually similar to the state of a repository after an
281 281 ``hg unbundle`` operation. However, the contents of the bundle are never
282 282 applied to the actual base repository.
283 283
284 284 Instances constructed directly are not usable as repository objects.
285 285 Use instance() or makebundlerepository() to create instances.
286 286 """
287 287
288 288 def __init__(self, bundlepath, url, tempparent):
289 289 self._tempparent = tempparent
290 290 self._url = url
291 291
292 292 self.ui.setconfig(b'phases', b'publish', False, b'bundlerepo')
293 293
294 294 self.tempfile = None
295 295 f = util.posixfile(bundlepath, b"rb")
296 296 bundle = exchange.readbundle(self.ui, f, bundlepath)
297 297
298 298 if isinstance(bundle, bundle2.unbundle20):
299 299 self._bundlefile = bundle
300 300 self._cgunpacker = None
301 301
302 302 cgpart = None
303 303 for part in bundle.iterparts(seekable=True):
304 304 if part.type == b'changegroup':
305 305 if cgpart:
306 306 raise NotImplementedError(
307 307 b"can't process multiple changegroups"
308 308 )
309 309 cgpart = part
310 310
311 311 self._handlebundle2part(bundle, part)
312 312
313 313 if not cgpart:
314 314 raise error.Abort(_(b"No changegroups found"))
315 315
316 316 # This is required to placate a later consumer, which expects
317 317 # the payload offset to be at the beginning of the changegroup.
318 318 # We need to do this after the iterparts() generator advances
319 319 # because iterparts() will seek to end of payload after the
320 320 # generator returns control to iterparts().
321 321 cgpart.seek(0, os.SEEK_SET)
322 322
323 323 elif isinstance(bundle, changegroup.cg1unpacker):
324 324 if bundle.compressed():
325 325 f = self._writetempbundle(
326 326 bundle.read, b'.hg10un', header=b'HG10UN'
327 327 )
328 328 bundle = exchange.readbundle(self.ui, f, bundlepath, self.vfs)
329 329
330 330 self._bundlefile = bundle
331 331 self._cgunpacker = bundle
332 332 else:
333 333 raise error.Abort(
334 334 _(b'bundle type %s cannot be read') % type(bundle)
335 335 )
336 336
337 337 # dict with the mapping 'filename' -> position in the changegroup.
338 338 self._cgfilespos = {}
339 339
340 340 self.firstnewrev = self.changelog.repotiprev + 1
341 341 phases.retractboundary(
342 342 self,
343 343 None,
344 344 phases.draft,
345 345 [ctx.node() for ctx in self[self.firstnewrev :]],
346 346 )
347 347
348 348 def _handlebundle2part(self, bundle, part):
349 349 if part.type != b'changegroup':
350 350 return
351 351
352 352 cgstream = part
353 353 version = part.params.get(b'version', b'01')
354 354 legalcgvers = changegroup.supportedincomingversions(self)
355 355 if version not in legalcgvers:
356 356 msg = _(b'Unsupported changegroup version: %s')
357 357 raise error.Abort(msg % version)
358 358 if bundle.compressed():
359 359 cgstream = self._writetempbundle(part.read, b'.cg%sun' % version)
360 360
361 361 self._cgunpacker = changegroup.getunbundler(version, cgstream, b'UN')
362 362
363 363 def _writetempbundle(self, readfn, suffix, header=b''):
364 364 """Write a temporary file to disk"""
365 365 fdtemp, temp = self.vfs.mkstemp(prefix=b"hg-bundle-", suffix=suffix)
366 366 self.tempfile = temp
367 367
368 368 with os.fdopen(fdtemp, 'wb') as fptemp:
369 369 fptemp.write(header)
370 370 while True:
371 371 chunk = readfn(2 ** 18)
372 372 if not chunk:
373 373 break
374 374 fptemp.write(chunk)
375 375
376 376 return self.vfs.open(self.tempfile, mode=b"rb")
377 377
378 378 @localrepo.unfilteredpropertycache
379 379 def _phasecache(self):
380 380 return bundlephasecache(self, self._phasedefaults)
381 381
382 382 @localrepo.unfilteredpropertycache
383 383 def changelog(self):
384 384 # consume the header if it exists
385 385 self._cgunpacker.changelogheader()
386 386 c = bundlechangelog(self.svfs, self._cgunpacker)
387 387 self.manstart = self._cgunpacker.tell()
388 388 return c
389 389
390 390 def _refreshchangelog(self):
391 391 # changelog for bundle repo are not filecache, this method is not
392 392 # applicable.
393 393 pass
394 394
395 395 @localrepo.unfilteredpropertycache
396 396 def manifestlog(self):
397 397 self._cgunpacker.seek(self.manstart)
398 398 # consume the header if it exists
399 399 self._cgunpacker.manifestheader()
400 400 linkmapper = self.unfiltered().changelog.rev
401 401 rootstore = bundlemanifest(
402 402 self.nodeconstants, self.svfs, self._cgunpacker, linkmapper
403 403 )
404 404 self.filestart = self._cgunpacker.tell()
405 405
406 406 return manifest.manifestlog(
407 407 self.svfs, self, rootstore, self.narrowmatch()
408 408 )
409 409
410 410 def _consumemanifest(self):
411 411 """Consumes the manifest portion of the bundle, setting filestart so the
412 412 file portion can be read."""
413 413 self._cgunpacker.seek(self.manstart)
414 414 self._cgunpacker.manifestheader()
415 415 for delta in self._cgunpacker.deltaiter():
416 416 pass
417 417 self.filestart = self._cgunpacker.tell()
418 418
419 419 @localrepo.unfilteredpropertycache
420 420 def manstart(self):
421 421 self.changelog
422 422 return self.manstart
423 423
424 424 @localrepo.unfilteredpropertycache
425 425 def filestart(self):
426 426 self.manifestlog
427 427
428 428 # If filestart was not set by self.manifestlog, that means the
429 429 # manifestlog implementation did not consume the manifests from the
430 430 # changegroup (ex: it might be consuming trees from a separate bundle2
431 431 # part instead). So we need to manually consume it.
432 432 if 'filestart' not in self.__dict__:
433 433 self._consumemanifest()
434 434
435 435 return self.filestart
436 436
437 437 def url(self):
438 438 return self._url
439 439
440 440 def file(self, f):
441 441 if not self._cgfilespos:
442 442 self._cgunpacker.seek(self.filestart)
443 443 self._cgfilespos = _getfilestarts(self._cgunpacker)
444 444
445 445 if f in self._cgfilespos:
446 446 self._cgunpacker.seek(self._cgfilespos[f])
447 447 linkmapper = self.unfiltered().changelog.rev
448 448 return bundlefilelog(self.svfs, f, self._cgunpacker, linkmapper)
449 449 else:
450 450 return super(bundlerepository, self).file(f)
451 451
452 452 def close(self):
453 453 """Close assigned bundle file immediately."""
454 454 self._bundlefile.close()
455 455 if self.tempfile is not None:
456 456 self.vfs.unlink(self.tempfile)
457 457 if self._tempparent:
458 458 shutil.rmtree(self._tempparent, True)
459 459
460 460 def cancopy(self):
461 461 return False
462 462
463 463 def peer(self):
464 464 return bundlepeer(self)
465 465
466 466 def getcwd(self):
467 467 return encoding.getcwd() # always outside the repo
468 468
469 469 # Check if parents exist in localrepo before setting
470 470 def setparents(self, p1, p2=None):
471 471 if p2 is None:
472 472 p2 = self.nullid
473 473 p1rev = self.changelog.rev(p1)
474 474 p2rev = self.changelog.rev(p2)
475 475 msg = _(b"setting parent to node %s that only exists in the bundle\n")
476 476 if self.changelog.repotiprev < p1rev:
477 477 self.ui.warn(msg % hex(p1))
478 478 if self.changelog.repotiprev < p2rev:
479 479 self.ui.warn(msg % hex(p2))
480 480 return super(bundlerepository, self).setparents(p1, p2)
481 481
482 482
483 483 def instance(ui, path, create, intents=None, createopts=None):
484 484 if create:
485 485 raise error.Abort(_(b'cannot create new bundle repository'))
486 486 # internal config: bundle.mainreporoot
487 487 parentpath = ui.config(b"bundle", b"mainreporoot")
488 488 if not parentpath:
489 489 # try to find the correct path to the working directory repo
490 490 parentpath = cmdutil.findrepo(encoding.getcwd())
491 491 if parentpath is None:
492 492 parentpath = b''
493 493 if parentpath:
494 494 # Try to make the full path relative so we get a nice, short URL.
495 495 # In particular, we don't want temp dir names in test outputs.
496 496 cwd = encoding.getcwd()
497 497 if parentpath == cwd:
498 498 parentpath = b''
499 499 else:
500 500 cwd = pathutil.normasprefix(cwd)
501 501 if parentpath.startswith(cwd):
502 502 parentpath = parentpath[len(cwd) :]
503 503 u = urlutil.url(path)
504 504 path = u.localpath()
505 505 if u.scheme == b'bundle':
506 506 s = path.split(b"+", 1)
507 507 if len(s) == 1:
508 508 repopath, bundlename = parentpath, s[0]
509 509 else:
510 510 repopath, bundlename = s
511 511 else:
512 512 repopath, bundlename = parentpath, path
513 513
514 514 return makebundlerepository(ui, repopath, bundlename)
515 515
516 516
517 517 def makebundlerepository(ui, repopath, bundlepath):
518 518 """Make a bundle repository object based on repo and bundle paths."""
519 519 if repopath:
520 520 url = b'bundle:%s+%s' % (util.expandpath(repopath), bundlepath)
521 521 else:
522 522 url = b'bundle:%s' % bundlepath
523 523
524 524 # Because we can't make any guarantees about the type of the base
525 525 # repository, we can't have a static class representing the bundle
526 526 # repository. We also can't make any guarantees about how to even
527 527 # call the base repository's constructor!
528 528 #
529 529 # So, our strategy is to go through ``localrepo.instance()`` to construct
530 530 # a repo instance. Then, we dynamically create a new type derived from
531 531 # both it and our ``bundlerepository`` class which overrides some
532 532 # functionality. We then change the type of the constructed repository
533 533 # to this new type and initialize the bundle-specific bits of it.
534 534
535 535 try:
536 536 repo = localrepo.instance(ui, repopath, create=False)
537 537 tempparent = None
538 538 except error.RepoError:
539 539 tempparent = pycompat.mkdtemp()
540 540 try:
541 541 repo = localrepo.instance(ui, tempparent, create=True)
542 542 except Exception:
543 543 shutil.rmtree(tempparent)
544 544 raise
545 545
546 546 class derivedbundlerepository(bundlerepository, repo.__class__):
547 547 pass
548 548
549 549 repo.__class__ = derivedbundlerepository
550 550 bundlerepository.__init__(repo, bundlepath, url, tempparent)
551 551
552 552 return repo
553 553
554 554
555 555 class bundletransactionmanager(object):
556 556 def transaction(self):
557 557 return None
558 558
559 559 def close(self):
560 560 raise NotImplementedError
561 561
562 562 def release(self):
563 563 raise NotImplementedError
564 564
565 565
566 566 def getremotechanges(
567 567 ui, repo, peer, onlyheads=None, bundlename=None, force=False
568 568 ):
569 569 """obtains a bundle of changes incoming from peer
570 570
571 571 "onlyheads" restricts the returned changes to those reachable from the
572 572 specified heads.
573 573 "bundlename", if given, stores the bundle to this file path permanently;
574 574 otherwise it's stored to a temp file and gets deleted again when you call
575 575 the returned "cleanupfn".
576 576 "force" indicates whether to proceed on unrelated repos.
577 577
578 578 Returns a tuple (local, csets, cleanupfn):
579 579
580 580 "local" is a local repo from which to obtain the actual incoming
581 581 changesets; it is a bundlerepo for the obtained bundle when the
582 582 original "peer" is remote.
583 583 "csets" lists the incoming changeset node ids.
584 584 "cleanupfn" must be called without arguments when you're done processing
585 585 the changes; it closes both the original "peer" and the one returned
586 586 here.
587 587 """
588 588 tmp = discovery.findcommonincoming(repo, peer, heads=onlyheads, force=force)
589 589 common, incoming, rheads = tmp
590 590 if not incoming:
591 591 try:
592 592 if bundlename:
593 593 os.unlink(bundlename)
594 594 except OSError:
595 595 pass
596 596 return repo, [], peer.close
597 597
598 598 commonset = set(common)
599 599 rheads = [x for x in rheads if x not in commonset]
600 600
601 601 bundle = None
602 602 bundlerepo = None
603 603 localrepo = peer.local()
604 604 if bundlename or not localrepo:
605 605 # create a bundle (uncompressed if peer repo is not local)
606 606
607 607 # developer config: devel.legacy.exchange
608 608 legexc = ui.configlist(b'devel', b'legacy.exchange')
609 609 forcebundle1 = b'bundle2' not in legexc and b'bundle1' in legexc
610 610 canbundle2 = (
611 611 not forcebundle1
612 612 and peer.capable(b'getbundle')
613 613 and peer.capable(b'bundle2')
614 614 )
615 615 if canbundle2:
616 616 with peer.commandexecutor() as e:
617 617 b2 = e.callcommand(
618 618 b'getbundle',
619 619 {
620 620 b'source': b'incoming',
621 621 b'common': common,
622 622 b'heads': rheads,
623 623 b'bundlecaps': exchange.caps20to10(
624 624 repo, role=b'client'
625 625 ),
626 626 b'cg': True,
627 627 },
628 628 ).result()
629 629
630 630 fname = bundle = changegroup.writechunks(
631 631 ui, b2._forwardchunks(), bundlename
632 632 )
633 633 else:
634 634 if peer.capable(b'getbundle'):
635 635 with peer.commandexecutor() as e:
636 636 cg = e.callcommand(
637 637 b'getbundle',
638 638 {
639 639 b'source': b'incoming',
640 640 b'common': common,
641 641 b'heads': rheads,
642 642 },
643 643 ).result()
644 644 elif onlyheads is None and not peer.capable(b'changegroupsubset'):
645 645 # compat with older servers when pulling all remote heads
646 646
647 647 with peer.commandexecutor() as e:
648 648 cg = e.callcommand(
649 649 b'changegroup',
650 650 {
651 651 b'nodes': incoming,
652 652 b'source': b'incoming',
653 653 },
654 654 ).result()
655 655
656 656 rheads = None
657 657 else:
658 658 with peer.commandexecutor() as e:
659 659 cg = e.callcommand(
660 660 b'changegroupsubset',
661 661 {
662 662 b'bases': incoming,
663 663 b'heads': rheads,
664 664 b'source': b'incoming',
665 665 },
666 666 ).result()
667 667
668 668 if localrepo:
669 669 bundletype = b"HG10BZ"
670 670 else:
671 671 bundletype = b"HG10UN"
672 672 fname = bundle = bundle2.writebundle(ui, cg, bundlename, bundletype)
673 673 # keep written bundle?
674 674 if bundlename:
675 675 bundle = None
676 676 if not localrepo:
677 677 # use the created uncompressed bundlerepo
678 678 localrepo = bundlerepo = makebundlerepository(
679 679 repo.baseui, repo.root, fname
680 680 )
681 681
682 682 # this repo contains local and peer now, so filter out local again
683 683 common = repo.heads()
684 684 if localrepo:
685 685 # Part of common may be remotely filtered
686 686 # So use an unfiltered version
687 687 # The discovery process probably need cleanup to avoid that
688 688 localrepo = localrepo.unfiltered()
689 689
690 690 csets = localrepo.changelog.findmissing(common, rheads)
691 691
692 692 if bundlerepo:
693 693 reponodes = [ctx.node() for ctx in bundlerepo[bundlerepo.firstnewrev :]]
694 694
695 695 with peer.commandexecutor() as e:
696 696 remotephases = e.callcommand(
697 697 b'listkeys',
698 698 {
699 699 b'namespace': b'phases',
700 700 },
701 701 ).result()
702 702
703 703 pullop = exchange.pulloperation(bundlerepo, peer, heads=reponodes)
704 704 pullop.trmanager = bundletransactionmanager()
705 705 exchange._pullapplyphases(pullop, remotephases)
706 706
707 707 def cleanup():
708 708 if bundlerepo:
709 709 bundlerepo.close()
710 710 if bundle:
711 711 os.unlink(bundle)
712 712 peer.close()
713 713
714 714 return (localrepo, csets, cleanup)
@@ -1,628 +1,625 b''
1 1 # changelog.py - changelog class for mercurial
2 2 #
3 3 # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 from .i18n import _
11 11 from .node import (
12 12 bin,
13 13 hex,
14 14 )
15 15 from .thirdparty import attr
16 16
17 17 from . import (
18 18 encoding,
19 19 error,
20 20 metadata,
21 21 pycompat,
22 22 revlog,
23 23 )
24 24 from .utils import (
25 25 dateutil,
26 26 stringutil,
27 27 )
28 28 from .revlogutils import (
29 29 constants as revlog_constants,
30 30 flagutil,
31 31 )
32 32
33 33 _defaultextra = {b'branch': b'default'}
34 34
35 35
36 36 def _string_escape(text):
37 37 """
38 38 >>> from .pycompat import bytechr as chr
39 39 >>> d = {b'nl': chr(10), b'bs': chr(92), b'cr': chr(13), b'nul': chr(0)}
40 40 >>> s = b"ab%(nl)scd%(bs)s%(bs)sn%(nul)s12ab%(cr)scd%(bs)s%(nl)s" % d
41 41 >>> s
42 42 'ab\\ncd\\\\\\\\n\\x0012ab\\rcd\\\\\\n'
43 43 >>> res = _string_escape(s)
44 44 >>> s == _string_unescape(res)
45 45 True
46 46 """
47 47 # subset of the string_escape codec
48 48 text = (
49 49 text.replace(b'\\', b'\\\\')
50 50 .replace(b'\n', b'\\n')
51 51 .replace(b'\r', b'\\r')
52 52 )
53 53 return text.replace(b'\0', b'\\0')
54 54
55 55
56 56 def _string_unescape(text):
57 57 if b'\\0' in text:
58 58 # fix up \0 without getting into trouble with \\0
59 59 text = text.replace(b'\\\\', b'\\\\\n')
60 60 text = text.replace(b'\\0', b'\0')
61 61 text = text.replace(b'\n', b'')
62 62 return stringutil.unescapestr(text)
63 63
64 64
65 65 def decodeextra(text):
66 66 """
67 67 >>> from .pycompat import bytechr as chr
68 68 >>> sorted(decodeextra(encodeextra({b'foo': b'bar', b'baz': chr(0) + b'2'})
69 69 ... ).items())
70 70 [('baz', '\\x002'), ('branch', 'default'), ('foo', 'bar')]
71 71 >>> sorted(decodeextra(encodeextra({b'foo': b'bar',
72 72 ... b'baz': chr(92) + chr(0) + b'2'})
73 73 ... ).items())
74 74 [('baz', '\\\\\\x002'), ('branch', 'default'), ('foo', 'bar')]
75 75 """
76 76 extra = _defaultextra.copy()
77 77 for l in text.split(b'\0'):
78 78 if l:
79 79 k, v = _string_unescape(l).split(b':', 1)
80 80 extra[k] = v
81 81 return extra
82 82
83 83
84 84 def encodeextra(d):
85 85 # keys must be sorted to produce a deterministic changelog entry
86 86 items = [_string_escape(b'%s:%s' % (k, d[k])) for k in sorted(d)]
87 87 return b"\0".join(items)
88 88
89 89
90 90 def stripdesc(desc):
91 91 """strip trailing whitespace and leading and trailing empty lines"""
92 92 return b'\n'.join([l.rstrip() for l in desc.splitlines()]).strip(b'\n')
93 93
94 94
95 95 class appender(object):
96 96 """the changelog index must be updated last on disk, so we use this class
97 97 to delay writes to it"""
98 98
99 99 def __init__(self, vfs, name, mode, buf):
100 100 self.data = buf
101 101 fp = vfs(name, mode)
102 102 self.fp = fp
103 103 self.offset = fp.tell()
104 104 self.size = vfs.fstat(fp).st_size
105 105 self._end = self.size
106 106
107 107 def end(self):
108 108 return self._end
109 109
110 110 def tell(self):
111 111 return self.offset
112 112
113 113 def flush(self):
114 114 pass
115 115
116 116 @property
117 117 def closed(self):
118 118 return self.fp.closed
119 119
120 120 def close(self):
121 121 self.fp.close()
122 122
123 123 def seek(self, offset, whence=0):
124 124 '''virtual file offset spans real file and data'''
125 125 if whence == 0:
126 126 self.offset = offset
127 127 elif whence == 1:
128 128 self.offset += offset
129 129 elif whence == 2:
130 130 self.offset = self.end() + offset
131 131 if self.offset < self.size:
132 132 self.fp.seek(self.offset)
133 133
134 134 def read(self, count=-1):
135 135 '''only trick here is reads that span real file and data'''
136 136 ret = b""
137 137 if self.offset < self.size:
138 138 s = self.fp.read(count)
139 139 ret = s
140 140 self.offset += len(s)
141 141 if count > 0:
142 142 count -= len(s)
143 143 if count != 0:
144 144 doff = self.offset - self.size
145 145 self.data.insert(0, b"".join(self.data))
146 146 del self.data[1:]
147 147 s = self.data[0][doff : doff + count]
148 148 self.offset += len(s)
149 149 ret += s
150 150 return ret
151 151
152 152 def write(self, s):
153 153 self.data.append(bytes(s))
154 154 self.offset += len(s)
155 155 self._end += len(s)
156 156
157 157 def __enter__(self):
158 158 self.fp.__enter__()
159 159 return self
160 160
161 161 def __exit__(self, *args):
162 162 return self.fp.__exit__(*args)
163 163
164 164
165 165 class _divertopener(object):
166 166 def __init__(self, opener, target):
167 167 self._opener = opener
168 168 self._target = target
169 169
170 170 def __call__(self, name, mode=b'r', checkambig=False, **kwargs):
171 171 if name != self._target:
172 172 return self._opener(name, mode, **kwargs)
173 173 return self._opener(name + b".a", mode, **kwargs)
174 174
175 175 def __getattr__(self, attr):
176 176 return getattr(self._opener, attr)
177 177
178 178
179 179 def _delayopener(opener, target, buf):
180 180 """build an opener that stores chunks in 'buf' instead of 'target'"""
181 181
182 182 def _delay(name, mode=b'r', checkambig=False, **kwargs):
183 183 if name != target:
184 184 return opener(name, mode, **kwargs)
185 185 assert not kwargs
186 186 return appender(opener, name, mode, buf)
187 187
188 188 return _delay
189 189
190 190
191 191 @attr.s
192 192 class _changelogrevision(object):
193 193 # Extensions might modify _defaultextra, so let the constructor below pass
194 194 # it in
195 195 extra = attr.ib()
196 196 manifest = attr.ib()
197 197 user = attr.ib(default=b'')
198 198 date = attr.ib(default=(0, 0))
199 199 files = attr.ib(default=attr.Factory(list))
200 200 filesadded = attr.ib(default=None)
201 201 filesremoved = attr.ib(default=None)
202 202 p1copies = attr.ib(default=None)
203 203 p2copies = attr.ib(default=None)
204 204 description = attr.ib(default=b'')
205 205 branchinfo = attr.ib(default=(_defaultextra[b'branch'], False))
206 206
207 207
208 208 class changelogrevision(object):
209 209 """Holds results of a parsed changelog revision.
210 210
211 211 Changelog revisions consist of multiple pieces of data, including
212 212 the manifest node, user, and date. This object exposes a view into
213 213 the parsed object.
214 214 """
215 215
216 216 __slots__ = (
217 217 '_offsets',
218 218 '_text',
219 219 '_sidedata',
220 220 '_cpsd',
221 221 '_changes',
222 222 )
223 223
224 224 def __new__(cls, cl, text, sidedata, cpsd):
225 225 if not text:
226 226 return _changelogrevision(extra=_defaultextra, manifest=cl.nullid)
227 227
228 228 self = super(changelogrevision, cls).__new__(cls)
229 229 # We could return here and implement the following as an __init__.
230 230 # But doing it here is equivalent and saves an extra function call.
231 231
232 232 # format used:
233 233 # nodeid\n : manifest node in ascii
234 234 # user\n : user, no \n or \r allowed
235 235 # time tz extra\n : date (time is int or float, timezone is int)
236 236 # : extra is metadata, encoded and separated by '\0'
237 237 # : older versions ignore it
238 238 # files\n\n : files modified by the cset, no \n or \r allowed
239 239 # (.*) : comment (free text, ideally utf-8)
240 240 #
241 241 # changelog v0 doesn't use extra
242 242
243 243 nl1 = text.index(b'\n')
244 244 nl2 = text.index(b'\n', nl1 + 1)
245 245 nl3 = text.index(b'\n', nl2 + 1)
246 246
247 247 # The list of files may be empty. Which means nl3 is the first of the
248 248 # double newline that precedes the description.
249 249 if text[nl3 + 1 : nl3 + 2] == b'\n':
250 250 doublenl = nl3
251 251 else:
252 252 doublenl = text.index(b'\n\n', nl3 + 1)
253 253
254 254 self._offsets = (nl1, nl2, nl3, doublenl)
255 255 self._text = text
256 256 self._sidedata = sidedata
257 257 self._cpsd = cpsd
258 258 self._changes = None
259 259
260 260 return self
261 261
262 262 @property
263 263 def manifest(self):
264 264 return bin(self._text[0 : self._offsets[0]])
265 265
266 266 @property
267 267 def user(self):
268 268 off = self._offsets
269 269 return encoding.tolocal(self._text[off[0] + 1 : off[1]])
270 270
271 271 @property
272 272 def _rawdate(self):
273 273 off = self._offsets
274 274 dateextra = self._text[off[1] + 1 : off[2]]
275 275 return dateextra.split(b' ', 2)[0:2]
276 276
277 277 @property
278 278 def _rawextra(self):
279 279 off = self._offsets
280 280 dateextra = self._text[off[1] + 1 : off[2]]
281 281 fields = dateextra.split(b' ', 2)
282 282 if len(fields) != 3:
283 283 return None
284 284
285 285 return fields[2]
286 286
287 287 @property
288 288 def date(self):
289 289 raw = self._rawdate
290 290 time = float(raw[0])
291 291 # Various tools did silly things with the timezone.
292 292 try:
293 293 timezone = int(raw[1])
294 294 except ValueError:
295 295 timezone = 0
296 296
297 297 return time, timezone
298 298
299 299 @property
300 300 def extra(self):
301 301 raw = self._rawextra
302 302 if raw is None:
303 303 return _defaultextra
304 304
305 305 return decodeextra(raw)
306 306
307 307 @property
308 308 def changes(self):
309 309 if self._changes is not None:
310 310 return self._changes
311 311 if self._cpsd:
312 312 changes = metadata.decode_files_sidedata(self._sidedata)
313 313 else:
314 314 changes = metadata.ChangingFiles(
315 315 touched=self.files or (),
316 316 added=self.filesadded or (),
317 317 removed=self.filesremoved or (),
318 318 p1_copies=self.p1copies or {},
319 319 p2_copies=self.p2copies or {},
320 320 )
321 321 self._changes = changes
322 322 return changes
323 323
324 324 @property
325 325 def files(self):
326 326 if self._cpsd:
327 327 return sorted(self.changes.touched)
328 328 off = self._offsets
329 329 if off[2] == off[3]:
330 330 return []
331 331
332 332 return self._text[off[2] + 1 : off[3]].split(b'\n')
333 333
334 334 @property
335 335 def filesadded(self):
336 336 if self._cpsd:
337 337 return self.changes.added
338 338 else:
339 339 rawindices = self.extra.get(b'filesadded')
340 340 if rawindices is None:
341 341 return None
342 342 return metadata.decodefileindices(self.files, rawindices)
343 343
344 344 @property
345 345 def filesremoved(self):
346 346 if self._cpsd:
347 347 return self.changes.removed
348 348 else:
349 349 rawindices = self.extra.get(b'filesremoved')
350 350 if rawindices is None:
351 351 return None
352 352 return metadata.decodefileindices(self.files, rawindices)
353 353
354 354 @property
355 355 def p1copies(self):
356 356 if self._cpsd:
357 357 return self.changes.copied_from_p1
358 358 else:
359 359 rawcopies = self.extra.get(b'p1copies')
360 360 if rawcopies is None:
361 361 return None
362 362 return metadata.decodecopies(self.files, rawcopies)
363 363
364 364 @property
365 365 def p2copies(self):
366 366 if self._cpsd:
367 367 return self.changes.copied_from_p2
368 368 else:
369 369 rawcopies = self.extra.get(b'p2copies')
370 370 if rawcopies is None:
371 371 return None
372 372 return metadata.decodecopies(self.files, rawcopies)
373 373
374 374 @property
375 375 def description(self):
376 376 return encoding.tolocal(self._text[self._offsets[3] + 2 :])
377 377
378 378 @property
379 379 def branchinfo(self):
380 380 extra = self.extra
381 381 return encoding.tolocal(extra.get(b"branch")), b'close' in extra
382 382
383 383
384 384 class changelog(revlog.revlog):
385 385 def __init__(self, opener, trypending=False, concurrencychecker=None):
386 386 """Load a changelog revlog using an opener.
387 387
388 388 If ``trypending`` is true, we attempt to load the index from a
389 389 ``00changelog.i.a`` file instead of the default ``00changelog.i``.
390 390 The ``00changelog.i.a`` file contains index (and possibly inline
391 391 revision) data for a transaction that hasn't been finalized yet.
392 392 It exists in a separate file to facilitate readers (such as
393 393 hooks processes) accessing data before a transaction is finalized.
394 394
395 395 ``concurrencychecker`` will be passed to the revlog init function, see
396 396 the documentation there.
397 397 """
398 398
399 indexfile = b'00changelog.i'
400 399 if trypending and opener.exists(b'00changelog.i.a'):
401 400 postfix = b'a'
402 401 else:
403 402 postfix = None
404 403
405 datafile = b'00changelog.d'
406 404 revlog.revlog.__init__(
407 405 self,
408 406 opener,
409 407 target=(revlog_constants.KIND_CHANGELOG, None),
408 radix=b'00changelog',
410 409 postfix=postfix,
411 indexfile=indexfile,
412 datafile=datafile,
413 410 checkambig=True,
414 411 mmaplargeindex=True,
415 412 persistentnodemap=opener.options.get(b'persistent-nodemap', False),
416 413 concurrencychecker=concurrencychecker,
417 414 )
418 415
419 416 if self._initempty and (self._format_version == revlog.REVLOGV1):
420 417 # changelogs don't benefit from generaldelta.
421 418
422 419 self._format_flags &= ~revlog.FLAG_GENERALDELTA
423 420 self._generaldelta = False
424 421
425 422 # Delta chains for changelogs tend to be very small because entries
426 423 # tend to be small and don't delta well with each. So disable delta
427 424 # chains.
428 425 self._storedeltachains = False
429 426
430 427 self._realopener = opener
431 428 self._delayed = False
432 429 self._delaybuf = None
433 430 self._divert = False
434 431 self._filteredrevs = frozenset()
435 432 self._filteredrevs_hashcache = {}
436 433 self._copiesstorage = opener.options.get(b'copies-storage')
437 434
438 435 @property
439 436 def filteredrevs(self):
440 437 return self._filteredrevs
441 438
442 439 @filteredrevs.setter
443 440 def filteredrevs(self, val):
444 441 # Ensure all updates go through this function
445 442 assert isinstance(val, frozenset)
446 443 self._filteredrevs = val
447 444 self._filteredrevs_hashcache = {}
448 445
449 446 def delayupdate(self, tr):
450 447 """delay visibility of index updates to other readers"""
451 448
452 449 if not self._delayed:
453 450 if len(self) == 0:
454 451 self._divert = True
455 452 if self._realopener.exists(self._indexfile + b'.a'):
456 453 self._realopener.unlink(self._indexfile + b'.a')
457 454 self.opener = _divertopener(self._realopener, self._indexfile)
458 455 else:
459 456 self._delaybuf = []
460 457 self.opener = _delayopener(
461 458 self._realopener, self._indexfile, self._delaybuf
462 459 )
463 460 self._delayed = True
464 461 tr.addpending(b'cl-%i' % id(self), self._writepending)
465 462 tr.addfinalize(b'cl-%i' % id(self), self._finalize)
466 463
467 464 def _finalize(self, tr):
468 465 """finalize index updates"""
469 466 self._delayed = False
470 467 self.opener = self._realopener
471 468 # move redirected index data back into place
472 469 if self._divert:
473 470 assert not self._delaybuf
474 471 tmpname = self._indexfile + b".a"
475 472 nfile = self.opener.open(tmpname)
476 473 nfile.close()
477 474 self.opener.rename(tmpname, self._indexfile, checkambig=True)
478 475 elif self._delaybuf:
479 476 fp = self.opener(self._indexfile, b'a', checkambig=True)
480 477 fp.write(b"".join(self._delaybuf))
481 478 fp.close()
482 479 self._delaybuf = None
483 480 self._divert = False
484 481 # split when we're done
485 482 self._enforceinlinesize(tr)
486 483
487 484 def _writepending(self, tr):
488 485 """create a file containing the unfinalized state for
489 486 pretxnchangegroup"""
490 487 if self._delaybuf:
491 488 # make a temporary copy of the index
492 489 fp1 = self._realopener(self._indexfile)
493 490 pendingfilename = self._indexfile + b".a"
494 491 # register as a temp file to ensure cleanup on failure
495 492 tr.registertmp(pendingfilename)
496 493 # write existing data
497 494 fp2 = self._realopener(pendingfilename, b"w")
498 495 fp2.write(fp1.read())
499 496 # add pending data
500 497 fp2.write(b"".join(self._delaybuf))
501 498 fp2.close()
502 499 # switch modes so finalize can simply rename
503 500 self._delaybuf = None
504 501 self._divert = True
505 502 self.opener = _divertopener(self._realopener, self._indexfile)
506 503
507 504 if self._divert:
508 505 return True
509 506
510 507 return False
511 508
512 509 def _enforceinlinesize(self, tr, fp=None):
513 510 if not self._delayed:
514 511 revlog.revlog._enforceinlinesize(self, tr, fp)
515 512
516 513 def read(self, nodeorrev):
517 514 """Obtain data from a parsed changelog revision.
518 515
519 516 Returns a 6-tuple of:
520 517
521 518 - manifest node in binary
522 519 - author/user as a localstr
523 520 - date as a 2-tuple of (time, timezone)
524 521 - list of files
525 522 - commit message as a localstr
526 523 - dict of extra metadata
527 524
528 525 Unless you need to access all fields, consider calling
529 526 ``changelogrevision`` instead, as it is faster for partial object
530 527 access.
531 528 """
532 529 d, s = self._revisiondata(nodeorrev)
533 530 c = changelogrevision(
534 531 self, d, s, self._copiesstorage == b'changeset-sidedata'
535 532 )
536 533 return (c.manifest, c.user, c.date, c.files, c.description, c.extra)
537 534
538 535 def changelogrevision(self, nodeorrev):
539 536 """Obtain a ``changelogrevision`` for a node or revision."""
540 537 text, sidedata = self._revisiondata(nodeorrev)
541 538 return changelogrevision(
542 539 self, text, sidedata, self._copiesstorage == b'changeset-sidedata'
543 540 )
544 541
545 542 def readfiles(self, nodeorrev):
546 543 """
547 544 short version of read that only returns the files modified by the cset
548 545 """
549 546 text = self.revision(nodeorrev)
550 547 if not text:
551 548 return []
552 549 last = text.index(b"\n\n")
553 550 l = text[:last].split(b'\n')
554 551 return l[3:]
555 552
556 553 def add(
557 554 self,
558 555 manifest,
559 556 files,
560 557 desc,
561 558 transaction,
562 559 p1,
563 560 p2,
564 561 user,
565 562 date=None,
566 563 extra=None,
567 564 ):
568 565 # Convert to UTF-8 encoded bytestrings as the very first
569 566 # thing: calling any method on a localstr object will turn it
570 567 # into a str object and the cached UTF-8 string is thus lost.
571 568 user, desc = encoding.fromlocal(user), encoding.fromlocal(desc)
572 569
573 570 user = user.strip()
574 571 # An empty username or a username with a "\n" will make the
575 572 # revision text contain two "\n\n" sequences -> corrupt
576 573 # repository since read cannot unpack the revision.
577 574 if not user:
578 575 raise error.StorageError(_(b"empty username"))
579 576 if b"\n" in user:
580 577 raise error.StorageError(
581 578 _(b"username %r contains a newline") % pycompat.bytestr(user)
582 579 )
583 580
584 581 desc = stripdesc(desc)
585 582
586 583 if date:
587 584 parseddate = b"%d %d" % dateutil.parsedate(date)
588 585 else:
589 586 parseddate = b"%d %d" % dateutil.makedate()
590 587 if extra:
591 588 branch = extra.get(b"branch")
592 589 if branch in (b"default", b""):
593 590 del extra[b"branch"]
594 591 elif branch in (b".", b"null", b"tip"):
595 592 raise error.StorageError(
596 593 _(b'the name \'%s\' is reserved') % branch
597 594 )
598 595 sortedfiles = sorted(files.touched)
599 596 flags = 0
600 597 sidedata = None
601 598 if self._copiesstorage == b'changeset-sidedata':
602 599 if files.has_copies_info:
603 600 flags |= flagutil.REVIDX_HASCOPIESINFO
604 601 sidedata = metadata.encode_files_sidedata(files)
605 602
606 603 if extra:
607 604 extra = encodeextra(extra)
608 605 parseddate = b"%s %s" % (parseddate, extra)
609 606 l = [hex(manifest), user, parseddate] + sortedfiles + [b"", desc]
610 607 text = b"\n".join(l)
611 608 rev = self.addrevision(
612 609 text, transaction, len(self), p1, p2, sidedata=sidedata, flags=flags
613 610 )
614 611 return self.node(rev)
615 612
616 613 def branchinfo(self, rev):
617 614 """return the branch name and open/close state of a revision
618 615
619 616 This function exists because creating a changectx object
620 617 just to access this is costly."""
621 618 return self.changelogrevision(rev).branchinfo
622 619
623 620 def _nodeduplicatecallback(self, transaction, rev):
624 621 # keep track of revisions that got "re-added", eg: unbunde of know rev.
625 622 #
626 623 # We track them in a list to preserve their order from the source bundle
627 624 duplicates = transaction.changes.setdefault(b'revduplicates', [])
628 625 duplicates.append(rev)
@@ -1,3931 +1,3931 b''
1 1 # cmdutil.py - help for command processing in mercurial
2 2 #
3 3 # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import copy as copymod
11 11 import errno
12 12 import os
13 13 import re
14 14
15 15 from .i18n import _
16 16 from .node import (
17 17 hex,
18 18 nullrev,
19 19 short,
20 20 )
21 21 from .pycompat import (
22 22 getattr,
23 23 open,
24 24 setattr,
25 25 )
26 26 from .thirdparty import attr
27 27
28 28 from . import (
29 29 bookmarks,
30 30 changelog,
31 31 copies,
32 32 crecord as crecordmod,
33 33 dirstateguard,
34 34 encoding,
35 35 error,
36 36 formatter,
37 37 logcmdutil,
38 38 match as matchmod,
39 39 merge as mergemod,
40 40 mergestate as mergestatemod,
41 41 mergeutil,
42 42 obsolete,
43 43 patch,
44 44 pathutil,
45 45 phases,
46 46 pycompat,
47 47 repair,
48 48 revlog,
49 49 rewriteutil,
50 50 scmutil,
51 51 state as statemod,
52 52 subrepoutil,
53 53 templatekw,
54 54 templater,
55 55 util,
56 56 vfs as vfsmod,
57 57 )
58 58
59 59 from .utils import (
60 60 dateutil,
61 61 stringutil,
62 62 )
63 63
64 64 from .revlogutils import (
65 65 constants as revlog_constants,
66 66 )
67 67
68 68 if pycompat.TYPE_CHECKING:
69 69 from typing import (
70 70 Any,
71 71 Dict,
72 72 )
73 73
74 74 for t in (Any, Dict):
75 75 assert t
76 76
77 77 stringio = util.stringio
78 78
79 79 # templates of common command options
80 80
81 81 dryrunopts = [
82 82 (b'n', b'dry-run', None, _(b'do not perform actions, just print output')),
83 83 ]
84 84
85 85 confirmopts = [
86 86 (b'', b'confirm', None, _(b'ask before applying actions')),
87 87 ]
88 88
89 89 remoteopts = [
90 90 (b'e', b'ssh', b'', _(b'specify ssh command to use'), _(b'CMD')),
91 91 (
92 92 b'',
93 93 b'remotecmd',
94 94 b'',
95 95 _(b'specify hg command to run on the remote side'),
96 96 _(b'CMD'),
97 97 ),
98 98 (
99 99 b'',
100 100 b'insecure',
101 101 None,
102 102 _(b'do not verify server certificate (ignoring web.cacerts config)'),
103 103 ),
104 104 ]
105 105
106 106 walkopts = [
107 107 (
108 108 b'I',
109 109 b'include',
110 110 [],
111 111 _(b'include names matching the given patterns'),
112 112 _(b'PATTERN'),
113 113 ),
114 114 (
115 115 b'X',
116 116 b'exclude',
117 117 [],
118 118 _(b'exclude names matching the given patterns'),
119 119 _(b'PATTERN'),
120 120 ),
121 121 ]
122 122
123 123 commitopts = [
124 124 (b'm', b'message', b'', _(b'use text as commit message'), _(b'TEXT')),
125 125 (b'l', b'logfile', b'', _(b'read commit message from file'), _(b'FILE')),
126 126 ]
127 127
128 128 commitopts2 = [
129 129 (
130 130 b'd',
131 131 b'date',
132 132 b'',
133 133 _(b'record the specified date as commit date'),
134 134 _(b'DATE'),
135 135 ),
136 136 (
137 137 b'u',
138 138 b'user',
139 139 b'',
140 140 _(b'record the specified user as committer'),
141 141 _(b'USER'),
142 142 ),
143 143 ]
144 144
145 145 commitopts3 = [
146 146 (b'D', b'currentdate', None, _(b'record the current date as commit date')),
147 147 (b'U', b'currentuser', None, _(b'record the current user as committer')),
148 148 ]
149 149
150 150 formatteropts = [
151 151 (b'T', b'template', b'', _(b'display with template'), _(b'TEMPLATE')),
152 152 ]
153 153
154 154 templateopts = [
155 155 (
156 156 b'',
157 157 b'style',
158 158 b'',
159 159 _(b'display using template map file (DEPRECATED)'),
160 160 _(b'STYLE'),
161 161 ),
162 162 (b'T', b'template', b'', _(b'display with template'), _(b'TEMPLATE')),
163 163 ]
164 164
165 165 logopts = [
166 166 (b'p', b'patch', None, _(b'show patch')),
167 167 (b'g', b'git', None, _(b'use git extended diff format')),
168 168 (b'l', b'limit', b'', _(b'limit number of changes displayed'), _(b'NUM')),
169 169 (b'M', b'no-merges', None, _(b'do not show merges')),
170 170 (b'', b'stat', None, _(b'output diffstat-style summary of changes')),
171 171 (b'G', b'graph', None, _(b"show the revision DAG")),
172 172 ] + templateopts
173 173
174 174 diffopts = [
175 175 (b'a', b'text', None, _(b'treat all files as text')),
176 176 (
177 177 b'g',
178 178 b'git',
179 179 None,
180 180 _(b'use git extended diff format (DEFAULT: diff.git)'),
181 181 ),
182 182 (b'', b'binary', None, _(b'generate binary diffs in git mode (default)')),
183 183 (b'', b'nodates', None, _(b'omit dates from diff headers')),
184 184 ]
185 185
186 186 diffwsopts = [
187 187 (
188 188 b'w',
189 189 b'ignore-all-space',
190 190 None,
191 191 _(b'ignore white space when comparing lines'),
192 192 ),
193 193 (
194 194 b'b',
195 195 b'ignore-space-change',
196 196 None,
197 197 _(b'ignore changes in the amount of white space'),
198 198 ),
199 199 (
200 200 b'B',
201 201 b'ignore-blank-lines',
202 202 None,
203 203 _(b'ignore changes whose lines are all blank'),
204 204 ),
205 205 (
206 206 b'Z',
207 207 b'ignore-space-at-eol',
208 208 None,
209 209 _(b'ignore changes in whitespace at EOL'),
210 210 ),
211 211 ]
212 212
213 213 diffopts2 = (
214 214 [
215 215 (b'', b'noprefix', None, _(b'omit a/ and b/ prefixes from filenames')),
216 216 (
217 217 b'p',
218 218 b'show-function',
219 219 None,
220 220 _(
221 221 b'show which function each change is in (DEFAULT: diff.showfunc)'
222 222 ),
223 223 ),
224 224 (b'', b'reverse', None, _(b'produce a diff that undoes the changes')),
225 225 ]
226 226 + diffwsopts
227 227 + [
228 228 (
229 229 b'U',
230 230 b'unified',
231 231 b'',
232 232 _(b'number of lines of context to show'),
233 233 _(b'NUM'),
234 234 ),
235 235 (b'', b'stat', None, _(b'output diffstat-style summary of changes')),
236 236 (
237 237 b'',
238 238 b'root',
239 239 b'',
240 240 _(b'produce diffs relative to subdirectory'),
241 241 _(b'DIR'),
242 242 ),
243 243 ]
244 244 )
245 245
246 246 mergetoolopts = [
247 247 (b't', b'tool', b'', _(b'specify merge tool'), _(b'TOOL')),
248 248 ]
249 249
250 250 similarityopts = [
251 251 (
252 252 b's',
253 253 b'similarity',
254 254 b'',
255 255 _(b'guess renamed files by similarity (0<=s<=100)'),
256 256 _(b'SIMILARITY'),
257 257 )
258 258 ]
259 259
260 260 subrepoopts = [(b'S', b'subrepos', None, _(b'recurse into subrepositories'))]
261 261
262 262 debugrevlogopts = [
263 263 (b'c', b'changelog', False, _(b'open changelog')),
264 264 (b'm', b'manifest', False, _(b'open manifest')),
265 265 (b'', b'dir', b'', _(b'open directory manifest')),
266 266 ]
267 267
268 268 # special string such that everything below this line will be ingored in the
269 269 # editor text
270 270 _linebelow = b"^HG: ------------------------ >8 ------------------------$"
271 271
272 272
273 273 def check_at_most_one_arg(opts, *args):
274 274 """abort if more than one of the arguments are in opts
275 275
276 276 Returns the unique argument or None if none of them were specified.
277 277 """
278 278
279 279 def to_display(name):
280 280 return pycompat.sysbytes(name).replace(b'_', b'-')
281 281
282 282 previous = None
283 283 for x in args:
284 284 if opts.get(x):
285 285 if previous:
286 286 raise error.InputError(
287 287 _(b'cannot specify both --%s and --%s')
288 288 % (to_display(previous), to_display(x))
289 289 )
290 290 previous = x
291 291 return previous
292 292
293 293
294 294 def check_incompatible_arguments(opts, first, others):
295 295 """abort if the first argument is given along with any of the others
296 296
297 297 Unlike check_at_most_one_arg(), `others` are not mutually exclusive
298 298 among themselves, and they're passed as a single collection.
299 299 """
300 300 for other in others:
301 301 check_at_most_one_arg(opts, first, other)
302 302
303 303
304 304 def resolvecommitoptions(ui, opts):
305 305 """modify commit options dict to handle related options
306 306
307 307 The return value indicates that ``rewrite.update-timestamp`` is the reason
308 308 the ``date`` option is set.
309 309 """
310 310 check_at_most_one_arg(opts, b'date', b'currentdate')
311 311 check_at_most_one_arg(opts, b'user', b'currentuser')
312 312
313 313 datemaydiffer = False # date-only change should be ignored?
314 314
315 315 if opts.get(b'currentdate'):
316 316 opts[b'date'] = b'%d %d' % dateutil.makedate()
317 317 elif (
318 318 not opts.get(b'date')
319 319 and ui.configbool(b'rewrite', b'update-timestamp')
320 320 and opts.get(b'currentdate') is None
321 321 ):
322 322 opts[b'date'] = b'%d %d' % dateutil.makedate()
323 323 datemaydiffer = True
324 324
325 325 if opts.get(b'currentuser'):
326 326 opts[b'user'] = ui.username()
327 327
328 328 return datemaydiffer
329 329
330 330
331 331 def checknotesize(ui, opts):
332 332 """make sure note is of valid format"""
333 333
334 334 note = opts.get(b'note')
335 335 if not note:
336 336 return
337 337
338 338 if len(note) > 255:
339 339 raise error.InputError(_(b"cannot store a note of more than 255 bytes"))
340 340 if b'\n' in note:
341 341 raise error.InputError(_(b"note cannot contain a newline"))
342 342
343 343
344 344 def ishunk(x):
345 345 hunkclasses = (crecordmod.uihunk, patch.recordhunk)
346 346 return isinstance(x, hunkclasses)
347 347
348 348
349 349 def newandmodified(chunks, originalchunks):
350 350 newlyaddedandmodifiedfiles = set()
351 351 alsorestore = set()
352 352 for chunk in chunks:
353 353 if (
354 354 ishunk(chunk)
355 355 and chunk.header.isnewfile()
356 356 and chunk not in originalchunks
357 357 ):
358 358 newlyaddedandmodifiedfiles.add(chunk.header.filename())
359 359 alsorestore.update(
360 360 set(chunk.header.files()) - {chunk.header.filename()}
361 361 )
362 362 return newlyaddedandmodifiedfiles, alsorestore
363 363
364 364
365 365 def parsealiases(cmd):
366 366 base_aliases = cmd.split(b"|")
367 367 all_aliases = set(base_aliases)
368 368 extra_aliases = []
369 369 for alias in base_aliases:
370 370 if b'-' in alias:
371 371 folded_alias = alias.replace(b'-', b'')
372 372 if folded_alias not in all_aliases:
373 373 all_aliases.add(folded_alias)
374 374 extra_aliases.append(folded_alias)
375 375 base_aliases.extend(extra_aliases)
376 376 return base_aliases
377 377
378 378
379 379 def setupwrapcolorwrite(ui):
380 380 # wrap ui.write so diff output can be labeled/colorized
381 381 def wrapwrite(orig, *args, **kw):
382 382 label = kw.pop('label', b'')
383 383 for chunk, l in patch.difflabel(lambda: args):
384 384 orig(chunk, label=label + l)
385 385
386 386 oldwrite = ui.write
387 387
388 388 def wrap(*args, **kwargs):
389 389 return wrapwrite(oldwrite, *args, **kwargs)
390 390
391 391 setattr(ui, 'write', wrap)
392 392 return oldwrite
393 393
394 394
395 395 def filterchunks(ui, originalhunks, usecurses, testfile, match, operation=None):
396 396 try:
397 397 if usecurses:
398 398 if testfile:
399 399 recordfn = crecordmod.testdecorator(
400 400 testfile, crecordmod.testchunkselector
401 401 )
402 402 else:
403 403 recordfn = crecordmod.chunkselector
404 404
405 405 return crecordmod.filterpatch(
406 406 ui, originalhunks, recordfn, operation
407 407 )
408 408 except crecordmod.fallbackerror as e:
409 409 ui.warn(b'%s\n' % e)
410 410 ui.warn(_(b'falling back to text mode\n'))
411 411
412 412 return patch.filterpatch(ui, originalhunks, match, operation)
413 413
414 414
415 415 def recordfilter(ui, originalhunks, match, operation=None):
416 416 """Prompts the user to filter the originalhunks and return a list of
417 417 selected hunks.
418 418 *operation* is used for to build ui messages to indicate the user what
419 419 kind of filtering they are doing: reverting, committing, shelving, etc.
420 420 (see patch.filterpatch).
421 421 """
422 422 usecurses = crecordmod.checkcurses(ui)
423 423 testfile = ui.config(b'experimental', b'crecordtest')
424 424 oldwrite = setupwrapcolorwrite(ui)
425 425 try:
426 426 newchunks, newopts = filterchunks(
427 427 ui, originalhunks, usecurses, testfile, match, operation
428 428 )
429 429 finally:
430 430 ui.write = oldwrite
431 431 return newchunks, newopts
432 432
433 433
434 434 def dorecord(
435 435 ui, repo, commitfunc, cmdsuggest, backupall, filterfn, *pats, **opts
436 436 ):
437 437 opts = pycompat.byteskwargs(opts)
438 438 if not ui.interactive():
439 439 if cmdsuggest:
440 440 msg = _(b'running non-interactively, use %s instead') % cmdsuggest
441 441 else:
442 442 msg = _(b'running non-interactively')
443 443 raise error.InputError(msg)
444 444
445 445 # make sure username is set before going interactive
446 446 if not opts.get(b'user'):
447 447 ui.username() # raise exception, username not provided
448 448
449 449 def recordfunc(ui, repo, message, match, opts):
450 450 """This is generic record driver.
451 451
452 452 Its job is to interactively filter local changes, and
453 453 accordingly prepare working directory into a state in which the
454 454 job can be delegated to a non-interactive commit command such as
455 455 'commit' or 'qrefresh'.
456 456
457 457 After the actual job is done by non-interactive command, the
458 458 working directory is restored to its original state.
459 459
460 460 In the end we'll record interesting changes, and everything else
461 461 will be left in place, so the user can continue working.
462 462 """
463 463 if not opts.get(b'interactive-unshelve'):
464 464 checkunfinished(repo, commit=True)
465 465 wctx = repo[None]
466 466 merge = len(wctx.parents()) > 1
467 467 if merge:
468 468 raise error.InputError(
469 469 _(
470 470 b'cannot partially commit a merge '
471 471 b'(use "hg commit" instead)'
472 472 )
473 473 )
474 474
475 475 def fail(f, msg):
476 476 raise error.InputError(b'%s: %s' % (f, msg))
477 477
478 478 force = opts.get(b'force')
479 479 if not force:
480 480 match = matchmod.badmatch(match, fail)
481 481
482 482 status = repo.status(match=match)
483 483
484 484 overrides = {(b'ui', b'commitsubrepos'): True}
485 485
486 486 with repo.ui.configoverride(overrides, b'record'):
487 487 # subrepoutil.precommit() modifies the status
488 488 tmpstatus = scmutil.status(
489 489 copymod.copy(status.modified),
490 490 copymod.copy(status.added),
491 491 copymod.copy(status.removed),
492 492 copymod.copy(status.deleted),
493 493 copymod.copy(status.unknown),
494 494 copymod.copy(status.ignored),
495 495 copymod.copy(status.clean), # pytype: disable=wrong-arg-count
496 496 )
497 497
498 498 # Force allows -X subrepo to skip the subrepo.
499 499 subs, commitsubs, newstate = subrepoutil.precommit(
500 500 repo.ui, wctx, tmpstatus, match, force=True
501 501 )
502 502 for s in subs:
503 503 if s in commitsubs:
504 504 dirtyreason = wctx.sub(s).dirtyreason(True)
505 505 raise error.Abort(dirtyreason)
506 506
507 507 if not force:
508 508 repo.checkcommitpatterns(wctx, match, status, fail)
509 509 diffopts = patch.difffeatureopts(
510 510 ui,
511 511 opts=opts,
512 512 whitespace=True,
513 513 section=b'commands',
514 514 configprefix=b'commit.interactive.',
515 515 )
516 516 diffopts.nodates = True
517 517 diffopts.git = True
518 518 diffopts.showfunc = True
519 519 originaldiff = patch.diff(repo, changes=status, opts=diffopts)
520 520 originalchunks = patch.parsepatch(originaldiff)
521 521 match = scmutil.match(repo[None], pats)
522 522
523 523 # 1. filter patch, since we are intending to apply subset of it
524 524 try:
525 525 chunks, newopts = filterfn(ui, originalchunks, match)
526 526 except error.PatchError as err:
527 527 raise error.InputError(_(b'error parsing patch: %s') % err)
528 528 opts.update(newopts)
529 529
530 530 # We need to keep a backup of files that have been newly added and
531 531 # modified during the recording process because there is a previous
532 532 # version without the edit in the workdir. We also will need to restore
533 533 # files that were the sources of renames so that the patch application
534 534 # works.
535 535 newlyaddedandmodifiedfiles, alsorestore = newandmodified(
536 536 chunks, originalchunks
537 537 )
538 538 contenders = set()
539 539 for h in chunks:
540 540 try:
541 541 contenders.update(set(h.files()))
542 542 except AttributeError:
543 543 pass
544 544
545 545 changed = status.modified + status.added + status.removed
546 546 newfiles = [f for f in changed if f in contenders]
547 547 if not newfiles:
548 548 ui.status(_(b'no changes to record\n'))
549 549 return 0
550 550
551 551 modified = set(status.modified)
552 552
553 553 # 2. backup changed files, so we can restore them in the end
554 554
555 555 if backupall:
556 556 tobackup = changed
557 557 else:
558 558 tobackup = [
559 559 f
560 560 for f in newfiles
561 561 if f in modified or f in newlyaddedandmodifiedfiles
562 562 ]
563 563 backups = {}
564 564 if tobackup:
565 565 backupdir = repo.vfs.join(b'record-backups')
566 566 try:
567 567 os.mkdir(backupdir)
568 568 except OSError as err:
569 569 if err.errno != errno.EEXIST:
570 570 raise
571 571 try:
572 572 # backup continues
573 573 for f in tobackup:
574 574 fd, tmpname = pycompat.mkstemp(
575 575 prefix=os.path.basename(f) + b'.', dir=backupdir
576 576 )
577 577 os.close(fd)
578 578 ui.debug(b'backup %r as %r\n' % (f, tmpname))
579 579 util.copyfile(repo.wjoin(f), tmpname, copystat=True)
580 580 backups[f] = tmpname
581 581
582 582 fp = stringio()
583 583 for c in chunks:
584 584 fname = c.filename()
585 585 if fname in backups:
586 586 c.write(fp)
587 587 dopatch = fp.tell()
588 588 fp.seek(0)
589 589
590 590 # 2.5 optionally review / modify patch in text editor
591 591 if opts.get(b'review', False):
592 592 patchtext = (
593 593 crecordmod.diffhelptext
594 594 + crecordmod.patchhelptext
595 595 + fp.read()
596 596 )
597 597 reviewedpatch = ui.edit(
598 598 patchtext, b"", action=b"diff", repopath=repo.path
599 599 )
600 600 fp.truncate(0)
601 601 fp.write(reviewedpatch)
602 602 fp.seek(0)
603 603
604 604 [os.unlink(repo.wjoin(c)) for c in newlyaddedandmodifiedfiles]
605 605 # 3a. apply filtered patch to clean repo (clean)
606 606 if backups:
607 607 m = scmutil.matchfiles(repo, set(backups.keys()) | alsorestore)
608 608 mergemod.revert_to(repo[b'.'], matcher=m)
609 609
610 610 # 3b. (apply)
611 611 if dopatch:
612 612 try:
613 613 ui.debug(b'applying patch\n')
614 614 ui.debug(fp.getvalue())
615 615 patch.internalpatch(ui, repo, fp, 1, eolmode=None)
616 616 except error.PatchError as err:
617 617 raise error.InputError(pycompat.bytestr(err))
618 618 del fp
619 619
620 620 # 4. We prepared working directory according to filtered
621 621 # patch. Now is the time to delegate the job to
622 622 # commit/qrefresh or the like!
623 623
624 624 # Make all of the pathnames absolute.
625 625 newfiles = [repo.wjoin(nf) for nf in newfiles]
626 626 return commitfunc(ui, repo, *newfiles, **pycompat.strkwargs(opts))
627 627 finally:
628 628 # 5. finally restore backed-up files
629 629 try:
630 630 dirstate = repo.dirstate
631 631 for realname, tmpname in pycompat.iteritems(backups):
632 632 ui.debug(b'restoring %r to %r\n' % (tmpname, realname))
633 633
634 634 if dirstate[realname] == b'n':
635 635 # without normallookup, restoring timestamp
636 636 # may cause partially committed files
637 637 # to be treated as unmodified
638 638 dirstate.normallookup(realname)
639 639
640 640 # copystat=True here and above are a hack to trick any
641 641 # editors that have f open that we haven't modified them.
642 642 #
643 643 # Also note that this racy as an editor could notice the
644 644 # file's mtime before we've finished writing it.
645 645 util.copyfile(tmpname, repo.wjoin(realname), copystat=True)
646 646 os.unlink(tmpname)
647 647 if tobackup:
648 648 os.rmdir(backupdir)
649 649 except OSError:
650 650 pass
651 651
652 652 def recordinwlock(ui, repo, message, match, opts):
653 653 with repo.wlock():
654 654 return recordfunc(ui, repo, message, match, opts)
655 655
656 656 return commit(ui, repo, recordinwlock, pats, opts)
657 657
658 658
659 659 class dirnode(object):
660 660 """
661 661 Represent a directory in user working copy with information required for
662 662 the purpose of tersing its status.
663 663
664 664 path is the path to the directory, without a trailing '/'
665 665
666 666 statuses is a set of statuses of all files in this directory (this includes
667 667 all the files in all the subdirectories too)
668 668
669 669 files is a list of files which are direct child of this directory
670 670
671 671 subdirs is a dictionary of sub-directory name as the key and it's own
672 672 dirnode object as the value
673 673 """
674 674
675 675 def __init__(self, dirpath):
676 676 self.path = dirpath
677 677 self.statuses = set()
678 678 self.files = []
679 679 self.subdirs = {}
680 680
681 681 def _addfileindir(self, filename, status):
682 682 """Add a file in this directory as a direct child."""
683 683 self.files.append((filename, status))
684 684
685 685 def addfile(self, filename, status):
686 686 """
687 687 Add a file to this directory or to its direct parent directory.
688 688
689 689 If the file is not direct child of this directory, we traverse to the
690 690 directory of which this file is a direct child of and add the file
691 691 there.
692 692 """
693 693
694 694 # the filename contains a path separator, it means it's not the direct
695 695 # child of this directory
696 696 if b'/' in filename:
697 697 subdir, filep = filename.split(b'/', 1)
698 698
699 699 # does the dirnode object for subdir exists
700 700 if subdir not in self.subdirs:
701 701 subdirpath = pathutil.join(self.path, subdir)
702 702 self.subdirs[subdir] = dirnode(subdirpath)
703 703
704 704 # try adding the file in subdir
705 705 self.subdirs[subdir].addfile(filep, status)
706 706
707 707 else:
708 708 self._addfileindir(filename, status)
709 709
710 710 if status not in self.statuses:
711 711 self.statuses.add(status)
712 712
713 713 def iterfilepaths(self):
714 714 """Yield (status, path) for files directly under this directory."""
715 715 for f, st in self.files:
716 716 yield st, pathutil.join(self.path, f)
717 717
718 718 def tersewalk(self, terseargs):
719 719 """
720 720 Yield (status, path) obtained by processing the status of this
721 721 dirnode.
722 722
723 723 terseargs is the string of arguments passed by the user with `--terse`
724 724 flag.
725 725
726 726 Following are the cases which can happen:
727 727
728 728 1) All the files in the directory (including all the files in its
729 729 subdirectories) share the same status and the user has asked us to terse
730 730 that status. -> yield (status, dirpath). dirpath will end in '/'.
731 731
732 732 2) Otherwise, we do following:
733 733
734 734 a) Yield (status, filepath) for all the files which are in this
735 735 directory (only the ones in this directory, not the subdirs)
736 736
737 737 b) Recurse the function on all the subdirectories of this
738 738 directory
739 739 """
740 740
741 741 if len(self.statuses) == 1:
742 742 onlyst = self.statuses.pop()
743 743
744 744 # Making sure we terse only when the status abbreviation is
745 745 # passed as terse argument
746 746 if onlyst in terseargs:
747 747 yield onlyst, self.path + b'/'
748 748 return
749 749
750 750 # add the files to status list
751 751 for st, fpath in self.iterfilepaths():
752 752 yield st, fpath
753 753
754 754 # recurse on the subdirs
755 755 for dirobj in self.subdirs.values():
756 756 for st, fpath in dirobj.tersewalk(terseargs):
757 757 yield st, fpath
758 758
759 759
760 760 def tersedir(statuslist, terseargs):
761 761 """
762 762 Terse the status if all the files in a directory shares the same status.
763 763
764 764 statuslist is scmutil.status() object which contains a list of files for
765 765 each status.
766 766 terseargs is string which is passed by the user as the argument to `--terse`
767 767 flag.
768 768
769 769 The function makes a tree of objects of dirnode class, and at each node it
770 770 stores the information required to know whether we can terse a certain
771 771 directory or not.
772 772 """
773 773 # the order matters here as that is used to produce final list
774 774 allst = (b'm', b'a', b'r', b'd', b'u', b'i', b'c')
775 775
776 776 # checking the argument validity
777 777 for s in pycompat.bytestr(terseargs):
778 778 if s not in allst:
779 779 raise error.InputError(_(b"'%s' not recognized") % s)
780 780
781 781 # creating a dirnode object for the root of the repo
782 782 rootobj = dirnode(b'')
783 783 pstatus = (
784 784 b'modified',
785 785 b'added',
786 786 b'deleted',
787 787 b'clean',
788 788 b'unknown',
789 789 b'ignored',
790 790 b'removed',
791 791 )
792 792
793 793 tersedict = {}
794 794 for attrname in pstatus:
795 795 statuschar = attrname[0:1]
796 796 for f in getattr(statuslist, attrname):
797 797 rootobj.addfile(f, statuschar)
798 798 tersedict[statuschar] = []
799 799
800 800 # we won't be tersing the root dir, so add files in it
801 801 for st, fpath in rootobj.iterfilepaths():
802 802 tersedict[st].append(fpath)
803 803
804 804 # process each sub-directory and build tersedict
805 805 for subdir in rootobj.subdirs.values():
806 806 for st, f in subdir.tersewalk(terseargs):
807 807 tersedict[st].append(f)
808 808
809 809 tersedlist = []
810 810 for st in allst:
811 811 tersedict[st].sort()
812 812 tersedlist.append(tersedict[st])
813 813
814 814 return scmutil.status(*tersedlist)
815 815
816 816
817 817 def _commentlines(raw):
818 818 '''Surround lineswith a comment char and a new line'''
819 819 lines = raw.splitlines()
820 820 commentedlines = [b'# %s' % line for line in lines]
821 821 return b'\n'.join(commentedlines) + b'\n'
822 822
823 823
824 824 @attr.s(frozen=True)
825 825 class morestatus(object):
826 826 reporoot = attr.ib()
827 827 unfinishedop = attr.ib()
828 828 unfinishedmsg = attr.ib()
829 829 activemerge = attr.ib()
830 830 unresolvedpaths = attr.ib()
831 831 _formattedpaths = attr.ib(init=False, default=set())
832 832 _label = b'status.morestatus'
833 833
834 834 def formatfile(self, path, fm):
835 835 self._formattedpaths.add(path)
836 836 if self.activemerge and path in self.unresolvedpaths:
837 837 fm.data(unresolved=True)
838 838
839 839 def formatfooter(self, fm):
840 840 if self.unfinishedop or self.unfinishedmsg:
841 841 fm.startitem()
842 842 fm.data(itemtype=b'morestatus')
843 843
844 844 if self.unfinishedop:
845 845 fm.data(unfinished=self.unfinishedop)
846 846 statemsg = (
847 847 _(b'The repository is in an unfinished *%s* state.')
848 848 % self.unfinishedop
849 849 )
850 850 fm.plain(b'%s\n' % _commentlines(statemsg), label=self._label)
851 851 if self.unfinishedmsg:
852 852 fm.data(unfinishedmsg=self.unfinishedmsg)
853 853
854 854 # May also start new data items.
855 855 self._formatconflicts(fm)
856 856
857 857 if self.unfinishedmsg:
858 858 fm.plain(
859 859 b'%s\n' % _commentlines(self.unfinishedmsg), label=self._label
860 860 )
861 861
862 862 def _formatconflicts(self, fm):
863 863 if not self.activemerge:
864 864 return
865 865
866 866 if self.unresolvedpaths:
867 867 mergeliststr = b'\n'.join(
868 868 [
869 869 b' %s'
870 870 % util.pathto(self.reporoot, encoding.getcwd(), path)
871 871 for path in self.unresolvedpaths
872 872 ]
873 873 )
874 874 msg = (
875 875 _(
876 876 b'''Unresolved merge conflicts:
877 877
878 878 %s
879 879
880 880 To mark files as resolved: hg resolve --mark FILE'''
881 881 )
882 882 % mergeliststr
883 883 )
884 884
885 885 # If any paths with unresolved conflicts were not previously
886 886 # formatted, output them now.
887 887 for f in self.unresolvedpaths:
888 888 if f in self._formattedpaths:
889 889 # Already output.
890 890 continue
891 891 fm.startitem()
892 892 # We can't claim to know the status of the file - it may just
893 893 # have been in one of the states that were not requested for
894 894 # display, so it could be anything.
895 895 fm.data(itemtype=b'file', path=f, unresolved=True)
896 896
897 897 else:
898 898 msg = _(b'No unresolved merge conflicts.')
899 899
900 900 fm.plain(b'%s\n' % _commentlines(msg), label=self._label)
901 901
902 902
903 903 def readmorestatus(repo):
904 904 """Returns a morestatus object if the repo has unfinished state."""
905 905 statetuple = statemod.getrepostate(repo)
906 906 mergestate = mergestatemod.mergestate.read(repo)
907 907 activemerge = mergestate.active()
908 908 if not statetuple and not activemerge:
909 909 return None
910 910
911 911 unfinishedop = unfinishedmsg = unresolved = None
912 912 if statetuple:
913 913 unfinishedop, unfinishedmsg = statetuple
914 914 if activemerge:
915 915 unresolved = sorted(mergestate.unresolved())
916 916 return morestatus(
917 917 repo.root, unfinishedop, unfinishedmsg, activemerge, unresolved
918 918 )
919 919
920 920
921 921 def findpossible(cmd, table, strict=False):
922 922 """
923 923 Return cmd -> (aliases, command table entry)
924 924 for each matching command.
925 925 Return debug commands (or their aliases) only if no normal command matches.
926 926 """
927 927 choice = {}
928 928 debugchoice = {}
929 929
930 930 if cmd in table:
931 931 # short-circuit exact matches, "log" alias beats "log|history"
932 932 keys = [cmd]
933 933 else:
934 934 keys = table.keys()
935 935
936 936 allcmds = []
937 937 for e in keys:
938 938 aliases = parsealiases(e)
939 939 allcmds.extend(aliases)
940 940 found = None
941 941 if cmd in aliases:
942 942 found = cmd
943 943 elif not strict:
944 944 for a in aliases:
945 945 if a.startswith(cmd):
946 946 found = a
947 947 break
948 948 if found is not None:
949 949 if aliases[0].startswith(b"debug") or found.startswith(b"debug"):
950 950 debugchoice[found] = (aliases, table[e])
951 951 else:
952 952 choice[found] = (aliases, table[e])
953 953
954 954 if not choice and debugchoice:
955 955 choice = debugchoice
956 956
957 957 return choice, allcmds
958 958
959 959
960 960 def findcmd(cmd, table, strict=True):
961 961 """Return (aliases, command table entry) for command string."""
962 962 choice, allcmds = findpossible(cmd, table, strict)
963 963
964 964 if cmd in choice:
965 965 return choice[cmd]
966 966
967 967 if len(choice) > 1:
968 968 clist = sorted(choice)
969 969 raise error.AmbiguousCommand(cmd, clist)
970 970
971 971 if choice:
972 972 return list(choice.values())[0]
973 973
974 974 raise error.UnknownCommand(cmd, allcmds)
975 975
976 976
977 977 def changebranch(ui, repo, revs, label, opts):
978 978 """Change the branch name of given revs to label"""
979 979
980 980 with repo.wlock(), repo.lock(), repo.transaction(b'branches'):
981 981 # abort in case of uncommitted merge or dirty wdir
982 982 bailifchanged(repo)
983 983 revs = scmutil.revrange(repo, revs)
984 984 if not revs:
985 985 raise error.InputError(b"empty revision set")
986 986 roots = repo.revs(b'roots(%ld)', revs)
987 987 if len(roots) > 1:
988 988 raise error.InputError(
989 989 _(b"cannot change branch of non-linear revisions")
990 990 )
991 991 rewriteutil.precheck(repo, revs, b'change branch of')
992 992
993 993 root = repo[roots.first()]
994 994 rpb = {parent.branch() for parent in root.parents()}
995 995 if (
996 996 not opts.get(b'force')
997 997 and label not in rpb
998 998 and label in repo.branchmap()
999 999 ):
1000 1000 raise error.InputError(
1001 1001 _(b"a branch of the same name already exists")
1002 1002 )
1003 1003
1004 1004 # make sure only topological heads
1005 1005 if repo.revs(b'heads(%ld) - head()', revs):
1006 1006 raise error.InputError(
1007 1007 _(b"cannot change branch in middle of a stack")
1008 1008 )
1009 1009
1010 1010 replacements = {}
1011 1011 # avoid import cycle mercurial.cmdutil -> mercurial.context ->
1012 1012 # mercurial.subrepo -> mercurial.cmdutil
1013 1013 from . import context
1014 1014
1015 1015 for rev in revs:
1016 1016 ctx = repo[rev]
1017 1017 oldbranch = ctx.branch()
1018 1018 # check if ctx has same branch
1019 1019 if oldbranch == label:
1020 1020 continue
1021 1021
1022 1022 def filectxfn(repo, newctx, path):
1023 1023 try:
1024 1024 return ctx[path]
1025 1025 except error.ManifestLookupError:
1026 1026 return None
1027 1027
1028 1028 ui.debug(
1029 1029 b"changing branch of '%s' from '%s' to '%s'\n"
1030 1030 % (hex(ctx.node()), oldbranch, label)
1031 1031 )
1032 1032 extra = ctx.extra()
1033 1033 extra[b'branch_change'] = hex(ctx.node())
1034 1034 # While changing branch of set of linear commits, make sure that
1035 1035 # we base our commits on new parent rather than old parent which
1036 1036 # was obsoleted while changing the branch
1037 1037 p1 = ctx.p1().node()
1038 1038 p2 = ctx.p2().node()
1039 1039 if p1 in replacements:
1040 1040 p1 = replacements[p1][0]
1041 1041 if p2 in replacements:
1042 1042 p2 = replacements[p2][0]
1043 1043
1044 1044 mc = context.memctx(
1045 1045 repo,
1046 1046 (p1, p2),
1047 1047 ctx.description(),
1048 1048 ctx.files(),
1049 1049 filectxfn,
1050 1050 user=ctx.user(),
1051 1051 date=ctx.date(),
1052 1052 extra=extra,
1053 1053 branch=label,
1054 1054 )
1055 1055
1056 1056 newnode = repo.commitctx(mc)
1057 1057 replacements[ctx.node()] = (newnode,)
1058 1058 ui.debug(b'new node id is %s\n' % hex(newnode))
1059 1059
1060 1060 # create obsmarkers and move bookmarks
1061 1061 scmutil.cleanupnodes(
1062 1062 repo, replacements, b'branch-change', fixphase=True
1063 1063 )
1064 1064
1065 1065 # move the working copy too
1066 1066 wctx = repo[None]
1067 1067 # in-progress merge is a bit too complex for now.
1068 1068 if len(wctx.parents()) == 1:
1069 1069 newid = replacements.get(wctx.p1().node())
1070 1070 if newid is not None:
1071 1071 # avoid import cycle mercurial.cmdutil -> mercurial.hg ->
1072 1072 # mercurial.cmdutil
1073 1073 from . import hg
1074 1074
1075 1075 hg.update(repo, newid[0], quietempty=True)
1076 1076
1077 1077 ui.status(_(b"changed branch on %d changesets\n") % len(replacements))
1078 1078
1079 1079
1080 1080 def findrepo(p):
1081 1081 while not os.path.isdir(os.path.join(p, b".hg")):
1082 1082 oldp, p = p, os.path.dirname(p)
1083 1083 if p == oldp:
1084 1084 return None
1085 1085
1086 1086 return p
1087 1087
1088 1088
1089 1089 def bailifchanged(repo, merge=True, hint=None):
1090 1090 """enforce the precondition that working directory must be clean.
1091 1091
1092 1092 'merge' can be set to false if a pending uncommitted merge should be
1093 1093 ignored (such as when 'update --check' runs).
1094 1094
1095 1095 'hint' is the usual hint given to Abort exception.
1096 1096 """
1097 1097
1098 1098 if merge and repo.dirstate.p2() != repo.nullid:
1099 1099 raise error.StateError(_(b'outstanding uncommitted merge'), hint=hint)
1100 1100 st = repo.status()
1101 1101 if st.modified or st.added or st.removed or st.deleted:
1102 1102 raise error.StateError(_(b'uncommitted changes'), hint=hint)
1103 1103 ctx = repo[None]
1104 1104 for s in sorted(ctx.substate):
1105 1105 ctx.sub(s).bailifchanged(hint=hint)
1106 1106
1107 1107
1108 1108 def logmessage(ui, opts):
1109 1109 """get the log message according to -m and -l option"""
1110 1110
1111 1111 check_at_most_one_arg(opts, b'message', b'logfile')
1112 1112
1113 1113 message = opts.get(b'message')
1114 1114 logfile = opts.get(b'logfile')
1115 1115
1116 1116 if not message and logfile:
1117 1117 try:
1118 1118 if isstdiofilename(logfile):
1119 1119 message = ui.fin.read()
1120 1120 else:
1121 1121 message = b'\n'.join(util.readfile(logfile).splitlines())
1122 1122 except IOError as inst:
1123 1123 raise error.Abort(
1124 1124 _(b"can't read commit message '%s': %s")
1125 1125 % (logfile, encoding.strtolocal(inst.strerror))
1126 1126 )
1127 1127 return message
1128 1128
1129 1129
1130 1130 def mergeeditform(ctxorbool, baseformname):
1131 1131 """return appropriate editform name (referencing a committemplate)
1132 1132
1133 1133 'ctxorbool' is either a ctx to be committed, or a bool indicating whether
1134 1134 merging is committed.
1135 1135
1136 1136 This returns baseformname with '.merge' appended if it is a merge,
1137 1137 otherwise '.normal' is appended.
1138 1138 """
1139 1139 if isinstance(ctxorbool, bool):
1140 1140 if ctxorbool:
1141 1141 return baseformname + b".merge"
1142 1142 elif len(ctxorbool.parents()) > 1:
1143 1143 return baseformname + b".merge"
1144 1144
1145 1145 return baseformname + b".normal"
1146 1146
1147 1147
1148 1148 def getcommiteditor(
1149 1149 edit=False, finishdesc=None, extramsg=None, editform=b'', **opts
1150 1150 ):
1151 1151 """get appropriate commit message editor according to '--edit' option
1152 1152
1153 1153 'finishdesc' is a function to be called with edited commit message
1154 1154 (= 'description' of the new changeset) just after editing, but
1155 1155 before checking empty-ness. It should return actual text to be
1156 1156 stored into history. This allows to change description before
1157 1157 storing.
1158 1158
1159 1159 'extramsg' is a extra message to be shown in the editor instead of
1160 1160 'Leave message empty to abort commit' line. 'HG: ' prefix and EOL
1161 1161 is automatically added.
1162 1162
1163 1163 'editform' is a dot-separated list of names, to distinguish
1164 1164 the purpose of commit text editing.
1165 1165
1166 1166 'getcommiteditor' returns 'commitforceeditor' regardless of
1167 1167 'edit', if one of 'finishdesc' or 'extramsg' is specified, because
1168 1168 they are specific for usage in MQ.
1169 1169 """
1170 1170 if edit or finishdesc or extramsg:
1171 1171 return lambda r, c, s: commitforceeditor(
1172 1172 r, c, s, finishdesc=finishdesc, extramsg=extramsg, editform=editform
1173 1173 )
1174 1174 elif editform:
1175 1175 return lambda r, c, s: commiteditor(r, c, s, editform=editform)
1176 1176 else:
1177 1177 return commiteditor
1178 1178
1179 1179
1180 1180 def _escapecommandtemplate(tmpl):
1181 1181 parts = []
1182 1182 for typ, start, end in templater.scantemplate(tmpl, raw=True):
1183 1183 if typ == b'string':
1184 1184 parts.append(stringutil.escapestr(tmpl[start:end]))
1185 1185 else:
1186 1186 parts.append(tmpl[start:end])
1187 1187 return b''.join(parts)
1188 1188
1189 1189
1190 1190 def rendercommandtemplate(ui, tmpl, props):
1191 1191 r"""Expand a literal template 'tmpl' in a way suitable for command line
1192 1192
1193 1193 '\' in outermost string is not taken as an escape character because it
1194 1194 is a directory separator on Windows.
1195 1195
1196 1196 >>> from . import ui as uimod
1197 1197 >>> ui = uimod.ui()
1198 1198 >>> rendercommandtemplate(ui, b'c:\\{path}', {b'path': b'foo'})
1199 1199 'c:\\foo'
1200 1200 >>> rendercommandtemplate(ui, b'{"c:\\{path}"}', {'path': b'foo'})
1201 1201 'c:{path}'
1202 1202 """
1203 1203 if not tmpl:
1204 1204 return tmpl
1205 1205 t = formatter.maketemplater(ui, _escapecommandtemplate(tmpl))
1206 1206 return t.renderdefault(props)
1207 1207
1208 1208
1209 1209 def rendertemplate(ctx, tmpl, props=None):
1210 1210 """Expand a literal template 'tmpl' byte-string against one changeset
1211 1211
1212 1212 Each props item must be a stringify-able value or a callable returning
1213 1213 such value, i.e. no bare list nor dict should be passed.
1214 1214 """
1215 1215 repo = ctx.repo()
1216 1216 tres = formatter.templateresources(repo.ui, repo)
1217 1217 t = formatter.maketemplater(
1218 1218 repo.ui, tmpl, defaults=templatekw.keywords, resources=tres
1219 1219 )
1220 1220 mapping = {b'ctx': ctx}
1221 1221 if props:
1222 1222 mapping.update(props)
1223 1223 return t.renderdefault(mapping)
1224 1224
1225 1225
1226 1226 def format_changeset_summary(ui, ctx, command=None, default_spec=None):
1227 1227 """Format a changeset summary (one line)."""
1228 1228 spec = None
1229 1229 if command:
1230 1230 spec = ui.config(
1231 1231 b'command-templates', b'oneline-summary.%s' % command, None
1232 1232 )
1233 1233 if not spec:
1234 1234 spec = ui.config(b'command-templates', b'oneline-summary')
1235 1235 if not spec:
1236 1236 spec = default_spec
1237 1237 if not spec:
1238 1238 spec = (
1239 1239 b'{separate(" ", '
1240 1240 b'label("oneline-summary.changeset", "{rev}:{node|short}")'
1241 1241 b', '
1242 1242 b'join(filter(namespaces % "{ifeq(namespace, "branches", "", join(names % "{label("oneline-summary.{namespace}", name)}", " "))}"), " ")'
1243 1243 b')} '
1244 1244 b'"{label("oneline-summary.desc", desc|firstline)}"'
1245 1245 )
1246 1246 text = rendertemplate(ctx, spec)
1247 1247 return text.split(b'\n')[0]
1248 1248
1249 1249
1250 1250 def _buildfntemplate(pat, total=None, seqno=None, revwidth=None, pathname=None):
1251 1251 r"""Convert old-style filename format string to template string
1252 1252
1253 1253 >>> _buildfntemplate(b'foo-%b-%n.patch', seqno=0)
1254 1254 'foo-{reporoot|basename}-{seqno}.patch'
1255 1255 >>> _buildfntemplate(b'%R{tags % "{tag}"}%H')
1256 1256 '{rev}{tags % "{tag}"}{node}'
1257 1257
1258 1258 '\' in outermost strings has to be escaped because it is a directory
1259 1259 separator on Windows:
1260 1260
1261 1261 >>> _buildfntemplate(b'c:\\tmp\\%R\\%n.patch', seqno=0)
1262 1262 'c:\\\\tmp\\\\{rev}\\\\{seqno}.patch'
1263 1263 >>> _buildfntemplate(b'\\\\foo\\bar.patch')
1264 1264 '\\\\\\\\foo\\\\bar.patch'
1265 1265 >>> _buildfntemplate(b'\\{tags % "{tag}"}')
1266 1266 '\\\\{tags % "{tag}"}'
1267 1267
1268 1268 but inner strings follow the template rules (i.e. '\' is taken as an
1269 1269 escape character):
1270 1270
1271 1271 >>> _buildfntemplate(br'{"c:\tmp"}', seqno=0)
1272 1272 '{"c:\\tmp"}'
1273 1273 """
1274 1274 expander = {
1275 1275 b'H': b'{node}',
1276 1276 b'R': b'{rev}',
1277 1277 b'h': b'{node|short}',
1278 1278 b'm': br'{sub(r"[^\w]", "_", desc|firstline)}',
1279 1279 b'r': b'{if(revwidth, pad(rev, revwidth, "0", left=True), rev)}',
1280 1280 b'%': b'%',
1281 1281 b'b': b'{reporoot|basename}',
1282 1282 }
1283 1283 if total is not None:
1284 1284 expander[b'N'] = b'{total}'
1285 1285 if seqno is not None:
1286 1286 expander[b'n'] = b'{seqno}'
1287 1287 if total is not None and seqno is not None:
1288 1288 expander[b'n'] = b'{pad(seqno, total|stringify|count, "0", left=True)}'
1289 1289 if pathname is not None:
1290 1290 expander[b's'] = b'{pathname|basename}'
1291 1291 expander[b'd'] = b'{if(pathname|dirname, pathname|dirname, ".")}'
1292 1292 expander[b'p'] = b'{pathname}'
1293 1293
1294 1294 newname = []
1295 1295 for typ, start, end in templater.scantemplate(pat, raw=True):
1296 1296 if typ != b'string':
1297 1297 newname.append(pat[start:end])
1298 1298 continue
1299 1299 i = start
1300 1300 while i < end:
1301 1301 n = pat.find(b'%', i, end)
1302 1302 if n < 0:
1303 1303 newname.append(stringutil.escapestr(pat[i:end]))
1304 1304 break
1305 1305 newname.append(stringutil.escapestr(pat[i:n]))
1306 1306 if n + 2 > end:
1307 1307 raise error.Abort(
1308 1308 _(b"incomplete format spec in output filename")
1309 1309 )
1310 1310 c = pat[n + 1 : n + 2]
1311 1311 i = n + 2
1312 1312 try:
1313 1313 newname.append(expander[c])
1314 1314 except KeyError:
1315 1315 raise error.Abort(
1316 1316 _(b"invalid format spec '%%%s' in output filename") % c
1317 1317 )
1318 1318 return b''.join(newname)
1319 1319
1320 1320
1321 1321 def makefilename(ctx, pat, **props):
1322 1322 if not pat:
1323 1323 return pat
1324 1324 tmpl = _buildfntemplate(pat, **props)
1325 1325 # BUG: alias expansion shouldn't be made against template fragments
1326 1326 # rewritten from %-format strings, but we have no easy way to partially
1327 1327 # disable the expansion.
1328 1328 return rendertemplate(ctx, tmpl, pycompat.byteskwargs(props))
1329 1329
1330 1330
1331 1331 def isstdiofilename(pat):
1332 1332 """True if the given pat looks like a filename denoting stdin/stdout"""
1333 1333 return not pat or pat == b'-'
1334 1334
1335 1335
1336 1336 class _unclosablefile(object):
1337 1337 def __init__(self, fp):
1338 1338 self._fp = fp
1339 1339
1340 1340 def close(self):
1341 1341 pass
1342 1342
1343 1343 def __iter__(self):
1344 1344 return iter(self._fp)
1345 1345
1346 1346 def __getattr__(self, attr):
1347 1347 return getattr(self._fp, attr)
1348 1348
1349 1349 def __enter__(self):
1350 1350 return self
1351 1351
1352 1352 def __exit__(self, exc_type, exc_value, exc_tb):
1353 1353 pass
1354 1354
1355 1355
1356 1356 def makefileobj(ctx, pat, mode=b'wb', **props):
1357 1357 writable = mode not in (b'r', b'rb')
1358 1358
1359 1359 if isstdiofilename(pat):
1360 1360 repo = ctx.repo()
1361 1361 if writable:
1362 1362 fp = repo.ui.fout
1363 1363 else:
1364 1364 fp = repo.ui.fin
1365 1365 return _unclosablefile(fp)
1366 1366 fn = makefilename(ctx, pat, **props)
1367 1367 return open(fn, mode)
1368 1368
1369 1369
1370 1370 def openstorage(repo, cmd, file_, opts, returnrevlog=False):
1371 1371 """opens the changelog, manifest, a filelog or a given revlog"""
1372 1372 cl = opts[b'changelog']
1373 1373 mf = opts[b'manifest']
1374 1374 dir = opts[b'dir']
1375 1375 msg = None
1376 1376 if cl and mf:
1377 1377 msg = _(b'cannot specify --changelog and --manifest at the same time')
1378 1378 elif cl and dir:
1379 1379 msg = _(b'cannot specify --changelog and --dir at the same time')
1380 1380 elif cl or mf or dir:
1381 1381 if file_:
1382 1382 msg = _(b'cannot specify filename with --changelog or --manifest')
1383 1383 elif not repo:
1384 1384 msg = _(
1385 1385 b'cannot specify --changelog or --manifest or --dir '
1386 1386 b'without a repository'
1387 1387 )
1388 1388 if msg:
1389 1389 raise error.InputError(msg)
1390 1390
1391 1391 r = None
1392 1392 if repo:
1393 1393 if cl:
1394 1394 r = repo.unfiltered().changelog
1395 1395 elif dir:
1396 1396 if not scmutil.istreemanifest(repo):
1397 1397 raise error.InputError(
1398 1398 _(
1399 1399 b"--dir can only be used on repos with "
1400 1400 b"treemanifest enabled"
1401 1401 )
1402 1402 )
1403 1403 if not dir.endswith(b'/'):
1404 1404 dir = dir + b'/'
1405 1405 dirlog = repo.manifestlog.getstorage(dir)
1406 1406 if len(dirlog):
1407 1407 r = dirlog
1408 1408 elif mf:
1409 1409 r = repo.manifestlog.getstorage(b'')
1410 1410 elif file_:
1411 1411 filelog = repo.file(file_)
1412 1412 if len(filelog):
1413 1413 r = filelog
1414 1414
1415 1415 # Not all storage may be revlogs. If requested, try to return an actual
1416 1416 # revlog instance.
1417 1417 if returnrevlog:
1418 1418 if isinstance(r, revlog.revlog):
1419 1419 pass
1420 1420 elif util.safehasattr(r, b'_revlog'):
1421 1421 r = r._revlog # pytype: disable=attribute-error
1422 1422 elif r is not None:
1423 1423 raise error.InputError(
1424 1424 _(b'%r does not appear to be a revlog') % r
1425 1425 )
1426 1426
1427 1427 if not r:
1428 1428 if not returnrevlog:
1429 1429 raise error.InputError(_(b'cannot give path to non-revlog'))
1430 1430
1431 1431 if not file_:
1432 1432 raise error.CommandError(cmd, _(b'invalid arguments'))
1433 1433 if not os.path.isfile(file_):
1434 1434 raise error.InputError(_(b"revlog '%s' not found") % file_)
1435 1435
1436 1436 target = (revlog_constants.KIND_OTHER, b'free-form:%s' % file_)
1437 1437 r = revlog.revlog(
1438 1438 vfsmod.vfs(encoding.getcwd(), audit=False),
1439 1439 target=target,
1440 indexfile=file_[:-2] + b".i",
1440 radix=file_[:-2],
1441 1441 )
1442 1442 return r
1443 1443
1444 1444
1445 1445 def openrevlog(repo, cmd, file_, opts):
1446 1446 """Obtain a revlog backing storage of an item.
1447 1447
1448 1448 This is similar to ``openstorage()`` except it always returns a revlog.
1449 1449
1450 1450 In most cases, a caller cares about the main storage object - not the
1451 1451 revlog backing it. Therefore, this function should only be used by code
1452 1452 that needs to examine low-level revlog implementation details. e.g. debug
1453 1453 commands.
1454 1454 """
1455 1455 return openstorage(repo, cmd, file_, opts, returnrevlog=True)
1456 1456
1457 1457
1458 1458 def copy(ui, repo, pats, opts, rename=False):
1459 1459 check_incompatible_arguments(opts, b'forget', [b'dry_run'])
1460 1460
1461 1461 # called with the repo lock held
1462 1462 #
1463 1463 # hgsep => pathname that uses "/" to separate directories
1464 1464 # ossep => pathname that uses os.sep to separate directories
1465 1465 cwd = repo.getcwd()
1466 1466 targets = {}
1467 1467 forget = opts.get(b"forget")
1468 1468 after = opts.get(b"after")
1469 1469 dryrun = opts.get(b"dry_run")
1470 1470 rev = opts.get(b'at_rev')
1471 1471 if rev:
1472 1472 if not forget and not after:
1473 1473 # TODO: Remove this restriction and make it also create the copy
1474 1474 # targets (and remove the rename source if rename==True).
1475 1475 raise error.InputError(_(b'--at-rev requires --after'))
1476 1476 ctx = scmutil.revsingle(repo, rev)
1477 1477 if len(ctx.parents()) > 1:
1478 1478 raise error.InputError(
1479 1479 _(b'cannot mark/unmark copy in merge commit')
1480 1480 )
1481 1481 else:
1482 1482 ctx = repo[None]
1483 1483
1484 1484 pctx = ctx.p1()
1485 1485
1486 1486 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
1487 1487
1488 1488 if forget:
1489 1489 if ctx.rev() is None:
1490 1490 new_ctx = ctx
1491 1491 else:
1492 1492 if len(ctx.parents()) > 1:
1493 1493 raise error.InputError(_(b'cannot unmark copy in merge commit'))
1494 1494 # avoid cycle context -> subrepo -> cmdutil
1495 1495 from . import context
1496 1496
1497 1497 rewriteutil.precheck(repo, [ctx.rev()], b'uncopy')
1498 1498 new_ctx = context.overlayworkingctx(repo)
1499 1499 new_ctx.setbase(ctx.p1())
1500 1500 mergemod.graft(repo, ctx, wctx=new_ctx)
1501 1501
1502 1502 match = scmutil.match(ctx, pats, opts)
1503 1503
1504 1504 current_copies = ctx.p1copies()
1505 1505 current_copies.update(ctx.p2copies())
1506 1506
1507 1507 uipathfn = scmutil.getuipathfn(repo)
1508 1508 for f in ctx.walk(match):
1509 1509 if f in current_copies:
1510 1510 new_ctx[f].markcopied(None)
1511 1511 elif match.exact(f):
1512 1512 ui.warn(
1513 1513 _(
1514 1514 b'%s: not unmarking as copy - file is not marked as copied\n'
1515 1515 )
1516 1516 % uipathfn(f)
1517 1517 )
1518 1518
1519 1519 if ctx.rev() is not None:
1520 1520 with repo.lock():
1521 1521 mem_ctx = new_ctx.tomemctx_for_amend(ctx)
1522 1522 new_node = mem_ctx.commit()
1523 1523
1524 1524 if repo.dirstate.p1() == ctx.node():
1525 1525 with repo.dirstate.parentchange():
1526 1526 scmutil.movedirstate(repo, repo[new_node])
1527 1527 replacements = {ctx.node(): [new_node]}
1528 1528 scmutil.cleanupnodes(
1529 1529 repo, replacements, b'uncopy', fixphase=True
1530 1530 )
1531 1531
1532 1532 return
1533 1533
1534 1534 pats = scmutil.expandpats(pats)
1535 1535 if not pats:
1536 1536 raise error.InputError(_(b'no source or destination specified'))
1537 1537 if len(pats) == 1:
1538 1538 raise error.InputError(_(b'no destination specified'))
1539 1539 dest = pats.pop()
1540 1540
1541 1541 def walkpat(pat):
1542 1542 srcs = []
1543 1543 # TODO: Inline and simplify the non-working-copy version of this code
1544 1544 # since it shares very little with the working-copy version of it.
1545 1545 ctx_to_walk = ctx if ctx.rev() is None else pctx
1546 1546 m = scmutil.match(ctx_to_walk, [pat], opts, globbed=True)
1547 1547 for abs in ctx_to_walk.walk(m):
1548 1548 rel = uipathfn(abs)
1549 1549 exact = m.exact(abs)
1550 1550 if abs not in ctx:
1551 1551 if abs in pctx:
1552 1552 if not after:
1553 1553 if exact:
1554 1554 ui.warn(
1555 1555 _(
1556 1556 b'%s: not copying - file has been marked '
1557 1557 b'for remove\n'
1558 1558 )
1559 1559 % rel
1560 1560 )
1561 1561 continue
1562 1562 else:
1563 1563 if exact:
1564 1564 ui.warn(
1565 1565 _(b'%s: not copying - file is not managed\n') % rel
1566 1566 )
1567 1567 continue
1568 1568
1569 1569 # abs: hgsep
1570 1570 # rel: ossep
1571 1571 srcs.append((abs, rel, exact))
1572 1572 return srcs
1573 1573
1574 1574 if ctx.rev() is not None:
1575 1575 rewriteutil.precheck(repo, [ctx.rev()], b'uncopy')
1576 1576 absdest = pathutil.canonpath(repo.root, cwd, dest)
1577 1577 if ctx.hasdir(absdest):
1578 1578 raise error.InputError(
1579 1579 _(b'%s: --at-rev does not support a directory as destination')
1580 1580 % uipathfn(absdest)
1581 1581 )
1582 1582 if absdest not in ctx:
1583 1583 raise error.InputError(
1584 1584 _(b'%s: copy destination does not exist in %s')
1585 1585 % (uipathfn(absdest), ctx)
1586 1586 )
1587 1587
1588 1588 # avoid cycle context -> subrepo -> cmdutil
1589 1589 from . import context
1590 1590
1591 1591 copylist = []
1592 1592 for pat in pats:
1593 1593 srcs = walkpat(pat)
1594 1594 if not srcs:
1595 1595 continue
1596 1596 for abs, rel, exact in srcs:
1597 1597 copylist.append(abs)
1598 1598
1599 1599 if not copylist:
1600 1600 raise error.InputError(_(b'no files to copy'))
1601 1601 # TODO: Add support for `hg cp --at-rev . foo bar dir` and
1602 1602 # `hg cp --at-rev . dir1 dir2`, preferably unifying the code with the
1603 1603 # existing functions below.
1604 1604 if len(copylist) != 1:
1605 1605 raise error.InputError(_(b'--at-rev requires a single source'))
1606 1606
1607 1607 new_ctx = context.overlayworkingctx(repo)
1608 1608 new_ctx.setbase(ctx.p1())
1609 1609 mergemod.graft(repo, ctx, wctx=new_ctx)
1610 1610
1611 1611 new_ctx.markcopied(absdest, copylist[0])
1612 1612
1613 1613 with repo.lock():
1614 1614 mem_ctx = new_ctx.tomemctx_for_amend(ctx)
1615 1615 new_node = mem_ctx.commit()
1616 1616
1617 1617 if repo.dirstate.p1() == ctx.node():
1618 1618 with repo.dirstate.parentchange():
1619 1619 scmutil.movedirstate(repo, repo[new_node])
1620 1620 replacements = {ctx.node(): [new_node]}
1621 1621 scmutil.cleanupnodes(repo, replacements, b'copy', fixphase=True)
1622 1622
1623 1623 return
1624 1624
1625 1625 # abssrc: hgsep
1626 1626 # relsrc: ossep
1627 1627 # otarget: ossep
1628 1628 def copyfile(abssrc, relsrc, otarget, exact):
1629 1629 abstarget = pathutil.canonpath(repo.root, cwd, otarget)
1630 1630 if b'/' in abstarget:
1631 1631 # We cannot normalize abstarget itself, this would prevent
1632 1632 # case only renames, like a => A.
1633 1633 abspath, absname = abstarget.rsplit(b'/', 1)
1634 1634 abstarget = repo.dirstate.normalize(abspath) + b'/' + absname
1635 1635 reltarget = repo.pathto(abstarget, cwd)
1636 1636 target = repo.wjoin(abstarget)
1637 1637 src = repo.wjoin(abssrc)
1638 1638 state = repo.dirstate[abstarget]
1639 1639
1640 1640 scmutil.checkportable(ui, abstarget)
1641 1641
1642 1642 # check for collisions
1643 1643 prevsrc = targets.get(abstarget)
1644 1644 if prevsrc is not None:
1645 1645 ui.warn(
1646 1646 _(b'%s: not overwriting - %s collides with %s\n')
1647 1647 % (
1648 1648 reltarget,
1649 1649 repo.pathto(abssrc, cwd),
1650 1650 repo.pathto(prevsrc, cwd),
1651 1651 )
1652 1652 )
1653 1653 return True # report a failure
1654 1654
1655 1655 # check for overwrites
1656 1656 exists = os.path.lexists(target)
1657 1657 samefile = False
1658 1658 if exists and abssrc != abstarget:
1659 1659 if repo.dirstate.normalize(abssrc) == repo.dirstate.normalize(
1660 1660 abstarget
1661 1661 ):
1662 1662 if not rename:
1663 1663 ui.warn(_(b"%s: can't copy - same file\n") % reltarget)
1664 1664 return True # report a failure
1665 1665 exists = False
1666 1666 samefile = True
1667 1667
1668 1668 if not after and exists or after and state in b'mn':
1669 1669 if not opts[b'force']:
1670 1670 if state in b'mn':
1671 1671 msg = _(b'%s: not overwriting - file already committed\n')
1672 1672 if after:
1673 1673 flags = b'--after --force'
1674 1674 else:
1675 1675 flags = b'--force'
1676 1676 if rename:
1677 1677 hint = (
1678 1678 _(
1679 1679 b"('hg rename %s' to replace the file by "
1680 1680 b'recording a rename)\n'
1681 1681 )
1682 1682 % flags
1683 1683 )
1684 1684 else:
1685 1685 hint = (
1686 1686 _(
1687 1687 b"('hg copy %s' to replace the file by "
1688 1688 b'recording a copy)\n'
1689 1689 )
1690 1690 % flags
1691 1691 )
1692 1692 else:
1693 1693 msg = _(b'%s: not overwriting - file exists\n')
1694 1694 if rename:
1695 1695 hint = _(
1696 1696 b"('hg rename --after' to record the rename)\n"
1697 1697 )
1698 1698 else:
1699 1699 hint = _(b"('hg copy --after' to record the copy)\n")
1700 1700 ui.warn(msg % reltarget)
1701 1701 ui.warn(hint)
1702 1702 return True # report a failure
1703 1703
1704 1704 if after:
1705 1705 if not exists:
1706 1706 if rename:
1707 1707 ui.warn(
1708 1708 _(b'%s: not recording move - %s does not exist\n')
1709 1709 % (relsrc, reltarget)
1710 1710 )
1711 1711 else:
1712 1712 ui.warn(
1713 1713 _(b'%s: not recording copy - %s does not exist\n')
1714 1714 % (relsrc, reltarget)
1715 1715 )
1716 1716 return True # report a failure
1717 1717 elif not dryrun:
1718 1718 try:
1719 1719 if exists:
1720 1720 os.unlink(target)
1721 1721 targetdir = os.path.dirname(target) or b'.'
1722 1722 if not os.path.isdir(targetdir):
1723 1723 os.makedirs(targetdir)
1724 1724 if samefile:
1725 1725 tmp = target + b"~hgrename"
1726 1726 os.rename(src, tmp)
1727 1727 os.rename(tmp, target)
1728 1728 else:
1729 1729 # Preserve stat info on renames, not on copies; this matches
1730 1730 # Linux CLI behavior.
1731 1731 util.copyfile(src, target, copystat=rename)
1732 1732 srcexists = True
1733 1733 except IOError as inst:
1734 1734 if inst.errno == errno.ENOENT:
1735 1735 ui.warn(_(b'%s: deleted in working directory\n') % relsrc)
1736 1736 srcexists = False
1737 1737 else:
1738 1738 ui.warn(
1739 1739 _(b'%s: cannot copy - %s\n')
1740 1740 % (relsrc, encoding.strtolocal(inst.strerror))
1741 1741 )
1742 1742 return True # report a failure
1743 1743
1744 1744 if ui.verbose or not exact:
1745 1745 if rename:
1746 1746 ui.status(_(b'moving %s to %s\n') % (relsrc, reltarget))
1747 1747 else:
1748 1748 ui.status(_(b'copying %s to %s\n') % (relsrc, reltarget))
1749 1749
1750 1750 targets[abstarget] = abssrc
1751 1751
1752 1752 # fix up dirstate
1753 1753 scmutil.dirstatecopy(
1754 1754 ui, repo, ctx, abssrc, abstarget, dryrun=dryrun, cwd=cwd
1755 1755 )
1756 1756 if rename and not dryrun:
1757 1757 if not after and srcexists and not samefile:
1758 1758 rmdir = repo.ui.configbool(b'experimental', b'removeemptydirs')
1759 1759 repo.wvfs.unlinkpath(abssrc, rmdir=rmdir)
1760 1760 ctx.forget([abssrc])
1761 1761
1762 1762 # pat: ossep
1763 1763 # dest ossep
1764 1764 # srcs: list of (hgsep, hgsep, ossep, bool)
1765 1765 # return: function that takes hgsep and returns ossep
1766 1766 def targetpathfn(pat, dest, srcs):
1767 1767 if os.path.isdir(pat):
1768 1768 abspfx = pathutil.canonpath(repo.root, cwd, pat)
1769 1769 abspfx = util.localpath(abspfx)
1770 1770 if destdirexists:
1771 1771 striplen = len(os.path.split(abspfx)[0])
1772 1772 else:
1773 1773 striplen = len(abspfx)
1774 1774 if striplen:
1775 1775 striplen += len(pycompat.ossep)
1776 1776 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
1777 1777 elif destdirexists:
1778 1778 res = lambda p: os.path.join(
1779 1779 dest, os.path.basename(util.localpath(p))
1780 1780 )
1781 1781 else:
1782 1782 res = lambda p: dest
1783 1783 return res
1784 1784
1785 1785 # pat: ossep
1786 1786 # dest ossep
1787 1787 # srcs: list of (hgsep, hgsep, ossep, bool)
1788 1788 # return: function that takes hgsep and returns ossep
1789 1789 def targetpathafterfn(pat, dest, srcs):
1790 1790 if matchmod.patkind(pat):
1791 1791 # a mercurial pattern
1792 1792 res = lambda p: os.path.join(
1793 1793 dest, os.path.basename(util.localpath(p))
1794 1794 )
1795 1795 else:
1796 1796 abspfx = pathutil.canonpath(repo.root, cwd, pat)
1797 1797 if len(abspfx) < len(srcs[0][0]):
1798 1798 # A directory. Either the target path contains the last
1799 1799 # component of the source path or it does not.
1800 1800 def evalpath(striplen):
1801 1801 score = 0
1802 1802 for s in srcs:
1803 1803 t = os.path.join(dest, util.localpath(s[0])[striplen:])
1804 1804 if os.path.lexists(t):
1805 1805 score += 1
1806 1806 return score
1807 1807
1808 1808 abspfx = util.localpath(abspfx)
1809 1809 striplen = len(abspfx)
1810 1810 if striplen:
1811 1811 striplen += len(pycompat.ossep)
1812 1812 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
1813 1813 score = evalpath(striplen)
1814 1814 striplen1 = len(os.path.split(abspfx)[0])
1815 1815 if striplen1:
1816 1816 striplen1 += len(pycompat.ossep)
1817 1817 if evalpath(striplen1) > score:
1818 1818 striplen = striplen1
1819 1819 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
1820 1820 else:
1821 1821 # a file
1822 1822 if destdirexists:
1823 1823 res = lambda p: os.path.join(
1824 1824 dest, os.path.basename(util.localpath(p))
1825 1825 )
1826 1826 else:
1827 1827 res = lambda p: dest
1828 1828 return res
1829 1829
1830 1830 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
1831 1831 if not destdirexists:
1832 1832 if len(pats) > 1 or matchmod.patkind(pats[0]):
1833 1833 raise error.InputError(
1834 1834 _(
1835 1835 b'with multiple sources, destination must be an '
1836 1836 b'existing directory'
1837 1837 )
1838 1838 )
1839 1839 if util.endswithsep(dest):
1840 1840 raise error.InputError(
1841 1841 _(b'destination %s is not a directory') % dest
1842 1842 )
1843 1843
1844 1844 tfn = targetpathfn
1845 1845 if after:
1846 1846 tfn = targetpathafterfn
1847 1847 copylist = []
1848 1848 for pat in pats:
1849 1849 srcs = walkpat(pat)
1850 1850 if not srcs:
1851 1851 continue
1852 1852 copylist.append((tfn(pat, dest, srcs), srcs))
1853 1853 if not copylist:
1854 1854 hint = None
1855 1855 if rename:
1856 1856 hint = _(b'maybe you meant to use --after --at-rev=.')
1857 1857 raise error.InputError(_(b'no files to copy'), hint=hint)
1858 1858
1859 1859 errors = 0
1860 1860 for targetpath, srcs in copylist:
1861 1861 for abssrc, relsrc, exact in srcs:
1862 1862 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
1863 1863 errors += 1
1864 1864
1865 1865 return errors != 0
1866 1866
1867 1867
1868 1868 ## facility to let extension process additional data into an import patch
1869 1869 # list of identifier to be executed in order
1870 1870 extrapreimport = [] # run before commit
1871 1871 extrapostimport = [] # run after commit
1872 1872 # mapping from identifier to actual import function
1873 1873 #
1874 1874 # 'preimport' are run before the commit is made and are provided the following
1875 1875 # arguments:
1876 1876 # - repo: the localrepository instance,
1877 1877 # - patchdata: data extracted from patch header (cf m.patch.patchheadermap),
1878 1878 # - extra: the future extra dictionary of the changeset, please mutate it,
1879 1879 # - opts: the import options.
1880 1880 # XXX ideally, we would just pass an ctx ready to be computed, that would allow
1881 1881 # mutation of in memory commit and more. Feel free to rework the code to get
1882 1882 # there.
1883 1883 extrapreimportmap = {}
1884 1884 # 'postimport' are run after the commit is made and are provided the following
1885 1885 # argument:
1886 1886 # - ctx: the changectx created by import.
1887 1887 extrapostimportmap = {}
1888 1888
1889 1889
1890 1890 def tryimportone(ui, repo, patchdata, parents, opts, msgs, updatefunc):
1891 1891 """Utility function used by commands.import to import a single patch
1892 1892
1893 1893 This function is explicitly defined here to help the evolve extension to
1894 1894 wrap this part of the import logic.
1895 1895
1896 1896 The API is currently a bit ugly because it a simple code translation from
1897 1897 the import command. Feel free to make it better.
1898 1898
1899 1899 :patchdata: a dictionary containing parsed patch data (such as from
1900 1900 ``patch.extract()``)
1901 1901 :parents: nodes that will be parent of the created commit
1902 1902 :opts: the full dict of option passed to the import command
1903 1903 :msgs: list to save commit message to.
1904 1904 (used in case we need to save it when failing)
1905 1905 :updatefunc: a function that update a repo to a given node
1906 1906 updatefunc(<repo>, <node>)
1907 1907 """
1908 1908 # avoid cycle context -> subrepo -> cmdutil
1909 1909 from . import context
1910 1910
1911 1911 tmpname = patchdata.get(b'filename')
1912 1912 message = patchdata.get(b'message')
1913 1913 user = opts.get(b'user') or patchdata.get(b'user')
1914 1914 date = opts.get(b'date') or patchdata.get(b'date')
1915 1915 branch = patchdata.get(b'branch')
1916 1916 nodeid = patchdata.get(b'nodeid')
1917 1917 p1 = patchdata.get(b'p1')
1918 1918 p2 = patchdata.get(b'p2')
1919 1919
1920 1920 nocommit = opts.get(b'no_commit')
1921 1921 importbranch = opts.get(b'import_branch')
1922 1922 update = not opts.get(b'bypass')
1923 1923 strip = opts[b"strip"]
1924 1924 prefix = opts[b"prefix"]
1925 1925 sim = float(opts.get(b'similarity') or 0)
1926 1926
1927 1927 if not tmpname:
1928 1928 return None, None, False
1929 1929
1930 1930 rejects = False
1931 1931
1932 1932 cmdline_message = logmessage(ui, opts)
1933 1933 if cmdline_message:
1934 1934 # pickup the cmdline msg
1935 1935 message = cmdline_message
1936 1936 elif message:
1937 1937 # pickup the patch msg
1938 1938 message = message.strip()
1939 1939 else:
1940 1940 # launch the editor
1941 1941 message = None
1942 1942 ui.debug(b'message:\n%s\n' % (message or b''))
1943 1943
1944 1944 if len(parents) == 1:
1945 1945 parents.append(repo[nullrev])
1946 1946 if opts.get(b'exact'):
1947 1947 if not nodeid or not p1:
1948 1948 raise error.InputError(_(b'not a Mercurial patch'))
1949 1949 p1 = repo[p1]
1950 1950 p2 = repo[p2 or nullrev]
1951 1951 elif p2:
1952 1952 try:
1953 1953 p1 = repo[p1]
1954 1954 p2 = repo[p2]
1955 1955 # Without any options, consider p2 only if the
1956 1956 # patch is being applied on top of the recorded
1957 1957 # first parent.
1958 1958 if p1 != parents[0]:
1959 1959 p1 = parents[0]
1960 1960 p2 = repo[nullrev]
1961 1961 except error.RepoError:
1962 1962 p1, p2 = parents
1963 1963 if p2.rev() == nullrev:
1964 1964 ui.warn(
1965 1965 _(
1966 1966 b"warning: import the patch as a normal revision\n"
1967 1967 b"(use --exact to import the patch as a merge)\n"
1968 1968 )
1969 1969 )
1970 1970 else:
1971 1971 p1, p2 = parents
1972 1972
1973 1973 n = None
1974 1974 if update:
1975 1975 if p1 != parents[0]:
1976 1976 updatefunc(repo, p1.node())
1977 1977 if p2 != parents[1]:
1978 1978 repo.setparents(p1.node(), p2.node())
1979 1979
1980 1980 if opts.get(b'exact') or importbranch:
1981 1981 repo.dirstate.setbranch(branch or b'default')
1982 1982
1983 1983 partial = opts.get(b'partial', False)
1984 1984 files = set()
1985 1985 try:
1986 1986 patch.patch(
1987 1987 ui,
1988 1988 repo,
1989 1989 tmpname,
1990 1990 strip=strip,
1991 1991 prefix=prefix,
1992 1992 files=files,
1993 1993 eolmode=None,
1994 1994 similarity=sim / 100.0,
1995 1995 )
1996 1996 except error.PatchError as e:
1997 1997 if not partial:
1998 1998 raise error.Abort(pycompat.bytestr(e))
1999 1999 if partial:
2000 2000 rejects = True
2001 2001
2002 2002 files = list(files)
2003 2003 if nocommit:
2004 2004 if message:
2005 2005 msgs.append(message)
2006 2006 else:
2007 2007 if opts.get(b'exact') or p2:
2008 2008 # If you got here, you either use --force and know what
2009 2009 # you are doing or used --exact or a merge patch while
2010 2010 # being updated to its first parent.
2011 2011 m = None
2012 2012 else:
2013 2013 m = scmutil.matchfiles(repo, files or [])
2014 2014 editform = mergeeditform(repo[None], b'import.normal')
2015 2015 if opts.get(b'exact'):
2016 2016 editor = None
2017 2017 else:
2018 2018 editor = getcommiteditor(
2019 2019 editform=editform, **pycompat.strkwargs(opts)
2020 2020 )
2021 2021 extra = {}
2022 2022 for idfunc in extrapreimport:
2023 2023 extrapreimportmap[idfunc](repo, patchdata, extra, opts)
2024 2024 overrides = {}
2025 2025 if partial:
2026 2026 overrides[(b'ui', b'allowemptycommit')] = True
2027 2027 if opts.get(b'secret'):
2028 2028 overrides[(b'phases', b'new-commit')] = b'secret'
2029 2029 with repo.ui.configoverride(overrides, b'import'):
2030 2030 n = repo.commit(
2031 2031 message, user, date, match=m, editor=editor, extra=extra
2032 2032 )
2033 2033 for idfunc in extrapostimport:
2034 2034 extrapostimportmap[idfunc](repo[n])
2035 2035 else:
2036 2036 if opts.get(b'exact') or importbranch:
2037 2037 branch = branch or b'default'
2038 2038 else:
2039 2039 branch = p1.branch()
2040 2040 store = patch.filestore()
2041 2041 try:
2042 2042 files = set()
2043 2043 try:
2044 2044 patch.patchrepo(
2045 2045 ui,
2046 2046 repo,
2047 2047 p1,
2048 2048 store,
2049 2049 tmpname,
2050 2050 strip,
2051 2051 prefix,
2052 2052 files,
2053 2053 eolmode=None,
2054 2054 )
2055 2055 except error.PatchError as e:
2056 2056 raise error.Abort(stringutil.forcebytestr(e))
2057 2057 if opts.get(b'exact'):
2058 2058 editor = None
2059 2059 else:
2060 2060 editor = getcommiteditor(editform=b'import.bypass')
2061 2061 memctx = context.memctx(
2062 2062 repo,
2063 2063 (p1.node(), p2.node()),
2064 2064 message,
2065 2065 files=files,
2066 2066 filectxfn=store,
2067 2067 user=user,
2068 2068 date=date,
2069 2069 branch=branch,
2070 2070 editor=editor,
2071 2071 )
2072 2072
2073 2073 overrides = {}
2074 2074 if opts.get(b'secret'):
2075 2075 overrides[(b'phases', b'new-commit')] = b'secret'
2076 2076 with repo.ui.configoverride(overrides, b'import'):
2077 2077 n = memctx.commit()
2078 2078 finally:
2079 2079 store.close()
2080 2080 if opts.get(b'exact') and nocommit:
2081 2081 # --exact with --no-commit is still useful in that it does merge
2082 2082 # and branch bits
2083 2083 ui.warn(_(b"warning: can't check exact import with --no-commit\n"))
2084 2084 elif opts.get(b'exact') and (not n or hex(n) != nodeid):
2085 2085 raise error.Abort(_(b'patch is damaged or loses information'))
2086 2086 msg = _(b'applied to working directory')
2087 2087 if n:
2088 2088 # i18n: refers to a short changeset id
2089 2089 msg = _(b'created %s') % short(n)
2090 2090 return msg, n, rejects
2091 2091
2092 2092
2093 2093 # facility to let extensions include additional data in an exported patch
2094 2094 # list of identifiers to be executed in order
2095 2095 extraexport = []
2096 2096 # mapping from identifier to actual export function
2097 2097 # function as to return a string to be added to the header or None
2098 2098 # it is given two arguments (sequencenumber, changectx)
2099 2099 extraexportmap = {}
2100 2100
2101 2101
2102 2102 def _exportsingle(repo, ctx, fm, match, switch_parent, seqno, diffopts):
2103 2103 node = scmutil.binnode(ctx)
2104 2104 parents = [p.node() for p in ctx.parents() if p]
2105 2105 branch = ctx.branch()
2106 2106 if switch_parent:
2107 2107 parents.reverse()
2108 2108
2109 2109 if parents:
2110 2110 prev = parents[0]
2111 2111 else:
2112 2112 prev = repo.nullid
2113 2113
2114 2114 fm.context(ctx=ctx)
2115 2115 fm.plain(b'# HG changeset patch\n')
2116 2116 fm.write(b'user', b'# User %s\n', ctx.user())
2117 2117 fm.plain(b'# Date %d %d\n' % ctx.date())
2118 2118 fm.write(b'date', b'# %s\n', fm.formatdate(ctx.date()))
2119 2119 fm.condwrite(
2120 2120 branch and branch != b'default', b'branch', b'# Branch %s\n', branch
2121 2121 )
2122 2122 fm.write(b'node', b'# Node ID %s\n', hex(node))
2123 2123 fm.plain(b'# Parent %s\n' % hex(prev))
2124 2124 if len(parents) > 1:
2125 2125 fm.plain(b'# Parent %s\n' % hex(parents[1]))
2126 2126 fm.data(parents=fm.formatlist(pycompat.maplist(hex, parents), name=b'node'))
2127 2127
2128 2128 # TODO: redesign extraexportmap function to support formatter
2129 2129 for headerid in extraexport:
2130 2130 header = extraexportmap[headerid](seqno, ctx)
2131 2131 if header is not None:
2132 2132 fm.plain(b'# %s\n' % header)
2133 2133
2134 2134 fm.write(b'desc', b'%s\n', ctx.description().rstrip())
2135 2135 fm.plain(b'\n')
2136 2136
2137 2137 if fm.isplain():
2138 2138 chunkiter = patch.diffui(repo, prev, node, match, opts=diffopts)
2139 2139 for chunk, label in chunkiter:
2140 2140 fm.plain(chunk, label=label)
2141 2141 else:
2142 2142 chunkiter = patch.diff(repo, prev, node, match, opts=diffopts)
2143 2143 # TODO: make it structured?
2144 2144 fm.data(diff=b''.join(chunkiter))
2145 2145
2146 2146
2147 2147 def _exportfile(repo, revs, fm, dest, switch_parent, diffopts, match):
2148 2148 """Export changesets to stdout or a single file"""
2149 2149 for seqno, rev in enumerate(revs, 1):
2150 2150 ctx = repo[rev]
2151 2151 if not dest.startswith(b'<'):
2152 2152 repo.ui.note(b"%s\n" % dest)
2153 2153 fm.startitem()
2154 2154 _exportsingle(repo, ctx, fm, match, switch_parent, seqno, diffopts)
2155 2155
2156 2156
2157 2157 def _exportfntemplate(
2158 2158 repo, revs, basefm, fntemplate, switch_parent, diffopts, match
2159 2159 ):
2160 2160 """Export changesets to possibly multiple files"""
2161 2161 total = len(revs)
2162 2162 revwidth = max(len(str(rev)) for rev in revs)
2163 2163 filemap = util.sortdict() # filename: [(seqno, rev), ...]
2164 2164
2165 2165 for seqno, rev in enumerate(revs, 1):
2166 2166 ctx = repo[rev]
2167 2167 dest = makefilename(
2168 2168 ctx, fntemplate, total=total, seqno=seqno, revwidth=revwidth
2169 2169 )
2170 2170 filemap.setdefault(dest, []).append((seqno, rev))
2171 2171
2172 2172 for dest in filemap:
2173 2173 with formatter.maybereopen(basefm, dest) as fm:
2174 2174 repo.ui.note(b"%s\n" % dest)
2175 2175 for seqno, rev in filemap[dest]:
2176 2176 fm.startitem()
2177 2177 ctx = repo[rev]
2178 2178 _exportsingle(
2179 2179 repo, ctx, fm, match, switch_parent, seqno, diffopts
2180 2180 )
2181 2181
2182 2182
2183 2183 def _prefetchchangedfiles(repo, revs, match):
2184 2184 allfiles = set()
2185 2185 for rev in revs:
2186 2186 for file in repo[rev].files():
2187 2187 if not match or match(file):
2188 2188 allfiles.add(file)
2189 2189 match = scmutil.matchfiles(repo, allfiles)
2190 2190 revmatches = [(rev, match) for rev in revs]
2191 2191 scmutil.prefetchfiles(repo, revmatches)
2192 2192
2193 2193
2194 2194 def export(
2195 2195 repo,
2196 2196 revs,
2197 2197 basefm,
2198 2198 fntemplate=b'hg-%h.patch',
2199 2199 switch_parent=False,
2200 2200 opts=None,
2201 2201 match=None,
2202 2202 ):
2203 2203 """export changesets as hg patches
2204 2204
2205 2205 Args:
2206 2206 repo: The repository from which we're exporting revisions.
2207 2207 revs: A list of revisions to export as revision numbers.
2208 2208 basefm: A formatter to which patches should be written.
2209 2209 fntemplate: An optional string to use for generating patch file names.
2210 2210 switch_parent: If True, show diffs against second parent when not nullid.
2211 2211 Default is false, which always shows diff against p1.
2212 2212 opts: diff options to use for generating the patch.
2213 2213 match: If specified, only export changes to files matching this matcher.
2214 2214
2215 2215 Returns:
2216 2216 Nothing.
2217 2217
2218 2218 Side Effect:
2219 2219 "HG Changeset Patch" data is emitted to one of the following
2220 2220 destinations:
2221 2221 fntemplate specified: Each rev is written to a unique file named using
2222 2222 the given template.
2223 2223 Otherwise: All revs will be written to basefm.
2224 2224 """
2225 2225 _prefetchchangedfiles(repo, revs, match)
2226 2226
2227 2227 if not fntemplate:
2228 2228 _exportfile(
2229 2229 repo, revs, basefm, b'<unnamed>', switch_parent, opts, match
2230 2230 )
2231 2231 else:
2232 2232 _exportfntemplate(
2233 2233 repo, revs, basefm, fntemplate, switch_parent, opts, match
2234 2234 )
2235 2235
2236 2236
2237 2237 def exportfile(repo, revs, fp, switch_parent=False, opts=None, match=None):
2238 2238 """Export changesets to the given file stream"""
2239 2239 _prefetchchangedfiles(repo, revs, match)
2240 2240
2241 2241 dest = getattr(fp, 'name', b'<unnamed>')
2242 2242 with formatter.formatter(repo.ui, fp, b'export', {}) as fm:
2243 2243 _exportfile(repo, revs, fm, dest, switch_parent, opts, match)
2244 2244
2245 2245
2246 2246 def showmarker(fm, marker, index=None):
2247 2247 """utility function to display obsolescence marker in a readable way
2248 2248
2249 2249 To be used by debug function."""
2250 2250 if index is not None:
2251 2251 fm.write(b'index', b'%i ', index)
2252 2252 fm.write(b'prednode', b'%s ', hex(marker.prednode()))
2253 2253 succs = marker.succnodes()
2254 2254 fm.condwrite(
2255 2255 succs,
2256 2256 b'succnodes',
2257 2257 b'%s ',
2258 2258 fm.formatlist(map(hex, succs), name=b'node'),
2259 2259 )
2260 2260 fm.write(b'flag', b'%X ', marker.flags())
2261 2261 parents = marker.parentnodes()
2262 2262 if parents is not None:
2263 2263 fm.write(
2264 2264 b'parentnodes',
2265 2265 b'{%s} ',
2266 2266 fm.formatlist(map(hex, parents), name=b'node', sep=b', '),
2267 2267 )
2268 2268 fm.write(b'date', b'(%s) ', fm.formatdate(marker.date()))
2269 2269 meta = marker.metadata().copy()
2270 2270 meta.pop(b'date', None)
2271 2271 smeta = pycompat.rapply(pycompat.maybebytestr, meta)
2272 2272 fm.write(
2273 2273 b'metadata', b'{%s}', fm.formatdict(smeta, fmt=b'%r: %r', sep=b', ')
2274 2274 )
2275 2275 fm.plain(b'\n')
2276 2276
2277 2277
2278 2278 def finddate(ui, repo, date):
2279 2279 """Find the tipmost changeset that matches the given date spec"""
2280 2280 mrevs = repo.revs(b'date(%s)', date)
2281 2281 try:
2282 2282 rev = mrevs.max()
2283 2283 except ValueError:
2284 2284 raise error.InputError(_(b"revision matching date not found"))
2285 2285
2286 2286 ui.status(
2287 2287 _(b"found revision %d from %s\n")
2288 2288 % (rev, dateutil.datestr(repo[rev].date()))
2289 2289 )
2290 2290 return b'%d' % rev
2291 2291
2292 2292
2293 2293 def add(ui, repo, match, prefix, uipathfn, explicitonly, **opts):
2294 2294 bad = []
2295 2295
2296 2296 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2297 2297 names = []
2298 2298 wctx = repo[None]
2299 2299 cca = None
2300 2300 abort, warn = scmutil.checkportabilityalert(ui)
2301 2301 if abort or warn:
2302 2302 cca = scmutil.casecollisionauditor(ui, abort, repo.dirstate)
2303 2303
2304 2304 match = repo.narrowmatch(match, includeexact=True)
2305 2305 badmatch = matchmod.badmatch(match, badfn)
2306 2306 dirstate = repo.dirstate
2307 2307 # We don't want to just call wctx.walk here, since it would return a lot of
2308 2308 # clean files, which we aren't interested in and takes time.
2309 2309 for f in sorted(
2310 2310 dirstate.walk(
2311 2311 badmatch,
2312 2312 subrepos=sorted(wctx.substate),
2313 2313 unknown=True,
2314 2314 ignored=False,
2315 2315 full=False,
2316 2316 )
2317 2317 ):
2318 2318 exact = match.exact(f)
2319 2319 if exact or not explicitonly and f not in wctx and repo.wvfs.lexists(f):
2320 2320 if cca:
2321 2321 cca(f)
2322 2322 names.append(f)
2323 2323 if ui.verbose or not exact:
2324 2324 ui.status(
2325 2325 _(b'adding %s\n') % uipathfn(f), label=b'ui.addremove.added'
2326 2326 )
2327 2327
2328 2328 for subpath in sorted(wctx.substate):
2329 2329 sub = wctx.sub(subpath)
2330 2330 try:
2331 2331 submatch = matchmod.subdirmatcher(subpath, match)
2332 2332 subprefix = repo.wvfs.reljoin(prefix, subpath)
2333 2333 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2334 2334 if opts.get('subrepos'):
2335 2335 bad.extend(
2336 2336 sub.add(ui, submatch, subprefix, subuipathfn, False, **opts)
2337 2337 )
2338 2338 else:
2339 2339 bad.extend(
2340 2340 sub.add(ui, submatch, subprefix, subuipathfn, True, **opts)
2341 2341 )
2342 2342 except error.LookupError:
2343 2343 ui.status(
2344 2344 _(b"skipping missing subrepository: %s\n") % uipathfn(subpath)
2345 2345 )
2346 2346
2347 2347 if not opts.get('dry_run'):
2348 2348 rejected = wctx.add(names, prefix)
2349 2349 bad.extend(f for f in rejected if f in match.files())
2350 2350 return bad
2351 2351
2352 2352
2353 2353 def addwebdirpath(repo, serverpath, webconf):
2354 2354 webconf[serverpath] = repo.root
2355 2355 repo.ui.debug(b'adding %s = %s\n' % (serverpath, repo.root))
2356 2356
2357 2357 for r in repo.revs(b'filelog("path:.hgsub")'):
2358 2358 ctx = repo[r]
2359 2359 for subpath in ctx.substate:
2360 2360 ctx.sub(subpath).addwebdirpath(serverpath, webconf)
2361 2361
2362 2362
2363 2363 def forget(
2364 2364 ui, repo, match, prefix, uipathfn, explicitonly, dryrun, interactive
2365 2365 ):
2366 2366 if dryrun and interactive:
2367 2367 raise error.InputError(
2368 2368 _(b"cannot specify both --dry-run and --interactive")
2369 2369 )
2370 2370 bad = []
2371 2371 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2372 2372 wctx = repo[None]
2373 2373 forgot = []
2374 2374
2375 2375 s = repo.status(match=matchmod.badmatch(match, badfn), clean=True)
2376 2376 forget = sorted(s.modified + s.added + s.deleted + s.clean)
2377 2377 if explicitonly:
2378 2378 forget = [f for f in forget if match.exact(f)]
2379 2379
2380 2380 for subpath in sorted(wctx.substate):
2381 2381 sub = wctx.sub(subpath)
2382 2382 submatch = matchmod.subdirmatcher(subpath, match)
2383 2383 subprefix = repo.wvfs.reljoin(prefix, subpath)
2384 2384 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2385 2385 try:
2386 2386 subbad, subforgot = sub.forget(
2387 2387 submatch,
2388 2388 subprefix,
2389 2389 subuipathfn,
2390 2390 dryrun=dryrun,
2391 2391 interactive=interactive,
2392 2392 )
2393 2393 bad.extend([subpath + b'/' + f for f in subbad])
2394 2394 forgot.extend([subpath + b'/' + f for f in subforgot])
2395 2395 except error.LookupError:
2396 2396 ui.status(
2397 2397 _(b"skipping missing subrepository: %s\n") % uipathfn(subpath)
2398 2398 )
2399 2399
2400 2400 if not explicitonly:
2401 2401 for f in match.files():
2402 2402 if f not in repo.dirstate and not repo.wvfs.isdir(f):
2403 2403 if f not in forgot:
2404 2404 if repo.wvfs.exists(f):
2405 2405 # Don't complain if the exact case match wasn't given.
2406 2406 # But don't do this until after checking 'forgot', so
2407 2407 # that subrepo files aren't normalized, and this op is
2408 2408 # purely from data cached by the status walk above.
2409 2409 if repo.dirstate.normalize(f) in repo.dirstate:
2410 2410 continue
2411 2411 ui.warn(
2412 2412 _(
2413 2413 b'not removing %s: '
2414 2414 b'file is already untracked\n'
2415 2415 )
2416 2416 % uipathfn(f)
2417 2417 )
2418 2418 bad.append(f)
2419 2419
2420 2420 if interactive:
2421 2421 responses = _(
2422 2422 b'[Ynsa?]'
2423 2423 b'$$ &Yes, forget this file'
2424 2424 b'$$ &No, skip this file'
2425 2425 b'$$ &Skip remaining files'
2426 2426 b'$$ Include &all remaining files'
2427 2427 b'$$ &? (display help)'
2428 2428 )
2429 2429 for filename in forget[:]:
2430 2430 r = ui.promptchoice(
2431 2431 _(b'forget %s %s') % (uipathfn(filename), responses)
2432 2432 )
2433 2433 if r == 4: # ?
2434 2434 while r == 4:
2435 2435 for c, t in ui.extractchoices(responses)[1]:
2436 2436 ui.write(b'%s - %s\n' % (c, encoding.lower(t)))
2437 2437 r = ui.promptchoice(
2438 2438 _(b'forget %s %s') % (uipathfn(filename), responses)
2439 2439 )
2440 2440 if r == 0: # yes
2441 2441 continue
2442 2442 elif r == 1: # no
2443 2443 forget.remove(filename)
2444 2444 elif r == 2: # Skip
2445 2445 fnindex = forget.index(filename)
2446 2446 del forget[fnindex:]
2447 2447 break
2448 2448 elif r == 3: # All
2449 2449 break
2450 2450
2451 2451 for f in forget:
2452 2452 if ui.verbose or not match.exact(f) or interactive:
2453 2453 ui.status(
2454 2454 _(b'removing %s\n') % uipathfn(f), label=b'ui.addremove.removed'
2455 2455 )
2456 2456
2457 2457 if not dryrun:
2458 2458 rejected = wctx.forget(forget, prefix)
2459 2459 bad.extend(f for f in rejected if f in match.files())
2460 2460 forgot.extend(f for f in forget if f not in rejected)
2461 2461 return bad, forgot
2462 2462
2463 2463
2464 2464 def files(ui, ctx, m, uipathfn, fm, fmt, subrepos):
2465 2465 ret = 1
2466 2466
2467 2467 needsfctx = ui.verbose or {b'size', b'flags'} & fm.datahint()
2468 2468 if fm.isplain() and not needsfctx:
2469 2469 # Fast path. The speed-up comes from skipping the formatter, and batching
2470 2470 # calls to ui.write.
2471 2471 buf = []
2472 2472 for f in ctx.matches(m):
2473 2473 buf.append(fmt % uipathfn(f))
2474 2474 if len(buf) > 100:
2475 2475 ui.write(b''.join(buf))
2476 2476 del buf[:]
2477 2477 ret = 0
2478 2478 if buf:
2479 2479 ui.write(b''.join(buf))
2480 2480 else:
2481 2481 for f in ctx.matches(m):
2482 2482 fm.startitem()
2483 2483 fm.context(ctx=ctx)
2484 2484 if needsfctx:
2485 2485 fc = ctx[f]
2486 2486 fm.write(b'size flags', b'% 10d % 1s ', fc.size(), fc.flags())
2487 2487 fm.data(path=f)
2488 2488 fm.plain(fmt % uipathfn(f))
2489 2489 ret = 0
2490 2490
2491 2491 for subpath in sorted(ctx.substate):
2492 2492 submatch = matchmod.subdirmatcher(subpath, m)
2493 2493 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2494 2494 if subrepos or m.exact(subpath) or any(submatch.files()):
2495 2495 sub = ctx.sub(subpath)
2496 2496 try:
2497 2497 recurse = m.exact(subpath) or subrepos
2498 2498 if (
2499 2499 sub.printfiles(ui, submatch, subuipathfn, fm, fmt, recurse)
2500 2500 == 0
2501 2501 ):
2502 2502 ret = 0
2503 2503 except error.LookupError:
2504 2504 ui.status(
2505 2505 _(b"skipping missing subrepository: %s\n")
2506 2506 % uipathfn(subpath)
2507 2507 )
2508 2508
2509 2509 return ret
2510 2510
2511 2511
2512 2512 def remove(
2513 2513 ui, repo, m, prefix, uipathfn, after, force, subrepos, dryrun, warnings=None
2514 2514 ):
2515 2515 ret = 0
2516 2516 s = repo.status(match=m, clean=True)
2517 2517 modified, added, deleted, clean = s.modified, s.added, s.deleted, s.clean
2518 2518
2519 2519 wctx = repo[None]
2520 2520
2521 2521 if warnings is None:
2522 2522 warnings = []
2523 2523 warn = True
2524 2524 else:
2525 2525 warn = False
2526 2526
2527 2527 subs = sorted(wctx.substate)
2528 2528 progress = ui.makeprogress(
2529 2529 _(b'searching'), total=len(subs), unit=_(b'subrepos')
2530 2530 )
2531 2531 for subpath in subs:
2532 2532 submatch = matchmod.subdirmatcher(subpath, m)
2533 2533 subprefix = repo.wvfs.reljoin(prefix, subpath)
2534 2534 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2535 2535 if subrepos or m.exact(subpath) or any(submatch.files()):
2536 2536 progress.increment()
2537 2537 sub = wctx.sub(subpath)
2538 2538 try:
2539 2539 if sub.removefiles(
2540 2540 submatch,
2541 2541 subprefix,
2542 2542 subuipathfn,
2543 2543 after,
2544 2544 force,
2545 2545 subrepos,
2546 2546 dryrun,
2547 2547 warnings,
2548 2548 ):
2549 2549 ret = 1
2550 2550 except error.LookupError:
2551 2551 warnings.append(
2552 2552 _(b"skipping missing subrepository: %s\n")
2553 2553 % uipathfn(subpath)
2554 2554 )
2555 2555 progress.complete()
2556 2556
2557 2557 # warn about failure to delete explicit files/dirs
2558 2558 deleteddirs = pathutil.dirs(deleted)
2559 2559 files = m.files()
2560 2560 progress = ui.makeprogress(
2561 2561 _(b'deleting'), total=len(files), unit=_(b'files')
2562 2562 )
2563 2563 for f in files:
2564 2564
2565 2565 def insubrepo():
2566 2566 for subpath in wctx.substate:
2567 2567 if f.startswith(subpath + b'/'):
2568 2568 return True
2569 2569 return False
2570 2570
2571 2571 progress.increment()
2572 2572 isdir = f in deleteddirs or wctx.hasdir(f)
2573 2573 if f in repo.dirstate or isdir or f == b'.' or insubrepo() or f in subs:
2574 2574 continue
2575 2575
2576 2576 if repo.wvfs.exists(f):
2577 2577 if repo.wvfs.isdir(f):
2578 2578 warnings.append(
2579 2579 _(b'not removing %s: no tracked files\n') % uipathfn(f)
2580 2580 )
2581 2581 else:
2582 2582 warnings.append(
2583 2583 _(b'not removing %s: file is untracked\n') % uipathfn(f)
2584 2584 )
2585 2585 # missing files will generate a warning elsewhere
2586 2586 ret = 1
2587 2587 progress.complete()
2588 2588
2589 2589 if force:
2590 2590 list = modified + deleted + clean + added
2591 2591 elif after:
2592 2592 list = deleted
2593 2593 remaining = modified + added + clean
2594 2594 progress = ui.makeprogress(
2595 2595 _(b'skipping'), total=len(remaining), unit=_(b'files')
2596 2596 )
2597 2597 for f in remaining:
2598 2598 progress.increment()
2599 2599 if ui.verbose or (f in files):
2600 2600 warnings.append(
2601 2601 _(b'not removing %s: file still exists\n') % uipathfn(f)
2602 2602 )
2603 2603 ret = 1
2604 2604 progress.complete()
2605 2605 else:
2606 2606 list = deleted + clean
2607 2607 progress = ui.makeprogress(
2608 2608 _(b'skipping'), total=(len(modified) + len(added)), unit=_(b'files')
2609 2609 )
2610 2610 for f in modified:
2611 2611 progress.increment()
2612 2612 warnings.append(
2613 2613 _(
2614 2614 b'not removing %s: file is modified (use -f'
2615 2615 b' to force removal)\n'
2616 2616 )
2617 2617 % uipathfn(f)
2618 2618 )
2619 2619 ret = 1
2620 2620 for f in added:
2621 2621 progress.increment()
2622 2622 warnings.append(
2623 2623 _(
2624 2624 b"not removing %s: file has been marked for add"
2625 2625 b" (use 'hg forget' to undo add)\n"
2626 2626 )
2627 2627 % uipathfn(f)
2628 2628 )
2629 2629 ret = 1
2630 2630 progress.complete()
2631 2631
2632 2632 list = sorted(list)
2633 2633 progress = ui.makeprogress(
2634 2634 _(b'deleting'), total=len(list), unit=_(b'files')
2635 2635 )
2636 2636 for f in list:
2637 2637 if ui.verbose or not m.exact(f):
2638 2638 progress.increment()
2639 2639 ui.status(
2640 2640 _(b'removing %s\n') % uipathfn(f), label=b'ui.addremove.removed'
2641 2641 )
2642 2642 progress.complete()
2643 2643
2644 2644 if not dryrun:
2645 2645 with repo.wlock():
2646 2646 if not after:
2647 2647 for f in list:
2648 2648 if f in added:
2649 2649 continue # we never unlink added files on remove
2650 2650 rmdir = repo.ui.configbool(
2651 2651 b'experimental', b'removeemptydirs'
2652 2652 )
2653 2653 repo.wvfs.unlinkpath(f, ignoremissing=True, rmdir=rmdir)
2654 2654 repo[None].forget(list)
2655 2655
2656 2656 if warn:
2657 2657 for warning in warnings:
2658 2658 ui.warn(warning)
2659 2659
2660 2660 return ret
2661 2661
2662 2662
2663 2663 def _catfmtneedsdata(fm):
2664 2664 return not fm.datahint() or b'data' in fm.datahint()
2665 2665
2666 2666
2667 2667 def _updatecatformatter(fm, ctx, matcher, path, decode):
2668 2668 """Hook for adding data to the formatter used by ``hg cat``.
2669 2669
2670 2670 Extensions (e.g., lfs) can wrap this to inject keywords/data, but must call
2671 2671 this method first."""
2672 2672
2673 2673 # data() can be expensive to fetch (e.g. lfs), so don't fetch it if it
2674 2674 # wasn't requested.
2675 2675 data = b''
2676 2676 if _catfmtneedsdata(fm):
2677 2677 data = ctx[path].data()
2678 2678 if decode:
2679 2679 data = ctx.repo().wwritedata(path, data)
2680 2680 fm.startitem()
2681 2681 fm.context(ctx=ctx)
2682 2682 fm.write(b'data', b'%s', data)
2683 2683 fm.data(path=path)
2684 2684
2685 2685
2686 2686 def cat(ui, repo, ctx, matcher, basefm, fntemplate, prefix, **opts):
2687 2687 err = 1
2688 2688 opts = pycompat.byteskwargs(opts)
2689 2689
2690 2690 def write(path):
2691 2691 filename = None
2692 2692 if fntemplate:
2693 2693 filename = makefilename(
2694 2694 ctx, fntemplate, pathname=os.path.join(prefix, path)
2695 2695 )
2696 2696 # attempt to create the directory if it does not already exist
2697 2697 try:
2698 2698 os.makedirs(os.path.dirname(filename))
2699 2699 except OSError:
2700 2700 pass
2701 2701 with formatter.maybereopen(basefm, filename) as fm:
2702 2702 _updatecatformatter(fm, ctx, matcher, path, opts.get(b'decode'))
2703 2703
2704 2704 # Automation often uses hg cat on single files, so special case it
2705 2705 # for performance to avoid the cost of parsing the manifest.
2706 2706 if len(matcher.files()) == 1 and not matcher.anypats():
2707 2707 file = matcher.files()[0]
2708 2708 mfl = repo.manifestlog
2709 2709 mfnode = ctx.manifestnode()
2710 2710 try:
2711 2711 if mfnode and mfl[mfnode].find(file)[0]:
2712 2712 if _catfmtneedsdata(basefm):
2713 2713 scmutil.prefetchfiles(repo, [(ctx.rev(), matcher)])
2714 2714 write(file)
2715 2715 return 0
2716 2716 except KeyError:
2717 2717 pass
2718 2718
2719 2719 if _catfmtneedsdata(basefm):
2720 2720 scmutil.prefetchfiles(repo, [(ctx.rev(), matcher)])
2721 2721
2722 2722 for abs in ctx.walk(matcher):
2723 2723 write(abs)
2724 2724 err = 0
2725 2725
2726 2726 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
2727 2727 for subpath in sorted(ctx.substate):
2728 2728 sub = ctx.sub(subpath)
2729 2729 try:
2730 2730 submatch = matchmod.subdirmatcher(subpath, matcher)
2731 2731 subprefix = os.path.join(prefix, subpath)
2732 2732 if not sub.cat(
2733 2733 submatch,
2734 2734 basefm,
2735 2735 fntemplate,
2736 2736 subprefix,
2737 2737 **pycompat.strkwargs(opts)
2738 2738 ):
2739 2739 err = 0
2740 2740 except error.RepoLookupError:
2741 2741 ui.status(
2742 2742 _(b"skipping missing subrepository: %s\n") % uipathfn(subpath)
2743 2743 )
2744 2744
2745 2745 return err
2746 2746
2747 2747
2748 2748 def commit(ui, repo, commitfunc, pats, opts):
2749 2749 '''commit the specified files or all outstanding changes'''
2750 2750 date = opts.get(b'date')
2751 2751 if date:
2752 2752 opts[b'date'] = dateutil.parsedate(date)
2753 2753 message = logmessage(ui, opts)
2754 2754 matcher = scmutil.match(repo[None], pats, opts)
2755 2755
2756 2756 dsguard = None
2757 2757 # extract addremove carefully -- this function can be called from a command
2758 2758 # that doesn't support addremove
2759 2759 if opts.get(b'addremove'):
2760 2760 dsguard = dirstateguard.dirstateguard(repo, b'commit')
2761 2761 with dsguard or util.nullcontextmanager():
2762 2762 if dsguard:
2763 2763 relative = scmutil.anypats(pats, opts)
2764 2764 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=relative)
2765 2765 if scmutil.addremove(repo, matcher, b"", uipathfn, opts) != 0:
2766 2766 raise error.Abort(
2767 2767 _(b"failed to mark all new/missing files as added/removed")
2768 2768 )
2769 2769
2770 2770 return commitfunc(ui, repo, message, matcher, opts)
2771 2771
2772 2772
2773 2773 def samefile(f, ctx1, ctx2):
2774 2774 if f in ctx1.manifest():
2775 2775 a = ctx1.filectx(f)
2776 2776 if f in ctx2.manifest():
2777 2777 b = ctx2.filectx(f)
2778 2778 return not a.cmp(b) and a.flags() == b.flags()
2779 2779 else:
2780 2780 return False
2781 2781 else:
2782 2782 return f not in ctx2.manifest()
2783 2783
2784 2784
2785 2785 def amend(ui, repo, old, extra, pats, opts):
2786 2786 # avoid cycle context -> subrepo -> cmdutil
2787 2787 from . import context
2788 2788
2789 2789 # amend will reuse the existing user if not specified, but the obsolete
2790 2790 # marker creation requires that the current user's name is specified.
2791 2791 if obsolete.isenabled(repo, obsolete.createmarkersopt):
2792 2792 ui.username() # raise exception if username not set
2793 2793
2794 2794 ui.note(_(b'amending changeset %s\n') % old)
2795 2795 base = old.p1()
2796 2796
2797 2797 with repo.wlock(), repo.lock(), repo.transaction(b'amend'):
2798 2798 # Participating changesets:
2799 2799 #
2800 2800 # wctx o - workingctx that contains changes from working copy
2801 2801 # | to go into amending commit
2802 2802 # |
2803 2803 # old o - changeset to amend
2804 2804 # |
2805 2805 # base o - first parent of the changeset to amend
2806 2806 wctx = repo[None]
2807 2807
2808 2808 # Copy to avoid mutating input
2809 2809 extra = extra.copy()
2810 2810 # Update extra dict from amended commit (e.g. to preserve graft
2811 2811 # source)
2812 2812 extra.update(old.extra())
2813 2813
2814 2814 # Also update it from the from the wctx
2815 2815 extra.update(wctx.extra())
2816 2816
2817 2817 # date-only change should be ignored?
2818 2818 datemaydiffer = resolvecommitoptions(ui, opts)
2819 2819
2820 2820 date = old.date()
2821 2821 if opts.get(b'date'):
2822 2822 date = dateutil.parsedate(opts.get(b'date'))
2823 2823 user = opts.get(b'user') or old.user()
2824 2824
2825 2825 if len(old.parents()) > 1:
2826 2826 # ctx.files() isn't reliable for merges, so fall back to the
2827 2827 # slower repo.status() method
2828 2828 st = base.status(old)
2829 2829 files = set(st.modified) | set(st.added) | set(st.removed)
2830 2830 else:
2831 2831 files = set(old.files())
2832 2832
2833 2833 # add/remove the files to the working copy if the "addremove" option
2834 2834 # was specified.
2835 2835 matcher = scmutil.match(wctx, pats, opts)
2836 2836 relative = scmutil.anypats(pats, opts)
2837 2837 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=relative)
2838 2838 if opts.get(b'addremove') and scmutil.addremove(
2839 2839 repo, matcher, b"", uipathfn, opts
2840 2840 ):
2841 2841 raise error.Abort(
2842 2842 _(b"failed to mark all new/missing files as added/removed")
2843 2843 )
2844 2844
2845 2845 # Check subrepos. This depends on in-place wctx._status update in
2846 2846 # subrepo.precommit(). To minimize the risk of this hack, we do
2847 2847 # nothing if .hgsub does not exist.
2848 2848 if b'.hgsub' in wctx or b'.hgsub' in old:
2849 2849 subs, commitsubs, newsubstate = subrepoutil.precommit(
2850 2850 ui, wctx, wctx._status, matcher
2851 2851 )
2852 2852 # amend should abort if commitsubrepos is enabled
2853 2853 assert not commitsubs
2854 2854 if subs:
2855 2855 subrepoutil.writestate(repo, newsubstate)
2856 2856
2857 2857 ms = mergestatemod.mergestate.read(repo)
2858 2858 mergeutil.checkunresolved(ms)
2859 2859
2860 2860 filestoamend = {f for f in wctx.files() if matcher(f)}
2861 2861
2862 2862 changes = len(filestoamend) > 0
2863 2863 if changes:
2864 2864 # Recompute copies (avoid recording a -> b -> a)
2865 2865 copied = copies.pathcopies(base, wctx, matcher)
2866 2866 if old.p2:
2867 2867 copied.update(copies.pathcopies(old.p2(), wctx, matcher))
2868 2868
2869 2869 # Prune files which were reverted by the updates: if old
2870 2870 # introduced file X and the file was renamed in the working
2871 2871 # copy, then those two files are the same and
2872 2872 # we can discard X from our list of files. Likewise if X
2873 2873 # was removed, it's no longer relevant. If X is missing (aka
2874 2874 # deleted), old X must be preserved.
2875 2875 files.update(filestoamend)
2876 2876 files = [
2877 2877 f
2878 2878 for f in files
2879 2879 if (f not in filestoamend or not samefile(f, wctx, base))
2880 2880 ]
2881 2881
2882 2882 def filectxfn(repo, ctx_, path):
2883 2883 try:
2884 2884 # If the file being considered is not amongst the files
2885 2885 # to be amended, we should return the file context from the
2886 2886 # old changeset. This avoids issues when only some files in
2887 2887 # the working copy are being amended but there are also
2888 2888 # changes to other files from the old changeset.
2889 2889 if path not in filestoamend:
2890 2890 return old.filectx(path)
2891 2891
2892 2892 # Return None for removed files.
2893 2893 if path in wctx.removed():
2894 2894 return None
2895 2895
2896 2896 fctx = wctx[path]
2897 2897 flags = fctx.flags()
2898 2898 mctx = context.memfilectx(
2899 2899 repo,
2900 2900 ctx_,
2901 2901 fctx.path(),
2902 2902 fctx.data(),
2903 2903 islink=b'l' in flags,
2904 2904 isexec=b'x' in flags,
2905 2905 copysource=copied.get(path),
2906 2906 )
2907 2907 return mctx
2908 2908 except KeyError:
2909 2909 return None
2910 2910
2911 2911 else:
2912 2912 ui.note(_(b'copying changeset %s to %s\n') % (old, base))
2913 2913
2914 2914 # Use version of files as in the old cset
2915 2915 def filectxfn(repo, ctx_, path):
2916 2916 try:
2917 2917 return old.filectx(path)
2918 2918 except KeyError:
2919 2919 return None
2920 2920
2921 2921 # See if we got a message from -m or -l, if not, open the editor with
2922 2922 # the message of the changeset to amend.
2923 2923 message = logmessage(ui, opts)
2924 2924
2925 2925 editform = mergeeditform(old, b'commit.amend')
2926 2926
2927 2927 if not message:
2928 2928 message = old.description()
2929 2929 # Default if message isn't provided and --edit is not passed is to
2930 2930 # invoke editor, but allow --no-edit. If somehow we don't have any
2931 2931 # description, let's always start the editor.
2932 2932 doedit = not message or opts.get(b'edit') in [True, None]
2933 2933 else:
2934 2934 # Default if message is provided is to not invoke editor, but allow
2935 2935 # --edit.
2936 2936 doedit = opts.get(b'edit') is True
2937 2937 editor = getcommiteditor(edit=doedit, editform=editform)
2938 2938
2939 2939 pureextra = extra.copy()
2940 2940 extra[b'amend_source'] = old.hex()
2941 2941
2942 2942 new = context.memctx(
2943 2943 repo,
2944 2944 parents=[base.node(), old.p2().node()],
2945 2945 text=message,
2946 2946 files=files,
2947 2947 filectxfn=filectxfn,
2948 2948 user=user,
2949 2949 date=date,
2950 2950 extra=extra,
2951 2951 editor=editor,
2952 2952 )
2953 2953
2954 2954 newdesc = changelog.stripdesc(new.description())
2955 2955 if (
2956 2956 (not changes)
2957 2957 and newdesc == old.description()
2958 2958 and user == old.user()
2959 2959 and (date == old.date() or datemaydiffer)
2960 2960 and pureextra == old.extra()
2961 2961 ):
2962 2962 # nothing changed. continuing here would create a new node
2963 2963 # anyway because of the amend_source noise.
2964 2964 #
2965 2965 # This not what we expect from amend.
2966 2966 return old.node()
2967 2967
2968 2968 commitphase = None
2969 2969 if opts.get(b'secret'):
2970 2970 commitphase = phases.secret
2971 2971 newid = repo.commitctx(new)
2972 2972 ms.reset()
2973 2973
2974 2974 # Reroute the working copy parent to the new changeset
2975 2975 repo.setparents(newid, repo.nullid)
2976 2976
2977 2977 # Fixing the dirstate because localrepo.commitctx does not update
2978 2978 # it. This is rather convenient because we did not need to update
2979 2979 # the dirstate for all the files in the new commit which commitctx
2980 2980 # could have done if it updated the dirstate. Now, we can
2981 2981 # selectively update the dirstate only for the amended files.
2982 2982 dirstate = repo.dirstate
2983 2983
2984 2984 # Update the state of the files which were added and modified in the
2985 2985 # amend to "normal" in the dirstate. We need to use "normallookup" since
2986 2986 # the files may have changed since the command started; using "normal"
2987 2987 # would mark them as clean but with uncommitted contents.
2988 2988 normalfiles = set(wctx.modified() + wctx.added()) & filestoamend
2989 2989 for f in normalfiles:
2990 2990 dirstate.normallookup(f)
2991 2991
2992 2992 # Update the state of files which were removed in the amend
2993 2993 # to "removed" in the dirstate.
2994 2994 removedfiles = set(wctx.removed()) & filestoamend
2995 2995 for f in removedfiles:
2996 2996 dirstate.drop(f)
2997 2997
2998 2998 mapping = {old.node(): (newid,)}
2999 2999 obsmetadata = None
3000 3000 if opts.get(b'note'):
3001 3001 obsmetadata = {b'note': encoding.fromlocal(opts[b'note'])}
3002 3002 backup = ui.configbool(b'rewrite', b'backup-bundle')
3003 3003 scmutil.cleanupnodes(
3004 3004 repo,
3005 3005 mapping,
3006 3006 b'amend',
3007 3007 metadata=obsmetadata,
3008 3008 fixphase=True,
3009 3009 targetphase=commitphase,
3010 3010 backup=backup,
3011 3011 )
3012 3012
3013 3013 return newid
3014 3014
3015 3015
3016 3016 def commiteditor(repo, ctx, subs, editform=b''):
3017 3017 if ctx.description():
3018 3018 return ctx.description()
3019 3019 return commitforceeditor(
3020 3020 repo, ctx, subs, editform=editform, unchangedmessagedetection=True
3021 3021 )
3022 3022
3023 3023
3024 3024 def commitforceeditor(
3025 3025 repo,
3026 3026 ctx,
3027 3027 subs,
3028 3028 finishdesc=None,
3029 3029 extramsg=None,
3030 3030 editform=b'',
3031 3031 unchangedmessagedetection=False,
3032 3032 ):
3033 3033 if not extramsg:
3034 3034 extramsg = _(b"Leave message empty to abort commit.")
3035 3035
3036 3036 forms = [e for e in editform.split(b'.') if e]
3037 3037 forms.insert(0, b'changeset')
3038 3038 templatetext = None
3039 3039 while forms:
3040 3040 ref = b'.'.join(forms)
3041 3041 if repo.ui.config(b'committemplate', ref):
3042 3042 templatetext = committext = buildcommittemplate(
3043 3043 repo, ctx, subs, extramsg, ref
3044 3044 )
3045 3045 break
3046 3046 forms.pop()
3047 3047 else:
3048 3048 committext = buildcommittext(repo, ctx, subs, extramsg)
3049 3049
3050 3050 # run editor in the repository root
3051 3051 olddir = encoding.getcwd()
3052 3052 os.chdir(repo.root)
3053 3053
3054 3054 # make in-memory changes visible to external process
3055 3055 tr = repo.currenttransaction()
3056 3056 repo.dirstate.write(tr)
3057 3057 pending = tr and tr.writepending() and repo.root
3058 3058
3059 3059 editortext = repo.ui.edit(
3060 3060 committext,
3061 3061 ctx.user(),
3062 3062 ctx.extra(),
3063 3063 editform=editform,
3064 3064 pending=pending,
3065 3065 repopath=repo.path,
3066 3066 action=b'commit',
3067 3067 )
3068 3068 text = editortext
3069 3069
3070 3070 # strip away anything below this special string (used for editors that want
3071 3071 # to display the diff)
3072 3072 stripbelow = re.search(_linebelow, text, flags=re.MULTILINE)
3073 3073 if stripbelow:
3074 3074 text = text[: stripbelow.start()]
3075 3075
3076 3076 text = re.sub(b"(?m)^HG:.*(\n|$)", b"", text)
3077 3077 os.chdir(olddir)
3078 3078
3079 3079 if finishdesc:
3080 3080 text = finishdesc(text)
3081 3081 if not text.strip():
3082 3082 raise error.InputError(_(b"empty commit message"))
3083 3083 if unchangedmessagedetection and editortext == templatetext:
3084 3084 raise error.InputError(_(b"commit message unchanged"))
3085 3085
3086 3086 return text
3087 3087
3088 3088
3089 3089 def buildcommittemplate(repo, ctx, subs, extramsg, ref):
3090 3090 ui = repo.ui
3091 3091 spec = formatter.reference_templatespec(ref)
3092 3092 t = logcmdutil.changesettemplater(ui, repo, spec)
3093 3093 t.t.cache.update(
3094 3094 (k, templater.unquotestring(v))
3095 3095 for k, v in repo.ui.configitems(b'committemplate')
3096 3096 )
3097 3097
3098 3098 if not extramsg:
3099 3099 extramsg = b'' # ensure that extramsg is string
3100 3100
3101 3101 ui.pushbuffer()
3102 3102 t.show(ctx, extramsg=extramsg)
3103 3103 return ui.popbuffer()
3104 3104
3105 3105
3106 3106 def hgprefix(msg):
3107 3107 return b"\n".join([b"HG: %s" % a for a in msg.split(b"\n") if a])
3108 3108
3109 3109
3110 3110 def buildcommittext(repo, ctx, subs, extramsg):
3111 3111 edittext = []
3112 3112 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
3113 3113 if ctx.description():
3114 3114 edittext.append(ctx.description())
3115 3115 edittext.append(b"")
3116 3116 edittext.append(b"") # Empty line between message and comments.
3117 3117 edittext.append(
3118 3118 hgprefix(
3119 3119 _(
3120 3120 b"Enter commit message."
3121 3121 b" Lines beginning with 'HG:' are removed."
3122 3122 )
3123 3123 )
3124 3124 )
3125 3125 edittext.append(hgprefix(extramsg))
3126 3126 edittext.append(b"HG: --")
3127 3127 edittext.append(hgprefix(_(b"user: %s") % ctx.user()))
3128 3128 if ctx.p2():
3129 3129 edittext.append(hgprefix(_(b"branch merge")))
3130 3130 if ctx.branch():
3131 3131 edittext.append(hgprefix(_(b"branch '%s'") % ctx.branch()))
3132 3132 if bookmarks.isactivewdirparent(repo):
3133 3133 edittext.append(hgprefix(_(b"bookmark '%s'") % repo._activebookmark))
3134 3134 edittext.extend([hgprefix(_(b"subrepo %s") % s) for s in subs])
3135 3135 edittext.extend([hgprefix(_(b"added %s") % f) for f in added])
3136 3136 edittext.extend([hgprefix(_(b"changed %s") % f) for f in modified])
3137 3137 edittext.extend([hgprefix(_(b"removed %s") % f) for f in removed])
3138 3138 if not added and not modified and not removed:
3139 3139 edittext.append(hgprefix(_(b"no files changed")))
3140 3140 edittext.append(b"")
3141 3141
3142 3142 return b"\n".join(edittext)
3143 3143
3144 3144
3145 3145 def commitstatus(repo, node, branch, bheads=None, tip=None, opts=None):
3146 3146 if opts is None:
3147 3147 opts = {}
3148 3148 ctx = repo[node]
3149 3149 parents = ctx.parents()
3150 3150
3151 3151 if tip is not None and repo.changelog.tip() == tip:
3152 3152 # avoid reporting something like "committed new head" when
3153 3153 # recommitting old changesets, and issue a helpful warning
3154 3154 # for most instances
3155 3155 repo.ui.warn(_(b"warning: commit already existed in the repository!\n"))
3156 3156 elif (
3157 3157 not opts.get(b'amend')
3158 3158 and bheads
3159 3159 and node not in bheads
3160 3160 and not any(
3161 3161 p.node() in bheads and p.branch() == branch for p in parents
3162 3162 )
3163 3163 ):
3164 3164 repo.ui.status(_(b'created new head\n'))
3165 3165 # The message is not printed for initial roots. For the other
3166 3166 # changesets, it is printed in the following situations:
3167 3167 #
3168 3168 # Par column: for the 2 parents with ...
3169 3169 # N: null or no parent
3170 3170 # B: parent is on another named branch
3171 3171 # C: parent is a regular non head changeset
3172 3172 # H: parent was a branch head of the current branch
3173 3173 # Msg column: whether we print "created new head" message
3174 3174 # In the following, it is assumed that there already exists some
3175 3175 # initial branch heads of the current branch, otherwise nothing is
3176 3176 # printed anyway.
3177 3177 #
3178 3178 # Par Msg Comment
3179 3179 # N N y additional topo root
3180 3180 #
3181 3181 # B N y additional branch root
3182 3182 # C N y additional topo head
3183 3183 # H N n usual case
3184 3184 #
3185 3185 # B B y weird additional branch root
3186 3186 # C B y branch merge
3187 3187 # H B n merge with named branch
3188 3188 #
3189 3189 # C C y additional head from merge
3190 3190 # C H n merge with a head
3191 3191 #
3192 3192 # H H n head merge: head count decreases
3193 3193
3194 3194 if not opts.get(b'close_branch'):
3195 3195 for r in parents:
3196 3196 if r.closesbranch() and r.branch() == branch:
3197 3197 repo.ui.status(
3198 3198 _(b'reopening closed branch head %d\n') % r.rev()
3199 3199 )
3200 3200
3201 3201 if repo.ui.debugflag:
3202 3202 repo.ui.write(
3203 3203 _(b'committed changeset %d:%s\n') % (ctx.rev(), ctx.hex())
3204 3204 )
3205 3205 elif repo.ui.verbose:
3206 3206 repo.ui.write(_(b'committed changeset %d:%s\n') % (ctx.rev(), ctx))
3207 3207
3208 3208
3209 3209 def postcommitstatus(repo, pats, opts):
3210 3210 return repo.status(match=scmutil.match(repo[None], pats, opts))
3211 3211
3212 3212
3213 3213 def revert(ui, repo, ctx, *pats, **opts):
3214 3214 opts = pycompat.byteskwargs(opts)
3215 3215 parent, p2 = repo.dirstate.parents()
3216 3216 node = ctx.node()
3217 3217
3218 3218 mf = ctx.manifest()
3219 3219 if node == p2:
3220 3220 parent = p2
3221 3221
3222 3222 # need all matching names in dirstate and manifest of target rev,
3223 3223 # so have to walk both. do not print errors if files exist in one
3224 3224 # but not other. in both cases, filesets should be evaluated against
3225 3225 # workingctx to get consistent result (issue4497). this means 'set:**'
3226 3226 # cannot be used to select missing files from target rev.
3227 3227
3228 3228 # `names` is a mapping for all elements in working copy and target revision
3229 3229 # The mapping is in the form:
3230 3230 # <abs path in repo> -> (<path from CWD>, <exactly specified by matcher?>)
3231 3231 names = {}
3232 3232 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
3233 3233
3234 3234 with repo.wlock():
3235 3235 ## filling of the `names` mapping
3236 3236 # walk dirstate to fill `names`
3237 3237
3238 3238 interactive = opts.get(b'interactive', False)
3239 3239 wctx = repo[None]
3240 3240 m = scmutil.match(wctx, pats, opts)
3241 3241
3242 3242 # we'll need this later
3243 3243 targetsubs = sorted(s for s in wctx.substate if m(s))
3244 3244
3245 3245 if not m.always():
3246 3246 matcher = matchmod.badmatch(m, lambda x, y: False)
3247 3247 for abs in wctx.walk(matcher):
3248 3248 names[abs] = m.exact(abs)
3249 3249
3250 3250 # walk target manifest to fill `names`
3251 3251
3252 3252 def badfn(path, msg):
3253 3253 if path in names:
3254 3254 return
3255 3255 if path in ctx.substate:
3256 3256 return
3257 3257 path_ = path + b'/'
3258 3258 for f in names:
3259 3259 if f.startswith(path_):
3260 3260 return
3261 3261 ui.warn(b"%s: %s\n" % (uipathfn(path), msg))
3262 3262
3263 3263 for abs in ctx.walk(matchmod.badmatch(m, badfn)):
3264 3264 if abs not in names:
3265 3265 names[abs] = m.exact(abs)
3266 3266
3267 3267 # Find status of all file in `names`.
3268 3268 m = scmutil.matchfiles(repo, names)
3269 3269
3270 3270 changes = repo.status(
3271 3271 node1=node, match=m, unknown=True, ignored=True, clean=True
3272 3272 )
3273 3273 else:
3274 3274 changes = repo.status(node1=node, match=m)
3275 3275 for kind in changes:
3276 3276 for abs in kind:
3277 3277 names[abs] = m.exact(abs)
3278 3278
3279 3279 m = scmutil.matchfiles(repo, names)
3280 3280
3281 3281 modified = set(changes.modified)
3282 3282 added = set(changes.added)
3283 3283 removed = set(changes.removed)
3284 3284 _deleted = set(changes.deleted)
3285 3285 unknown = set(changes.unknown)
3286 3286 unknown.update(changes.ignored)
3287 3287 clean = set(changes.clean)
3288 3288 modadded = set()
3289 3289
3290 3290 # We need to account for the state of the file in the dirstate,
3291 3291 # even when we revert against something else than parent. This will
3292 3292 # slightly alter the behavior of revert (doing back up or not, delete
3293 3293 # or just forget etc).
3294 3294 if parent == node:
3295 3295 dsmodified = modified
3296 3296 dsadded = added
3297 3297 dsremoved = removed
3298 3298 # store all local modifications, useful later for rename detection
3299 3299 localchanges = dsmodified | dsadded
3300 3300 modified, added, removed = set(), set(), set()
3301 3301 else:
3302 3302 changes = repo.status(node1=parent, match=m)
3303 3303 dsmodified = set(changes.modified)
3304 3304 dsadded = set(changes.added)
3305 3305 dsremoved = set(changes.removed)
3306 3306 # store all local modifications, useful later for rename detection
3307 3307 localchanges = dsmodified | dsadded
3308 3308
3309 3309 # only take into account for removes between wc and target
3310 3310 clean |= dsremoved - removed
3311 3311 dsremoved &= removed
3312 3312 # distinct between dirstate remove and other
3313 3313 removed -= dsremoved
3314 3314
3315 3315 modadded = added & dsmodified
3316 3316 added -= modadded
3317 3317
3318 3318 # tell newly modified apart.
3319 3319 dsmodified &= modified
3320 3320 dsmodified |= modified & dsadded # dirstate added may need backup
3321 3321 modified -= dsmodified
3322 3322
3323 3323 # We need to wait for some post-processing to update this set
3324 3324 # before making the distinction. The dirstate will be used for
3325 3325 # that purpose.
3326 3326 dsadded = added
3327 3327
3328 3328 # in case of merge, files that are actually added can be reported as
3329 3329 # modified, we need to post process the result
3330 3330 if p2 != repo.nullid:
3331 3331 mergeadd = set(dsmodified)
3332 3332 for path in dsmodified:
3333 3333 if path in mf:
3334 3334 mergeadd.remove(path)
3335 3335 dsadded |= mergeadd
3336 3336 dsmodified -= mergeadd
3337 3337
3338 3338 # if f is a rename, update `names` to also revert the source
3339 3339 for f in localchanges:
3340 3340 src = repo.dirstate.copied(f)
3341 3341 # XXX should we check for rename down to target node?
3342 3342 if src and src not in names and repo.dirstate[src] == b'r':
3343 3343 dsremoved.add(src)
3344 3344 names[src] = True
3345 3345
3346 3346 # determine the exact nature of the deleted changesets
3347 3347 deladded = set(_deleted)
3348 3348 for path in _deleted:
3349 3349 if path in mf:
3350 3350 deladded.remove(path)
3351 3351 deleted = _deleted - deladded
3352 3352
3353 3353 # distinguish between file to forget and the other
3354 3354 added = set()
3355 3355 for abs in dsadded:
3356 3356 if repo.dirstate[abs] != b'a':
3357 3357 added.add(abs)
3358 3358 dsadded -= added
3359 3359
3360 3360 for abs in deladded:
3361 3361 if repo.dirstate[abs] == b'a':
3362 3362 dsadded.add(abs)
3363 3363 deladded -= dsadded
3364 3364
3365 3365 # For files marked as removed, we check if an unknown file is present at
3366 3366 # the same path. If a such file exists it may need to be backed up.
3367 3367 # Making the distinction at this stage helps have simpler backup
3368 3368 # logic.
3369 3369 removunk = set()
3370 3370 for abs in removed:
3371 3371 target = repo.wjoin(abs)
3372 3372 if os.path.lexists(target):
3373 3373 removunk.add(abs)
3374 3374 removed -= removunk
3375 3375
3376 3376 dsremovunk = set()
3377 3377 for abs in dsremoved:
3378 3378 target = repo.wjoin(abs)
3379 3379 if os.path.lexists(target):
3380 3380 dsremovunk.add(abs)
3381 3381 dsremoved -= dsremovunk
3382 3382
3383 3383 # action to be actually performed by revert
3384 3384 # (<list of file>, message>) tuple
3385 3385 actions = {
3386 3386 b'revert': ([], _(b'reverting %s\n')),
3387 3387 b'add': ([], _(b'adding %s\n')),
3388 3388 b'remove': ([], _(b'removing %s\n')),
3389 3389 b'drop': ([], _(b'removing %s\n')),
3390 3390 b'forget': ([], _(b'forgetting %s\n')),
3391 3391 b'undelete': ([], _(b'undeleting %s\n')),
3392 3392 b'noop': (None, _(b'no changes needed to %s\n')),
3393 3393 b'unknown': (None, _(b'file not managed: %s\n')),
3394 3394 }
3395 3395
3396 3396 # "constant" that convey the backup strategy.
3397 3397 # All set to `discard` if `no-backup` is set do avoid checking
3398 3398 # no_backup lower in the code.
3399 3399 # These values are ordered for comparison purposes
3400 3400 backupinteractive = 3 # do backup if interactively modified
3401 3401 backup = 2 # unconditionally do backup
3402 3402 check = 1 # check if the existing file differs from target
3403 3403 discard = 0 # never do backup
3404 3404 if opts.get(b'no_backup'):
3405 3405 backupinteractive = backup = check = discard
3406 3406 if interactive:
3407 3407 dsmodifiedbackup = backupinteractive
3408 3408 else:
3409 3409 dsmodifiedbackup = backup
3410 3410 tobackup = set()
3411 3411
3412 3412 backupanddel = actions[b'remove']
3413 3413 if not opts.get(b'no_backup'):
3414 3414 backupanddel = actions[b'drop']
3415 3415
3416 3416 disptable = (
3417 3417 # dispatch table:
3418 3418 # file state
3419 3419 # action
3420 3420 # make backup
3421 3421 ## Sets that results that will change file on disk
3422 3422 # Modified compared to target, no local change
3423 3423 (modified, actions[b'revert'], discard),
3424 3424 # Modified compared to target, but local file is deleted
3425 3425 (deleted, actions[b'revert'], discard),
3426 3426 # Modified compared to target, local change
3427 3427 (dsmodified, actions[b'revert'], dsmodifiedbackup),
3428 3428 # Added since target
3429 3429 (added, actions[b'remove'], discard),
3430 3430 # Added in working directory
3431 3431 (dsadded, actions[b'forget'], discard),
3432 3432 # Added since target, have local modification
3433 3433 (modadded, backupanddel, backup),
3434 3434 # Added since target but file is missing in working directory
3435 3435 (deladded, actions[b'drop'], discard),
3436 3436 # Removed since target, before working copy parent
3437 3437 (removed, actions[b'add'], discard),
3438 3438 # Same as `removed` but an unknown file exists at the same path
3439 3439 (removunk, actions[b'add'], check),
3440 3440 # Removed since targe, marked as such in working copy parent
3441 3441 (dsremoved, actions[b'undelete'], discard),
3442 3442 # Same as `dsremoved` but an unknown file exists at the same path
3443 3443 (dsremovunk, actions[b'undelete'], check),
3444 3444 ## the following sets does not result in any file changes
3445 3445 # File with no modification
3446 3446 (clean, actions[b'noop'], discard),
3447 3447 # Existing file, not tracked anywhere
3448 3448 (unknown, actions[b'unknown'], discard),
3449 3449 )
3450 3450
3451 3451 for abs, exact in sorted(names.items()):
3452 3452 # target file to be touch on disk (relative to cwd)
3453 3453 target = repo.wjoin(abs)
3454 3454 # search the entry in the dispatch table.
3455 3455 # if the file is in any of these sets, it was touched in the working
3456 3456 # directory parent and we are sure it needs to be reverted.
3457 3457 for table, (xlist, msg), dobackup in disptable:
3458 3458 if abs not in table:
3459 3459 continue
3460 3460 if xlist is not None:
3461 3461 xlist.append(abs)
3462 3462 if dobackup:
3463 3463 # If in interactive mode, don't automatically create
3464 3464 # .orig files (issue4793)
3465 3465 if dobackup == backupinteractive:
3466 3466 tobackup.add(abs)
3467 3467 elif backup <= dobackup or wctx[abs].cmp(ctx[abs]):
3468 3468 absbakname = scmutil.backuppath(ui, repo, abs)
3469 3469 bakname = os.path.relpath(
3470 3470 absbakname, start=repo.root
3471 3471 )
3472 3472 ui.note(
3473 3473 _(b'saving current version of %s as %s\n')
3474 3474 % (uipathfn(abs), uipathfn(bakname))
3475 3475 )
3476 3476 if not opts.get(b'dry_run'):
3477 3477 if interactive:
3478 3478 util.copyfile(target, absbakname)
3479 3479 else:
3480 3480 util.rename(target, absbakname)
3481 3481 if opts.get(b'dry_run'):
3482 3482 if ui.verbose or not exact:
3483 3483 ui.status(msg % uipathfn(abs))
3484 3484 elif exact:
3485 3485 ui.warn(msg % uipathfn(abs))
3486 3486 break
3487 3487
3488 3488 if not opts.get(b'dry_run'):
3489 3489 needdata = (b'revert', b'add', b'undelete')
3490 3490 oplist = [actions[name][0] for name in needdata]
3491 3491 prefetch = scmutil.prefetchfiles
3492 3492 matchfiles = scmutil.matchfiles(
3493 3493 repo, [f for sublist in oplist for f in sublist]
3494 3494 )
3495 3495 prefetch(
3496 3496 repo,
3497 3497 [(ctx.rev(), matchfiles)],
3498 3498 )
3499 3499 match = scmutil.match(repo[None], pats)
3500 3500 _performrevert(
3501 3501 repo,
3502 3502 ctx,
3503 3503 names,
3504 3504 uipathfn,
3505 3505 actions,
3506 3506 match,
3507 3507 interactive,
3508 3508 tobackup,
3509 3509 )
3510 3510
3511 3511 if targetsubs:
3512 3512 # Revert the subrepos on the revert list
3513 3513 for sub in targetsubs:
3514 3514 try:
3515 3515 wctx.sub(sub).revert(
3516 3516 ctx.substate[sub], *pats, **pycompat.strkwargs(opts)
3517 3517 )
3518 3518 except KeyError:
3519 3519 raise error.Abort(
3520 3520 b"subrepository '%s' does not exist in %s!"
3521 3521 % (sub, short(ctx.node()))
3522 3522 )
3523 3523
3524 3524
3525 3525 def _performrevert(
3526 3526 repo,
3527 3527 ctx,
3528 3528 names,
3529 3529 uipathfn,
3530 3530 actions,
3531 3531 match,
3532 3532 interactive=False,
3533 3533 tobackup=None,
3534 3534 ):
3535 3535 """function that actually perform all the actions computed for revert
3536 3536
3537 3537 This is an independent function to let extension to plug in and react to
3538 3538 the imminent revert.
3539 3539
3540 3540 Make sure you have the working directory locked when calling this function.
3541 3541 """
3542 3542 parent, p2 = repo.dirstate.parents()
3543 3543 node = ctx.node()
3544 3544 excluded_files = []
3545 3545
3546 3546 def checkout(f):
3547 3547 fc = ctx[f]
3548 3548 repo.wwrite(f, fc.data(), fc.flags())
3549 3549
3550 3550 def doremove(f):
3551 3551 try:
3552 3552 rmdir = repo.ui.configbool(b'experimental', b'removeemptydirs')
3553 3553 repo.wvfs.unlinkpath(f, rmdir=rmdir)
3554 3554 except OSError:
3555 3555 pass
3556 3556 repo.dirstate.remove(f)
3557 3557
3558 3558 def prntstatusmsg(action, f):
3559 3559 exact = names[f]
3560 3560 if repo.ui.verbose or not exact:
3561 3561 repo.ui.status(actions[action][1] % uipathfn(f))
3562 3562
3563 3563 audit_path = pathutil.pathauditor(repo.root, cached=True)
3564 3564 for f in actions[b'forget'][0]:
3565 3565 if interactive:
3566 3566 choice = repo.ui.promptchoice(
3567 3567 _(b"forget added file %s (Yn)?$$ &Yes $$ &No") % uipathfn(f)
3568 3568 )
3569 3569 if choice == 0:
3570 3570 prntstatusmsg(b'forget', f)
3571 3571 repo.dirstate.drop(f)
3572 3572 else:
3573 3573 excluded_files.append(f)
3574 3574 else:
3575 3575 prntstatusmsg(b'forget', f)
3576 3576 repo.dirstate.drop(f)
3577 3577 for f in actions[b'remove'][0]:
3578 3578 audit_path(f)
3579 3579 if interactive:
3580 3580 choice = repo.ui.promptchoice(
3581 3581 _(b"remove added file %s (Yn)?$$ &Yes $$ &No") % uipathfn(f)
3582 3582 )
3583 3583 if choice == 0:
3584 3584 prntstatusmsg(b'remove', f)
3585 3585 doremove(f)
3586 3586 else:
3587 3587 excluded_files.append(f)
3588 3588 else:
3589 3589 prntstatusmsg(b'remove', f)
3590 3590 doremove(f)
3591 3591 for f in actions[b'drop'][0]:
3592 3592 audit_path(f)
3593 3593 prntstatusmsg(b'drop', f)
3594 3594 repo.dirstate.remove(f)
3595 3595
3596 3596 normal = None
3597 3597 if node == parent:
3598 3598 # We're reverting to our parent. If possible, we'd like status
3599 3599 # to report the file as clean. We have to use normallookup for
3600 3600 # merges to avoid losing information about merged/dirty files.
3601 3601 if p2 != repo.nullid:
3602 3602 normal = repo.dirstate.normallookup
3603 3603 else:
3604 3604 normal = repo.dirstate.normal
3605 3605
3606 3606 newlyaddedandmodifiedfiles = set()
3607 3607 if interactive:
3608 3608 # Prompt the user for changes to revert
3609 3609 torevert = [f for f in actions[b'revert'][0] if f not in excluded_files]
3610 3610 m = scmutil.matchfiles(repo, torevert)
3611 3611 diffopts = patch.difffeatureopts(
3612 3612 repo.ui,
3613 3613 whitespace=True,
3614 3614 section=b'commands',
3615 3615 configprefix=b'revert.interactive.',
3616 3616 )
3617 3617 diffopts.nodates = True
3618 3618 diffopts.git = True
3619 3619 operation = b'apply'
3620 3620 if node == parent:
3621 3621 if repo.ui.configbool(
3622 3622 b'experimental', b'revert.interactive.select-to-keep'
3623 3623 ):
3624 3624 operation = b'keep'
3625 3625 else:
3626 3626 operation = b'discard'
3627 3627
3628 3628 if operation == b'apply':
3629 3629 diff = patch.diff(repo, None, ctx.node(), m, opts=diffopts)
3630 3630 else:
3631 3631 diff = patch.diff(repo, ctx.node(), None, m, opts=diffopts)
3632 3632 originalchunks = patch.parsepatch(diff)
3633 3633
3634 3634 try:
3635 3635
3636 3636 chunks, opts = recordfilter(
3637 3637 repo.ui, originalchunks, match, operation=operation
3638 3638 )
3639 3639 if operation == b'discard':
3640 3640 chunks = patch.reversehunks(chunks)
3641 3641
3642 3642 except error.PatchError as err:
3643 3643 raise error.Abort(_(b'error parsing patch: %s') % err)
3644 3644
3645 3645 # FIXME: when doing an interactive revert of a copy, there's no way of
3646 3646 # performing a partial revert of the added file, the only option is
3647 3647 # "remove added file <name> (Yn)?", so we don't need to worry about the
3648 3648 # alsorestore value. Ideally we'd be able to partially revert
3649 3649 # copied/renamed files.
3650 3650 newlyaddedandmodifiedfiles, unusedalsorestore = newandmodified(
3651 3651 chunks, originalchunks
3652 3652 )
3653 3653 if tobackup is None:
3654 3654 tobackup = set()
3655 3655 # Apply changes
3656 3656 fp = stringio()
3657 3657 # chunks are serialized per file, but files aren't sorted
3658 3658 for f in sorted({c.header.filename() for c in chunks if ishunk(c)}):
3659 3659 prntstatusmsg(b'revert', f)
3660 3660 files = set()
3661 3661 for c in chunks:
3662 3662 if ishunk(c):
3663 3663 abs = c.header.filename()
3664 3664 # Create a backup file only if this hunk should be backed up
3665 3665 if c.header.filename() in tobackup:
3666 3666 target = repo.wjoin(abs)
3667 3667 bakname = scmutil.backuppath(repo.ui, repo, abs)
3668 3668 util.copyfile(target, bakname)
3669 3669 tobackup.remove(abs)
3670 3670 if abs not in files:
3671 3671 files.add(abs)
3672 3672 if operation == b'keep':
3673 3673 checkout(abs)
3674 3674 c.write(fp)
3675 3675 dopatch = fp.tell()
3676 3676 fp.seek(0)
3677 3677 if dopatch:
3678 3678 try:
3679 3679 patch.internalpatch(repo.ui, repo, fp, 1, eolmode=None)
3680 3680 except error.PatchError as err:
3681 3681 raise error.Abort(pycompat.bytestr(err))
3682 3682 del fp
3683 3683 else:
3684 3684 for f in actions[b'revert'][0]:
3685 3685 prntstatusmsg(b'revert', f)
3686 3686 checkout(f)
3687 3687 if normal:
3688 3688 normal(f)
3689 3689
3690 3690 for f in actions[b'add'][0]:
3691 3691 # Don't checkout modified files, they are already created by the diff
3692 3692 if f not in newlyaddedandmodifiedfiles:
3693 3693 prntstatusmsg(b'add', f)
3694 3694 checkout(f)
3695 3695 repo.dirstate.add(f)
3696 3696
3697 3697 normal = repo.dirstate.normallookup
3698 3698 if node == parent and p2 == repo.nullid:
3699 3699 normal = repo.dirstate.normal
3700 3700 for f in actions[b'undelete'][0]:
3701 3701 if interactive:
3702 3702 choice = repo.ui.promptchoice(
3703 3703 _(b"add back removed file %s (Yn)?$$ &Yes $$ &No") % f
3704 3704 )
3705 3705 if choice == 0:
3706 3706 prntstatusmsg(b'undelete', f)
3707 3707 checkout(f)
3708 3708 normal(f)
3709 3709 else:
3710 3710 excluded_files.append(f)
3711 3711 else:
3712 3712 prntstatusmsg(b'undelete', f)
3713 3713 checkout(f)
3714 3714 normal(f)
3715 3715
3716 3716 copied = copies.pathcopies(repo[parent], ctx)
3717 3717
3718 3718 for f in (
3719 3719 actions[b'add'][0] + actions[b'undelete'][0] + actions[b'revert'][0]
3720 3720 ):
3721 3721 if f in copied:
3722 3722 repo.dirstate.copy(copied[f], f)
3723 3723
3724 3724
3725 3725 # a list of (ui, repo, otherpeer, opts, missing) functions called by
3726 3726 # commands.outgoing. "missing" is "missing" of the result of
3727 3727 # "findcommonoutgoing()"
3728 3728 outgoinghooks = util.hooks()
3729 3729
3730 3730 # a list of (ui, repo) functions called by commands.summary
3731 3731 summaryhooks = util.hooks()
3732 3732
3733 3733 # a list of (ui, repo, opts, changes) functions called by commands.summary.
3734 3734 #
3735 3735 # functions should return tuple of booleans below, if 'changes' is None:
3736 3736 # (whether-incomings-are-needed, whether-outgoings-are-needed)
3737 3737 #
3738 3738 # otherwise, 'changes' is a tuple of tuples below:
3739 3739 # - (sourceurl, sourcebranch, sourcepeer, incoming)
3740 3740 # - (desturl, destbranch, destpeer, outgoing)
3741 3741 summaryremotehooks = util.hooks()
3742 3742
3743 3743
3744 3744 def checkunfinished(repo, commit=False, skipmerge=False):
3745 3745 """Look for an unfinished multistep operation, like graft, and abort
3746 3746 if found. It's probably good to check this right before
3747 3747 bailifchanged().
3748 3748 """
3749 3749 # Check for non-clearable states first, so things like rebase will take
3750 3750 # precedence over update.
3751 3751 for state in statemod._unfinishedstates:
3752 3752 if (
3753 3753 state._clearable
3754 3754 or (commit and state._allowcommit)
3755 3755 or state._reportonly
3756 3756 ):
3757 3757 continue
3758 3758 if state.isunfinished(repo):
3759 3759 raise error.StateError(state.msg(), hint=state.hint())
3760 3760
3761 3761 for s in statemod._unfinishedstates:
3762 3762 if (
3763 3763 not s._clearable
3764 3764 or (commit and s._allowcommit)
3765 3765 or (s._opname == b'merge' and skipmerge)
3766 3766 or s._reportonly
3767 3767 ):
3768 3768 continue
3769 3769 if s.isunfinished(repo):
3770 3770 raise error.StateError(s.msg(), hint=s.hint())
3771 3771
3772 3772
3773 3773 def clearunfinished(repo):
3774 3774 """Check for unfinished operations (as above), and clear the ones
3775 3775 that are clearable.
3776 3776 """
3777 3777 for state in statemod._unfinishedstates:
3778 3778 if state._reportonly:
3779 3779 continue
3780 3780 if not state._clearable and state.isunfinished(repo):
3781 3781 raise error.StateError(state.msg(), hint=state.hint())
3782 3782
3783 3783 for s in statemod._unfinishedstates:
3784 3784 if s._opname == b'merge' or s._reportonly:
3785 3785 continue
3786 3786 if s._clearable and s.isunfinished(repo):
3787 3787 util.unlink(repo.vfs.join(s._fname))
3788 3788
3789 3789
3790 3790 def getunfinishedstate(repo):
3791 3791 """Checks for unfinished operations and returns statecheck object
3792 3792 for it"""
3793 3793 for state in statemod._unfinishedstates:
3794 3794 if state.isunfinished(repo):
3795 3795 return state
3796 3796 return None
3797 3797
3798 3798
3799 3799 def howtocontinue(repo):
3800 3800 """Check for an unfinished operation and return the command to finish
3801 3801 it.
3802 3802
3803 3803 statemod._unfinishedstates list is checked for an unfinished operation
3804 3804 and the corresponding message to finish it is generated if a method to
3805 3805 continue is supported by the operation.
3806 3806
3807 3807 Returns a (msg, warning) tuple. 'msg' is a string and 'warning' is
3808 3808 a boolean.
3809 3809 """
3810 3810 contmsg = _(b"continue: %s")
3811 3811 for state in statemod._unfinishedstates:
3812 3812 if not state._continueflag:
3813 3813 continue
3814 3814 if state.isunfinished(repo):
3815 3815 return contmsg % state.continuemsg(), True
3816 3816 if repo[None].dirty(missing=True, merge=False, branch=False):
3817 3817 return contmsg % _(b"hg commit"), False
3818 3818 return None, None
3819 3819
3820 3820
3821 3821 def checkafterresolved(repo):
3822 3822 """Inform the user about the next action after completing hg resolve
3823 3823
3824 3824 If there's a an unfinished operation that supports continue flag,
3825 3825 howtocontinue will yield repo.ui.warn as the reporter.
3826 3826
3827 3827 Otherwise, it will yield repo.ui.note.
3828 3828 """
3829 3829 msg, warning = howtocontinue(repo)
3830 3830 if msg is not None:
3831 3831 if warning:
3832 3832 repo.ui.warn(b"%s\n" % msg)
3833 3833 else:
3834 3834 repo.ui.note(b"%s\n" % msg)
3835 3835
3836 3836
3837 3837 def wrongtooltocontinue(repo, task):
3838 3838 """Raise an abort suggesting how to properly continue if there is an
3839 3839 active task.
3840 3840
3841 3841 Uses howtocontinue() to find the active task.
3842 3842
3843 3843 If there's no task (repo.ui.note for 'hg commit'), it does not offer
3844 3844 a hint.
3845 3845 """
3846 3846 after = howtocontinue(repo)
3847 3847 hint = None
3848 3848 if after[1]:
3849 3849 hint = after[0]
3850 3850 raise error.StateError(_(b'no %s in progress') % task, hint=hint)
3851 3851
3852 3852
3853 3853 def abortgraft(ui, repo, graftstate):
3854 3854 """abort the interrupted graft and rollbacks to the state before interrupted
3855 3855 graft"""
3856 3856 if not graftstate.exists():
3857 3857 raise error.StateError(_(b"no interrupted graft to abort"))
3858 3858 statedata = readgraftstate(repo, graftstate)
3859 3859 newnodes = statedata.get(b'newnodes')
3860 3860 if newnodes is None:
3861 3861 # and old graft state which does not have all the data required to abort
3862 3862 # the graft
3863 3863 raise error.Abort(_(b"cannot abort using an old graftstate"))
3864 3864
3865 3865 # changeset from which graft operation was started
3866 3866 if len(newnodes) > 0:
3867 3867 startctx = repo[newnodes[0]].p1()
3868 3868 else:
3869 3869 startctx = repo[b'.']
3870 3870 # whether to strip or not
3871 3871 cleanup = False
3872 3872
3873 3873 if newnodes:
3874 3874 newnodes = [repo[r].rev() for r in newnodes]
3875 3875 cleanup = True
3876 3876 # checking that none of the newnodes turned public or is public
3877 3877 immutable = [c for c in newnodes if not repo[c].mutable()]
3878 3878 if immutable:
3879 3879 repo.ui.warn(
3880 3880 _(b"cannot clean up public changesets %s\n")
3881 3881 % b', '.join(bytes(repo[r]) for r in immutable),
3882 3882 hint=_(b"see 'hg help phases' for details"),
3883 3883 )
3884 3884 cleanup = False
3885 3885
3886 3886 # checking that no new nodes are created on top of grafted revs
3887 3887 desc = set(repo.changelog.descendants(newnodes))
3888 3888 if desc - set(newnodes):
3889 3889 repo.ui.warn(
3890 3890 _(
3891 3891 b"new changesets detected on destination "
3892 3892 b"branch, can't strip\n"
3893 3893 )
3894 3894 )
3895 3895 cleanup = False
3896 3896
3897 3897 if cleanup:
3898 3898 with repo.wlock(), repo.lock():
3899 3899 mergemod.clean_update(startctx)
3900 3900 # stripping the new nodes created
3901 3901 strippoints = [
3902 3902 c.node() for c in repo.set(b"roots(%ld)", newnodes)
3903 3903 ]
3904 3904 repair.strip(repo.ui, repo, strippoints, backup=False)
3905 3905
3906 3906 if not cleanup:
3907 3907 # we don't update to the startnode if we can't strip
3908 3908 startctx = repo[b'.']
3909 3909 mergemod.clean_update(startctx)
3910 3910
3911 3911 ui.status(_(b"graft aborted\n"))
3912 3912 ui.status(_(b"working directory is now at %s\n") % startctx.hex()[:12])
3913 3913 graftstate.delete()
3914 3914 return 0
3915 3915
3916 3916
3917 3917 def readgraftstate(repo, graftstate):
3918 3918 # type: (Any, statemod.cmdstate) -> Dict[bytes, Any]
3919 3919 """read the graft state file and return a dict of the data stored in it"""
3920 3920 try:
3921 3921 return graftstate.read()
3922 3922 except error.CorruptedState:
3923 3923 nodes = repo.vfs.read(b'graftstate').splitlines()
3924 3924 return {b'nodes': nodes}
3925 3925
3926 3926
3927 3927 def hgabortgraft(ui, repo):
3928 3928 """abort logic for aborting graft using 'hg abort'"""
3929 3929 with repo.wlock():
3930 3930 graftstate = statemod.cmdstate(repo, b'graftstate')
3931 3931 return abortgraft(ui, repo, graftstate)
@@ -1,286 +1,286 b''
1 1 # filelog.py - file history class for mercurial
2 2 #
3 3 # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 from .i18n import _
11 11 from .node import nullrev
12 12 from . import (
13 13 error,
14 14 revlog,
15 15 )
16 16 from .interfaces import (
17 17 repository,
18 18 util as interfaceutil,
19 19 )
20 20 from .utils import storageutil
21 21 from .revlogutils import (
22 22 constants as revlog_constants,
23 23 )
24 24
25 25
26 26 @interfaceutil.implementer(repository.ifilestorage)
27 27 class filelog(object):
28 28 def __init__(self, opener, path):
29 29 self._revlog = revlog.revlog(
30 30 opener,
31 31 # XXX should use the unencoded path
32 32 target=(revlog_constants.KIND_FILELOG, path),
33 indexfile=b'/'.join((b'data', path + b'.i')),
33 radix=b'/'.join((b'data', path)),
34 34 censorable=True,
35 35 )
36 36 # Full name of the user visible file, relative to the repository root.
37 37 # Used by LFS.
38 38 self._revlog.filename = path
39 39 self.nullid = self._revlog.nullid
40 40
41 41 def __len__(self):
42 42 return len(self._revlog)
43 43
44 44 def __iter__(self):
45 45 return self._revlog.__iter__()
46 46
47 47 def hasnode(self, node):
48 48 if node in (self.nullid, nullrev):
49 49 return False
50 50
51 51 try:
52 52 self._revlog.rev(node)
53 53 return True
54 54 except (TypeError, ValueError, IndexError, error.LookupError):
55 55 return False
56 56
57 57 def revs(self, start=0, stop=None):
58 58 return self._revlog.revs(start=start, stop=stop)
59 59
60 60 def parents(self, node):
61 61 return self._revlog.parents(node)
62 62
63 63 def parentrevs(self, rev):
64 64 return self._revlog.parentrevs(rev)
65 65
66 66 def rev(self, node):
67 67 return self._revlog.rev(node)
68 68
69 69 def node(self, rev):
70 70 return self._revlog.node(rev)
71 71
72 72 def lookup(self, node):
73 73 return storageutil.fileidlookup(
74 74 self._revlog, node, self._revlog._indexfile
75 75 )
76 76
77 77 def linkrev(self, rev):
78 78 return self._revlog.linkrev(rev)
79 79
80 80 def commonancestorsheads(self, node1, node2):
81 81 return self._revlog.commonancestorsheads(node1, node2)
82 82
83 83 # Used by dagop.blockdescendants().
84 84 def descendants(self, revs):
85 85 return self._revlog.descendants(revs)
86 86
87 87 def heads(self, start=None, stop=None):
88 88 return self._revlog.heads(start, stop)
89 89
90 90 # Used by hgweb, children extension.
91 91 def children(self, node):
92 92 return self._revlog.children(node)
93 93
94 94 def iscensored(self, rev):
95 95 return self._revlog.iscensored(rev)
96 96
97 97 def revision(self, node, _df=None, raw=False):
98 98 return self._revlog.revision(node, _df=_df, raw=raw)
99 99
100 100 def rawdata(self, node, _df=None):
101 101 return self._revlog.rawdata(node, _df=_df)
102 102
103 103 def emitrevisions(
104 104 self,
105 105 nodes,
106 106 nodesorder=None,
107 107 revisiondata=False,
108 108 assumehaveparentrevisions=False,
109 109 deltamode=repository.CG_DELTAMODE_STD,
110 110 sidedata_helpers=None,
111 111 ):
112 112 return self._revlog.emitrevisions(
113 113 nodes,
114 114 nodesorder=nodesorder,
115 115 revisiondata=revisiondata,
116 116 assumehaveparentrevisions=assumehaveparentrevisions,
117 117 deltamode=deltamode,
118 118 sidedata_helpers=sidedata_helpers,
119 119 )
120 120
121 121 def addrevision(
122 122 self,
123 123 revisiondata,
124 124 transaction,
125 125 linkrev,
126 126 p1,
127 127 p2,
128 128 node=None,
129 129 flags=revlog.REVIDX_DEFAULT_FLAGS,
130 130 cachedelta=None,
131 131 ):
132 132 return self._revlog.addrevision(
133 133 revisiondata,
134 134 transaction,
135 135 linkrev,
136 136 p1,
137 137 p2,
138 138 node=node,
139 139 flags=flags,
140 140 cachedelta=cachedelta,
141 141 )
142 142
143 143 def addgroup(
144 144 self,
145 145 deltas,
146 146 linkmapper,
147 147 transaction,
148 148 addrevisioncb=None,
149 149 duplicaterevisioncb=None,
150 150 maybemissingparents=False,
151 151 ):
152 152 if maybemissingparents:
153 153 raise error.Abort(
154 154 _(
155 155 b'revlog storage does not support missing '
156 156 b'parents write mode'
157 157 )
158 158 )
159 159
160 160 return self._revlog.addgroup(
161 161 deltas,
162 162 linkmapper,
163 163 transaction,
164 164 addrevisioncb=addrevisioncb,
165 165 duplicaterevisioncb=duplicaterevisioncb,
166 166 )
167 167
168 168 def getstrippoint(self, minlink):
169 169 return self._revlog.getstrippoint(minlink)
170 170
171 171 def strip(self, minlink, transaction):
172 172 return self._revlog.strip(minlink, transaction)
173 173
174 174 def censorrevision(self, tr, node, tombstone=b''):
175 175 return self._revlog.censorrevision(tr, node, tombstone=tombstone)
176 176
177 177 def files(self):
178 178 return self._revlog.files()
179 179
180 180 def read(self, node):
181 181 return storageutil.filtermetadata(self.revision(node))
182 182
183 183 def add(self, text, meta, transaction, link, p1=None, p2=None):
184 184 if meta or text.startswith(b'\1\n'):
185 185 text = storageutil.packmeta(meta, text)
186 186 rev = self.addrevision(text, transaction, link, p1, p2)
187 187 return self.node(rev)
188 188
189 189 def renamed(self, node):
190 190 return storageutil.filerevisioncopied(self, node)
191 191
192 192 def size(self, rev):
193 193 """return the size of a given revision"""
194 194
195 195 # for revisions with renames, we have to go the slow way
196 196 node = self.node(rev)
197 197 if self.renamed(node):
198 198 return len(self.read(node))
199 199 if self.iscensored(rev):
200 200 return 0
201 201
202 202 # XXX if self.read(node).startswith("\1\n"), this returns (size+4)
203 203 return self._revlog.size(rev)
204 204
205 205 def cmp(self, node, text):
206 206 """compare text with a given file revision
207 207
208 208 returns True if text is different than what is stored.
209 209 """
210 210 return not storageutil.filedataequivalent(self, node, text)
211 211
212 212 def verifyintegrity(self, state):
213 213 return self._revlog.verifyintegrity(state)
214 214
215 215 def storageinfo(
216 216 self,
217 217 exclusivefiles=False,
218 218 sharedfiles=False,
219 219 revisionscount=False,
220 220 trackedsize=False,
221 221 storedsize=False,
222 222 ):
223 223 return self._revlog.storageinfo(
224 224 exclusivefiles=exclusivefiles,
225 225 sharedfiles=sharedfiles,
226 226 revisionscount=revisionscount,
227 227 trackedsize=trackedsize,
228 228 storedsize=storedsize,
229 229 )
230 230
231 231 # Used by repo upgrade.
232 232 def clone(self, tr, destrevlog, **kwargs):
233 233 if not isinstance(destrevlog, filelog):
234 234 raise error.ProgrammingError(b'expected filelog to clone()')
235 235
236 236 return self._revlog.clone(tr, destrevlog._revlog, **kwargs)
237 237
238 238
239 239 class narrowfilelog(filelog):
240 240 """Filelog variation to be used with narrow stores."""
241 241
242 242 def __init__(self, opener, path, narrowmatch):
243 243 super(narrowfilelog, self).__init__(opener, path)
244 244 self._narrowmatch = narrowmatch
245 245
246 246 def renamed(self, node):
247 247 res = super(narrowfilelog, self).renamed(node)
248 248
249 249 # Renames that come from outside the narrowspec are problematic
250 250 # because we may lack the base text for the rename. This can result
251 251 # in code attempting to walk the ancestry or compute a diff
252 252 # encountering a missing revision. We address this by silently
253 253 # removing rename metadata if the source file is outside the
254 254 # narrow spec.
255 255 #
256 256 # A better solution would be to see if the base revision is available,
257 257 # rather than assuming it isn't.
258 258 #
259 259 # An even better solution would be to teach all consumers of rename
260 260 # metadata that the base revision may not be available.
261 261 #
262 262 # TODO consider better ways of doing this.
263 263 if res and not self._narrowmatch(res[0]):
264 264 return None
265 265
266 266 return res
267 267
268 268 def size(self, rev):
269 269 # Because we have a custom renamed() that may lie, we need to call
270 270 # the base renamed() to report accurate results.
271 271 node = self.node(rev)
272 272 if super(narrowfilelog, self).renamed(node):
273 273 return len(self.read(node))
274 274 else:
275 275 return super(narrowfilelog, self).size(rev)
276 276
277 277 def cmp(self, node, text):
278 278 # We don't call `super` because narrow parents can be buggy in case of a
279 279 # ambiguous dirstate. Always take the slow path until there is a better
280 280 # fix, see issue6150.
281 281
282 282 # Censored files compare against the empty file.
283 283 if self.iscensored(self.rev(node)):
284 284 return text != b''
285 285
286 286 return self.read(node) != text
@@ -1,2376 +1,2374 b''
1 1 # manifest.py - manifest revision class for mercurial
2 2 #
3 3 # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import heapq
11 11 import itertools
12 12 import struct
13 13 import weakref
14 14
15 15 from .i18n import _
16 16 from .node import (
17 17 bin,
18 18 hex,
19 19 nullrev,
20 20 )
21 21 from .pycompat import getattr
22 22 from . import (
23 23 encoding,
24 24 error,
25 25 match as matchmod,
26 26 mdiff,
27 27 pathutil,
28 28 policy,
29 29 pycompat,
30 30 revlog,
31 31 util,
32 32 )
33 33 from .interfaces import (
34 34 repository,
35 35 util as interfaceutil,
36 36 )
37 37 from .revlogutils import (
38 38 constants as revlog_constants,
39 39 )
40 40
41 41 parsers = policy.importmod('parsers')
42 42 propertycache = util.propertycache
43 43
44 44 # Allow tests to more easily test the alternate path in manifestdict.fastdelta()
45 45 FASTDELTA_TEXTDIFF_THRESHOLD = 1000
46 46
47 47
48 48 def _parse(nodelen, data):
49 49 # This method does a little bit of excessive-looking
50 50 # precondition checking. This is so that the behavior of this
51 51 # class exactly matches its C counterpart to try and help
52 52 # prevent surprise breakage for anyone that develops against
53 53 # the pure version.
54 54 if data and data[-1:] != b'\n':
55 55 raise ValueError(b'Manifest did not end in a newline.')
56 56 prev = None
57 57 for l in data.splitlines():
58 58 if prev is not None and prev > l:
59 59 raise ValueError(b'Manifest lines not in sorted order.')
60 60 prev = l
61 61 f, n = l.split(b'\0')
62 62 nl = len(n)
63 63 flags = n[-1:]
64 64 if flags in _manifestflags:
65 65 n = n[:-1]
66 66 nl -= 1
67 67 else:
68 68 flags = b''
69 69 if nl != 2 * nodelen:
70 70 raise ValueError(b'Invalid manifest line')
71 71
72 72 yield f, bin(n), flags
73 73
74 74
75 75 def _text(it):
76 76 files = []
77 77 lines = []
78 78 for f, n, fl in it:
79 79 files.append(f)
80 80 # if this is changed to support newlines in filenames,
81 81 # be sure to check the templates/ dir again (especially *-raw.tmpl)
82 82 lines.append(b"%s\0%s%s\n" % (f, hex(n), fl))
83 83
84 84 _checkforbidden(files)
85 85 return b''.join(lines)
86 86
87 87
88 88 class lazymanifestiter(object):
89 89 def __init__(self, lm):
90 90 self.pos = 0
91 91 self.lm = lm
92 92
93 93 def __iter__(self):
94 94 return self
95 95
96 96 def next(self):
97 97 try:
98 98 data, pos = self.lm._get(self.pos)
99 99 except IndexError:
100 100 raise StopIteration
101 101 if pos == -1:
102 102 self.pos += 1
103 103 return data[0]
104 104 self.pos += 1
105 105 zeropos = data.find(b'\x00', pos)
106 106 return data[pos:zeropos]
107 107
108 108 __next__ = next
109 109
110 110
111 111 class lazymanifestiterentries(object):
112 112 def __init__(self, lm):
113 113 self.lm = lm
114 114 self.pos = 0
115 115
116 116 def __iter__(self):
117 117 return self
118 118
119 119 def next(self):
120 120 try:
121 121 data, pos = self.lm._get(self.pos)
122 122 except IndexError:
123 123 raise StopIteration
124 124 if pos == -1:
125 125 self.pos += 1
126 126 return data
127 127 zeropos = data.find(b'\x00', pos)
128 128 nlpos = data.find(b'\n', pos)
129 129 if zeropos == -1 or nlpos == -1 or nlpos < zeropos:
130 130 raise error.StorageError(b'Invalid manifest line')
131 131 flags = data[nlpos - 1 : nlpos]
132 132 if flags in _manifestflags:
133 133 hlen = nlpos - zeropos - 2
134 134 else:
135 135 hlen = nlpos - zeropos - 1
136 136 flags = b''
137 137 if hlen != 2 * self.lm._nodelen:
138 138 raise error.StorageError(b'Invalid manifest line')
139 139 hashval = unhexlify(
140 140 data, self.lm.extrainfo[self.pos], zeropos + 1, hlen
141 141 )
142 142 self.pos += 1
143 143 return (data[pos:zeropos], hashval, flags)
144 144
145 145 __next__ = next
146 146
147 147
148 148 def unhexlify(data, extra, pos, length):
149 149 s = bin(data[pos : pos + length])
150 150 if extra:
151 151 s += chr(extra & 0xFF)
152 152 return s
153 153
154 154
155 155 def _cmp(a, b):
156 156 return (a > b) - (a < b)
157 157
158 158
159 159 _manifestflags = {b'', b'l', b't', b'x'}
160 160
161 161
162 162 class _lazymanifest(object):
163 163 """A pure python manifest backed by a byte string. It is supplimented with
164 164 internal lists as it is modified, until it is compacted back to a pure byte
165 165 string.
166 166
167 167 ``data`` is the initial manifest data.
168 168
169 169 ``positions`` is a list of offsets, one per manifest entry. Positive
170 170 values are offsets into ``data``, negative values are offsets into the
171 171 ``extradata`` list. When an entry is removed, its entry is dropped from
172 172 ``positions``. The values are encoded such that when walking the list and
173 173 indexing into ``data`` or ``extradata`` as appropriate, the entries are
174 174 sorted by filename.
175 175
176 176 ``extradata`` is a list of (key, hash, flags) for entries that were added or
177 177 modified since the manifest was created or compacted.
178 178 """
179 179
180 180 def __init__(
181 181 self,
182 182 nodelen,
183 183 data,
184 184 positions=None,
185 185 extrainfo=None,
186 186 extradata=None,
187 187 hasremovals=False,
188 188 ):
189 189 self._nodelen = nodelen
190 190 if positions is None:
191 191 self.positions = self.findlines(data)
192 192 self.extrainfo = [0] * len(self.positions)
193 193 self.data = data
194 194 self.extradata = []
195 195 self.hasremovals = False
196 196 else:
197 197 self.positions = positions[:]
198 198 self.extrainfo = extrainfo[:]
199 199 self.extradata = extradata[:]
200 200 self.data = data
201 201 self.hasremovals = hasremovals
202 202
203 203 def findlines(self, data):
204 204 if not data:
205 205 return []
206 206 pos = data.find(b"\n")
207 207 if pos == -1 or data[-1:] != b'\n':
208 208 raise ValueError(b"Manifest did not end in a newline.")
209 209 positions = [0]
210 210 prev = data[: data.find(b'\x00')]
211 211 while pos < len(data) - 1 and pos != -1:
212 212 positions.append(pos + 1)
213 213 nexts = data[pos + 1 : data.find(b'\x00', pos + 1)]
214 214 if nexts < prev:
215 215 raise ValueError(b"Manifest lines not in sorted order.")
216 216 prev = nexts
217 217 pos = data.find(b"\n", pos + 1)
218 218 return positions
219 219
220 220 def _get(self, index):
221 221 # get the position encoded in pos:
222 222 # positive number is an index in 'data'
223 223 # negative number is in extrapieces
224 224 pos = self.positions[index]
225 225 if pos >= 0:
226 226 return self.data, pos
227 227 return self.extradata[-pos - 1], -1
228 228
229 229 def _getkey(self, pos):
230 230 if pos >= 0:
231 231 return self.data[pos : self.data.find(b'\x00', pos + 1)]
232 232 return self.extradata[-pos - 1][0]
233 233
234 234 def bsearch(self, key):
235 235 first = 0
236 236 last = len(self.positions) - 1
237 237
238 238 while first <= last:
239 239 midpoint = (first + last) // 2
240 240 nextpos = self.positions[midpoint]
241 241 candidate = self._getkey(nextpos)
242 242 r = _cmp(key, candidate)
243 243 if r == 0:
244 244 return midpoint
245 245 else:
246 246 if r < 0:
247 247 last = midpoint - 1
248 248 else:
249 249 first = midpoint + 1
250 250 return -1
251 251
252 252 def bsearch2(self, key):
253 253 # same as the above, but will always return the position
254 254 # done for performance reasons
255 255 first = 0
256 256 last = len(self.positions) - 1
257 257
258 258 while first <= last:
259 259 midpoint = (first + last) // 2
260 260 nextpos = self.positions[midpoint]
261 261 candidate = self._getkey(nextpos)
262 262 r = _cmp(key, candidate)
263 263 if r == 0:
264 264 return (midpoint, True)
265 265 else:
266 266 if r < 0:
267 267 last = midpoint - 1
268 268 else:
269 269 first = midpoint + 1
270 270 return (first, False)
271 271
272 272 def __contains__(self, key):
273 273 return self.bsearch(key) != -1
274 274
275 275 def __getitem__(self, key):
276 276 if not isinstance(key, bytes):
277 277 raise TypeError(b"getitem: manifest keys must be a bytes.")
278 278 needle = self.bsearch(key)
279 279 if needle == -1:
280 280 raise KeyError
281 281 data, pos = self._get(needle)
282 282 if pos == -1:
283 283 return (data[1], data[2])
284 284 zeropos = data.find(b'\x00', pos)
285 285 nlpos = data.find(b'\n', zeropos)
286 286 assert 0 <= needle <= len(self.positions)
287 287 assert len(self.extrainfo) == len(self.positions)
288 288 if zeropos == -1 or nlpos == -1 or nlpos < zeropos:
289 289 raise error.StorageError(b'Invalid manifest line')
290 290 hlen = nlpos - zeropos - 1
291 291 flags = data[nlpos - 1 : nlpos]
292 292 if flags in _manifestflags:
293 293 hlen -= 1
294 294 else:
295 295 flags = b''
296 296 if hlen != 2 * self._nodelen:
297 297 raise error.StorageError(b'Invalid manifest line')
298 298 hashval = unhexlify(data, self.extrainfo[needle], zeropos + 1, hlen)
299 299 return (hashval, flags)
300 300
301 301 def __delitem__(self, key):
302 302 needle, found = self.bsearch2(key)
303 303 if not found:
304 304 raise KeyError
305 305 cur = self.positions[needle]
306 306 self.positions = self.positions[:needle] + self.positions[needle + 1 :]
307 307 self.extrainfo = self.extrainfo[:needle] + self.extrainfo[needle + 1 :]
308 308 if cur >= 0:
309 309 # This does NOT unsort the list as far as the search functions are
310 310 # concerned, as they only examine lines mapped by self.positions.
311 311 self.data = self.data[:cur] + b'\x00' + self.data[cur + 1 :]
312 312 self.hasremovals = True
313 313
314 314 def __setitem__(self, key, value):
315 315 if not isinstance(key, bytes):
316 316 raise TypeError(b"setitem: manifest keys must be a byte string.")
317 317 if not isinstance(value, tuple) or len(value) != 2:
318 318 raise TypeError(
319 319 b"Manifest values must be a tuple of (node, flags)."
320 320 )
321 321 hashval = value[0]
322 322 if not isinstance(hashval, bytes) or len(hashval) not in (20, 32):
323 323 raise TypeError(b"node must be a 20-byte or 32-byte byte string")
324 324 flags = value[1]
325 325 if not isinstance(flags, bytes) or len(flags) > 1:
326 326 raise TypeError(b"flags must a 0 or 1 byte string, got %r", flags)
327 327 needle, found = self.bsearch2(key)
328 328 if found:
329 329 # put the item
330 330 pos = self.positions[needle]
331 331 if pos < 0:
332 332 self.extradata[-pos - 1] = (key, hashval, value[1])
333 333 else:
334 334 # just don't bother
335 335 self.extradata.append((key, hashval, value[1]))
336 336 self.positions[needle] = -len(self.extradata)
337 337 else:
338 338 # not found, put it in with extra positions
339 339 self.extradata.append((key, hashval, value[1]))
340 340 self.positions = (
341 341 self.positions[:needle]
342 342 + [-len(self.extradata)]
343 343 + self.positions[needle:]
344 344 )
345 345 self.extrainfo = (
346 346 self.extrainfo[:needle] + [0] + self.extrainfo[needle:]
347 347 )
348 348
349 349 def copy(self):
350 350 # XXX call _compact like in C?
351 351 return _lazymanifest(
352 352 self._nodelen,
353 353 self.data,
354 354 self.positions,
355 355 self.extrainfo,
356 356 self.extradata,
357 357 self.hasremovals,
358 358 )
359 359
360 360 def _compact(self):
361 361 # hopefully not called TOO often
362 362 if len(self.extradata) == 0 and not self.hasremovals:
363 363 return
364 364 l = []
365 365 i = 0
366 366 offset = 0
367 367 self.extrainfo = [0] * len(self.positions)
368 368 while i < len(self.positions):
369 369 if self.positions[i] >= 0:
370 370 cur = self.positions[i]
371 371 last_cut = cur
372 372
373 373 # Collect all contiguous entries in the buffer at the current
374 374 # offset, breaking out only for added/modified items held in
375 375 # extradata, or a deleted line prior to the next position.
376 376 while True:
377 377 self.positions[i] = offset
378 378 i += 1
379 379 if i == len(self.positions) or self.positions[i] < 0:
380 380 break
381 381
382 382 # A removed file has no positions[] entry, but does have an
383 383 # overwritten first byte. Break out and find the end of the
384 384 # current good entry/entries if there is a removed file
385 385 # before the next position.
386 386 if (
387 387 self.hasremovals
388 388 and self.data.find(b'\n\x00', cur, self.positions[i])
389 389 != -1
390 390 ):
391 391 break
392 392
393 393 offset += self.positions[i] - cur
394 394 cur = self.positions[i]
395 395 end_cut = self.data.find(b'\n', cur)
396 396 if end_cut != -1:
397 397 end_cut += 1
398 398 offset += end_cut - cur
399 399 l.append(self.data[last_cut:end_cut])
400 400 else:
401 401 while i < len(self.positions) and self.positions[i] < 0:
402 402 cur = self.positions[i]
403 403 t = self.extradata[-cur - 1]
404 404 l.append(self._pack(t))
405 405 self.positions[i] = offset
406 406 # Hashes are either 20 bytes (old sha1s) or 32
407 407 # bytes (new non-sha1).
408 408 hlen = 20
409 409 if len(t[1]) > 25:
410 410 hlen = 32
411 411 if len(t[1]) > hlen:
412 412 self.extrainfo[i] = ord(t[1][hlen + 1])
413 413 offset += len(l[-1])
414 414 i += 1
415 415 self.data = b''.join(l)
416 416 self.hasremovals = False
417 417 self.extradata = []
418 418
419 419 def _pack(self, d):
420 420 n = d[1]
421 421 assert len(n) in (20, 32)
422 422 return d[0] + b'\x00' + hex(n) + d[2] + b'\n'
423 423
424 424 def text(self):
425 425 self._compact()
426 426 return self.data
427 427
428 428 def diff(self, m2, clean=False):
429 429 '''Finds changes between the current manifest and m2.'''
430 430 # XXX think whether efficiency matters here
431 431 diff = {}
432 432
433 433 for fn, e1, flags in self.iterentries():
434 434 if fn not in m2:
435 435 diff[fn] = (e1, flags), (None, b'')
436 436 else:
437 437 e2 = m2[fn]
438 438 if (e1, flags) != e2:
439 439 diff[fn] = (e1, flags), e2
440 440 elif clean:
441 441 diff[fn] = None
442 442
443 443 for fn, e2, flags in m2.iterentries():
444 444 if fn not in self:
445 445 diff[fn] = (None, b''), (e2, flags)
446 446
447 447 return diff
448 448
449 449 def iterentries(self):
450 450 return lazymanifestiterentries(self)
451 451
452 452 def iterkeys(self):
453 453 return lazymanifestiter(self)
454 454
455 455 def __iter__(self):
456 456 return lazymanifestiter(self)
457 457
458 458 def __len__(self):
459 459 return len(self.positions)
460 460
461 461 def filtercopy(self, filterfn):
462 462 # XXX should be optimized
463 463 c = _lazymanifest(self._nodelen, b'')
464 464 for f, n, fl in self.iterentries():
465 465 if filterfn(f):
466 466 c[f] = n, fl
467 467 return c
468 468
469 469
470 470 try:
471 471 _lazymanifest = parsers.lazymanifest
472 472 except AttributeError:
473 473 pass
474 474
475 475
476 476 @interfaceutil.implementer(repository.imanifestdict)
477 477 class manifestdict(object):
478 478 def __init__(self, nodelen, data=b''):
479 479 self._nodelen = nodelen
480 480 self._lm = _lazymanifest(nodelen, data)
481 481
482 482 def __getitem__(self, key):
483 483 return self._lm[key][0]
484 484
485 485 def find(self, key):
486 486 return self._lm[key]
487 487
488 488 def __len__(self):
489 489 return len(self._lm)
490 490
491 491 def __nonzero__(self):
492 492 # nonzero is covered by the __len__ function, but implementing it here
493 493 # makes it easier for extensions to override.
494 494 return len(self._lm) != 0
495 495
496 496 __bool__ = __nonzero__
497 497
498 498 def __setitem__(self, key, node):
499 499 self._lm[key] = node, self.flags(key)
500 500
501 501 def __contains__(self, key):
502 502 if key is None:
503 503 return False
504 504 return key in self._lm
505 505
506 506 def __delitem__(self, key):
507 507 del self._lm[key]
508 508
509 509 def __iter__(self):
510 510 return self._lm.__iter__()
511 511
512 512 def iterkeys(self):
513 513 return self._lm.iterkeys()
514 514
515 515 def keys(self):
516 516 return list(self.iterkeys())
517 517
518 518 def filesnotin(self, m2, match=None):
519 519 '''Set of files in this manifest that are not in the other'''
520 520 if match is not None:
521 521 match = matchmod.badmatch(match, lambda path, msg: None)
522 522 sm2 = set(m2.walk(match))
523 523 return {f for f in self.walk(match) if f not in sm2}
524 524 return {f for f in self if f not in m2}
525 525
526 526 @propertycache
527 527 def _dirs(self):
528 528 return pathutil.dirs(self)
529 529
530 530 def dirs(self):
531 531 return self._dirs
532 532
533 533 def hasdir(self, dir):
534 534 return dir in self._dirs
535 535
536 536 def _filesfastpath(self, match):
537 537 """Checks whether we can correctly and quickly iterate over matcher
538 538 files instead of over manifest files."""
539 539 files = match.files()
540 540 return len(files) < 100 and (
541 541 match.isexact()
542 542 or (match.prefix() and all(fn in self for fn in files))
543 543 )
544 544
545 545 def walk(self, match):
546 546 """Generates matching file names.
547 547
548 548 Equivalent to manifest.matches(match).iterkeys(), but without creating
549 549 an entirely new manifest.
550 550
551 551 It also reports nonexistent files by marking them bad with match.bad().
552 552 """
553 553 if match.always():
554 554 for f in iter(self):
555 555 yield f
556 556 return
557 557
558 558 fset = set(match.files())
559 559
560 560 # avoid the entire walk if we're only looking for specific files
561 561 if self._filesfastpath(match):
562 562 for fn in sorted(fset):
563 563 if fn in self:
564 564 yield fn
565 565 return
566 566
567 567 for fn in self:
568 568 if fn in fset:
569 569 # specified pattern is the exact name
570 570 fset.remove(fn)
571 571 if match(fn):
572 572 yield fn
573 573
574 574 # for dirstate.walk, files=[''] means "walk the whole tree".
575 575 # follow that here, too
576 576 fset.discard(b'')
577 577
578 578 for fn in sorted(fset):
579 579 if not self.hasdir(fn):
580 580 match.bad(fn, None)
581 581
582 582 def _matches(self, match):
583 583 '''generate a new manifest filtered by the match argument'''
584 584 if match.always():
585 585 return self.copy()
586 586
587 587 if self._filesfastpath(match):
588 588 m = manifestdict(self._nodelen)
589 589 lm = self._lm
590 590 for fn in match.files():
591 591 if fn in lm:
592 592 m._lm[fn] = lm[fn]
593 593 return m
594 594
595 595 m = manifestdict(self._nodelen)
596 596 m._lm = self._lm.filtercopy(match)
597 597 return m
598 598
599 599 def diff(self, m2, match=None, clean=False):
600 600 """Finds changes between the current manifest and m2.
601 601
602 602 Args:
603 603 m2: the manifest to which this manifest should be compared.
604 604 clean: if true, include files unchanged between these manifests
605 605 with a None value in the returned dictionary.
606 606
607 607 The result is returned as a dict with filename as key and
608 608 values of the form ((n1,fl1),(n2,fl2)), where n1/n2 is the
609 609 nodeid in the current/other manifest and fl1/fl2 is the flag
610 610 in the current/other manifest. Where the file does not exist,
611 611 the nodeid will be None and the flags will be the empty
612 612 string.
613 613 """
614 614 if match:
615 615 m1 = self._matches(match)
616 616 m2 = m2._matches(match)
617 617 return m1.diff(m2, clean=clean)
618 618 return self._lm.diff(m2._lm, clean)
619 619
620 620 def setflag(self, key, flag):
621 621 if flag not in _manifestflags:
622 622 raise TypeError(b"Invalid manifest flag set.")
623 623 self._lm[key] = self[key], flag
624 624
625 625 def get(self, key, default=None):
626 626 try:
627 627 return self._lm[key][0]
628 628 except KeyError:
629 629 return default
630 630
631 631 def flags(self, key):
632 632 try:
633 633 return self._lm[key][1]
634 634 except KeyError:
635 635 return b''
636 636
637 637 def copy(self):
638 638 c = manifestdict(self._nodelen)
639 639 c._lm = self._lm.copy()
640 640 return c
641 641
642 642 def items(self):
643 643 return (x[:2] for x in self._lm.iterentries())
644 644
645 645 def iteritems(self):
646 646 return (x[:2] for x in self._lm.iterentries())
647 647
648 648 def iterentries(self):
649 649 return self._lm.iterentries()
650 650
651 651 def text(self):
652 652 # most likely uses native version
653 653 return self._lm.text()
654 654
655 655 def fastdelta(self, base, changes):
656 656 """Given a base manifest text as a bytearray and a list of changes
657 657 relative to that text, compute a delta that can be used by revlog.
658 658 """
659 659 delta = []
660 660 dstart = None
661 661 dend = None
662 662 dline = [b""]
663 663 start = 0
664 664 # zero copy representation of base as a buffer
665 665 addbuf = util.buffer(base)
666 666
667 667 changes = list(changes)
668 668 if len(changes) < FASTDELTA_TEXTDIFF_THRESHOLD:
669 669 # start with a readonly loop that finds the offset of
670 670 # each line and creates the deltas
671 671 for f, todelete in changes:
672 672 # bs will either be the index of the item or the insert point
673 673 start, end = _msearch(addbuf, f, start)
674 674 if not todelete:
675 675 h, fl = self._lm[f]
676 676 l = b"%s\0%s%s\n" % (f, hex(h), fl)
677 677 else:
678 678 if start == end:
679 679 # item we want to delete was not found, error out
680 680 raise AssertionError(
681 681 _(b"failed to remove %s from manifest") % f
682 682 )
683 683 l = b""
684 684 if dstart is not None and dstart <= start and dend >= start:
685 685 if dend < end:
686 686 dend = end
687 687 if l:
688 688 dline.append(l)
689 689 else:
690 690 if dstart is not None:
691 691 delta.append([dstart, dend, b"".join(dline)])
692 692 dstart = start
693 693 dend = end
694 694 dline = [l]
695 695
696 696 if dstart is not None:
697 697 delta.append([dstart, dend, b"".join(dline)])
698 698 # apply the delta to the base, and get a delta for addrevision
699 699 deltatext, arraytext = _addlistdelta(base, delta)
700 700 else:
701 701 # For large changes, it's much cheaper to just build the text and
702 702 # diff it.
703 703 arraytext = bytearray(self.text())
704 704 deltatext = mdiff.textdiff(
705 705 util.buffer(base), util.buffer(arraytext)
706 706 )
707 707
708 708 return arraytext, deltatext
709 709
710 710
711 711 def _msearch(m, s, lo=0, hi=None):
712 712 """return a tuple (start, end) that says where to find s within m.
713 713
714 714 If the string is found m[start:end] are the line containing
715 715 that string. If start == end the string was not found and
716 716 they indicate the proper sorted insertion point.
717 717
718 718 m should be a buffer, a memoryview or a byte string.
719 719 s is a byte string"""
720 720
721 721 def advance(i, c):
722 722 while i < lenm and m[i : i + 1] != c:
723 723 i += 1
724 724 return i
725 725
726 726 if not s:
727 727 return (lo, lo)
728 728 lenm = len(m)
729 729 if not hi:
730 730 hi = lenm
731 731 while lo < hi:
732 732 mid = (lo + hi) // 2
733 733 start = mid
734 734 while start > 0 and m[start - 1 : start] != b'\n':
735 735 start -= 1
736 736 end = advance(start, b'\0')
737 737 if bytes(m[start:end]) < s:
738 738 # we know that after the null there are 40 bytes of sha1
739 739 # this translates to the bisect lo = mid + 1
740 740 lo = advance(end + 40, b'\n') + 1
741 741 else:
742 742 # this translates to the bisect hi = mid
743 743 hi = start
744 744 end = advance(lo, b'\0')
745 745 found = m[lo:end]
746 746 if s == found:
747 747 # we know that after the null there are 40 bytes of sha1
748 748 end = advance(end + 40, b'\n')
749 749 return (lo, end + 1)
750 750 else:
751 751 return (lo, lo)
752 752
753 753
754 754 def _checkforbidden(l):
755 755 """Check filenames for illegal characters."""
756 756 for f in l:
757 757 if b'\n' in f or b'\r' in f:
758 758 raise error.StorageError(
759 759 _(b"'\\n' and '\\r' disallowed in filenames: %r")
760 760 % pycompat.bytestr(f)
761 761 )
762 762
763 763
764 764 # apply the changes collected during the bisect loop to our addlist
765 765 # return a delta suitable for addrevision
766 766 def _addlistdelta(addlist, x):
767 767 # for large addlist arrays, building a new array is cheaper
768 768 # than repeatedly modifying the existing one
769 769 currentposition = 0
770 770 newaddlist = bytearray()
771 771
772 772 for start, end, content in x:
773 773 newaddlist += addlist[currentposition:start]
774 774 if content:
775 775 newaddlist += bytearray(content)
776 776
777 777 currentposition = end
778 778
779 779 newaddlist += addlist[currentposition:]
780 780
781 781 deltatext = b"".join(
782 782 struct.pack(b">lll", start, end, len(content)) + content
783 783 for start, end, content in x
784 784 )
785 785 return deltatext, newaddlist
786 786
787 787
788 788 def _splittopdir(f):
789 789 if b'/' in f:
790 790 dir, subpath = f.split(b'/', 1)
791 791 return dir + b'/', subpath
792 792 else:
793 793 return b'', f
794 794
795 795
796 796 _noop = lambda s: None
797 797
798 798
799 799 @interfaceutil.implementer(repository.imanifestdict)
800 800 class treemanifest(object):
801 801 def __init__(self, nodeconstants, dir=b'', text=b''):
802 802 self._dir = dir
803 803 self.nodeconstants = nodeconstants
804 804 self._node = self.nodeconstants.nullid
805 805 self._nodelen = self.nodeconstants.nodelen
806 806 self._loadfunc = _noop
807 807 self._copyfunc = _noop
808 808 self._dirty = False
809 809 self._dirs = {}
810 810 self._lazydirs = {}
811 811 # Using _lazymanifest here is a little slower than plain old dicts
812 812 self._files = {}
813 813 self._flags = {}
814 814 if text:
815 815
816 816 def readsubtree(subdir, subm):
817 817 raise AssertionError(
818 818 b'treemanifest constructor only accepts flat manifests'
819 819 )
820 820
821 821 self.parse(text, readsubtree)
822 822 self._dirty = True # Mark flat manifest dirty after parsing
823 823
824 824 def _subpath(self, path):
825 825 return self._dir + path
826 826
827 827 def _loadalllazy(self):
828 828 selfdirs = self._dirs
829 829 subpath = self._subpath
830 830 for d, (node, readsubtree, docopy) in pycompat.iteritems(
831 831 self._lazydirs
832 832 ):
833 833 if docopy:
834 834 selfdirs[d] = readsubtree(subpath(d), node).copy()
835 835 else:
836 836 selfdirs[d] = readsubtree(subpath(d), node)
837 837 self._lazydirs = {}
838 838
839 839 def _loadlazy(self, d):
840 840 v = self._lazydirs.get(d)
841 841 if v:
842 842 node, readsubtree, docopy = v
843 843 if docopy:
844 844 self._dirs[d] = readsubtree(self._subpath(d), node).copy()
845 845 else:
846 846 self._dirs[d] = readsubtree(self._subpath(d), node)
847 847 del self._lazydirs[d]
848 848
849 849 def _loadchildrensetlazy(self, visit):
850 850 if not visit:
851 851 return None
852 852 if visit == b'all' or visit == b'this':
853 853 self._loadalllazy()
854 854 return None
855 855
856 856 loadlazy = self._loadlazy
857 857 for k in visit:
858 858 loadlazy(k + b'/')
859 859 return visit
860 860
861 861 def _loaddifflazy(self, t1, t2):
862 862 """load items in t1 and t2 if they're needed for diffing.
863 863
864 864 The criteria currently is:
865 865 - if it's not present in _lazydirs in either t1 or t2, load it in the
866 866 other (it may already be loaded or it may not exist, doesn't matter)
867 867 - if it's present in _lazydirs in both, compare the nodeid; if it
868 868 differs, load it in both
869 869 """
870 870 toloadlazy = []
871 871 for d, v1 in pycompat.iteritems(t1._lazydirs):
872 872 v2 = t2._lazydirs.get(d)
873 873 if not v2 or v2[0] != v1[0]:
874 874 toloadlazy.append(d)
875 875 for d, v1 in pycompat.iteritems(t2._lazydirs):
876 876 if d not in t1._lazydirs:
877 877 toloadlazy.append(d)
878 878
879 879 for d in toloadlazy:
880 880 t1._loadlazy(d)
881 881 t2._loadlazy(d)
882 882
883 883 def __len__(self):
884 884 self._load()
885 885 size = len(self._files)
886 886 self._loadalllazy()
887 887 for m in self._dirs.values():
888 888 size += m.__len__()
889 889 return size
890 890
891 891 def __nonzero__(self):
892 892 # Faster than "__len() != 0" since it avoids loading sub-manifests
893 893 return not self._isempty()
894 894
895 895 __bool__ = __nonzero__
896 896
897 897 def _isempty(self):
898 898 self._load() # for consistency; already loaded by all callers
899 899 # See if we can skip loading everything.
900 900 if self._files or (
901 901 self._dirs and any(not m._isempty() for m in self._dirs.values())
902 902 ):
903 903 return False
904 904 self._loadalllazy()
905 905 return not self._dirs or all(m._isempty() for m in self._dirs.values())
906 906
907 907 @encoding.strmethod
908 908 def __repr__(self):
909 909 return (
910 910 b'<treemanifest dir=%s, node=%s, loaded=%r, dirty=%r at 0x%x>'
911 911 % (
912 912 self._dir,
913 913 hex(self._node),
914 914 bool(self._loadfunc is _noop),
915 915 self._dirty,
916 916 id(self),
917 917 )
918 918 )
919 919
920 920 def dir(self):
921 921 """The directory that this tree manifest represents, including a
922 922 trailing '/'. Empty string for the repo root directory."""
923 923 return self._dir
924 924
925 925 def node(self):
926 926 """This node of this instance. nullid for unsaved instances. Should
927 927 be updated when the instance is read or written from a revlog.
928 928 """
929 929 assert not self._dirty
930 930 return self._node
931 931
932 932 def setnode(self, node):
933 933 self._node = node
934 934 self._dirty = False
935 935
936 936 def iterentries(self):
937 937 self._load()
938 938 self._loadalllazy()
939 939 for p, n in sorted(
940 940 itertools.chain(self._dirs.items(), self._files.items())
941 941 ):
942 942 if p in self._files:
943 943 yield self._subpath(p), n, self._flags.get(p, b'')
944 944 else:
945 945 for x in n.iterentries():
946 946 yield x
947 947
948 948 def items(self):
949 949 self._load()
950 950 self._loadalllazy()
951 951 for p, n in sorted(
952 952 itertools.chain(self._dirs.items(), self._files.items())
953 953 ):
954 954 if p in self._files:
955 955 yield self._subpath(p), n
956 956 else:
957 957 for f, sn in pycompat.iteritems(n):
958 958 yield f, sn
959 959
960 960 iteritems = items
961 961
962 962 def iterkeys(self):
963 963 self._load()
964 964 self._loadalllazy()
965 965 for p in sorted(itertools.chain(self._dirs, self._files)):
966 966 if p in self._files:
967 967 yield self._subpath(p)
968 968 else:
969 969 for f in self._dirs[p]:
970 970 yield f
971 971
972 972 def keys(self):
973 973 return list(self.iterkeys())
974 974
975 975 def __iter__(self):
976 976 return self.iterkeys()
977 977
978 978 def __contains__(self, f):
979 979 if f is None:
980 980 return False
981 981 self._load()
982 982 dir, subpath = _splittopdir(f)
983 983 if dir:
984 984 self._loadlazy(dir)
985 985
986 986 if dir not in self._dirs:
987 987 return False
988 988
989 989 return self._dirs[dir].__contains__(subpath)
990 990 else:
991 991 return f in self._files
992 992
993 993 def get(self, f, default=None):
994 994 self._load()
995 995 dir, subpath = _splittopdir(f)
996 996 if dir:
997 997 self._loadlazy(dir)
998 998
999 999 if dir not in self._dirs:
1000 1000 return default
1001 1001 return self._dirs[dir].get(subpath, default)
1002 1002 else:
1003 1003 return self._files.get(f, default)
1004 1004
1005 1005 def __getitem__(self, f):
1006 1006 self._load()
1007 1007 dir, subpath = _splittopdir(f)
1008 1008 if dir:
1009 1009 self._loadlazy(dir)
1010 1010
1011 1011 return self._dirs[dir].__getitem__(subpath)
1012 1012 else:
1013 1013 return self._files[f]
1014 1014
1015 1015 def flags(self, f):
1016 1016 self._load()
1017 1017 dir, subpath = _splittopdir(f)
1018 1018 if dir:
1019 1019 self._loadlazy(dir)
1020 1020
1021 1021 if dir not in self._dirs:
1022 1022 return b''
1023 1023 return self._dirs[dir].flags(subpath)
1024 1024 else:
1025 1025 if f in self._lazydirs or f in self._dirs:
1026 1026 return b''
1027 1027 return self._flags.get(f, b'')
1028 1028
1029 1029 def find(self, f):
1030 1030 self._load()
1031 1031 dir, subpath = _splittopdir(f)
1032 1032 if dir:
1033 1033 self._loadlazy(dir)
1034 1034
1035 1035 return self._dirs[dir].find(subpath)
1036 1036 else:
1037 1037 return self._files[f], self._flags.get(f, b'')
1038 1038
1039 1039 def __delitem__(self, f):
1040 1040 self._load()
1041 1041 dir, subpath = _splittopdir(f)
1042 1042 if dir:
1043 1043 self._loadlazy(dir)
1044 1044
1045 1045 self._dirs[dir].__delitem__(subpath)
1046 1046 # If the directory is now empty, remove it
1047 1047 if self._dirs[dir]._isempty():
1048 1048 del self._dirs[dir]
1049 1049 else:
1050 1050 del self._files[f]
1051 1051 if f in self._flags:
1052 1052 del self._flags[f]
1053 1053 self._dirty = True
1054 1054
1055 1055 def __setitem__(self, f, n):
1056 1056 assert n is not None
1057 1057 self._load()
1058 1058 dir, subpath = _splittopdir(f)
1059 1059 if dir:
1060 1060 self._loadlazy(dir)
1061 1061 if dir not in self._dirs:
1062 1062 self._dirs[dir] = treemanifest(
1063 1063 self.nodeconstants, self._subpath(dir)
1064 1064 )
1065 1065 self._dirs[dir].__setitem__(subpath, n)
1066 1066 else:
1067 1067 # manifest nodes are either 20 bytes or 32 bytes,
1068 1068 # depending on the hash in use. Assert this as historically
1069 1069 # sometimes extra bytes were added.
1070 1070 assert len(n) in (20, 32)
1071 1071 self._files[f] = n
1072 1072 self._dirty = True
1073 1073
1074 1074 def _load(self):
1075 1075 if self._loadfunc is not _noop:
1076 1076 lf, self._loadfunc = self._loadfunc, _noop
1077 1077 lf(self)
1078 1078 elif self._copyfunc is not _noop:
1079 1079 cf, self._copyfunc = self._copyfunc, _noop
1080 1080 cf(self)
1081 1081
1082 1082 def setflag(self, f, flags):
1083 1083 """Set the flags (symlink, executable) for path f."""
1084 1084 if flags not in _manifestflags:
1085 1085 raise TypeError(b"Invalid manifest flag set.")
1086 1086 self._load()
1087 1087 dir, subpath = _splittopdir(f)
1088 1088 if dir:
1089 1089 self._loadlazy(dir)
1090 1090 if dir not in self._dirs:
1091 1091 self._dirs[dir] = treemanifest(
1092 1092 self.nodeconstants, self._subpath(dir)
1093 1093 )
1094 1094 self._dirs[dir].setflag(subpath, flags)
1095 1095 else:
1096 1096 self._flags[f] = flags
1097 1097 self._dirty = True
1098 1098
1099 1099 def copy(self):
1100 1100 copy = treemanifest(self.nodeconstants, self._dir)
1101 1101 copy._node = self._node
1102 1102 copy._dirty = self._dirty
1103 1103 if self._copyfunc is _noop:
1104 1104
1105 1105 def _copyfunc(s):
1106 1106 self._load()
1107 1107 s._lazydirs = {
1108 1108 d: (n, r, True)
1109 1109 for d, (n, r, c) in pycompat.iteritems(self._lazydirs)
1110 1110 }
1111 1111 sdirs = s._dirs
1112 1112 for d, v in pycompat.iteritems(self._dirs):
1113 1113 sdirs[d] = v.copy()
1114 1114 s._files = dict.copy(self._files)
1115 1115 s._flags = dict.copy(self._flags)
1116 1116
1117 1117 if self._loadfunc is _noop:
1118 1118 _copyfunc(copy)
1119 1119 else:
1120 1120 copy._copyfunc = _copyfunc
1121 1121 else:
1122 1122 copy._copyfunc = self._copyfunc
1123 1123 return copy
1124 1124
1125 1125 def filesnotin(self, m2, match=None):
1126 1126 '''Set of files in this manifest that are not in the other'''
1127 1127 if match and not match.always():
1128 1128 m1 = self._matches(match)
1129 1129 m2 = m2._matches(match)
1130 1130 return m1.filesnotin(m2)
1131 1131
1132 1132 files = set()
1133 1133
1134 1134 def _filesnotin(t1, t2):
1135 1135 if t1._node == t2._node and not t1._dirty and not t2._dirty:
1136 1136 return
1137 1137 t1._load()
1138 1138 t2._load()
1139 1139 self._loaddifflazy(t1, t2)
1140 1140 for d, m1 in pycompat.iteritems(t1._dirs):
1141 1141 if d in t2._dirs:
1142 1142 m2 = t2._dirs[d]
1143 1143 _filesnotin(m1, m2)
1144 1144 else:
1145 1145 files.update(m1.iterkeys())
1146 1146
1147 1147 for fn in t1._files:
1148 1148 if fn not in t2._files:
1149 1149 files.add(t1._subpath(fn))
1150 1150
1151 1151 _filesnotin(self, m2)
1152 1152 return files
1153 1153
1154 1154 @propertycache
1155 1155 def _alldirs(self):
1156 1156 return pathutil.dirs(self)
1157 1157
1158 1158 def dirs(self):
1159 1159 return self._alldirs
1160 1160
1161 1161 def hasdir(self, dir):
1162 1162 self._load()
1163 1163 topdir, subdir = _splittopdir(dir)
1164 1164 if topdir:
1165 1165 self._loadlazy(topdir)
1166 1166 if topdir in self._dirs:
1167 1167 return self._dirs[topdir].hasdir(subdir)
1168 1168 return False
1169 1169 dirslash = dir + b'/'
1170 1170 return dirslash in self._dirs or dirslash in self._lazydirs
1171 1171
1172 1172 def walk(self, match):
1173 1173 """Generates matching file names.
1174 1174
1175 1175 It also reports nonexistent files by marking them bad with match.bad().
1176 1176 """
1177 1177 if match.always():
1178 1178 for f in iter(self):
1179 1179 yield f
1180 1180 return
1181 1181
1182 1182 fset = set(match.files())
1183 1183
1184 1184 for fn in self._walk(match):
1185 1185 if fn in fset:
1186 1186 # specified pattern is the exact name
1187 1187 fset.remove(fn)
1188 1188 yield fn
1189 1189
1190 1190 # for dirstate.walk, files=[''] means "walk the whole tree".
1191 1191 # follow that here, too
1192 1192 fset.discard(b'')
1193 1193
1194 1194 for fn in sorted(fset):
1195 1195 if not self.hasdir(fn):
1196 1196 match.bad(fn, None)
1197 1197
1198 1198 def _walk(self, match):
1199 1199 '''Recursively generates matching file names for walk().'''
1200 1200 visit = match.visitchildrenset(self._dir[:-1])
1201 1201 if not visit:
1202 1202 return
1203 1203
1204 1204 # yield this dir's files and walk its submanifests
1205 1205 self._load()
1206 1206 visit = self._loadchildrensetlazy(visit)
1207 1207 for p in sorted(list(self._dirs) + list(self._files)):
1208 1208 if p in self._files:
1209 1209 fullp = self._subpath(p)
1210 1210 if match(fullp):
1211 1211 yield fullp
1212 1212 else:
1213 1213 if not visit or p[:-1] in visit:
1214 1214 for f in self._dirs[p]._walk(match):
1215 1215 yield f
1216 1216
1217 1217 def _matches(self, match):
1218 1218 """recursively generate a new manifest filtered by the match argument."""
1219 1219 if match.always():
1220 1220 return self.copy()
1221 1221 return self._matches_inner(match)
1222 1222
1223 1223 def _matches_inner(self, match):
1224 1224 if match.always():
1225 1225 return self.copy()
1226 1226
1227 1227 visit = match.visitchildrenset(self._dir[:-1])
1228 1228 if visit == b'all':
1229 1229 return self.copy()
1230 1230 ret = treemanifest(self.nodeconstants, self._dir)
1231 1231 if not visit:
1232 1232 return ret
1233 1233
1234 1234 self._load()
1235 1235 for fn in self._files:
1236 1236 # While visitchildrenset *usually* lists only subdirs, this is
1237 1237 # actually up to the matcher and may have some files in the set().
1238 1238 # If visit == 'this', we should obviously look at the files in this
1239 1239 # directory; if visit is a set, and fn is in it, we should inspect
1240 1240 # fn (but no need to inspect things not in the set).
1241 1241 if visit != b'this' and fn not in visit:
1242 1242 continue
1243 1243 fullp = self._subpath(fn)
1244 1244 # visitchildrenset isn't perfect, we still need to call the regular
1245 1245 # matcher code to further filter results.
1246 1246 if not match(fullp):
1247 1247 continue
1248 1248 ret._files[fn] = self._files[fn]
1249 1249 if fn in self._flags:
1250 1250 ret._flags[fn] = self._flags[fn]
1251 1251
1252 1252 visit = self._loadchildrensetlazy(visit)
1253 1253 for dir, subm in pycompat.iteritems(self._dirs):
1254 1254 if visit and dir[:-1] not in visit:
1255 1255 continue
1256 1256 m = subm._matches_inner(match)
1257 1257 if not m._isempty():
1258 1258 ret._dirs[dir] = m
1259 1259
1260 1260 if not ret._isempty():
1261 1261 ret._dirty = True
1262 1262 return ret
1263 1263
1264 1264 def fastdelta(self, base, changes):
1265 1265 raise FastdeltaUnavailable()
1266 1266
1267 1267 def diff(self, m2, match=None, clean=False):
1268 1268 """Finds changes between the current manifest and m2.
1269 1269
1270 1270 Args:
1271 1271 m2: the manifest to which this manifest should be compared.
1272 1272 clean: if true, include files unchanged between these manifests
1273 1273 with a None value in the returned dictionary.
1274 1274
1275 1275 The result is returned as a dict with filename as key and
1276 1276 values of the form ((n1,fl1),(n2,fl2)), where n1/n2 is the
1277 1277 nodeid in the current/other manifest and fl1/fl2 is the flag
1278 1278 in the current/other manifest. Where the file does not exist,
1279 1279 the nodeid will be None and the flags will be the empty
1280 1280 string.
1281 1281 """
1282 1282 if match and not match.always():
1283 1283 m1 = self._matches(match)
1284 1284 m2 = m2._matches(match)
1285 1285 return m1.diff(m2, clean=clean)
1286 1286 result = {}
1287 1287 emptytree = treemanifest(self.nodeconstants)
1288 1288
1289 1289 def _iterativediff(t1, t2, stack):
1290 1290 """compares two tree manifests and append new tree-manifests which
1291 1291 needs to be compared to stack"""
1292 1292 if t1._node == t2._node and not t1._dirty and not t2._dirty:
1293 1293 return
1294 1294 t1._load()
1295 1295 t2._load()
1296 1296 self._loaddifflazy(t1, t2)
1297 1297
1298 1298 for d, m1 in pycompat.iteritems(t1._dirs):
1299 1299 m2 = t2._dirs.get(d, emptytree)
1300 1300 stack.append((m1, m2))
1301 1301
1302 1302 for d, m2 in pycompat.iteritems(t2._dirs):
1303 1303 if d not in t1._dirs:
1304 1304 stack.append((emptytree, m2))
1305 1305
1306 1306 for fn, n1 in pycompat.iteritems(t1._files):
1307 1307 fl1 = t1._flags.get(fn, b'')
1308 1308 n2 = t2._files.get(fn, None)
1309 1309 fl2 = t2._flags.get(fn, b'')
1310 1310 if n1 != n2 or fl1 != fl2:
1311 1311 result[t1._subpath(fn)] = ((n1, fl1), (n2, fl2))
1312 1312 elif clean:
1313 1313 result[t1._subpath(fn)] = None
1314 1314
1315 1315 for fn, n2 in pycompat.iteritems(t2._files):
1316 1316 if fn not in t1._files:
1317 1317 fl2 = t2._flags.get(fn, b'')
1318 1318 result[t2._subpath(fn)] = ((None, b''), (n2, fl2))
1319 1319
1320 1320 stackls = []
1321 1321 _iterativediff(self, m2, stackls)
1322 1322 while stackls:
1323 1323 t1, t2 = stackls.pop()
1324 1324 # stackls is populated in the function call
1325 1325 _iterativediff(t1, t2, stackls)
1326 1326 return result
1327 1327
1328 1328 def unmodifiedsince(self, m2):
1329 1329 return not self._dirty and not m2._dirty and self._node == m2._node
1330 1330
1331 1331 def parse(self, text, readsubtree):
1332 1332 selflazy = self._lazydirs
1333 1333 for f, n, fl in _parse(self._nodelen, text):
1334 1334 if fl == b't':
1335 1335 f = f + b'/'
1336 1336 # False below means "doesn't need to be copied" and can use the
1337 1337 # cached value from readsubtree directly.
1338 1338 selflazy[f] = (n, readsubtree, False)
1339 1339 elif b'/' in f:
1340 1340 # This is a flat manifest, so use __setitem__ and setflag rather
1341 1341 # than assigning directly to _files and _flags, so we can
1342 1342 # assign a path in a subdirectory, and to mark dirty (compared
1343 1343 # to nullid).
1344 1344 self[f] = n
1345 1345 if fl:
1346 1346 self.setflag(f, fl)
1347 1347 else:
1348 1348 # Assigning to _files and _flags avoids marking as dirty,
1349 1349 # and should be a little faster.
1350 1350 self._files[f] = n
1351 1351 if fl:
1352 1352 self._flags[f] = fl
1353 1353
1354 1354 def text(self):
1355 1355 """Get the full data of this manifest as a bytestring."""
1356 1356 self._load()
1357 1357 return _text(self.iterentries())
1358 1358
1359 1359 def dirtext(self):
1360 1360 """Get the full data of this directory as a bytestring. Make sure that
1361 1361 any submanifests have been written first, so their nodeids are correct.
1362 1362 """
1363 1363 self._load()
1364 1364 flags = self.flags
1365 1365 lazydirs = [
1366 1366 (d[:-1], v[0], b't') for d, v in pycompat.iteritems(self._lazydirs)
1367 1367 ]
1368 1368 dirs = [(d[:-1], self._dirs[d]._node, b't') for d in self._dirs]
1369 1369 files = [(f, self._files[f], flags(f)) for f in self._files]
1370 1370 return _text(sorted(dirs + files + lazydirs))
1371 1371
1372 1372 def read(self, gettext, readsubtree):
1373 1373 def _load_for_read(s):
1374 1374 s.parse(gettext(), readsubtree)
1375 1375 s._dirty = False
1376 1376
1377 1377 self._loadfunc = _load_for_read
1378 1378
1379 1379 def writesubtrees(self, m1, m2, writesubtree, match):
1380 1380 self._load() # for consistency; should never have any effect here
1381 1381 m1._load()
1382 1382 m2._load()
1383 1383 emptytree = treemanifest(self.nodeconstants)
1384 1384
1385 1385 def getnode(m, d):
1386 1386 ld = m._lazydirs.get(d)
1387 1387 if ld:
1388 1388 return ld[0]
1389 1389 return m._dirs.get(d, emptytree)._node
1390 1390
1391 1391 # let's skip investigating things that `match` says we do not need.
1392 1392 visit = match.visitchildrenset(self._dir[:-1])
1393 1393 visit = self._loadchildrensetlazy(visit)
1394 1394 if visit == b'this' or visit == b'all':
1395 1395 visit = None
1396 1396 for d, subm in pycompat.iteritems(self._dirs):
1397 1397 if visit and d[:-1] not in visit:
1398 1398 continue
1399 1399 subp1 = getnode(m1, d)
1400 1400 subp2 = getnode(m2, d)
1401 1401 if subp1 == self.nodeconstants.nullid:
1402 1402 subp1, subp2 = subp2, subp1
1403 1403 writesubtree(subm, subp1, subp2, match)
1404 1404
1405 1405 def walksubtrees(self, matcher=None):
1406 1406 """Returns an iterator of the subtrees of this manifest, including this
1407 1407 manifest itself.
1408 1408
1409 1409 If `matcher` is provided, it only returns subtrees that match.
1410 1410 """
1411 1411 if matcher and not matcher.visitdir(self._dir[:-1]):
1412 1412 return
1413 1413 if not matcher or matcher(self._dir[:-1]):
1414 1414 yield self
1415 1415
1416 1416 self._load()
1417 1417 # OPT: use visitchildrenset to avoid loading everything.
1418 1418 self._loadalllazy()
1419 1419 for d, subm in pycompat.iteritems(self._dirs):
1420 1420 for subtree in subm.walksubtrees(matcher=matcher):
1421 1421 yield subtree
1422 1422
1423 1423
1424 1424 class manifestfulltextcache(util.lrucachedict):
1425 1425 """File-backed LRU cache for the manifest cache
1426 1426
1427 1427 File consists of entries, up to EOF:
1428 1428
1429 1429 - 20 bytes node, 4 bytes length, <length> manifest data
1430 1430
1431 1431 These are written in reverse cache order (oldest to newest).
1432 1432
1433 1433 """
1434 1434
1435 1435 _file = b'manifestfulltextcache'
1436 1436
1437 1437 def __init__(self, max):
1438 1438 super(manifestfulltextcache, self).__init__(max)
1439 1439 self._dirty = False
1440 1440 self._read = False
1441 1441 self._opener = None
1442 1442
1443 1443 def read(self):
1444 1444 if self._read or self._opener is None:
1445 1445 return
1446 1446
1447 1447 try:
1448 1448 with self._opener(self._file) as fp:
1449 1449 set = super(manifestfulltextcache, self).__setitem__
1450 1450 # ignore trailing data, this is a cache, corruption is skipped
1451 1451 while True:
1452 1452 # TODO do we need to do work here for sha1 portability?
1453 1453 node = fp.read(20)
1454 1454 if len(node) < 20:
1455 1455 break
1456 1456 try:
1457 1457 size = struct.unpack(b'>L', fp.read(4))[0]
1458 1458 except struct.error:
1459 1459 break
1460 1460 value = bytearray(fp.read(size))
1461 1461 if len(value) != size:
1462 1462 break
1463 1463 set(node, value)
1464 1464 except IOError:
1465 1465 # the file is allowed to be missing
1466 1466 pass
1467 1467
1468 1468 self._read = True
1469 1469 self._dirty = False
1470 1470
1471 1471 def write(self):
1472 1472 if not self._dirty or self._opener is None:
1473 1473 return
1474 1474 # rotate backwards to the first used node
1475 1475 try:
1476 1476 with self._opener(
1477 1477 self._file, b'w', atomictemp=True, checkambig=True
1478 1478 ) as fp:
1479 1479 node = self._head.prev
1480 1480 while True:
1481 1481 if node.key in self._cache:
1482 1482 fp.write(node.key)
1483 1483 fp.write(struct.pack(b'>L', len(node.value)))
1484 1484 fp.write(node.value)
1485 1485 if node is self._head:
1486 1486 break
1487 1487 node = node.prev
1488 1488 except IOError:
1489 1489 # We could not write the cache (eg: permission error)
1490 1490 # the content can be missing.
1491 1491 #
1492 1492 # We could try harder and see if we could recreate a wcache
1493 1493 # directory were we coudl write too.
1494 1494 #
1495 1495 # XXX the error pass silently, having some way to issue an error
1496 1496 # log `ui.log` would be nice.
1497 1497 pass
1498 1498
1499 1499 def __len__(self):
1500 1500 if not self._read:
1501 1501 self.read()
1502 1502 return super(manifestfulltextcache, self).__len__()
1503 1503
1504 1504 def __contains__(self, k):
1505 1505 if not self._read:
1506 1506 self.read()
1507 1507 return super(manifestfulltextcache, self).__contains__(k)
1508 1508
1509 1509 def __iter__(self):
1510 1510 if not self._read:
1511 1511 self.read()
1512 1512 return super(manifestfulltextcache, self).__iter__()
1513 1513
1514 1514 def __getitem__(self, k):
1515 1515 if not self._read:
1516 1516 self.read()
1517 1517 # the cache lru order can change on read
1518 1518 setdirty = self._cache.get(k) is not self._head
1519 1519 value = super(manifestfulltextcache, self).__getitem__(k)
1520 1520 if setdirty:
1521 1521 self._dirty = True
1522 1522 return value
1523 1523
1524 1524 def __setitem__(self, k, v):
1525 1525 if not self._read:
1526 1526 self.read()
1527 1527 super(manifestfulltextcache, self).__setitem__(k, v)
1528 1528 self._dirty = True
1529 1529
1530 1530 def __delitem__(self, k):
1531 1531 if not self._read:
1532 1532 self.read()
1533 1533 super(manifestfulltextcache, self).__delitem__(k)
1534 1534 self._dirty = True
1535 1535
1536 1536 def get(self, k, default=None):
1537 1537 if not self._read:
1538 1538 self.read()
1539 1539 return super(manifestfulltextcache, self).get(k, default=default)
1540 1540
1541 1541 def clear(self, clear_persisted_data=False):
1542 1542 super(manifestfulltextcache, self).clear()
1543 1543 if clear_persisted_data:
1544 1544 self._dirty = True
1545 1545 self.write()
1546 1546 self._read = False
1547 1547
1548 1548
1549 1549 # and upper bound of what we expect from compression
1550 1550 # (real live value seems to be "3")
1551 1551 MAXCOMPRESSION = 3
1552 1552
1553 1553
1554 1554 class FastdeltaUnavailable(Exception):
1555 1555 """Exception raised when fastdelta isn't usable on a manifest."""
1556 1556
1557 1557
1558 1558 @interfaceutil.implementer(repository.imanifeststorage)
1559 1559 class manifestrevlog(object):
1560 1560 """A revlog that stores manifest texts. This is responsible for caching the
1561 1561 full-text manifest contents.
1562 1562 """
1563 1563
1564 1564 def __init__(
1565 1565 self,
1566 1566 nodeconstants,
1567 1567 opener,
1568 1568 tree=b'',
1569 1569 dirlogcache=None,
1570 indexfile=None,
1571 1570 treemanifest=False,
1572 1571 ):
1573 1572 """Constructs a new manifest revlog
1574 1573
1575 1574 `indexfile` - used by extensions to have two manifests at once, like
1576 1575 when transitioning between flatmanifeset and treemanifests.
1577 1576
1578 1577 `treemanifest` - used to indicate this is a tree manifest revlog. Opener
1579 1578 options can also be used to make this a tree manifest revlog. The opener
1580 1579 option takes precedence, so if it is set to True, we ignore whatever
1581 1580 value is passed in to the constructor.
1582 1581 """
1583 1582 self.nodeconstants = nodeconstants
1584 1583 # During normal operations, we expect to deal with not more than four
1585 1584 # revs at a time (such as during commit --amend). When rebasing large
1586 1585 # stacks of commits, the number can go up, hence the config knob below.
1587 1586 cachesize = 4
1588 1587 optiontreemanifest = False
1589 1588 opts = getattr(opener, 'options', None)
1590 1589 if opts is not None:
1591 1590 cachesize = opts.get(b'manifestcachesize', cachesize)
1592 1591 optiontreemanifest = opts.get(b'treemanifest', False)
1593 1592
1594 1593 self._treeondisk = optiontreemanifest or treemanifest
1595 1594
1596 1595 self._fulltextcache = manifestfulltextcache(cachesize)
1597 1596
1598 1597 if tree:
1599 1598 assert self._treeondisk, b'opts is %r' % opts
1600 1599
1601 if indexfile is None:
1602 indexfile = b'00manifest.i'
1600 radix = b'00manifest'
1603 1601 if tree:
1604 indexfile = b"meta/" + tree + indexfile
1602 radix = b"meta/" + tree + radix
1605 1603
1606 1604 self.tree = tree
1607 1605
1608 1606 # The dirlogcache is kept on the root manifest log
1609 1607 if tree:
1610 1608 self._dirlogcache = dirlogcache
1611 1609 else:
1612 1610 self._dirlogcache = {b'': self}
1613 1611
1614 1612 self._revlog = revlog.revlog(
1615 1613 opener,
1616 1614 target=(revlog_constants.KIND_MANIFESTLOG, self.tree),
1617 indexfile=indexfile,
1615 radix=radix,
1618 1616 # only root indexfile is cached
1619 1617 checkambig=not bool(tree),
1620 1618 mmaplargeindex=True,
1621 1619 upperboundcomp=MAXCOMPRESSION,
1622 1620 persistentnodemap=opener.options.get(b'persistent-nodemap', False),
1623 1621 )
1624 1622
1625 1623 self.index = self._revlog.index
1626 1624 self._generaldelta = self._revlog._generaldelta
1627 1625
1628 1626 def _setupmanifestcachehooks(self, repo):
1629 1627 """Persist the manifestfulltextcache on lock release"""
1630 1628 if not util.safehasattr(repo, b'_wlockref'):
1631 1629 return
1632 1630
1633 1631 self._fulltextcache._opener = repo.wcachevfs
1634 1632 if repo._currentlock(repo._wlockref) is None:
1635 1633 return
1636 1634
1637 1635 reporef = weakref.ref(repo)
1638 1636 manifestrevlogref = weakref.ref(self)
1639 1637
1640 1638 def persistmanifestcache(success):
1641 1639 # Repo is in an unknown state, do not persist.
1642 1640 if not success:
1643 1641 return
1644 1642
1645 1643 repo = reporef()
1646 1644 self = manifestrevlogref()
1647 1645 if repo is None or self is None:
1648 1646 return
1649 1647 if repo.manifestlog.getstorage(b'') is not self:
1650 1648 # there's a different manifest in play now, abort
1651 1649 return
1652 1650 self._fulltextcache.write()
1653 1651
1654 1652 repo._afterlock(persistmanifestcache)
1655 1653
1656 1654 @property
1657 1655 def fulltextcache(self):
1658 1656 return self._fulltextcache
1659 1657
1660 1658 def clearcaches(self, clear_persisted_data=False):
1661 1659 self._revlog.clearcaches()
1662 1660 self._fulltextcache.clear(clear_persisted_data=clear_persisted_data)
1663 1661 self._dirlogcache = {self.tree: self}
1664 1662
1665 1663 def dirlog(self, d):
1666 1664 if d:
1667 1665 assert self._treeondisk
1668 1666 if d not in self._dirlogcache:
1669 1667 mfrevlog = manifestrevlog(
1670 1668 self.nodeconstants,
1671 1669 self.opener,
1672 1670 d,
1673 1671 self._dirlogcache,
1674 1672 treemanifest=self._treeondisk,
1675 1673 )
1676 1674 self._dirlogcache[d] = mfrevlog
1677 1675 return self._dirlogcache[d]
1678 1676
1679 1677 def add(
1680 1678 self,
1681 1679 m,
1682 1680 transaction,
1683 1681 link,
1684 1682 p1,
1685 1683 p2,
1686 1684 added,
1687 1685 removed,
1688 1686 readtree=None,
1689 1687 match=None,
1690 1688 ):
1691 1689 """add some manifest entry in to the manifest log
1692 1690
1693 1691 input:
1694 1692
1695 1693 m: the manifest dict we want to store
1696 1694 transaction: the open transaction
1697 1695 p1: manifest-node of p1
1698 1696 p2: manifest-node of p2
1699 1697 added: file added/changed compared to parent
1700 1698 removed: file removed compared to parent
1701 1699
1702 1700 tree manifest input:
1703 1701
1704 1702 readtree: a function to read a subtree
1705 1703 match: a filematcher for the subpart of the tree manifest
1706 1704 """
1707 1705 try:
1708 1706 if p1 not in self.fulltextcache:
1709 1707 raise FastdeltaUnavailable()
1710 1708 # If our first parent is in the manifest cache, we can
1711 1709 # compute a delta here using properties we know about the
1712 1710 # manifest up-front, which may save time later for the
1713 1711 # revlog layer.
1714 1712
1715 1713 _checkforbidden(added)
1716 1714 # combine the changed lists into one sorted iterator
1717 1715 work = heapq.merge(
1718 1716 [(x, False) for x in sorted(added)],
1719 1717 [(x, True) for x in sorted(removed)],
1720 1718 )
1721 1719
1722 1720 arraytext, deltatext = m.fastdelta(self.fulltextcache[p1], work)
1723 1721 cachedelta = self._revlog.rev(p1), deltatext
1724 1722 text = util.buffer(arraytext)
1725 1723 rev = self._revlog.addrevision(
1726 1724 text, transaction, link, p1, p2, cachedelta
1727 1725 )
1728 1726 n = self._revlog.node(rev)
1729 1727 except FastdeltaUnavailable:
1730 1728 # The first parent manifest isn't already loaded or the
1731 1729 # manifest implementation doesn't support fastdelta, so
1732 1730 # we'll just encode a fulltext of the manifest and pass
1733 1731 # that through to the revlog layer, and let it handle the
1734 1732 # delta process.
1735 1733 if self._treeondisk:
1736 1734 assert readtree, b"readtree must be set for treemanifest writes"
1737 1735 assert match, b"match must be specified for treemanifest writes"
1738 1736 m1 = readtree(self.tree, p1)
1739 1737 m2 = readtree(self.tree, p2)
1740 1738 n = self._addtree(
1741 1739 m, transaction, link, m1, m2, readtree, match=match
1742 1740 )
1743 1741 arraytext = None
1744 1742 else:
1745 1743 text = m.text()
1746 1744 rev = self._revlog.addrevision(text, transaction, link, p1, p2)
1747 1745 n = self._revlog.node(rev)
1748 1746 arraytext = bytearray(text)
1749 1747
1750 1748 if arraytext is not None:
1751 1749 self.fulltextcache[n] = arraytext
1752 1750
1753 1751 return n
1754 1752
1755 1753 def _addtree(self, m, transaction, link, m1, m2, readtree, match):
1756 1754 # If the manifest is unchanged compared to one parent,
1757 1755 # don't write a new revision
1758 1756 if self.tree != b'' and (
1759 1757 m.unmodifiedsince(m1) or m.unmodifiedsince(m2)
1760 1758 ):
1761 1759 return m.node()
1762 1760
1763 1761 def writesubtree(subm, subp1, subp2, match):
1764 1762 sublog = self.dirlog(subm.dir())
1765 1763 sublog.add(
1766 1764 subm,
1767 1765 transaction,
1768 1766 link,
1769 1767 subp1,
1770 1768 subp2,
1771 1769 None,
1772 1770 None,
1773 1771 readtree=readtree,
1774 1772 match=match,
1775 1773 )
1776 1774
1777 1775 m.writesubtrees(m1, m2, writesubtree, match)
1778 1776 text = m.dirtext()
1779 1777 n = None
1780 1778 if self.tree != b'':
1781 1779 # Double-check whether contents are unchanged to one parent
1782 1780 if text == m1.dirtext():
1783 1781 n = m1.node()
1784 1782 elif text == m2.dirtext():
1785 1783 n = m2.node()
1786 1784
1787 1785 if not n:
1788 1786 rev = self._revlog.addrevision(
1789 1787 text, transaction, link, m1.node(), m2.node()
1790 1788 )
1791 1789 n = self._revlog.node(rev)
1792 1790
1793 1791 # Save nodeid so parent manifest can calculate its nodeid
1794 1792 m.setnode(n)
1795 1793 return n
1796 1794
1797 1795 def __len__(self):
1798 1796 return len(self._revlog)
1799 1797
1800 1798 def __iter__(self):
1801 1799 return self._revlog.__iter__()
1802 1800
1803 1801 def rev(self, node):
1804 1802 return self._revlog.rev(node)
1805 1803
1806 1804 def node(self, rev):
1807 1805 return self._revlog.node(rev)
1808 1806
1809 1807 def lookup(self, value):
1810 1808 return self._revlog.lookup(value)
1811 1809
1812 1810 def parentrevs(self, rev):
1813 1811 return self._revlog.parentrevs(rev)
1814 1812
1815 1813 def parents(self, node):
1816 1814 return self._revlog.parents(node)
1817 1815
1818 1816 def linkrev(self, rev):
1819 1817 return self._revlog.linkrev(rev)
1820 1818
1821 1819 def checksize(self):
1822 1820 return self._revlog.checksize()
1823 1821
1824 1822 def revision(self, node, _df=None, raw=False):
1825 1823 return self._revlog.revision(node, _df=_df, raw=raw)
1826 1824
1827 1825 def rawdata(self, node, _df=None):
1828 1826 return self._revlog.rawdata(node, _df=_df)
1829 1827
1830 1828 def revdiff(self, rev1, rev2):
1831 1829 return self._revlog.revdiff(rev1, rev2)
1832 1830
1833 1831 def cmp(self, node, text):
1834 1832 return self._revlog.cmp(node, text)
1835 1833
1836 1834 def deltaparent(self, rev):
1837 1835 return self._revlog.deltaparent(rev)
1838 1836
1839 1837 def emitrevisions(
1840 1838 self,
1841 1839 nodes,
1842 1840 nodesorder=None,
1843 1841 revisiondata=False,
1844 1842 assumehaveparentrevisions=False,
1845 1843 deltamode=repository.CG_DELTAMODE_STD,
1846 1844 sidedata_helpers=None,
1847 1845 ):
1848 1846 return self._revlog.emitrevisions(
1849 1847 nodes,
1850 1848 nodesorder=nodesorder,
1851 1849 revisiondata=revisiondata,
1852 1850 assumehaveparentrevisions=assumehaveparentrevisions,
1853 1851 deltamode=deltamode,
1854 1852 sidedata_helpers=sidedata_helpers,
1855 1853 )
1856 1854
1857 1855 def addgroup(
1858 1856 self,
1859 1857 deltas,
1860 1858 linkmapper,
1861 1859 transaction,
1862 1860 alwayscache=False,
1863 1861 addrevisioncb=None,
1864 1862 duplicaterevisioncb=None,
1865 1863 ):
1866 1864 return self._revlog.addgroup(
1867 1865 deltas,
1868 1866 linkmapper,
1869 1867 transaction,
1870 1868 alwayscache=alwayscache,
1871 1869 addrevisioncb=addrevisioncb,
1872 1870 duplicaterevisioncb=duplicaterevisioncb,
1873 1871 )
1874 1872
1875 1873 def rawsize(self, rev):
1876 1874 return self._revlog.rawsize(rev)
1877 1875
1878 1876 def getstrippoint(self, minlink):
1879 1877 return self._revlog.getstrippoint(minlink)
1880 1878
1881 1879 def strip(self, minlink, transaction):
1882 1880 return self._revlog.strip(minlink, transaction)
1883 1881
1884 1882 def files(self):
1885 1883 return self._revlog.files()
1886 1884
1887 1885 def clone(self, tr, destrevlog, **kwargs):
1888 1886 if not isinstance(destrevlog, manifestrevlog):
1889 1887 raise error.ProgrammingError(b'expected manifestrevlog to clone()')
1890 1888
1891 1889 return self._revlog.clone(tr, destrevlog._revlog, **kwargs)
1892 1890
1893 1891 def storageinfo(
1894 1892 self,
1895 1893 exclusivefiles=False,
1896 1894 sharedfiles=False,
1897 1895 revisionscount=False,
1898 1896 trackedsize=False,
1899 1897 storedsize=False,
1900 1898 ):
1901 1899 return self._revlog.storageinfo(
1902 1900 exclusivefiles=exclusivefiles,
1903 1901 sharedfiles=sharedfiles,
1904 1902 revisionscount=revisionscount,
1905 1903 trackedsize=trackedsize,
1906 1904 storedsize=storedsize,
1907 1905 )
1908 1906
1909 1907 @property
1910 1908 def opener(self):
1911 1909 return self._revlog.opener
1912 1910
1913 1911 @opener.setter
1914 1912 def opener(self, value):
1915 1913 self._revlog.opener = value
1916 1914
1917 1915
1918 1916 @interfaceutil.implementer(repository.imanifestlog)
1919 1917 class manifestlog(object):
1920 1918 """A collection class representing the collection of manifest snapshots
1921 1919 referenced by commits in the repository.
1922 1920
1923 1921 In this situation, 'manifest' refers to the abstract concept of a snapshot
1924 1922 of the list of files in the given commit. Consumers of the output of this
1925 1923 class do not care about the implementation details of the actual manifests
1926 1924 they receive (i.e. tree or flat or lazily loaded, etc)."""
1927 1925
1928 1926 def __init__(self, opener, repo, rootstore, narrowmatch):
1929 1927 self.nodeconstants = repo.nodeconstants
1930 1928 usetreemanifest = False
1931 1929 cachesize = 4
1932 1930
1933 1931 opts = getattr(opener, 'options', None)
1934 1932 if opts is not None:
1935 1933 usetreemanifest = opts.get(b'treemanifest', usetreemanifest)
1936 1934 cachesize = opts.get(b'manifestcachesize', cachesize)
1937 1935
1938 1936 self._treemanifests = usetreemanifest
1939 1937
1940 1938 self._rootstore = rootstore
1941 1939 self._rootstore._setupmanifestcachehooks(repo)
1942 1940 self._narrowmatch = narrowmatch
1943 1941
1944 1942 # A cache of the manifestctx or treemanifestctx for each directory
1945 1943 self._dirmancache = {}
1946 1944 self._dirmancache[b''] = util.lrucachedict(cachesize)
1947 1945
1948 1946 self._cachesize = cachesize
1949 1947
1950 1948 def __getitem__(self, node):
1951 1949 """Retrieves the manifest instance for the given node. Throws a
1952 1950 LookupError if not found.
1953 1951 """
1954 1952 return self.get(b'', node)
1955 1953
1956 1954 def get(self, tree, node, verify=True):
1957 1955 """Retrieves the manifest instance for the given node. Throws a
1958 1956 LookupError if not found.
1959 1957
1960 1958 `verify` - if True an exception will be thrown if the node is not in
1961 1959 the revlog
1962 1960 """
1963 1961 if node in self._dirmancache.get(tree, ()):
1964 1962 return self._dirmancache[tree][node]
1965 1963
1966 1964 if not self._narrowmatch.always():
1967 1965 if not self._narrowmatch.visitdir(tree[:-1]):
1968 1966 return excludeddirmanifestctx(self.nodeconstants, tree, node)
1969 1967 if tree:
1970 1968 if self._rootstore._treeondisk:
1971 1969 if verify:
1972 1970 # Side-effect is LookupError is raised if node doesn't
1973 1971 # exist.
1974 1972 self.getstorage(tree).rev(node)
1975 1973
1976 1974 m = treemanifestctx(self, tree, node)
1977 1975 else:
1978 1976 raise error.Abort(
1979 1977 _(
1980 1978 b"cannot ask for manifest directory '%s' in a flat "
1981 1979 b"manifest"
1982 1980 )
1983 1981 % tree
1984 1982 )
1985 1983 else:
1986 1984 if verify:
1987 1985 # Side-effect is LookupError is raised if node doesn't exist.
1988 1986 self._rootstore.rev(node)
1989 1987
1990 1988 if self._treemanifests:
1991 1989 m = treemanifestctx(self, b'', node)
1992 1990 else:
1993 1991 m = manifestctx(self, node)
1994 1992
1995 1993 if node != self.nodeconstants.nullid:
1996 1994 mancache = self._dirmancache.get(tree)
1997 1995 if not mancache:
1998 1996 mancache = util.lrucachedict(self._cachesize)
1999 1997 self._dirmancache[tree] = mancache
2000 1998 mancache[node] = m
2001 1999 return m
2002 2000
2003 2001 def getstorage(self, tree):
2004 2002 return self._rootstore.dirlog(tree)
2005 2003
2006 2004 def clearcaches(self, clear_persisted_data=False):
2007 2005 self._dirmancache.clear()
2008 2006 self._rootstore.clearcaches(clear_persisted_data=clear_persisted_data)
2009 2007
2010 2008 def rev(self, node):
2011 2009 return self._rootstore.rev(node)
2012 2010
2013 2011 def update_caches(self, transaction):
2014 2012 return self._rootstore._revlog.update_caches(transaction=transaction)
2015 2013
2016 2014
2017 2015 @interfaceutil.implementer(repository.imanifestrevisionwritable)
2018 2016 class memmanifestctx(object):
2019 2017 def __init__(self, manifestlog):
2020 2018 self._manifestlog = manifestlog
2021 2019 self._manifestdict = manifestdict(manifestlog.nodeconstants.nodelen)
2022 2020
2023 2021 def _storage(self):
2024 2022 return self._manifestlog.getstorage(b'')
2025 2023
2026 2024 def copy(self):
2027 2025 memmf = memmanifestctx(self._manifestlog)
2028 2026 memmf._manifestdict = self.read().copy()
2029 2027 return memmf
2030 2028
2031 2029 def read(self):
2032 2030 return self._manifestdict
2033 2031
2034 2032 def write(self, transaction, link, p1, p2, added, removed, match=None):
2035 2033 return self._storage().add(
2036 2034 self._manifestdict,
2037 2035 transaction,
2038 2036 link,
2039 2037 p1,
2040 2038 p2,
2041 2039 added,
2042 2040 removed,
2043 2041 match=match,
2044 2042 )
2045 2043
2046 2044
2047 2045 @interfaceutil.implementer(repository.imanifestrevisionstored)
2048 2046 class manifestctx(object):
2049 2047 """A class representing a single revision of a manifest, including its
2050 2048 contents, its parent revs, and its linkrev.
2051 2049 """
2052 2050
2053 2051 def __init__(self, manifestlog, node):
2054 2052 self._manifestlog = manifestlog
2055 2053 self._data = None
2056 2054
2057 2055 self._node = node
2058 2056
2059 2057 # TODO: We eventually want p1, p2, and linkrev exposed on this class,
2060 2058 # but let's add it later when something needs it and we can load it
2061 2059 # lazily.
2062 2060 # self.p1, self.p2 = store.parents(node)
2063 2061 # rev = store.rev(node)
2064 2062 # self.linkrev = store.linkrev(rev)
2065 2063
2066 2064 def _storage(self):
2067 2065 return self._manifestlog.getstorage(b'')
2068 2066
2069 2067 def node(self):
2070 2068 return self._node
2071 2069
2072 2070 def copy(self):
2073 2071 memmf = memmanifestctx(self._manifestlog)
2074 2072 memmf._manifestdict = self.read().copy()
2075 2073 return memmf
2076 2074
2077 2075 @propertycache
2078 2076 def parents(self):
2079 2077 return self._storage().parents(self._node)
2080 2078
2081 2079 def read(self):
2082 2080 if self._data is None:
2083 2081 nc = self._manifestlog.nodeconstants
2084 2082 if self._node == nc.nullid:
2085 2083 self._data = manifestdict(nc.nodelen)
2086 2084 else:
2087 2085 store = self._storage()
2088 2086 if self._node in store.fulltextcache:
2089 2087 text = pycompat.bytestr(store.fulltextcache[self._node])
2090 2088 else:
2091 2089 text = store.revision(self._node)
2092 2090 arraytext = bytearray(text)
2093 2091 store.fulltextcache[self._node] = arraytext
2094 2092 self._data = manifestdict(nc.nodelen, text)
2095 2093 return self._data
2096 2094
2097 2095 def readfast(self, shallow=False):
2098 2096 """Calls either readdelta or read, based on which would be less work.
2099 2097 readdelta is called if the delta is against the p1, and therefore can be
2100 2098 read quickly.
2101 2099
2102 2100 If `shallow` is True, nothing changes since this is a flat manifest.
2103 2101 """
2104 2102 store = self._storage()
2105 2103 r = store.rev(self._node)
2106 2104 deltaparent = store.deltaparent(r)
2107 2105 if deltaparent != nullrev and deltaparent in store.parentrevs(r):
2108 2106 return self.readdelta()
2109 2107 return self.read()
2110 2108
2111 2109 def readdelta(self, shallow=False):
2112 2110 """Returns a manifest containing just the entries that are present
2113 2111 in this manifest, but not in its p1 manifest. This is efficient to read
2114 2112 if the revlog delta is already p1.
2115 2113
2116 2114 Changing the value of `shallow` has no effect on flat manifests.
2117 2115 """
2118 2116 store = self._storage()
2119 2117 r = store.rev(self._node)
2120 2118 d = mdiff.patchtext(store.revdiff(store.deltaparent(r), r))
2121 2119 return manifestdict(store.nodeconstants.nodelen, d)
2122 2120
2123 2121 def find(self, key):
2124 2122 return self.read().find(key)
2125 2123
2126 2124
2127 2125 @interfaceutil.implementer(repository.imanifestrevisionwritable)
2128 2126 class memtreemanifestctx(object):
2129 2127 def __init__(self, manifestlog, dir=b''):
2130 2128 self._manifestlog = manifestlog
2131 2129 self._dir = dir
2132 2130 self._treemanifest = treemanifest(manifestlog.nodeconstants)
2133 2131
2134 2132 def _storage(self):
2135 2133 return self._manifestlog.getstorage(b'')
2136 2134
2137 2135 def copy(self):
2138 2136 memmf = memtreemanifestctx(self._manifestlog, dir=self._dir)
2139 2137 memmf._treemanifest = self._treemanifest.copy()
2140 2138 return memmf
2141 2139
2142 2140 def read(self):
2143 2141 return self._treemanifest
2144 2142
2145 2143 def write(self, transaction, link, p1, p2, added, removed, match=None):
2146 2144 def readtree(dir, node):
2147 2145 return self._manifestlog.get(dir, node).read()
2148 2146
2149 2147 return self._storage().add(
2150 2148 self._treemanifest,
2151 2149 transaction,
2152 2150 link,
2153 2151 p1,
2154 2152 p2,
2155 2153 added,
2156 2154 removed,
2157 2155 readtree=readtree,
2158 2156 match=match,
2159 2157 )
2160 2158
2161 2159
2162 2160 @interfaceutil.implementer(repository.imanifestrevisionstored)
2163 2161 class treemanifestctx(object):
2164 2162 def __init__(self, manifestlog, dir, node):
2165 2163 self._manifestlog = manifestlog
2166 2164 self._dir = dir
2167 2165 self._data = None
2168 2166
2169 2167 self._node = node
2170 2168
2171 2169 # TODO: Load p1/p2/linkrev lazily. They need to be lazily loaded so that
2172 2170 # we can instantiate treemanifestctx objects for directories we don't
2173 2171 # have on disk.
2174 2172 # self.p1, self.p2 = store.parents(node)
2175 2173 # rev = store.rev(node)
2176 2174 # self.linkrev = store.linkrev(rev)
2177 2175
2178 2176 def _storage(self):
2179 2177 narrowmatch = self._manifestlog._narrowmatch
2180 2178 if not narrowmatch.always():
2181 2179 if not narrowmatch.visitdir(self._dir[:-1]):
2182 2180 return excludedmanifestrevlog(
2183 2181 self._manifestlog.nodeconstants, self._dir
2184 2182 )
2185 2183 return self._manifestlog.getstorage(self._dir)
2186 2184
2187 2185 def read(self):
2188 2186 if self._data is None:
2189 2187 store = self._storage()
2190 2188 if self._node == self._manifestlog.nodeconstants.nullid:
2191 2189 self._data = treemanifest(self._manifestlog.nodeconstants)
2192 2190 # TODO accessing non-public API
2193 2191 elif store._treeondisk:
2194 2192 m = treemanifest(self._manifestlog.nodeconstants, dir=self._dir)
2195 2193
2196 2194 def gettext():
2197 2195 return store.revision(self._node)
2198 2196
2199 2197 def readsubtree(dir, subm):
2200 2198 # Set verify to False since we need to be able to create
2201 2199 # subtrees for trees that don't exist on disk.
2202 2200 return self._manifestlog.get(dir, subm, verify=False).read()
2203 2201
2204 2202 m.read(gettext, readsubtree)
2205 2203 m.setnode(self._node)
2206 2204 self._data = m
2207 2205 else:
2208 2206 if self._node in store.fulltextcache:
2209 2207 text = pycompat.bytestr(store.fulltextcache[self._node])
2210 2208 else:
2211 2209 text = store.revision(self._node)
2212 2210 arraytext = bytearray(text)
2213 2211 store.fulltextcache[self._node] = arraytext
2214 2212 self._data = treemanifest(
2215 2213 self._manifestlog.nodeconstants, dir=self._dir, text=text
2216 2214 )
2217 2215
2218 2216 return self._data
2219 2217
2220 2218 def node(self):
2221 2219 return self._node
2222 2220
2223 2221 def copy(self):
2224 2222 memmf = memtreemanifestctx(self._manifestlog, dir=self._dir)
2225 2223 memmf._treemanifest = self.read().copy()
2226 2224 return memmf
2227 2225
2228 2226 @propertycache
2229 2227 def parents(self):
2230 2228 return self._storage().parents(self._node)
2231 2229
2232 2230 def readdelta(self, shallow=False):
2233 2231 """Returns a manifest containing just the entries that are present
2234 2232 in this manifest, but not in its p1 manifest. This is efficient to read
2235 2233 if the revlog delta is already p1.
2236 2234
2237 2235 If `shallow` is True, this will read the delta for this directory,
2238 2236 without recursively reading subdirectory manifests. Instead, any
2239 2237 subdirectory entry will be reported as it appears in the manifest, i.e.
2240 2238 the subdirectory will be reported among files and distinguished only by
2241 2239 its 't' flag.
2242 2240 """
2243 2241 store = self._storage()
2244 2242 if shallow:
2245 2243 r = store.rev(self._node)
2246 2244 d = mdiff.patchtext(store.revdiff(store.deltaparent(r), r))
2247 2245 return manifestdict(store.nodeconstants.nodelen, d)
2248 2246 else:
2249 2247 # Need to perform a slow delta
2250 2248 r0 = store.deltaparent(store.rev(self._node))
2251 2249 m0 = self._manifestlog.get(self._dir, store.node(r0)).read()
2252 2250 m1 = self.read()
2253 2251 md = treemanifest(self._manifestlog.nodeconstants, dir=self._dir)
2254 2252 for f, ((n0, fl0), (n1, fl1)) in pycompat.iteritems(m0.diff(m1)):
2255 2253 if n1:
2256 2254 md[f] = n1
2257 2255 if fl1:
2258 2256 md.setflag(f, fl1)
2259 2257 return md
2260 2258
2261 2259 def readfast(self, shallow=False):
2262 2260 """Calls either readdelta or read, based on which would be less work.
2263 2261 readdelta is called if the delta is against the p1, and therefore can be
2264 2262 read quickly.
2265 2263
2266 2264 If `shallow` is True, it only returns the entries from this manifest,
2267 2265 and not any submanifests.
2268 2266 """
2269 2267 store = self._storage()
2270 2268 r = store.rev(self._node)
2271 2269 deltaparent = store.deltaparent(r)
2272 2270 if deltaparent != nullrev and deltaparent in store.parentrevs(r):
2273 2271 return self.readdelta(shallow=shallow)
2274 2272
2275 2273 if shallow:
2276 2274 return manifestdict(
2277 2275 store.nodeconstants.nodelen, store.revision(self._node)
2278 2276 )
2279 2277 else:
2280 2278 return self.read()
2281 2279
2282 2280 def find(self, key):
2283 2281 return self.read().find(key)
2284 2282
2285 2283
2286 2284 class excludeddir(treemanifest):
2287 2285 """Stand-in for a directory that is excluded from the repository.
2288 2286
2289 2287 With narrowing active on a repository that uses treemanifests,
2290 2288 some of the directory revlogs will be excluded from the resulting
2291 2289 clone. This is a huge storage win for clients, but means we need
2292 2290 some sort of pseudo-manifest to surface to internals so we can
2293 2291 detect a merge conflict outside the narrowspec. That's what this
2294 2292 class is: it stands in for a directory whose node is known, but
2295 2293 whose contents are unknown.
2296 2294 """
2297 2295
2298 2296 def __init__(self, nodeconstants, dir, node):
2299 2297 super(excludeddir, self).__init__(nodeconstants, dir)
2300 2298 self._node = node
2301 2299 # Add an empty file, which will be included by iterators and such,
2302 2300 # appearing as the directory itself (i.e. something like "dir/")
2303 2301 self._files[b''] = node
2304 2302 self._flags[b''] = b't'
2305 2303
2306 2304 # Manifests outside the narrowspec should never be modified, so avoid
2307 2305 # copying. This makes a noticeable difference when there are very many
2308 2306 # directories outside the narrowspec. Also, it makes sense for the copy to
2309 2307 # be of the same type as the original, which would not happen with the
2310 2308 # super type's copy().
2311 2309 def copy(self):
2312 2310 return self
2313 2311
2314 2312
2315 2313 class excludeddirmanifestctx(treemanifestctx):
2316 2314 """context wrapper for excludeddir - see that docstring for rationale"""
2317 2315
2318 2316 def __init__(self, nodeconstants, dir, node):
2319 2317 self.nodeconstants = nodeconstants
2320 2318 self._dir = dir
2321 2319 self._node = node
2322 2320
2323 2321 def read(self):
2324 2322 return excludeddir(self.nodeconstants, self._dir, self._node)
2325 2323
2326 2324 def readfast(self, shallow=False):
2327 2325 # special version of readfast since we don't have underlying storage
2328 2326 return self.read()
2329 2327
2330 2328 def write(self, *args):
2331 2329 raise error.ProgrammingError(
2332 2330 b'attempt to write manifest from excluded dir %s' % self._dir
2333 2331 )
2334 2332
2335 2333
2336 2334 class excludedmanifestrevlog(manifestrevlog):
2337 2335 """Stand-in for excluded treemanifest revlogs.
2338 2336
2339 2337 When narrowing is active on a treemanifest repository, we'll have
2340 2338 references to directories we can't see due to the revlog being
2341 2339 skipped. This class exists to conform to the manifestrevlog
2342 2340 interface for those directories and proactively prevent writes to
2343 2341 outside the narrowspec.
2344 2342 """
2345 2343
2346 2344 def __init__(self, nodeconstants, dir):
2347 2345 self.nodeconstants = nodeconstants
2348 2346 self._dir = dir
2349 2347
2350 2348 def __len__(self):
2351 2349 raise error.ProgrammingError(
2352 2350 b'attempt to get length of excluded dir %s' % self._dir
2353 2351 )
2354 2352
2355 2353 def rev(self, node):
2356 2354 raise error.ProgrammingError(
2357 2355 b'attempt to get rev from excluded dir %s' % self._dir
2358 2356 )
2359 2357
2360 2358 def linkrev(self, node):
2361 2359 raise error.ProgrammingError(
2362 2360 b'attempt to get linkrev from excluded dir %s' % self._dir
2363 2361 )
2364 2362
2365 2363 def node(self, rev):
2366 2364 raise error.ProgrammingError(
2367 2365 b'attempt to get node from excluded dir %s' % self._dir
2368 2366 )
2369 2367
2370 2368 def add(self, *args, **kwargs):
2371 2369 # We should never write entries in dirlogs outside the narrow clone.
2372 2370 # However, the method still gets called from writesubtree() in
2373 2371 # _addtree(), so we need to handle it. We should possibly make that
2374 2372 # avoid calling add() with a clean manifest (_dirty is always False
2375 2373 # in excludeddir instances).
2376 2374 pass
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now