##// END OF EJS Templates
py3: introduce and use pycompat.getargspec...
Augie Fackler -
r36196:64600233 default
parent child Browse files
Show More
@@ -1,1730 +1,1735
1 1 # perf.py - performance test routines
2 2 '''helper extension to measure performance'''
3 3
4 4 # "historical portability" policy of perf.py:
5 5 #
6 6 # We have to do:
7 7 # - make perf.py "loadable" with as wide Mercurial version as possible
8 8 # This doesn't mean that perf commands work correctly with that Mercurial.
9 9 # BTW, perf.py itself has been available since 1.1 (or eb240755386d).
10 10 # - make historical perf command work correctly with as wide Mercurial
11 11 # version as possible
12 12 #
13 13 # We have to do, if possible with reasonable cost:
14 14 # - make recent perf command for historical feature work correctly
15 15 # with early Mercurial
16 16 #
17 17 # We don't have to do:
18 18 # - make perf command for recent feature work correctly with early
19 19 # Mercurial
20 20
21 21 from __future__ import absolute_import
22 22 import functools
23 23 import gc
24 24 import os
25 25 import random
26 26 import struct
27 27 import sys
28 28 import threading
29 29 import time
30 30 from mercurial import (
31 31 changegroup,
32 32 cmdutil,
33 33 commands,
34 34 copies,
35 35 error,
36 36 extensions,
37 37 mdiff,
38 38 merge,
39 39 revlog,
40 40 util,
41 41 )
42 42
43 43 # for "historical portability":
44 44 # try to import modules separately (in dict order), and ignore
45 45 # failure, because these aren't available with early Mercurial
46 46 try:
47 47 from mercurial import branchmap # since 2.5 (or bcee63733aad)
48 48 except ImportError:
49 49 pass
50 50 try:
51 51 from mercurial import obsolete # since 2.3 (or ad0d6c2b3279)
52 52 except ImportError:
53 53 pass
54 54 try:
55 55 from mercurial import registrar # since 3.7 (or 37d50250b696)
56 56 dir(registrar) # forcibly load it
57 57 except ImportError:
58 58 registrar = None
59 59 try:
60 60 from mercurial import repoview # since 2.5 (or 3a6ddacb7198)
61 61 except ImportError:
62 62 pass
63 63 try:
64 64 from mercurial import scmutil # since 1.9 (or 8b252e826c68)
65 65 except ImportError:
66 66 pass
67 try:
68 from mercurial import pycompat
69 getargspec = pycompat.getargspec # added to module after 4.5
70 except (ImportError, AttributeError):
71 import inspect
72 getargspec = inspect.getargspec
67 73
68 74 # for "historical portability":
69 75 # define util.safehasattr forcibly, because util.safehasattr has been
70 76 # available since 1.9.3 (or 94b200a11cf7)
71 77 _undefined = object()
72 78 def safehasattr(thing, attr):
73 79 return getattr(thing, attr, _undefined) is not _undefined
74 80 setattr(util, 'safehasattr', safehasattr)
75 81
76 82 # for "historical portability":
77 83 # define util.timer forcibly, because util.timer has been available
78 84 # since ae5d60bb70c9
79 85 if safehasattr(time, 'perf_counter'):
80 86 util.timer = time.perf_counter
81 87 elif os.name == 'nt':
82 88 util.timer = time.clock
83 89 else:
84 90 util.timer = time.time
85 91
86 92 # for "historical portability":
87 93 # use locally defined empty option list, if formatteropts isn't
88 94 # available, because commands.formatteropts has been available since
89 95 # 3.2 (or 7a7eed5176a4), even though formatting itself has been
90 96 # available since 2.2 (or ae5f92e154d3)
91 97 formatteropts = getattr(cmdutil, "formatteropts",
92 98 getattr(commands, "formatteropts", []))
93 99
94 100 # for "historical portability":
95 101 # use locally defined option list, if debugrevlogopts isn't available,
96 102 # because commands.debugrevlogopts has been available since 3.7 (or
97 103 # 5606f7d0d063), even though cmdutil.openrevlog() has been available
98 104 # since 1.9 (or a79fea6b3e77).
99 105 revlogopts = getattr(cmdutil, "debugrevlogopts",
100 106 getattr(commands, "debugrevlogopts", [
101 107 ('c', 'changelog', False, ('open changelog')),
102 108 ('m', 'manifest', False, ('open manifest')),
103 109 ('', 'dir', False, ('open directory manifest')),
104 110 ]))
105 111
106 112 cmdtable = {}
107 113
108 114 # for "historical portability":
109 115 # define parsealiases locally, because cmdutil.parsealiases has been
110 116 # available since 1.5 (or 6252852b4332)
111 117 def parsealiases(cmd):
112 118 return cmd.lstrip("^").split("|")
113 119
114 120 if safehasattr(registrar, 'command'):
115 121 command = registrar.command(cmdtable)
116 122 elif safehasattr(cmdutil, 'command'):
117 import inspect
118 123 command = cmdutil.command(cmdtable)
119 if 'norepo' not in inspect.getargspec(command)[0]:
124 if 'norepo' not in getargspec(command).args:
120 125 # for "historical portability":
121 126 # wrap original cmdutil.command, because "norepo" option has
122 127 # been available since 3.1 (or 75a96326cecb)
123 128 _command = command
124 129 def command(name, options=(), synopsis=None, norepo=False):
125 130 if norepo:
126 131 commands.norepo += ' %s' % ' '.join(parsealiases(name))
127 132 return _command(name, list(options), synopsis)
128 133 else:
129 134 # for "historical portability":
130 135 # define "@command" annotation locally, because cmdutil.command
131 136 # has been available since 1.9 (or 2daa5179e73f)
132 137 def command(name, options=(), synopsis=None, norepo=False):
133 138 def decorator(func):
134 139 if synopsis:
135 140 cmdtable[name] = func, list(options), synopsis
136 141 else:
137 142 cmdtable[name] = func, list(options)
138 143 if norepo:
139 144 commands.norepo += ' %s' % ' '.join(parsealiases(name))
140 145 return func
141 146 return decorator
142 147
143 148 try:
144 149 import mercurial.registrar
145 150 import mercurial.configitems
146 151 configtable = {}
147 152 configitem = mercurial.registrar.configitem(configtable)
148 153 configitem('perf', 'presleep',
149 154 default=mercurial.configitems.dynamicdefault,
150 155 )
151 156 configitem('perf', 'stub',
152 157 default=mercurial.configitems.dynamicdefault,
153 158 )
154 159 configitem('perf', 'parentscount',
155 160 default=mercurial.configitems.dynamicdefault,
156 161 )
157 162 except (ImportError, AttributeError):
158 163 pass
159 164
160 165 def getlen(ui):
161 166 if ui.configbool("perf", "stub", False):
162 167 return lambda x: 1
163 168 return len
164 169
165 170 def gettimer(ui, opts=None):
166 171 """return a timer function and formatter: (timer, formatter)
167 172
168 173 This function exists to gather the creation of formatter in a single
169 174 place instead of duplicating it in all performance commands."""
170 175
171 176 # enforce an idle period before execution to counteract power management
172 177 # experimental config: perf.presleep
173 178 time.sleep(getint(ui, "perf", "presleep", 1))
174 179
175 180 if opts is None:
176 181 opts = {}
177 182 # redirect all to stderr unless buffer api is in use
178 183 if not ui._buffers:
179 184 ui = ui.copy()
180 185 uifout = safeattrsetter(ui, 'fout', ignoremissing=True)
181 186 if uifout:
182 187 # for "historical portability":
183 188 # ui.fout/ferr have been available since 1.9 (or 4e1ccd4c2b6d)
184 189 uifout.set(ui.ferr)
185 190
186 191 # get a formatter
187 192 uiformatter = getattr(ui, 'formatter', None)
188 193 if uiformatter:
189 194 fm = uiformatter('perf', opts)
190 195 else:
191 196 # for "historical portability":
192 197 # define formatter locally, because ui.formatter has been
193 198 # available since 2.2 (or ae5f92e154d3)
194 199 from mercurial import node
195 200 class defaultformatter(object):
196 201 """Minimized composition of baseformatter and plainformatter
197 202 """
198 203 def __init__(self, ui, topic, opts):
199 204 self._ui = ui
200 205 if ui.debugflag:
201 206 self.hexfunc = node.hex
202 207 else:
203 208 self.hexfunc = node.short
204 209 def __nonzero__(self):
205 210 return False
206 211 __bool__ = __nonzero__
207 212 def startitem(self):
208 213 pass
209 214 def data(self, **data):
210 215 pass
211 216 def write(self, fields, deftext, *fielddata, **opts):
212 217 self._ui.write(deftext % fielddata, **opts)
213 218 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
214 219 if cond:
215 220 self._ui.write(deftext % fielddata, **opts)
216 221 def plain(self, text, **opts):
217 222 self._ui.write(text, **opts)
218 223 def end(self):
219 224 pass
220 225 fm = defaultformatter(ui, 'perf', opts)
221 226
222 227 # stub function, runs code only once instead of in a loop
223 228 # experimental config: perf.stub
224 229 if ui.configbool("perf", "stub", False):
225 230 return functools.partial(stub_timer, fm), fm
226 231 return functools.partial(_timer, fm), fm
227 232
228 233 def stub_timer(fm, func, title=None):
229 234 func()
230 235
231 236 def _timer(fm, func, title=None):
232 237 gc.collect()
233 238 results = []
234 239 begin = util.timer()
235 240 count = 0
236 241 while True:
237 242 ostart = os.times()
238 243 cstart = util.timer()
239 244 r = func()
240 245 cstop = util.timer()
241 246 ostop = os.times()
242 247 count += 1
243 248 a, b = ostart, ostop
244 249 results.append((cstop - cstart, b[0] - a[0], b[1]-a[1]))
245 250 if cstop - begin > 3 and count >= 100:
246 251 break
247 252 if cstop - begin > 10 and count >= 3:
248 253 break
249 254
250 255 fm.startitem()
251 256
252 257 if title:
253 258 fm.write('title', '! %s\n', title)
254 259 if r:
255 260 fm.write('result', '! result: %s\n', r)
256 261 m = min(results)
257 262 fm.plain('!')
258 263 fm.write('wall', ' wall %f', m[0])
259 264 fm.write('comb', ' comb %f', m[1] + m[2])
260 265 fm.write('user', ' user %f', m[1])
261 266 fm.write('sys', ' sys %f', m[2])
262 267 fm.write('count', ' (best of %d)', count)
263 268 fm.plain('\n')
264 269
265 270 # utilities for historical portability
266 271
267 272 def getint(ui, section, name, default):
268 273 # for "historical portability":
269 274 # ui.configint has been available since 1.9 (or fa2b596db182)
270 275 v = ui.config(section, name, None)
271 276 if v is None:
272 277 return default
273 278 try:
274 279 return int(v)
275 280 except ValueError:
276 281 raise error.ConfigError(("%s.%s is not an integer ('%s')")
277 282 % (section, name, v))
278 283
279 284 def safeattrsetter(obj, name, ignoremissing=False):
280 285 """Ensure that 'obj' has 'name' attribute before subsequent setattr
281 286
282 287 This function is aborted, if 'obj' doesn't have 'name' attribute
283 288 at runtime. This avoids overlooking removal of an attribute, which
284 289 breaks assumption of performance measurement, in the future.
285 290
286 291 This function returns the object to (1) assign a new value, and
287 292 (2) restore an original value to the attribute.
288 293
289 294 If 'ignoremissing' is true, missing 'name' attribute doesn't cause
290 295 abortion, and this function returns None. This is useful to
291 296 examine an attribute, which isn't ensured in all Mercurial
292 297 versions.
293 298 """
294 299 if not util.safehasattr(obj, name):
295 300 if ignoremissing:
296 301 return None
297 302 raise error.Abort(("missing attribute %s of %s might break assumption"
298 303 " of performance measurement") % (name, obj))
299 304
300 305 origvalue = getattr(obj, name)
301 306 class attrutil(object):
302 307 def set(self, newvalue):
303 308 setattr(obj, name, newvalue)
304 309 def restore(self):
305 310 setattr(obj, name, origvalue)
306 311
307 312 return attrutil()
308 313
309 314 # utilities to examine each internal API changes
310 315
311 316 def getbranchmapsubsettable():
312 317 # for "historical portability":
313 318 # subsettable is defined in:
314 319 # - branchmap since 2.9 (or 175c6fd8cacc)
315 320 # - repoview since 2.5 (or 59a9f18d4587)
316 321 for mod in (branchmap, repoview):
317 322 subsettable = getattr(mod, 'subsettable', None)
318 323 if subsettable:
319 324 return subsettable
320 325
321 326 # bisecting in bcee63733aad::59a9f18d4587 can reach here (both
322 327 # branchmap and repoview modules exist, but subsettable attribute
323 328 # doesn't)
324 329 raise error.Abort(("perfbranchmap not available with this Mercurial"),
325 330 hint="use 2.5 or later")
326 331
327 332 def getsvfs(repo):
328 333 """Return appropriate object to access files under .hg/store
329 334 """
330 335 # for "historical portability":
331 336 # repo.svfs has been available since 2.3 (or 7034365089bf)
332 337 svfs = getattr(repo, 'svfs', None)
333 338 if svfs:
334 339 return svfs
335 340 else:
336 341 return getattr(repo, 'sopener')
337 342
338 343 def getvfs(repo):
339 344 """Return appropriate object to access files under .hg
340 345 """
341 346 # for "historical portability":
342 347 # repo.vfs has been available since 2.3 (or 7034365089bf)
343 348 vfs = getattr(repo, 'vfs', None)
344 349 if vfs:
345 350 return vfs
346 351 else:
347 352 return getattr(repo, 'opener')
348 353
349 354 def repocleartagscachefunc(repo):
350 355 """Return the function to clear tags cache according to repo internal API
351 356 """
352 357 if util.safehasattr(repo, '_tagscache'): # since 2.0 (or 9dca7653b525)
353 358 # in this case, setattr(repo, '_tagscache', None) or so isn't
354 359 # correct way to clear tags cache, because existing code paths
355 360 # expect _tagscache to be a structured object.
356 361 def clearcache():
357 362 # _tagscache has been filteredpropertycache since 2.5 (or
358 363 # 98c867ac1330), and delattr() can't work in such case
359 364 if '_tagscache' in vars(repo):
360 365 del repo.__dict__['_tagscache']
361 366 return clearcache
362 367
363 368 repotags = safeattrsetter(repo, '_tags', ignoremissing=True)
364 369 if repotags: # since 1.4 (or 5614a628d173)
365 370 return lambda : repotags.set(None)
366 371
367 372 repotagscache = safeattrsetter(repo, 'tagscache', ignoremissing=True)
368 373 if repotagscache: # since 0.6 (or d7df759d0e97)
369 374 return lambda : repotagscache.set(None)
370 375
371 376 # Mercurial earlier than 0.6 (or d7df759d0e97) logically reaches
372 377 # this point, but it isn't so problematic, because:
373 378 # - repo.tags of such Mercurial isn't "callable", and repo.tags()
374 379 # in perftags() causes failure soon
375 380 # - perf.py itself has been available since 1.1 (or eb240755386d)
376 381 raise error.Abort(("tags API of this hg command is unknown"))
377 382
378 383 # utilities to clear cache
379 384
380 385 def clearfilecache(repo, attrname):
381 386 unfi = repo.unfiltered()
382 387 if attrname in vars(unfi):
383 388 delattr(unfi, attrname)
384 389 unfi._filecache.pop(attrname, None)
385 390
386 391 # perf commands
387 392
388 393 @command('perfwalk', formatteropts)
389 394 def perfwalk(ui, repo, *pats, **opts):
390 395 timer, fm = gettimer(ui, opts)
391 396 m = scmutil.match(repo[None], pats, {})
392 397 timer(lambda: len(list(repo.dirstate.walk(m, subrepos=[], unknown=True,
393 398 ignored=False))))
394 399 fm.end()
395 400
396 401 @command('perfannotate', formatteropts)
397 402 def perfannotate(ui, repo, f, **opts):
398 403 timer, fm = gettimer(ui, opts)
399 404 fc = repo['.'][f]
400 405 timer(lambda: len(fc.annotate(True)))
401 406 fm.end()
402 407
403 408 @command('perfstatus',
404 409 [('u', 'unknown', False,
405 410 'ask status to look for unknown files')] + formatteropts)
406 411 def perfstatus(ui, repo, **opts):
407 412 #m = match.always(repo.root, repo.getcwd())
408 413 #timer(lambda: sum(map(len, repo.dirstate.status(m, [], False, False,
409 414 # False))))
410 415 timer, fm = gettimer(ui, opts)
411 416 timer(lambda: sum(map(len, repo.status(unknown=opts['unknown']))))
412 417 fm.end()
413 418
414 419 @command('perfaddremove', formatteropts)
415 420 def perfaddremove(ui, repo, **opts):
416 421 timer, fm = gettimer(ui, opts)
417 422 try:
418 423 oldquiet = repo.ui.quiet
419 424 repo.ui.quiet = True
420 425 matcher = scmutil.match(repo[None])
421 426 timer(lambda: scmutil.addremove(repo, matcher, "", dry_run=True))
422 427 finally:
423 428 repo.ui.quiet = oldquiet
424 429 fm.end()
425 430
426 431 def clearcaches(cl):
427 432 # behave somewhat consistently across internal API changes
428 433 if util.safehasattr(cl, 'clearcaches'):
429 434 cl.clearcaches()
430 435 elif util.safehasattr(cl, '_nodecache'):
431 436 from mercurial.node import nullid, nullrev
432 437 cl._nodecache = {nullid: nullrev}
433 438 cl._nodepos = None
434 439
435 440 @command('perfheads', formatteropts)
436 441 def perfheads(ui, repo, **opts):
437 442 timer, fm = gettimer(ui, opts)
438 443 cl = repo.changelog
439 444 def d():
440 445 len(cl.headrevs())
441 446 clearcaches(cl)
442 447 timer(d)
443 448 fm.end()
444 449
445 450 @command('perftags', formatteropts)
446 451 def perftags(ui, repo, **opts):
447 452 import mercurial.changelog
448 453 import mercurial.manifest
449 454 timer, fm = gettimer(ui, opts)
450 455 svfs = getsvfs(repo)
451 456 repocleartagscache = repocleartagscachefunc(repo)
452 457 def t():
453 458 repo.changelog = mercurial.changelog.changelog(svfs)
454 459 repo.manifestlog = mercurial.manifest.manifestlog(svfs, repo)
455 460 repocleartagscache()
456 461 return len(repo.tags())
457 462 timer(t)
458 463 fm.end()
459 464
460 465 @command('perfancestors', formatteropts)
461 466 def perfancestors(ui, repo, **opts):
462 467 timer, fm = gettimer(ui, opts)
463 468 heads = repo.changelog.headrevs()
464 469 def d():
465 470 for a in repo.changelog.ancestors(heads):
466 471 pass
467 472 timer(d)
468 473 fm.end()
469 474
470 475 @command('perfancestorset', formatteropts)
471 476 def perfancestorset(ui, repo, revset, **opts):
472 477 timer, fm = gettimer(ui, opts)
473 478 revs = repo.revs(revset)
474 479 heads = repo.changelog.headrevs()
475 480 def d():
476 481 s = repo.changelog.ancestors(heads)
477 482 for rev in revs:
478 483 rev in s
479 484 timer(d)
480 485 fm.end()
481 486
482 487 @command('perfbookmarks', formatteropts)
483 488 def perfbookmarks(ui, repo, **opts):
484 489 """benchmark parsing bookmarks from disk to memory"""
485 490 timer, fm = gettimer(ui, opts)
486 491 def d():
487 492 clearfilecache(repo, '_bookmarks')
488 493 repo._bookmarks
489 494 timer(d)
490 495 fm.end()
491 496
492 497 @command('perfbundleread', formatteropts, 'BUNDLE')
493 498 def perfbundleread(ui, repo, bundlepath, **opts):
494 499 """Benchmark reading of bundle files.
495 500
496 501 This command is meant to isolate the I/O part of bundle reading as
497 502 much as possible.
498 503 """
499 504 from mercurial import (
500 505 bundle2,
501 506 exchange,
502 507 streamclone,
503 508 )
504 509
505 510 def makebench(fn):
506 511 def run():
507 512 with open(bundlepath, 'rb') as fh:
508 513 bundle = exchange.readbundle(ui, fh, bundlepath)
509 514 fn(bundle)
510 515
511 516 return run
512 517
513 518 def makereadnbytes(size):
514 519 def run():
515 520 with open(bundlepath, 'rb') as fh:
516 521 bundle = exchange.readbundle(ui, fh, bundlepath)
517 522 while bundle.read(size):
518 523 pass
519 524
520 525 return run
521 526
522 527 def makestdioread(size):
523 528 def run():
524 529 with open(bundlepath, 'rb') as fh:
525 530 while fh.read(size):
526 531 pass
527 532
528 533 return run
529 534
530 535 # bundle1
531 536
532 537 def deltaiter(bundle):
533 538 for delta in bundle.deltaiter():
534 539 pass
535 540
536 541 def iterchunks(bundle):
537 542 for chunk in bundle.getchunks():
538 543 pass
539 544
540 545 # bundle2
541 546
542 547 def forwardchunks(bundle):
543 548 for chunk in bundle._forwardchunks():
544 549 pass
545 550
546 551 def iterparts(bundle):
547 552 for part in bundle.iterparts():
548 553 pass
549 554
550 555 def iterpartsseekable(bundle):
551 556 for part in bundle.iterparts(seekable=True):
552 557 pass
553 558
554 559 def seek(bundle):
555 560 for part in bundle.iterparts(seekable=True):
556 561 part.seek(0, os.SEEK_END)
557 562
558 563 def makepartreadnbytes(size):
559 564 def run():
560 565 with open(bundlepath, 'rb') as fh:
561 566 bundle = exchange.readbundle(ui, fh, bundlepath)
562 567 for part in bundle.iterparts():
563 568 while part.read(size):
564 569 pass
565 570
566 571 return run
567 572
568 573 benches = [
569 574 (makestdioread(8192), 'read(8k)'),
570 575 (makestdioread(16384), 'read(16k)'),
571 576 (makestdioread(32768), 'read(32k)'),
572 577 (makestdioread(131072), 'read(128k)'),
573 578 ]
574 579
575 580 with open(bundlepath, 'rb') as fh:
576 581 bundle = exchange.readbundle(ui, fh, bundlepath)
577 582
578 583 if isinstance(bundle, changegroup.cg1unpacker):
579 584 benches.extend([
580 585 (makebench(deltaiter), 'cg1 deltaiter()'),
581 586 (makebench(iterchunks), 'cg1 getchunks()'),
582 587 (makereadnbytes(8192), 'cg1 read(8k)'),
583 588 (makereadnbytes(16384), 'cg1 read(16k)'),
584 589 (makereadnbytes(32768), 'cg1 read(32k)'),
585 590 (makereadnbytes(131072), 'cg1 read(128k)'),
586 591 ])
587 592 elif isinstance(bundle, bundle2.unbundle20):
588 593 benches.extend([
589 594 (makebench(forwardchunks), 'bundle2 forwardchunks()'),
590 595 (makebench(iterparts), 'bundle2 iterparts()'),
591 596 (makebench(iterpartsseekable), 'bundle2 iterparts() seekable'),
592 597 (makebench(seek), 'bundle2 part seek()'),
593 598 (makepartreadnbytes(8192), 'bundle2 part read(8k)'),
594 599 (makepartreadnbytes(16384), 'bundle2 part read(16k)'),
595 600 (makepartreadnbytes(32768), 'bundle2 part read(32k)'),
596 601 (makepartreadnbytes(131072), 'bundle2 part read(128k)'),
597 602 ])
598 603 elif isinstance(bundle, streamclone.streamcloneapplier):
599 604 raise error.Abort('stream clone bundles not supported')
600 605 else:
601 606 raise error.Abort('unhandled bundle type: %s' % type(bundle))
602 607
603 608 for fn, title in benches:
604 609 timer, fm = gettimer(ui, opts)
605 610 timer(fn, title=title)
606 611 fm.end()
607 612
608 613 @command('perfchangegroupchangelog', formatteropts +
609 614 [('', 'version', '02', 'changegroup version'),
610 615 ('r', 'rev', '', 'revisions to add to changegroup')])
611 616 def perfchangegroupchangelog(ui, repo, version='02', rev=None, **opts):
612 617 """Benchmark producing a changelog group for a changegroup.
613 618
614 619 This measures the time spent processing the changelog during a
615 620 bundle operation. This occurs during `hg bundle` and on a server
616 621 processing a `getbundle` wire protocol request (handles clones
617 622 and pull requests).
618 623
619 624 By default, all revisions are added to the changegroup.
620 625 """
621 626 cl = repo.changelog
622 627 revs = [cl.lookup(r) for r in repo.revs(rev or 'all()')]
623 628 bundler = changegroup.getbundler(version, repo)
624 629
625 630 def lookup(node):
626 631 # The real bundler reads the revision in order to access the
627 632 # manifest node and files list. Do that here.
628 633 cl.read(node)
629 634 return node
630 635
631 636 def d():
632 637 for chunk in bundler.group(revs, cl, lookup):
633 638 pass
634 639
635 640 timer, fm = gettimer(ui, opts)
636 641 timer(d)
637 642 fm.end()
638 643
639 644 @command('perfdirs', formatteropts)
640 645 def perfdirs(ui, repo, **opts):
641 646 timer, fm = gettimer(ui, opts)
642 647 dirstate = repo.dirstate
643 648 'a' in dirstate
644 649 def d():
645 650 dirstate.hasdir('a')
646 651 del dirstate._map._dirs
647 652 timer(d)
648 653 fm.end()
649 654
650 655 @command('perfdirstate', formatteropts)
651 656 def perfdirstate(ui, repo, **opts):
652 657 timer, fm = gettimer(ui, opts)
653 658 "a" in repo.dirstate
654 659 def d():
655 660 repo.dirstate.invalidate()
656 661 "a" in repo.dirstate
657 662 timer(d)
658 663 fm.end()
659 664
660 665 @command('perfdirstatedirs', formatteropts)
661 666 def perfdirstatedirs(ui, repo, **opts):
662 667 timer, fm = gettimer(ui, opts)
663 668 "a" in repo.dirstate
664 669 def d():
665 670 repo.dirstate.hasdir("a")
666 671 del repo.dirstate._map._dirs
667 672 timer(d)
668 673 fm.end()
669 674
670 675 @command('perfdirstatefoldmap', formatteropts)
671 676 def perfdirstatefoldmap(ui, repo, **opts):
672 677 timer, fm = gettimer(ui, opts)
673 678 dirstate = repo.dirstate
674 679 'a' in dirstate
675 680 def d():
676 681 dirstate._map.filefoldmap.get('a')
677 682 del dirstate._map.filefoldmap
678 683 timer(d)
679 684 fm.end()
680 685
681 686 @command('perfdirfoldmap', formatteropts)
682 687 def perfdirfoldmap(ui, repo, **opts):
683 688 timer, fm = gettimer(ui, opts)
684 689 dirstate = repo.dirstate
685 690 'a' in dirstate
686 691 def d():
687 692 dirstate._map.dirfoldmap.get('a')
688 693 del dirstate._map.dirfoldmap
689 694 del dirstate._map._dirs
690 695 timer(d)
691 696 fm.end()
692 697
693 698 @command('perfdirstatewrite', formatteropts)
694 699 def perfdirstatewrite(ui, repo, **opts):
695 700 timer, fm = gettimer(ui, opts)
696 701 ds = repo.dirstate
697 702 "a" in ds
698 703 def d():
699 704 ds._dirty = True
700 705 ds.write(repo.currenttransaction())
701 706 timer(d)
702 707 fm.end()
703 708
704 709 @command('perfmergecalculate',
705 710 [('r', 'rev', '.', 'rev to merge against')] + formatteropts)
706 711 def perfmergecalculate(ui, repo, rev, **opts):
707 712 timer, fm = gettimer(ui, opts)
708 713 wctx = repo[None]
709 714 rctx = scmutil.revsingle(repo, rev, rev)
710 715 ancestor = wctx.ancestor(rctx)
711 716 # we don't want working dir files to be stat'd in the benchmark, so prime
712 717 # that cache
713 718 wctx.dirty()
714 719 def d():
715 720 # acceptremote is True because we don't want prompts in the middle of
716 721 # our benchmark
717 722 merge.calculateupdates(repo, wctx, rctx, [ancestor], False, False,
718 723 acceptremote=True, followcopies=True)
719 724 timer(d)
720 725 fm.end()
721 726
722 727 @command('perfpathcopies', [], "REV REV")
723 728 def perfpathcopies(ui, repo, rev1, rev2, **opts):
724 729 timer, fm = gettimer(ui, opts)
725 730 ctx1 = scmutil.revsingle(repo, rev1, rev1)
726 731 ctx2 = scmutil.revsingle(repo, rev2, rev2)
727 732 def d():
728 733 copies.pathcopies(ctx1, ctx2)
729 734 timer(d)
730 735 fm.end()
731 736
732 737 @command('perfphases',
733 738 [('', 'full', False, 'include file reading time too'),
734 739 ], "")
735 740 def perfphases(ui, repo, **opts):
736 741 """benchmark phasesets computation"""
737 742 timer, fm = gettimer(ui, opts)
738 743 _phases = repo._phasecache
739 744 full = opts.get('full')
740 745 def d():
741 746 phases = _phases
742 747 if full:
743 748 clearfilecache(repo, '_phasecache')
744 749 phases = repo._phasecache
745 750 phases.invalidate()
746 751 phases.loadphaserevs(repo)
747 752 timer(d)
748 753 fm.end()
749 754
750 755 @command('perfmanifest', [], 'REV')
751 756 def perfmanifest(ui, repo, rev, **opts):
752 757 timer, fm = gettimer(ui, opts)
753 758 ctx = scmutil.revsingle(repo, rev, rev)
754 759 t = ctx.manifestnode()
755 760 def d():
756 761 repo.manifestlog.clearcaches()
757 762 repo.manifestlog[t].read()
758 763 timer(d)
759 764 fm.end()
760 765
761 766 @command('perfchangeset', formatteropts)
762 767 def perfchangeset(ui, repo, rev, **opts):
763 768 timer, fm = gettimer(ui, opts)
764 769 n = repo[rev].node()
765 770 def d():
766 771 repo.changelog.read(n)
767 772 #repo.changelog._cache = None
768 773 timer(d)
769 774 fm.end()
770 775
771 776 @command('perfindex', formatteropts)
772 777 def perfindex(ui, repo, **opts):
773 778 import mercurial.revlog
774 779 timer, fm = gettimer(ui, opts)
775 780 mercurial.revlog._prereadsize = 2**24 # disable lazy parser in old hg
776 781 n = repo["tip"].node()
777 782 svfs = getsvfs(repo)
778 783 def d():
779 784 cl = mercurial.revlog.revlog(svfs, "00changelog.i")
780 785 cl.rev(n)
781 786 timer(d)
782 787 fm.end()
783 788
784 789 @command('perfstartup', formatteropts)
785 790 def perfstartup(ui, repo, **opts):
786 791 timer, fm = gettimer(ui, opts)
787 792 cmd = sys.argv[0]
788 793 def d():
789 794 if os.name != 'nt':
790 795 os.system("HGRCPATH= %s version -q > /dev/null" % cmd)
791 796 else:
792 797 os.environ['HGRCPATH'] = ' '
793 798 os.system("%s version -q > NUL" % cmd)
794 799 timer(d)
795 800 fm.end()
796 801
797 802 @command('perfparents', formatteropts)
798 803 def perfparents(ui, repo, **opts):
799 804 timer, fm = gettimer(ui, opts)
800 805 # control the number of commits perfparents iterates over
801 806 # experimental config: perf.parentscount
802 807 count = getint(ui, "perf", "parentscount", 1000)
803 808 if len(repo.changelog) < count:
804 809 raise error.Abort("repo needs %d commits for this test" % count)
805 810 repo = repo.unfiltered()
806 811 nl = [repo.changelog.node(i) for i in xrange(count)]
807 812 def d():
808 813 for n in nl:
809 814 repo.changelog.parents(n)
810 815 timer(d)
811 816 fm.end()
812 817
813 818 @command('perfctxfiles', formatteropts)
814 819 def perfctxfiles(ui, repo, x, **opts):
815 820 x = int(x)
816 821 timer, fm = gettimer(ui, opts)
817 822 def d():
818 823 len(repo[x].files())
819 824 timer(d)
820 825 fm.end()
821 826
822 827 @command('perfrawfiles', formatteropts)
823 828 def perfrawfiles(ui, repo, x, **opts):
824 829 x = int(x)
825 830 timer, fm = gettimer(ui, opts)
826 831 cl = repo.changelog
827 832 def d():
828 833 len(cl.read(x)[3])
829 834 timer(d)
830 835 fm.end()
831 836
832 837 @command('perflookup', formatteropts)
833 838 def perflookup(ui, repo, rev, **opts):
834 839 timer, fm = gettimer(ui, opts)
835 840 timer(lambda: len(repo.lookup(rev)))
836 841 fm.end()
837 842
838 843 @command('perfrevrange', formatteropts)
839 844 def perfrevrange(ui, repo, *specs, **opts):
840 845 timer, fm = gettimer(ui, opts)
841 846 revrange = scmutil.revrange
842 847 timer(lambda: len(revrange(repo, specs)))
843 848 fm.end()
844 849
845 850 @command('perfnodelookup', formatteropts)
846 851 def perfnodelookup(ui, repo, rev, **opts):
847 852 timer, fm = gettimer(ui, opts)
848 853 import mercurial.revlog
849 854 mercurial.revlog._prereadsize = 2**24 # disable lazy parser in old hg
850 855 n = repo[rev].node()
851 856 cl = mercurial.revlog.revlog(getsvfs(repo), "00changelog.i")
852 857 def d():
853 858 cl.rev(n)
854 859 clearcaches(cl)
855 860 timer(d)
856 861 fm.end()
857 862
858 863 @command('perflog',
859 864 [('', 'rename', False, 'ask log to follow renames')] + formatteropts)
860 865 def perflog(ui, repo, rev=None, **opts):
861 866 if rev is None:
862 867 rev=[]
863 868 timer, fm = gettimer(ui, opts)
864 869 ui.pushbuffer()
865 870 timer(lambda: commands.log(ui, repo, rev=rev, date='', user='',
866 871 copies=opts.get('rename')))
867 872 ui.popbuffer()
868 873 fm.end()
869 874
870 875 @command('perfmoonwalk', formatteropts)
871 876 def perfmoonwalk(ui, repo, **opts):
872 877 """benchmark walking the changelog backwards
873 878
874 879 This also loads the changelog data for each revision in the changelog.
875 880 """
876 881 timer, fm = gettimer(ui, opts)
877 882 def moonwalk():
878 883 for i in xrange(len(repo), -1, -1):
879 884 ctx = repo[i]
880 885 ctx.branch() # read changelog data (in addition to the index)
881 886 timer(moonwalk)
882 887 fm.end()
883 888
884 889 @command('perftemplating', formatteropts)
885 890 def perftemplating(ui, repo, rev=None, **opts):
886 891 if rev is None:
887 892 rev=[]
888 893 timer, fm = gettimer(ui, opts)
889 894 ui.pushbuffer()
890 895 timer(lambda: commands.log(ui, repo, rev=rev, date='', user='',
891 896 template='{date|shortdate} [{rev}:{node|short}]'
892 897 ' {author|person}: {desc|firstline}\n'))
893 898 ui.popbuffer()
894 899 fm.end()
895 900
896 901 @command('perfcca', formatteropts)
897 902 def perfcca(ui, repo, **opts):
898 903 timer, fm = gettimer(ui, opts)
899 904 timer(lambda: scmutil.casecollisionauditor(ui, False, repo.dirstate))
900 905 fm.end()
901 906
902 907 @command('perffncacheload', formatteropts)
903 908 def perffncacheload(ui, repo, **opts):
904 909 timer, fm = gettimer(ui, opts)
905 910 s = repo.store
906 911 def d():
907 912 s.fncache._load()
908 913 timer(d)
909 914 fm.end()
910 915
911 916 @command('perffncachewrite', formatteropts)
912 917 def perffncachewrite(ui, repo, **opts):
913 918 timer, fm = gettimer(ui, opts)
914 919 s = repo.store
915 920 s.fncache._load()
916 921 lock = repo.lock()
917 922 tr = repo.transaction('perffncachewrite')
918 923 def d():
919 924 s.fncache._dirty = True
920 925 s.fncache.write(tr)
921 926 timer(d)
922 927 tr.close()
923 928 lock.release()
924 929 fm.end()
925 930
926 931 @command('perffncacheencode', formatteropts)
927 932 def perffncacheencode(ui, repo, **opts):
928 933 timer, fm = gettimer(ui, opts)
929 934 s = repo.store
930 935 s.fncache._load()
931 936 def d():
932 937 for p in s.fncache.entries:
933 938 s.encode(p)
934 939 timer(d)
935 940 fm.end()
936 941
937 942 def _bdiffworker(q, ready, done):
938 943 while not done.is_set():
939 944 pair = q.get()
940 945 while pair is not None:
941 946 mdiff.textdiff(*pair)
942 947 q.task_done()
943 948 pair = q.get()
944 949 q.task_done() # for the None one
945 950 with ready:
946 951 ready.wait()
947 952
948 953 @command('perfbdiff', revlogopts + formatteropts + [
949 954 ('', 'count', 1, 'number of revisions to test (when using --startrev)'),
950 955 ('', 'alldata', False, 'test bdiffs for all associated revisions'),
951 956 ('', 'threads', 0, 'number of thread to use (disable with 0)'),
952 957 ],
953 958
954 959 '-c|-m|FILE REV')
955 960 def perfbdiff(ui, repo, file_, rev=None, count=None, threads=0, **opts):
956 961 """benchmark a bdiff between revisions
957 962
958 963 By default, benchmark a bdiff between its delta parent and itself.
959 964
960 965 With ``--count``, benchmark bdiffs between delta parents and self for N
961 966 revisions starting at the specified revision.
962 967
963 968 With ``--alldata``, assume the requested revision is a changeset and
964 969 measure bdiffs for all changes related to that changeset (manifest
965 970 and filelogs).
966 971 """
967 972 if opts['alldata']:
968 973 opts['changelog'] = True
969 974
970 975 if opts.get('changelog') or opts.get('manifest'):
971 976 file_, rev = None, file_
972 977 elif rev is None:
973 978 raise error.CommandError('perfbdiff', 'invalid arguments')
974 979
975 980 textpairs = []
976 981
977 982 r = cmdutil.openrevlog(repo, 'perfbdiff', file_, opts)
978 983
979 984 startrev = r.rev(r.lookup(rev))
980 985 for rev in range(startrev, min(startrev + count, len(r) - 1)):
981 986 if opts['alldata']:
982 987 # Load revisions associated with changeset.
983 988 ctx = repo[rev]
984 989 mtext = repo.manifestlog._revlog.revision(ctx.manifestnode())
985 990 for pctx in ctx.parents():
986 991 pman = repo.manifestlog._revlog.revision(pctx.manifestnode())
987 992 textpairs.append((pman, mtext))
988 993
989 994 # Load filelog revisions by iterating manifest delta.
990 995 man = ctx.manifest()
991 996 pman = ctx.p1().manifest()
992 997 for filename, change in pman.diff(man).items():
993 998 fctx = repo.file(filename)
994 999 f1 = fctx.revision(change[0][0] or -1)
995 1000 f2 = fctx.revision(change[1][0] or -1)
996 1001 textpairs.append((f1, f2))
997 1002 else:
998 1003 dp = r.deltaparent(rev)
999 1004 textpairs.append((r.revision(dp), r.revision(rev)))
1000 1005
1001 1006 withthreads = threads > 0
1002 1007 if not withthreads:
1003 1008 def d():
1004 1009 for pair in textpairs:
1005 1010 mdiff.textdiff(*pair)
1006 1011 else:
1007 1012 q = util.queue()
1008 1013 for i in xrange(threads):
1009 1014 q.put(None)
1010 1015 ready = threading.Condition()
1011 1016 done = threading.Event()
1012 1017 for i in xrange(threads):
1013 1018 threading.Thread(target=_bdiffworker, args=(q, ready, done)).start()
1014 1019 q.join()
1015 1020 def d():
1016 1021 for pair in textpairs:
1017 1022 q.put(pair)
1018 1023 for i in xrange(threads):
1019 1024 q.put(None)
1020 1025 with ready:
1021 1026 ready.notify_all()
1022 1027 q.join()
1023 1028 timer, fm = gettimer(ui, opts)
1024 1029 timer(d)
1025 1030 fm.end()
1026 1031
1027 1032 if withthreads:
1028 1033 done.set()
1029 1034 for i in xrange(threads):
1030 1035 q.put(None)
1031 1036 with ready:
1032 1037 ready.notify_all()
1033 1038
1034 1039 @command('perfunidiff', revlogopts + formatteropts + [
1035 1040 ('', 'count', 1, 'number of revisions to test (when using --startrev)'),
1036 1041 ('', 'alldata', False, 'test unidiffs for all associated revisions'),
1037 1042 ], '-c|-m|FILE REV')
1038 1043 def perfunidiff(ui, repo, file_, rev=None, count=None, **opts):
1039 1044 """benchmark a unified diff between revisions
1040 1045
1041 1046 This doesn't include any copy tracing - it's just a unified diff
1042 1047 of the texts.
1043 1048
1044 1049 By default, benchmark a diff between its delta parent and itself.
1045 1050
1046 1051 With ``--count``, benchmark diffs between delta parents and self for N
1047 1052 revisions starting at the specified revision.
1048 1053
1049 1054 With ``--alldata``, assume the requested revision is a changeset and
1050 1055 measure diffs for all changes related to that changeset (manifest
1051 1056 and filelogs).
1052 1057 """
1053 1058 if opts['alldata']:
1054 1059 opts['changelog'] = True
1055 1060
1056 1061 if opts.get('changelog') or opts.get('manifest'):
1057 1062 file_, rev = None, file_
1058 1063 elif rev is None:
1059 1064 raise error.CommandError('perfunidiff', 'invalid arguments')
1060 1065
1061 1066 textpairs = []
1062 1067
1063 1068 r = cmdutil.openrevlog(repo, 'perfunidiff', file_, opts)
1064 1069
1065 1070 startrev = r.rev(r.lookup(rev))
1066 1071 for rev in range(startrev, min(startrev + count, len(r) - 1)):
1067 1072 if opts['alldata']:
1068 1073 # Load revisions associated with changeset.
1069 1074 ctx = repo[rev]
1070 1075 mtext = repo.manifestlog._revlog.revision(ctx.manifestnode())
1071 1076 for pctx in ctx.parents():
1072 1077 pman = repo.manifestlog._revlog.revision(pctx.manifestnode())
1073 1078 textpairs.append((pman, mtext))
1074 1079
1075 1080 # Load filelog revisions by iterating manifest delta.
1076 1081 man = ctx.manifest()
1077 1082 pman = ctx.p1().manifest()
1078 1083 for filename, change in pman.diff(man).items():
1079 1084 fctx = repo.file(filename)
1080 1085 f1 = fctx.revision(change[0][0] or -1)
1081 1086 f2 = fctx.revision(change[1][0] or -1)
1082 1087 textpairs.append((f1, f2))
1083 1088 else:
1084 1089 dp = r.deltaparent(rev)
1085 1090 textpairs.append((r.revision(dp), r.revision(rev)))
1086 1091
1087 1092 def d():
1088 1093 for left, right in textpairs:
1089 1094 # The date strings don't matter, so we pass empty strings.
1090 1095 headerlines, hunks = mdiff.unidiff(
1091 1096 left, '', right, '', 'left', 'right', binary=False)
1092 1097 # consume iterators in roughly the way patch.py does
1093 1098 b'\n'.join(headerlines)
1094 1099 b''.join(sum((list(hlines) for hrange, hlines in hunks), []))
1095 1100 timer, fm = gettimer(ui, opts)
1096 1101 timer(d)
1097 1102 fm.end()
1098 1103
1099 1104 @command('perfdiffwd', formatteropts)
1100 1105 def perfdiffwd(ui, repo, **opts):
1101 1106 """Profile diff of working directory changes"""
1102 1107 timer, fm = gettimer(ui, opts)
1103 1108 options = {
1104 1109 'w': 'ignore_all_space',
1105 1110 'b': 'ignore_space_change',
1106 1111 'B': 'ignore_blank_lines',
1107 1112 }
1108 1113
1109 1114 for diffopt in ('', 'w', 'b', 'B', 'wB'):
1110 1115 opts = dict((options[c], '1') for c in diffopt)
1111 1116 def d():
1112 1117 ui.pushbuffer()
1113 1118 commands.diff(ui, repo, **opts)
1114 1119 ui.popbuffer()
1115 1120 title = 'diffopts: %s' % (diffopt and ('-' + diffopt) or 'none')
1116 1121 timer(d, title)
1117 1122 fm.end()
1118 1123
1119 1124 @command('perfrevlogindex', revlogopts + formatteropts,
1120 1125 '-c|-m|FILE')
1121 1126 def perfrevlogindex(ui, repo, file_=None, **opts):
1122 1127 """Benchmark operations against a revlog index.
1123 1128
1124 1129 This tests constructing a revlog instance, reading index data,
1125 1130 parsing index data, and performing various operations related to
1126 1131 index data.
1127 1132 """
1128 1133
1129 1134 rl = cmdutil.openrevlog(repo, 'perfrevlogindex', file_, opts)
1130 1135
1131 1136 opener = getattr(rl, 'opener') # trick linter
1132 1137 indexfile = rl.indexfile
1133 1138 data = opener.read(indexfile)
1134 1139
1135 1140 header = struct.unpack('>I', data[0:4])[0]
1136 1141 version = header & 0xFFFF
1137 1142 if version == 1:
1138 1143 revlogio = revlog.revlogio()
1139 1144 inline = header & (1 << 16)
1140 1145 else:
1141 1146 raise error.Abort(('unsupported revlog version: %d') % version)
1142 1147
1143 1148 rllen = len(rl)
1144 1149
1145 1150 node0 = rl.node(0)
1146 1151 node25 = rl.node(rllen // 4)
1147 1152 node50 = rl.node(rllen // 2)
1148 1153 node75 = rl.node(rllen // 4 * 3)
1149 1154 node100 = rl.node(rllen - 1)
1150 1155
1151 1156 allrevs = range(rllen)
1152 1157 allrevsrev = list(reversed(allrevs))
1153 1158 allnodes = [rl.node(rev) for rev in range(rllen)]
1154 1159 allnodesrev = list(reversed(allnodes))
1155 1160
1156 1161 def constructor():
1157 1162 revlog.revlog(opener, indexfile)
1158 1163
1159 1164 def read():
1160 1165 with opener(indexfile) as fh:
1161 1166 fh.read()
1162 1167
1163 1168 def parseindex():
1164 1169 revlogio.parseindex(data, inline)
1165 1170
1166 1171 def getentry(revornode):
1167 1172 index = revlogio.parseindex(data, inline)[0]
1168 1173 index[revornode]
1169 1174
1170 1175 def getentries(revs, count=1):
1171 1176 index = revlogio.parseindex(data, inline)[0]
1172 1177
1173 1178 for i in range(count):
1174 1179 for rev in revs:
1175 1180 index[rev]
1176 1181
1177 1182 def resolvenode(node):
1178 1183 nodemap = revlogio.parseindex(data, inline)[1]
1179 1184 # This only works for the C code.
1180 1185 if nodemap is None:
1181 1186 return
1182 1187
1183 1188 try:
1184 1189 nodemap[node]
1185 1190 except error.RevlogError:
1186 1191 pass
1187 1192
1188 1193 def resolvenodes(nodes, count=1):
1189 1194 nodemap = revlogio.parseindex(data, inline)[1]
1190 1195 if nodemap is None:
1191 1196 return
1192 1197
1193 1198 for i in range(count):
1194 1199 for node in nodes:
1195 1200 try:
1196 1201 nodemap[node]
1197 1202 except error.RevlogError:
1198 1203 pass
1199 1204
1200 1205 benches = [
1201 1206 (constructor, 'revlog constructor'),
1202 1207 (read, 'read'),
1203 1208 (parseindex, 'create index object'),
1204 1209 (lambda: getentry(0), 'retrieve index entry for rev 0'),
1205 1210 (lambda: resolvenode('a' * 20), 'look up missing node'),
1206 1211 (lambda: resolvenode(node0), 'look up node at rev 0'),
1207 1212 (lambda: resolvenode(node25), 'look up node at 1/4 len'),
1208 1213 (lambda: resolvenode(node50), 'look up node at 1/2 len'),
1209 1214 (lambda: resolvenode(node75), 'look up node at 3/4 len'),
1210 1215 (lambda: resolvenode(node100), 'look up node at tip'),
1211 1216 # 2x variation is to measure caching impact.
1212 1217 (lambda: resolvenodes(allnodes),
1213 1218 'look up all nodes (forward)'),
1214 1219 (lambda: resolvenodes(allnodes, 2),
1215 1220 'look up all nodes 2x (forward)'),
1216 1221 (lambda: resolvenodes(allnodesrev),
1217 1222 'look up all nodes (reverse)'),
1218 1223 (lambda: resolvenodes(allnodesrev, 2),
1219 1224 'look up all nodes 2x (reverse)'),
1220 1225 (lambda: getentries(allrevs),
1221 1226 'retrieve all index entries (forward)'),
1222 1227 (lambda: getentries(allrevs, 2),
1223 1228 'retrieve all index entries 2x (forward)'),
1224 1229 (lambda: getentries(allrevsrev),
1225 1230 'retrieve all index entries (reverse)'),
1226 1231 (lambda: getentries(allrevsrev, 2),
1227 1232 'retrieve all index entries 2x (reverse)'),
1228 1233 ]
1229 1234
1230 1235 for fn, title in benches:
1231 1236 timer, fm = gettimer(ui, opts)
1232 1237 timer(fn, title=title)
1233 1238 fm.end()
1234 1239
1235 1240 @command('perfrevlogrevisions', revlogopts + formatteropts +
1236 1241 [('d', 'dist', 100, 'distance between the revisions'),
1237 1242 ('s', 'startrev', 0, 'revision to start reading at'),
1238 1243 ('', 'reverse', False, 'read in reverse')],
1239 1244 '-c|-m|FILE')
1240 1245 def perfrevlogrevisions(ui, repo, file_=None, startrev=0, reverse=False,
1241 1246 **opts):
1242 1247 """Benchmark reading a series of revisions from a revlog.
1243 1248
1244 1249 By default, we read every ``-d/--dist`` revision from 0 to tip of
1245 1250 the specified revlog.
1246 1251
1247 1252 The start revision can be defined via ``-s/--startrev``.
1248 1253 """
1249 1254 rl = cmdutil.openrevlog(repo, 'perfrevlogrevisions', file_, opts)
1250 1255 rllen = getlen(ui)(rl)
1251 1256
1252 1257 def d():
1253 1258 rl.clearcaches()
1254 1259
1255 1260 beginrev = startrev
1256 1261 endrev = rllen
1257 1262 dist = opts['dist']
1258 1263
1259 1264 if reverse:
1260 1265 beginrev, endrev = endrev, beginrev
1261 1266 dist = -1 * dist
1262 1267
1263 1268 for x in xrange(beginrev, endrev, dist):
1264 1269 # Old revisions don't support passing int.
1265 1270 n = rl.node(x)
1266 1271 rl.revision(n)
1267 1272
1268 1273 timer, fm = gettimer(ui, opts)
1269 1274 timer(d)
1270 1275 fm.end()
1271 1276
1272 1277 @command('perfrevlogchunks', revlogopts + formatteropts +
1273 1278 [('e', 'engines', '', 'compression engines to use'),
1274 1279 ('s', 'startrev', 0, 'revision to start at')],
1275 1280 '-c|-m|FILE')
1276 1281 def perfrevlogchunks(ui, repo, file_=None, engines=None, startrev=0, **opts):
1277 1282 """Benchmark operations on revlog chunks.
1278 1283
1279 1284 Logically, each revlog is a collection of fulltext revisions. However,
1280 1285 stored within each revlog are "chunks" of possibly compressed data. This
1281 1286 data needs to be read and decompressed or compressed and written.
1282 1287
1283 1288 This command measures the time it takes to read+decompress and recompress
1284 1289 chunks in a revlog. It effectively isolates I/O and compression performance.
1285 1290 For measurements of higher-level operations like resolving revisions,
1286 1291 see ``perfrevlogrevisions`` and ``perfrevlogrevision``.
1287 1292 """
1288 1293 rl = cmdutil.openrevlog(repo, 'perfrevlogchunks', file_, opts)
1289 1294
1290 1295 # _chunkraw was renamed to _getsegmentforrevs.
1291 1296 try:
1292 1297 segmentforrevs = rl._getsegmentforrevs
1293 1298 except AttributeError:
1294 1299 segmentforrevs = rl._chunkraw
1295 1300
1296 1301 # Verify engines argument.
1297 1302 if engines:
1298 1303 engines = set(e.strip() for e in engines.split(','))
1299 1304 for engine in engines:
1300 1305 try:
1301 1306 util.compressionengines[engine]
1302 1307 except KeyError:
1303 1308 raise error.Abort('unknown compression engine: %s' % engine)
1304 1309 else:
1305 1310 engines = []
1306 1311 for e in util.compengines:
1307 1312 engine = util.compengines[e]
1308 1313 try:
1309 1314 if engine.available():
1310 1315 engine.revlogcompressor().compress('dummy')
1311 1316 engines.append(e)
1312 1317 except NotImplementedError:
1313 1318 pass
1314 1319
1315 1320 revs = list(rl.revs(startrev, len(rl) - 1))
1316 1321
1317 1322 def rlfh(rl):
1318 1323 if rl._inline:
1319 1324 return getsvfs(repo)(rl.indexfile)
1320 1325 else:
1321 1326 return getsvfs(repo)(rl.datafile)
1322 1327
1323 1328 def doread():
1324 1329 rl.clearcaches()
1325 1330 for rev in revs:
1326 1331 segmentforrevs(rev, rev)
1327 1332
1328 1333 def doreadcachedfh():
1329 1334 rl.clearcaches()
1330 1335 fh = rlfh(rl)
1331 1336 for rev in revs:
1332 1337 segmentforrevs(rev, rev, df=fh)
1333 1338
1334 1339 def doreadbatch():
1335 1340 rl.clearcaches()
1336 1341 segmentforrevs(revs[0], revs[-1])
1337 1342
1338 1343 def doreadbatchcachedfh():
1339 1344 rl.clearcaches()
1340 1345 fh = rlfh(rl)
1341 1346 segmentforrevs(revs[0], revs[-1], df=fh)
1342 1347
1343 1348 def dochunk():
1344 1349 rl.clearcaches()
1345 1350 fh = rlfh(rl)
1346 1351 for rev in revs:
1347 1352 rl._chunk(rev, df=fh)
1348 1353
1349 1354 chunks = [None]
1350 1355
1351 1356 def dochunkbatch():
1352 1357 rl.clearcaches()
1353 1358 fh = rlfh(rl)
1354 1359 # Save chunks as a side-effect.
1355 1360 chunks[0] = rl._chunks(revs, df=fh)
1356 1361
1357 1362 def docompress(compressor):
1358 1363 rl.clearcaches()
1359 1364
1360 1365 try:
1361 1366 # Swap in the requested compression engine.
1362 1367 oldcompressor = rl._compressor
1363 1368 rl._compressor = compressor
1364 1369 for chunk in chunks[0]:
1365 1370 rl.compress(chunk)
1366 1371 finally:
1367 1372 rl._compressor = oldcompressor
1368 1373
1369 1374 benches = [
1370 1375 (lambda: doread(), 'read'),
1371 1376 (lambda: doreadcachedfh(), 'read w/ reused fd'),
1372 1377 (lambda: doreadbatch(), 'read batch'),
1373 1378 (lambda: doreadbatchcachedfh(), 'read batch w/ reused fd'),
1374 1379 (lambda: dochunk(), 'chunk'),
1375 1380 (lambda: dochunkbatch(), 'chunk batch'),
1376 1381 ]
1377 1382
1378 1383 for engine in sorted(engines):
1379 1384 compressor = util.compengines[engine].revlogcompressor()
1380 1385 benches.append((functools.partial(docompress, compressor),
1381 1386 'compress w/ %s' % engine))
1382 1387
1383 1388 for fn, title in benches:
1384 1389 timer, fm = gettimer(ui, opts)
1385 1390 timer(fn, title=title)
1386 1391 fm.end()
1387 1392
1388 1393 @command('perfrevlogrevision', revlogopts + formatteropts +
1389 1394 [('', 'cache', False, 'use caches instead of clearing')],
1390 1395 '-c|-m|FILE REV')
1391 1396 def perfrevlogrevision(ui, repo, file_, rev=None, cache=None, **opts):
1392 1397 """Benchmark obtaining a revlog revision.
1393 1398
1394 1399 Obtaining a revlog revision consists of roughly the following steps:
1395 1400
1396 1401 1. Compute the delta chain
1397 1402 2. Obtain the raw chunks for that delta chain
1398 1403 3. Decompress each raw chunk
1399 1404 4. Apply binary patches to obtain fulltext
1400 1405 5. Verify hash of fulltext
1401 1406
1402 1407 This command measures the time spent in each of these phases.
1403 1408 """
1404 1409 if opts.get('changelog') or opts.get('manifest'):
1405 1410 file_, rev = None, file_
1406 1411 elif rev is None:
1407 1412 raise error.CommandError('perfrevlogrevision', 'invalid arguments')
1408 1413
1409 1414 r = cmdutil.openrevlog(repo, 'perfrevlogrevision', file_, opts)
1410 1415
1411 1416 # _chunkraw was renamed to _getsegmentforrevs.
1412 1417 try:
1413 1418 segmentforrevs = r._getsegmentforrevs
1414 1419 except AttributeError:
1415 1420 segmentforrevs = r._chunkraw
1416 1421
1417 1422 node = r.lookup(rev)
1418 1423 rev = r.rev(node)
1419 1424
1420 1425 def getrawchunks(data, chain):
1421 1426 start = r.start
1422 1427 length = r.length
1423 1428 inline = r._inline
1424 1429 iosize = r._io.size
1425 1430 buffer = util.buffer
1426 1431 offset = start(chain[0])
1427 1432
1428 1433 chunks = []
1429 1434 ladd = chunks.append
1430 1435
1431 1436 for rev in chain:
1432 1437 chunkstart = start(rev)
1433 1438 if inline:
1434 1439 chunkstart += (rev + 1) * iosize
1435 1440 chunklength = length(rev)
1436 1441 ladd(buffer(data, chunkstart - offset, chunklength))
1437 1442
1438 1443 return chunks
1439 1444
1440 1445 def dodeltachain(rev):
1441 1446 if not cache:
1442 1447 r.clearcaches()
1443 1448 r._deltachain(rev)
1444 1449
1445 1450 def doread(chain):
1446 1451 if not cache:
1447 1452 r.clearcaches()
1448 1453 segmentforrevs(chain[0], chain[-1])
1449 1454
1450 1455 def dorawchunks(data, chain):
1451 1456 if not cache:
1452 1457 r.clearcaches()
1453 1458 getrawchunks(data, chain)
1454 1459
1455 1460 def dodecompress(chunks):
1456 1461 decomp = r.decompress
1457 1462 for chunk in chunks:
1458 1463 decomp(chunk)
1459 1464
1460 1465 def dopatch(text, bins):
1461 1466 if not cache:
1462 1467 r.clearcaches()
1463 1468 mdiff.patches(text, bins)
1464 1469
1465 1470 def dohash(text):
1466 1471 if not cache:
1467 1472 r.clearcaches()
1468 1473 r.checkhash(text, node, rev=rev)
1469 1474
1470 1475 def dorevision():
1471 1476 if not cache:
1472 1477 r.clearcaches()
1473 1478 r.revision(node)
1474 1479
1475 1480 chain = r._deltachain(rev)[0]
1476 1481 data = segmentforrevs(chain[0], chain[-1])[1]
1477 1482 rawchunks = getrawchunks(data, chain)
1478 1483 bins = r._chunks(chain)
1479 1484 text = str(bins[0])
1480 1485 bins = bins[1:]
1481 1486 text = mdiff.patches(text, bins)
1482 1487
1483 1488 benches = [
1484 1489 (lambda: dorevision(), 'full'),
1485 1490 (lambda: dodeltachain(rev), 'deltachain'),
1486 1491 (lambda: doread(chain), 'read'),
1487 1492 (lambda: dorawchunks(data, chain), 'rawchunks'),
1488 1493 (lambda: dodecompress(rawchunks), 'decompress'),
1489 1494 (lambda: dopatch(text, bins), 'patch'),
1490 1495 (lambda: dohash(text), 'hash'),
1491 1496 ]
1492 1497
1493 1498 for fn, title in benches:
1494 1499 timer, fm = gettimer(ui, opts)
1495 1500 timer(fn, title=title)
1496 1501 fm.end()
1497 1502
1498 1503 @command('perfrevset',
1499 1504 [('C', 'clear', False, 'clear volatile cache between each call.'),
1500 1505 ('', 'contexts', False, 'obtain changectx for each revision')]
1501 1506 + formatteropts, "REVSET")
1502 1507 def perfrevset(ui, repo, expr, clear=False, contexts=False, **opts):
1503 1508 """benchmark the execution time of a revset
1504 1509
1505 1510 Use the --clean option if need to evaluate the impact of build volatile
1506 1511 revisions set cache on the revset execution. Volatile cache hold filtered
1507 1512 and obsolete related cache."""
1508 1513 timer, fm = gettimer(ui, opts)
1509 1514 def d():
1510 1515 if clear:
1511 1516 repo.invalidatevolatilesets()
1512 1517 if contexts:
1513 1518 for ctx in repo.set(expr): pass
1514 1519 else:
1515 1520 for r in repo.revs(expr): pass
1516 1521 timer(d)
1517 1522 fm.end()
1518 1523
1519 1524 @command('perfvolatilesets',
1520 1525 [('', 'clear-obsstore', False, 'drop obsstore between each call.'),
1521 1526 ] + formatteropts)
1522 1527 def perfvolatilesets(ui, repo, *names, **opts):
1523 1528 """benchmark the computation of various volatile set
1524 1529
1525 1530 Volatile set computes element related to filtering and obsolescence."""
1526 1531 timer, fm = gettimer(ui, opts)
1527 1532 repo = repo.unfiltered()
1528 1533
1529 1534 def getobs(name):
1530 1535 def d():
1531 1536 repo.invalidatevolatilesets()
1532 1537 if opts['clear_obsstore']:
1533 1538 clearfilecache(repo, 'obsstore')
1534 1539 obsolete.getrevs(repo, name)
1535 1540 return d
1536 1541
1537 1542 allobs = sorted(obsolete.cachefuncs)
1538 1543 if names:
1539 1544 allobs = [n for n in allobs if n in names]
1540 1545
1541 1546 for name in allobs:
1542 1547 timer(getobs(name), title=name)
1543 1548
1544 1549 def getfiltered(name):
1545 1550 def d():
1546 1551 repo.invalidatevolatilesets()
1547 1552 if opts['clear_obsstore']:
1548 1553 clearfilecache(repo, 'obsstore')
1549 1554 repoview.filterrevs(repo, name)
1550 1555 return d
1551 1556
1552 1557 allfilter = sorted(repoview.filtertable)
1553 1558 if names:
1554 1559 allfilter = [n for n in allfilter if n in names]
1555 1560
1556 1561 for name in allfilter:
1557 1562 timer(getfiltered(name), title=name)
1558 1563 fm.end()
1559 1564
1560 1565 @command('perfbranchmap',
1561 1566 [('f', 'full', False,
1562 1567 'Includes build time of subset'),
1563 1568 ('', 'clear-revbranch', False,
1564 1569 'purge the revbranch cache between computation'),
1565 1570 ] + formatteropts)
1566 1571 def perfbranchmap(ui, repo, full=False, clear_revbranch=False, **opts):
1567 1572 """benchmark the update of a branchmap
1568 1573
1569 1574 This benchmarks the full repo.branchmap() call with read and write disabled
1570 1575 """
1571 1576 timer, fm = gettimer(ui, opts)
1572 1577 def getbranchmap(filtername):
1573 1578 """generate a benchmark function for the filtername"""
1574 1579 if filtername is None:
1575 1580 view = repo
1576 1581 else:
1577 1582 view = repo.filtered(filtername)
1578 1583 def d():
1579 1584 if clear_revbranch:
1580 1585 repo.revbranchcache()._clear()
1581 1586 if full:
1582 1587 view._branchcaches.clear()
1583 1588 else:
1584 1589 view._branchcaches.pop(filtername, None)
1585 1590 view.branchmap()
1586 1591 return d
1587 1592 # add filter in smaller subset to bigger subset
1588 1593 possiblefilters = set(repoview.filtertable)
1589 1594 subsettable = getbranchmapsubsettable()
1590 1595 allfilters = []
1591 1596 while possiblefilters:
1592 1597 for name in possiblefilters:
1593 1598 subset = subsettable.get(name)
1594 1599 if subset not in possiblefilters:
1595 1600 break
1596 1601 else:
1597 1602 assert False, 'subset cycle %s!' % possiblefilters
1598 1603 allfilters.append(name)
1599 1604 possiblefilters.remove(name)
1600 1605
1601 1606 # warm the cache
1602 1607 if not full:
1603 1608 for name in allfilters:
1604 1609 repo.filtered(name).branchmap()
1605 1610 # add unfiltered
1606 1611 allfilters.append(None)
1607 1612
1608 1613 branchcacheread = safeattrsetter(branchmap, 'read')
1609 1614 branchcachewrite = safeattrsetter(branchmap.branchcache, 'write')
1610 1615 branchcacheread.set(lambda repo: None)
1611 1616 branchcachewrite.set(lambda bc, repo: None)
1612 1617 try:
1613 1618 for name in allfilters:
1614 1619 timer(getbranchmap(name), title=str(name))
1615 1620 finally:
1616 1621 branchcacheread.restore()
1617 1622 branchcachewrite.restore()
1618 1623 fm.end()
1619 1624
1620 1625 @command('perfloadmarkers')
1621 1626 def perfloadmarkers(ui, repo):
1622 1627 """benchmark the time to parse the on-disk markers for a repo
1623 1628
1624 1629 Result is the number of markers in the repo."""
1625 1630 timer, fm = gettimer(ui)
1626 1631 svfs = getsvfs(repo)
1627 1632 timer(lambda: len(obsolete.obsstore(svfs)))
1628 1633 fm.end()
1629 1634
1630 1635 @command('perflrucachedict', formatteropts +
1631 1636 [('', 'size', 4, 'size of cache'),
1632 1637 ('', 'gets', 10000, 'number of key lookups'),
1633 1638 ('', 'sets', 10000, 'number of key sets'),
1634 1639 ('', 'mixed', 10000, 'number of mixed mode operations'),
1635 1640 ('', 'mixedgetfreq', 50, 'frequency of get vs set ops in mixed mode')],
1636 1641 norepo=True)
1637 1642 def perflrucache(ui, size=4, gets=10000, sets=10000, mixed=10000,
1638 1643 mixedgetfreq=50, **opts):
1639 1644 def doinit():
1640 1645 for i in xrange(10000):
1641 1646 util.lrucachedict(size)
1642 1647
1643 1648 values = []
1644 1649 for i in xrange(size):
1645 1650 values.append(random.randint(0, sys.maxint))
1646 1651
1647 1652 # Get mode fills the cache and tests raw lookup performance with no
1648 1653 # eviction.
1649 1654 getseq = []
1650 1655 for i in xrange(gets):
1651 1656 getseq.append(random.choice(values))
1652 1657
1653 1658 def dogets():
1654 1659 d = util.lrucachedict(size)
1655 1660 for v in values:
1656 1661 d[v] = v
1657 1662 for key in getseq:
1658 1663 value = d[key]
1659 1664 value # silence pyflakes warning
1660 1665
1661 1666 # Set mode tests insertion speed with cache eviction.
1662 1667 setseq = []
1663 1668 for i in xrange(sets):
1664 1669 setseq.append(random.randint(0, sys.maxint))
1665 1670
1666 1671 def dosets():
1667 1672 d = util.lrucachedict(size)
1668 1673 for v in setseq:
1669 1674 d[v] = v
1670 1675
1671 1676 # Mixed mode randomly performs gets and sets with eviction.
1672 1677 mixedops = []
1673 1678 for i in xrange(mixed):
1674 1679 r = random.randint(0, 100)
1675 1680 if r < mixedgetfreq:
1676 1681 op = 0
1677 1682 else:
1678 1683 op = 1
1679 1684
1680 1685 mixedops.append((op, random.randint(0, size * 2)))
1681 1686
1682 1687 def domixed():
1683 1688 d = util.lrucachedict(size)
1684 1689
1685 1690 for op, v in mixedops:
1686 1691 if op == 0:
1687 1692 try:
1688 1693 d[v]
1689 1694 except KeyError:
1690 1695 pass
1691 1696 else:
1692 1697 d[v] = v
1693 1698
1694 1699 benches = [
1695 1700 (doinit, 'init'),
1696 1701 (dogets, 'gets'),
1697 1702 (dosets, 'sets'),
1698 1703 (domixed, 'mixed')
1699 1704 ]
1700 1705
1701 1706 for fn, title in benches:
1702 1707 timer, fm = gettimer(ui, opts)
1703 1708 timer(fn, title=title)
1704 1709 fm.end()
1705 1710
1706 1711 @command('perfwrite', formatteropts)
1707 1712 def perfwrite(ui, repo, **opts):
1708 1713 """microbenchmark ui.write
1709 1714 """
1710 1715 timer, fm = gettimer(ui, opts)
1711 1716 def write():
1712 1717 for i in range(100000):
1713 1718 ui.write(('Testing write performance\n'))
1714 1719 timer(write)
1715 1720 fm.end()
1716 1721
1717 1722 def uisetup(ui):
1718 1723 if (util.safehasattr(cmdutil, 'openrevlog') and
1719 1724 not util.safehasattr(commands, 'debugrevlogopts')):
1720 1725 # for "historical portability":
1721 1726 # In this case, Mercurial should be 1.9 (or a79fea6b3e77) -
1722 1727 # 3.7 (or 5606f7d0d063). Therefore, '--dir' option for
1723 1728 # openrevlog() should cause failure, because it has been
1724 1729 # available since 3.5 (or 49c583ca48c4).
1725 1730 def openrevlog(orig, repo, cmd, file_, opts):
1726 1731 if opts.get('dir') and not util.safehasattr(repo, 'dirlog'):
1727 1732 raise error.Abort("This version doesn't support --dir option",
1728 1733 hint="use 3.5 or later")
1729 1734 return orig(repo, cmd, file_, opts)
1730 1735 extensions.wrapfunction(cmdutil, 'openrevlog', openrevlog)
@@ -1,722 +1,718
1 1 # extensions.py - extension handling for mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import functools
11 11 import imp
12 12 import inspect
13 13 import os
14 14
15 15 from .i18n import (
16 16 _,
17 17 gettext,
18 18 )
19 19
20 20 from . import (
21 21 cmdutil,
22 22 configitems,
23 23 error,
24 24 pycompat,
25 25 util,
26 26 )
27 27
28 28 _extensions = {}
29 29 _disabledextensions = {}
30 30 _aftercallbacks = {}
31 31 _order = []
32 32 _builtin = {
33 33 'hbisect',
34 34 'bookmarks',
35 35 'color',
36 36 'parentrevspec',
37 37 'progress',
38 38 'interhg',
39 39 'inotify',
40 40 'hgcia'
41 41 }
42 42
43 43 def extensions(ui=None):
44 44 if ui:
45 45 def enabled(name):
46 46 for format in ['%s', 'hgext.%s']:
47 47 conf = ui.config('extensions', format % name)
48 48 if conf is not None and not conf.startswith('!'):
49 49 return True
50 50 else:
51 51 enabled = lambda name: True
52 52 for name in _order:
53 53 module = _extensions[name]
54 54 if module and enabled(name):
55 55 yield name, module
56 56
57 57 def find(name):
58 58 '''return module with given extension name'''
59 59 mod = None
60 60 try:
61 61 mod = _extensions[name]
62 62 except KeyError:
63 63 for k, v in _extensions.iteritems():
64 64 if k.endswith('.' + name) or k.endswith('/' + name):
65 65 mod = v
66 66 break
67 67 if not mod:
68 68 raise KeyError(name)
69 69 return mod
70 70
71 71 def loadpath(path, module_name):
72 72 module_name = module_name.replace('.', '_')
73 73 path = util.normpath(util.expandpath(path))
74 74 module_name = pycompat.fsdecode(module_name)
75 75 path = pycompat.fsdecode(path)
76 76 if os.path.isdir(path):
77 77 # module/__init__.py style
78 78 d, f = os.path.split(path)
79 79 fd, fpath, desc = imp.find_module(f, [d])
80 80 return imp.load_module(module_name, fd, fpath, desc)
81 81 else:
82 82 try:
83 83 return imp.load_source(module_name, path)
84 84 except IOError as exc:
85 85 if not exc.filename:
86 86 exc.filename = path # python does not fill this
87 87 raise
88 88
89 89 def _importh(name):
90 90 """import and return the <name> module"""
91 91 mod = __import__(pycompat.sysstr(name))
92 92 components = name.split('.')
93 93 for comp in components[1:]:
94 94 mod = getattr(mod, comp)
95 95 return mod
96 96
97 97 def _importext(name, path=None, reportfunc=None):
98 98 if path:
99 99 # the module will be loaded in sys.modules
100 100 # choose an unique name so that it doesn't
101 101 # conflicts with other modules
102 102 mod = loadpath(path, 'hgext.%s' % name)
103 103 else:
104 104 try:
105 105 mod = _importh("hgext.%s" % name)
106 106 except ImportError as err:
107 107 if reportfunc:
108 108 reportfunc(err, "hgext.%s" % name, "hgext3rd.%s" % name)
109 109 try:
110 110 mod = _importh("hgext3rd.%s" % name)
111 111 except ImportError as err:
112 112 if reportfunc:
113 113 reportfunc(err, "hgext3rd.%s" % name, name)
114 114 mod = _importh(name)
115 115 return mod
116 116
117 117 def _reportimporterror(ui, err, failed, next):
118 118 # note: this ui.debug happens before --debug is processed,
119 119 # Use --config ui.debug=1 to see them.
120 120 ui.debug('could not import %s (%s): trying %s\n'
121 121 % (failed, util.forcebytestr(err), next))
122 122 if ui.debugflag:
123 123 ui.traceback()
124 124
125 125 # attributes set by registrar.command
126 126 _cmdfuncattrs = ('norepo', 'optionalrepo', 'inferrepo')
127 127
128 128 def _validatecmdtable(ui, cmdtable):
129 129 """Check if extension commands have required attributes"""
130 130 for c, e in cmdtable.iteritems():
131 131 f = e[0]
132 132 if getattr(f, '_deprecatedregistrar', False):
133 133 ui.deprecwarn("cmdutil.command is deprecated, use "
134 134 "registrar.command to register '%s'" % c, '4.6')
135 135 missing = [a for a in _cmdfuncattrs if not util.safehasattr(f, a)]
136 136 if not missing:
137 137 for option in e[1]:
138 138 default = option[2]
139 139 if isinstance(default, type(u'')):
140 140 raise error.ProgrammingError(
141 141 "option '%s.%s' has a unicode default value"
142 142 % (c, option[1]),
143 143 hint=("change the %s.%s default value to a "
144 144 "non-unicode string" % (c, option[1])))
145 145 continue
146 146 raise error.ProgrammingError(
147 147 'missing attributes: %s' % ', '.join(missing),
148 148 hint="use @command decorator to register '%s'" % c)
149 149
150 150 def load(ui, name, path):
151 151 if name.startswith('hgext.') or name.startswith('hgext/'):
152 152 shortname = name[6:]
153 153 else:
154 154 shortname = name
155 155 if shortname in _builtin:
156 156 return None
157 157 if shortname in _extensions:
158 158 return _extensions[shortname]
159 159 _extensions[shortname] = None
160 160 mod = _importext(name, path, bind(_reportimporterror, ui))
161 161
162 162 # Before we do anything with the extension, check against minimum stated
163 163 # compatibility. This gives extension authors a mechanism to have their
164 164 # extensions short circuit when loaded with a known incompatible version
165 165 # of Mercurial.
166 166 minver = getattr(mod, 'minimumhgversion', None)
167 167 if minver and util.versiontuple(minver, 2) > util.versiontuple(n=2):
168 168 ui.warn(_('(third party extension %s requires version %s or newer '
169 169 'of Mercurial; disabling)\n') % (shortname, minver))
170 170 return
171 171 _validatecmdtable(ui, getattr(mod, 'cmdtable', {}))
172 172
173 173 _extensions[shortname] = mod
174 174 _order.append(shortname)
175 175 for fn in _aftercallbacks.get(shortname, []):
176 176 fn(loaded=True)
177 177 return mod
178 178
179 179 def _runuisetup(name, ui):
180 180 uisetup = getattr(_extensions[name], 'uisetup', None)
181 181 if uisetup:
182 182 try:
183 183 uisetup(ui)
184 184 except Exception as inst:
185 185 ui.traceback(force=True)
186 186 msg = util.forcebytestr(inst)
187 187 ui.warn(_("*** failed to set up extension %s: %s\n") % (name, msg))
188 188 return False
189 189 return True
190 190
191 191 def _runextsetup(name, ui):
192 192 extsetup = getattr(_extensions[name], 'extsetup', None)
193 193 if extsetup:
194 194 try:
195 195 try:
196 196 extsetup(ui)
197 197 except TypeError:
198 # Try to use getfullargspec (Python 3) first, and fall
199 # back to getargspec only if it doesn't exist so as to
200 # avoid warnings.
201 if getattr(inspect, 'getfullargspec',
202 getattr(inspect, 'getargspec'))(extsetup).args:
198 if pycompat.getargspec(extsetup).args:
203 199 raise
204 200 extsetup() # old extsetup with no ui argument
205 201 except Exception as inst:
206 202 ui.traceback(force=True)
207 203 msg = util.forcebytestr(inst)
208 204 ui.warn(_("*** failed to set up extension %s: %s\n") % (name, msg))
209 205 return False
210 206 return True
211 207
212 208 def loadall(ui, whitelist=None):
213 209 result = ui.configitems("extensions")
214 210 if whitelist is not None:
215 211 result = [(k, v) for (k, v) in result if k in whitelist]
216 212 newindex = len(_order)
217 213 for (name, path) in result:
218 214 if path:
219 215 if path[0:1] == '!':
220 216 _disabledextensions[name] = path[1:]
221 217 continue
222 218 try:
223 219 load(ui, name, path)
224 220 except Exception as inst:
225 221 msg = util.forcebytestr(inst)
226 222 if path:
227 223 ui.warn(_("*** failed to import extension %s from %s: %s\n")
228 224 % (name, path, msg))
229 225 else:
230 226 ui.warn(_("*** failed to import extension %s: %s\n")
231 227 % (name, msg))
232 228 if isinstance(inst, error.Hint) and inst.hint:
233 229 ui.warn(_("*** (%s)\n") % inst.hint)
234 230 ui.traceback()
235 231 # list of (objname, loadermod, loadername) tuple:
236 232 # - objname is the name of an object in extension module,
237 233 # from which extra information is loaded
238 234 # - loadermod is the module where loader is placed
239 235 # - loadername is the name of the function,
240 236 # which takes (ui, extensionname, extraobj) arguments
241 237 #
242 238 # This one is for the list of item that must be run before running any setup
243 239 earlyextraloaders = [
244 240 ('configtable', configitems, 'loadconfigtable'),
245 241 ]
246 242 _loadextra(ui, newindex, earlyextraloaders)
247 243
248 244 broken = set()
249 245 for name in _order[newindex:]:
250 246 if not _runuisetup(name, ui):
251 247 broken.add(name)
252 248
253 249 for name in _order[newindex:]:
254 250 if name in broken:
255 251 continue
256 252 if not _runextsetup(name, ui):
257 253 broken.add(name)
258 254
259 255 for name in broken:
260 256 _extensions[name] = None
261 257
262 258 # Call aftercallbacks that were never met.
263 259 for shortname in _aftercallbacks:
264 260 if shortname in _extensions:
265 261 continue
266 262
267 263 for fn in _aftercallbacks[shortname]:
268 264 fn(loaded=False)
269 265
270 266 # loadall() is called multiple times and lingering _aftercallbacks
271 267 # entries could result in double execution. See issue4646.
272 268 _aftercallbacks.clear()
273 269
274 270 # delay importing avoids cyclic dependency (especially commands)
275 271 from . import (
276 272 color,
277 273 commands,
278 274 filemerge,
279 275 fileset,
280 276 revset,
281 277 templatefilters,
282 278 templatekw,
283 279 templater,
284 280 )
285 281
286 282 # list of (objname, loadermod, loadername) tuple:
287 283 # - objname is the name of an object in extension module,
288 284 # from which extra information is loaded
289 285 # - loadermod is the module where loader is placed
290 286 # - loadername is the name of the function,
291 287 # which takes (ui, extensionname, extraobj) arguments
292 288 extraloaders = [
293 289 ('cmdtable', commands, 'loadcmdtable'),
294 290 ('colortable', color, 'loadcolortable'),
295 291 ('filesetpredicate', fileset, 'loadpredicate'),
296 292 ('internalmerge', filemerge, 'loadinternalmerge'),
297 293 ('revsetpredicate', revset, 'loadpredicate'),
298 294 ('templatefilter', templatefilters, 'loadfilter'),
299 295 ('templatefunc', templater, 'loadfunction'),
300 296 ('templatekeyword', templatekw, 'loadkeyword'),
301 297 ]
302 298 _loadextra(ui, newindex, extraloaders)
303 299
304 300 def _loadextra(ui, newindex, extraloaders):
305 301 for name in _order[newindex:]:
306 302 module = _extensions[name]
307 303 if not module:
308 304 continue # loading this module failed
309 305
310 306 for objname, loadermod, loadername in extraloaders:
311 307 extraobj = getattr(module, objname, None)
312 308 if extraobj is not None:
313 309 getattr(loadermod, loadername)(ui, name, extraobj)
314 310
315 311 def afterloaded(extension, callback):
316 312 '''Run the specified function after a named extension is loaded.
317 313
318 314 If the named extension is already loaded, the callback will be called
319 315 immediately.
320 316
321 317 If the named extension never loads, the callback will be called after
322 318 all extensions have been loaded.
323 319
324 320 The callback receives the named argument ``loaded``, which is a boolean
325 321 indicating whether the dependent extension actually loaded.
326 322 '''
327 323
328 324 if extension in _extensions:
329 325 # Report loaded as False if the extension is disabled
330 326 loaded = (_extensions[extension] is not None)
331 327 callback(loaded=loaded)
332 328 else:
333 329 _aftercallbacks.setdefault(extension, []).append(callback)
334 330
335 331 def bind(func, *args):
336 332 '''Partial function application
337 333
338 334 Returns a new function that is the partial application of args and kwargs
339 335 to func. For example,
340 336
341 337 f(1, 2, bar=3) === bind(f, 1)(2, bar=3)'''
342 338 assert callable(func)
343 339 def closure(*a, **kw):
344 340 return func(*(args + a), **kw)
345 341 return closure
346 342
347 343 def _updatewrapper(wrap, origfn, unboundwrapper):
348 344 '''Copy and add some useful attributes to wrapper'''
349 345 try:
350 346 wrap.__name__ = origfn.__name__
351 347 except AttributeError:
352 348 pass
353 349 wrap.__module__ = getattr(origfn, '__module__')
354 350 wrap.__doc__ = getattr(origfn, '__doc__')
355 351 wrap.__dict__.update(getattr(origfn, '__dict__', {}))
356 352 wrap._origfunc = origfn
357 353 wrap._unboundwrapper = unboundwrapper
358 354
359 355 def wrapcommand(table, command, wrapper, synopsis=None, docstring=None):
360 356 '''Wrap the command named `command' in table
361 357
362 358 Replace command in the command table with wrapper. The wrapped command will
363 359 be inserted into the command table specified by the table argument.
364 360
365 361 The wrapper will be called like
366 362
367 363 wrapper(orig, *args, **kwargs)
368 364
369 365 where orig is the original (wrapped) function, and *args, **kwargs
370 366 are the arguments passed to it.
371 367
372 368 Optionally append to the command synopsis and docstring, used for help.
373 369 For example, if your extension wraps the ``bookmarks`` command to add the
374 370 flags ``--remote`` and ``--all`` you might call this function like so:
375 371
376 372 synopsis = ' [-a] [--remote]'
377 373 docstring = """
378 374
379 375 The ``remotenames`` extension adds the ``--remote`` and ``--all`` (``-a``)
380 376 flags to the bookmarks command. Either flag will show the remote bookmarks
381 377 known to the repository; ``--remote`` will also suppress the output of the
382 378 local bookmarks.
383 379 """
384 380
385 381 extensions.wrapcommand(commands.table, 'bookmarks', exbookmarks,
386 382 synopsis, docstring)
387 383 '''
388 384 assert callable(wrapper)
389 385 aliases, entry = cmdutil.findcmd(command, table)
390 386 for alias, e in table.iteritems():
391 387 if e is entry:
392 388 key = alias
393 389 break
394 390
395 391 origfn = entry[0]
396 392 wrap = functools.partial(util.checksignature(wrapper),
397 393 util.checksignature(origfn))
398 394 _updatewrapper(wrap, origfn, wrapper)
399 395 if docstring is not None:
400 396 wrap.__doc__ += docstring
401 397
402 398 newentry = list(entry)
403 399 newentry[0] = wrap
404 400 if synopsis is not None:
405 401 newentry[2] += synopsis
406 402 table[key] = tuple(newentry)
407 403 return entry
408 404
409 405 def wrapfilecache(cls, propname, wrapper):
410 406 """Wraps a filecache property.
411 407
412 408 These can't be wrapped using the normal wrapfunction.
413 409 """
414 410 propname = pycompat.sysstr(propname)
415 411 assert callable(wrapper)
416 412 for currcls in cls.__mro__:
417 413 if propname in currcls.__dict__:
418 414 origfn = currcls.__dict__[propname].func
419 415 assert callable(origfn)
420 416 def wrap(*args, **kwargs):
421 417 return wrapper(origfn, *args, **kwargs)
422 418 currcls.__dict__[propname].func = wrap
423 419 break
424 420
425 421 if currcls is object:
426 422 raise AttributeError(r"type '%s' has no property '%s'" % (
427 423 cls, propname))
428 424
429 425 class wrappedfunction(object):
430 426 '''context manager for temporarily wrapping a function'''
431 427
432 428 def __init__(self, container, funcname, wrapper):
433 429 assert callable(wrapper)
434 430 self._container = container
435 431 self._funcname = funcname
436 432 self._wrapper = wrapper
437 433
438 434 def __enter__(self):
439 435 wrapfunction(self._container, self._funcname, self._wrapper)
440 436
441 437 def __exit__(self, exctype, excvalue, traceback):
442 438 unwrapfunction(self._container, self._funcname, self._wrapper)
443 439
444 440 def wrapfunction(container, funcname, wrapper):
445 441 '''Wrap the function named funcname in container
446 442
447 443 Replace the funcname member in the given container with the specified
448 444 wrapper. The container is typically a module, class, or instance.
449 445
450 446 The wrapper will be called like
451 447
452 448 wrapper(orig, *args, **kwargs)
453 449
454 450 where orig is the original (wrapped) function, and *args, **kwargs
455 451 are the arguments passed to it.
456 452
457 453 Wrapping methods of the repository object is not recommended since
458 454 it conflicts with extensions that extend the repository by
459 455 subclassing. All extensions that need to extend methods of
460 456 localrepository should use this subclassing trick: namely,
461 457 reposetup() should look like
462 458
463 459 def reposetup(ui, repo):
464 460 class myrepo(repo.__class__):
465 461 def whatever(self, *args, **kwargs):
466 462 [...extension stuff...]
467 463 super(myrepo, self).whatever(*args, **kwargs)
468 464 [...extension stuff...]
469 465
470 466 repo.__class__ = myrepo
471 467
472 468 In general, combining wrapfunction() with subclassing does not
473 469 work. Since you cannot control what other extensions are loaded by
474 470 your end users, you should play nicely with others by using the
475 471 subclass trick.
476 472 '''
477 473 assert callable(wrapper)
478 474
479 475 origfn = getattr(container, funcname)
480 476 assert callable(origfn)
481 477 if inspect.ismodule(container):
482 478 # origfn is not an instance or class method. "partial" can be used.
483 479 # "partial" won't insert a frame in traceback.
484 480 wrap = functools.partial(wrapper, origfn)
485 481 else:
486 482 # "partial" cannot be safely used. Emulate its effect by using "bind".
487 483 # The downside is one more frame in traceback.
488 484 wrap = bind(wrapper, origfn)
489 485 _updatewrapper(wrap, origfn, wrapper)
490 486 setattr(container, funcname, wrap)
491 487 return origfn
492 488
493 489 def unwrapfunction(container, funcname, wrapper=None):
494 490 '''undo wrapfunction
495 491
496 492 If wrappers is None, undo the last wrap. Otherwise removes the wrapper
497 493 from the chain of wrappers.
498 494
499 495 Return the removed wrapper.
500 496 Raise IndexError if wrapper is None and nothing to unwrap; ValueError if
501 497 wrapper is not None but is not found in the wrapper chain.
502 498 '''
503 499 chain = getwrapperchain(container, funcname)
504 500 origfn = chain.pop()
505 501 if wrapper is None:
506 502 wrapper = chain[0]
507 503 chain.remove(wrapper)
508 504 setattr(container, funcname, origfn)
509 505 for w in reversed(chain):
510 506 wrapfunction(container, funcname, w)
511 507 return wrapper
512 508
513 509 def getwrapperchain(container, funcname):
514 510 '''get a chain of wrappers of a function
515 511
516 512 Return a list of functions: [newest wrapper, ..., oldest wrapper, origfunc]
517 513
518 514 The wrapper functions are the ones passed to wrapfunction, whose first
519 515 argument is origfunc.
520 516 '''
521 517 result = []
522 518 fn = getattr(container, funcname)
523 519 while fn:
524 520 assert callable(fn)
525 521 result.append(getattr(fn, '_unboundwrapper', fn))
526 522 fn = getattr(fn, '_origfunc', None)
527 523 return result
528 524
529 525 def _disabledpaths(strip_init=False):
530 526 '''find paths of disabled extensions. returns a dict of {name: path}
531 527 removes /__init__.py from packages if strip_init is True'''
532 528 import hgext
533 529 extpath = os.path.dirname(
534 530 os.path.abspath(pycompat.fsencode(hgext.__file__)))
535 531 try: # might not be a filesystem path
536 532 files = os.listdir(extpath)
537 533 except OSError:
538 534 return {}
539 535
540 536 exts = {}
541 537 for e in files:
542 538 if e.endswith('.py'):
543 539 name = e.rsplit('.', 1)[0]
544 540 path = os.path.join(extpath, e)
545 541 else:
546 542 name = e
547 543 path = os.path.join(extpath, e, '__init__.py')
548 544 if not os.path.exists(path):
549 545 continue
550 546 if strip_init:
551 547 path = os.path.dirname(path)
552 548 if name in exts or name in _order or name == '__init__':
553 549 continue
554 550 exts[name] = path
555 551 for name, path in _disabledextensions.iteritems():
556 552 # If no path was provided for a disabled extension (e.g. "color=!"),
557 553 # don't replace the path we already found by the scan above.
558 554 if path:
559 555 exts[name] = path
560 556 return exts
561 557
562 558 def _moduledoc(file):
563 559 '''return the top-level python documentation for the given file
564 560
565 561 Loosely inspired by pydoc.source_synopsis(), but rewritten to
566 562 handle triple quotes and to return the whole text instead of just
567 563 the synopsis'''
568 564 result = []
569 565
570 566 line = file.readline()
571 567 while line[:1] == '#' or not line.strip():
572 568 line = file.readline()
573 569 if not line:
574 570 break
575 571
576 572 start = line[:3]
577 573 if start == '"""' or start == "'''":
578 574 line = line[3:]
579 575 while line:
580 576 if line.rstrip().endswith(start):
581 577 line = line.split(start)[0]
582 578 if line:
583 579 result.append(line)
584 580 break
585 581 elif not line:
586 582 return None # unmatched delimiter
587 583 result.append(line)
588 584 line = file.readline()
589 585 else:
590 586 return None
591 587
592 588 return ''.join(result)
593 589
594 590 def _disabledhelp(path):
595 591 '''retrieve help synopsis of a disabled extension (without importing)'''
596 592 try:
597 593 file = open(path)
598 594 except IOError:
599 595 return
600 596 else:
601 597 doc = _moduledoc(file)
602 598 file.close()
603 599
604 600 if doc: # extracting localized synopsis
605 601 return gettext(doc)
606 602 else:
607 603 return _('(no help text available)')
608 604
609 605 def disabled():
610 606 '''find disabled extensions from hgext. returns a dict of {name: desc}'''
611 607 try:
612 608 from hgext import __index__
613 609 return dict((name, gettext(desc))
614 610 for name, desc in __index__.docs.iteritems()
615 611 if name not in _order)
616 612 except (ImportError, AttributeError):
617 613 pass
618 614
619 615 paths = _disabledpaths()
620 616 if not paths:
621 617 return {}
622 618
623 619 exts = {}
624 620 for name, path in paths.iteritems():
625 621 doc = _disabledhelp(path)
626 622 if doc:
627 623 exts[name] = doc.splitlines()[0]
628 624
629 625 return exts
630 626
631 627 def disabledext(name):
632 628 '''find a specific disabled extension from hgext. returns desc'''
633 629 try:
634 630 from hgext import __index__
635 631 if name in _order: # enabled
636 632 return
637 633 else:
638 634 return gettext(__index__.docs.get(name))
639 635 except (ImportError, AttributeError):
640 636 pass
641 637
642 638 paths = _disabledpaths()
643 639 if name in paths:
644 640 return _disabledhelp(paths[name])
645 641
646 642 def disabledcmd(ui, cmd, strict=False):
647 643 '''import disabled extensions until cmd is found.
648 644 returns (cmdname, extname, module)'''
649 645
650 646 paths = _disabledpaths(strip_init=True)
651 647 if not paths:
652 648 raise error.UnknownCommand(cmd)
653 649
654 650 def findcmd(cmd, name, path):
655 651 try:
656 652 mod = loadpath(path, 'hgext.%s' % name)
657 653 except Exception:
658 654 return
659 655 try:
660 656 aliases, entry = cmdutil.findcmd(cmd,
661 657 getattr(mod, 'cmdtable', {}), strict)
662 658 except (error.AmbiguousCommand, error.UnknownCommand):
663 659 return
664 660 except Exception:
665 661 ui.warn(_('warning: error finding commands in %s\n') % path)
666 662 ui.traceback()
667 663 return
668 664 for c in aliases:
669 665 if c.startswith(cmd):
670 666 cmd = c
671 667 break
672 668 else:
673 669 cmd = aliases[0]
674 670 return (cmd, name, mod)
675 671
676 672 ext = None
677 673 # first, search for an extension with the same name as the command
678 674 path = paths.pop(cmd, None)
679 675 if path:
680 676 ext = findcmd(cmd, cmd, path)
681 677 if not ext:
682 678 # otherwise, interrogate each extension until there's a match
683 679 for name, path in paths.iteritems():
684 680 ext = findcmd(cmd, name, path)
685 681 if ext:
686 682 break
687 683 if ext and 'DEPRECATED' not in ext.__doc__:
688 684 return ext
689 685
690 686 raise error.UnknownCommand(cmd)
691 687
692 688 def enabled(shortname=True):
693 689 '''return a dict of {name: desc} of extensions'''
694 690 exts = {}
695 691 for ename, ext in extensions():
696 692 doc = (gettext(ext.__doc__) or _('(no help text available)'))
697 693 if shortname:
698 694 ename = ename.split('.')[-1]
699 695 exts[ename] = doc.splitlines()[0].strip()
700 696
701 697 return exts
702 698
703 699 def notloaded():
704 700 '''return short names of extensions that failed to load'''
705 701 return [name for name, mod in _extensions.iteritems() if mod is None]
706 702
707 703 def moduleversion(module):
708 704 '''return version information from given module as a string'''
709 705 if (util.safehasattr(module, 'getversion')
710 706 and callable(module.getversion)):
711 707 version = module.getversion()
712 708 elif util.safehasattr(module, '__version__'):
713 709 version = module.__version__
714 710 else:
715 711 version = ''
716 712 if isinstance(version, (list, tuple)):
717 713 version = '.'.join(str(o) for o in version)
718 714 return version
719 715
720 716 def ismoduleinternal(module):
721 717 exttestedwith = getattr(module, 'testedwith', None)
722 718 return exttestedwith == "ships-with-hg-core"
@@ -1,2275 +1,2274
1 1 # localrepo.py - read/write repository class for mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import errno
11 11 import hashlib
12 import inspect
13 12 import os
14 13 import random
15 14 import time
16 15 import weakref
17 16
18 17 from .i18n import _
19 18 from .node import (
20 19 hex,
21 20 nullid,
22 21 short,
23 22 )
24 23 from . import (
25 24 bookmarks,
26 25 branchmap,
27 26 bundle2,
28 27 changegroup,
29 28 changelog,
30 29 color,
31 30 context,
32 31 dirstate,
33 32 dirstateguard,
34 33 discovery,
35 34 encoding,
36 35 error,
37 36 exchange,
38 37 extensions,
39 38 filelog,
40 39 hook,
41 40 lock as lockmod,
42 41 manifest,
43 42 match as matchmod,
44 43 merge as mergemod,
45 44 mergeutil,
46 45 namespaces,
47 46 obsolete,
48 47 pathutil,
49 48 peer,
50 49 phases,
51 50 pushkey,
52 51 pycompat,
53 52 repository,
54 53 repoview,
55 54 revset,
56 55 revsetlang,
57 56 scmutil,
58 57 sparse,
59 58 store,
60 59 subrepoutil,
61 60 tags as tagsmod,
62 61 transaction,
63 62 txnutil,
64 63 util,
65 64 vfs as vfsmod,
66 65 )
67 66
68 67 release = lockmod.release
69 68 urlerr = util.urlerr
70 69 urlreq = util.urlreq
71 70
72 71 # set of (path, vfs-location) tuples. vfs-location is:
73 72 # - 'plain for vfs relative paths
74 73 # - '' for svfs relative paths
75 74 _cachedfiles = set()
76 75
77 76 class _basefilecache(scmutil.filecache):
78 77 """All filecache usage on repo are done for logic that should be unfiltered
79 78 """
80 79 def __get__(self, repo, type=None):
81 80 if repo is None:
82 81 return self
83 82 return super(_basefilecache, self).__get__(repo.unfiltered(), type)
84 83 def __set__(self, repo, value):
85 84 return super(_basefilecache, self).__set__(repo.unfiltered(), value)
86 85 def __delete__(self, repo):
87 86 return super(_basefilecache, self).__delete__(repo.unfiltered())
88 87
89 88 class repofilecache(_basefilecache):
90 89 """filecache for files in .hg but outside of .hg/store"""
91 90 def __init__(self, *paths):
92 91 super(repofilecache, self).__init__(*paths)
93 92 for path in paths:
94 93 _cachedfiles.add((path, 'plain'))
95 94
96 95 def join(self, obj, fname):
97 96 return obj.vfs.join(fname)
98 97
99 98 class storecache(_basefilecache):
100 99 """filecache for files in the store"""
101 100 def __init__(self, *paths):
102 101 super(storecache, self).__init__(*paths)
103 102 for path in paths:
104 103 _cachedfiles.add((path, ''))
105 104
106 105 def join(self, obj, fname):
107 106 return obj.sjoin(fname)
108 107
109 108 def isfilecached(repo, name):
110 109 """check if a repo has already cached "name" filecache-ed property
111 110
112 111 This returns (cachedobj-or-None, iscached) tuple.
113 112 """
114 113 cacheentry = repo.unfiltered()._filecache.get(name, None)
115 114 if not cacheentry:
116 115 return None, False
117 116 return cacheentry.obj, True
118 117
119 118 class unfilteredpropertycache(util.propertycache):
120 119 """propertycache that apply to unfiltered repo only"""
121 120
122 121 def __get__(self, repo, type=None):
123 122 unfi = repo.unfiltered()
124 123 if unfi is repo:
125 124 return super(unfilteredpropertycache, self).__get__(unfi)
126 125 return getattr(unfi, self.name)
127 126
128 127 class filteredpropertycache(util.propertycache):
129 128 """propertycache that must take filtering in account"""
130 129
131 130 def cachevalue(self, obj, value):
132 131 object.__setattr__(obj, self.name, value)
133 132
134 133
135 134 def hasunfilteredcache(repo, name):
136 135 """check if a repo has an unfilteredpropertycache value for <name>"""
137 136 return name in vars(repo.unfiltered())
138 137
139 138 def unfilteredmethod(orig):
140 139 """decorate method that always need to be run on unfiltered version"""
141 140 def wrapper(repo, *args, **kwargs):
142 141 return orig(repo.unfiltered(), *args, **kwargs)
143 142 return wrapper
144 143
145 144 moderncaps = {'lookup', 'branchmap', 'pushkey', 'known', 'getbundle',
146 145 'unbundle'}
147 146 legacycaps = moderncaps.union({'changegroupsubset'})
148 147
149 148 class localpeer(repository.peer):
150 149 '''peer for a local repo; reflects only the most recent API'''
151 150
152 151 def __init__(self, repo, caps=None):
153 152 super(localpeer, self).__init__()
154 153
155 154 if caps is None:
156 155 caps = moderncaps.copy()
157 156 self._repo = repo.filtered('served')
158 157 self._ui = repo.ui
159 158 self._caps = repo._restrictcapabilities(caps)
160 159
161 160 # Begin of _basepeer interface.
162 161
163 162 @util.propertycache
164 163 def ui(self):
165 164 return self._ui
166 165
167 166 def url(self):
168 167 return self._repo.url()
169 168
170 169 def local(self):
171 170 return self._repo
172 171
173 172 def peer(self):
174 173 return self
175 174
176 175 def canpush(self):
177 176 return True
178 177
179 178 def close(self):
180 179 self._repo.close()
181 180
182 181 # End of _basepeer interface.
183 182
184 183 # Begin of _basewirecommands interface.
185 184
186 185 def branchmap(self):
187 186 return self._repo.branchmap()
188 187
189 188 def capabilities(self):
190 189 return self._caps
191 190
192 191 def debugwireargs(self, one, two, three=None, four=None, five=None):
193 192 """Used to test argument passing over the wire"""
194 193 return "%s %s %s %s %s" % (one, two, three, four, five)
195 194
196 195 def getbundle(self, source, heads=None, common=None, bundlecaps=None,
197 196 **kwargs):
198 197 chunks = exchange.getbundlechunks(self._repo, source, heads=heads,
199 198 common=common, bundlecaps=bundlecaps,
200 199 **kwargs)[1]
201 200 cb = util.chunkbuffer(chunks)
202 201
203 202 if exchange.bundle2requested(bundlecaps):
204 203 # When requesting a bundle2, getbundle returns a stream to make the
205 204 # wire level function happier. We need to build a proper object
206 205 # from it in local peer.
207 206 return bundle2.getunbundler(self.ui, cb)
208 207 else:
209 208 return changegroup.getunbundler('01', cb, None)
210 209
211 210 def heads(self):
212 211 return self._repo.heads()
213 212
214 213 def known(self, nodes):
215 214 return self._repo.known(nodes)
216 215
217 216 def listkeys(self, namespace):
218 217 return self._repo.listkeys(namespace)
219 218
220 219 def lookup(self, key):
221 220 return self._repo.lookup(key)
222 221
223 222 def pushkey(self, namespace, key, old, new):
224 223 return self._repo.pushkey(namespace, key, old, new)
225 224
226 225 def stream_out(self):
227 226 raise error.Abort(_('cannot perform stream clone against local '
228 227 'peer'))
229 228
230 229 def unbundle(self, cg, heads, url):
231 230 """apply a bundle on a repo
232 231
233 232 This function handles the repo locking itself."""
234 233 try:
235 234 try:
236 235 cg = exchange.readbundle(self.ui, cg, None)
237 236 ret = exchange.unbundle(self._repo, cg, heads, 'push', url)
238 237 if util.safehasattr(ret, 'getchunks'):
239 238 # This is a bundle20 object, turn it into an unbundler.
240 239 # This little dance should be dropped eventually when the
241 240 # API is finally improved.
242 241 stream = util.chunkbuffer(ret.getchunks())
243 242 ret = bundle2.getunbundler(self.ui, stream)
244 243 return ret
245 244 except Exception as exc:
246 245 # If the exception contains output salvaged from a bundle2
247 246 # reply, we need to make sure it is printed before continuing
248 247 # to fail. So we build a bundle2 with such output and consume
249 248 # it directly.
250 249 #
251 250 # This is not very elegant but allows a "simple" solution for
252 251 # issue4594
253 252 output = getattr(exc, '_bundle2salvagedoutput', ())
254 253 if output:
255 254 bundler = bundle2.bundle20(self._repo.ui)
256 255 for out in output:
257 256 bundler.addpart(out)
258 257 stream = util.chunkbuffer(bundler.getchunks())
259 258 b = bundle2.getunbundler(self.ui, stream)
260 259 bundle2.processbundle(self._repo, b)
261 260 raise
262 261 except error.PushRaced as exc:
263 262 raise error.ResponseError(_('push failed:'), str(exc))
264 263
265 264 # End of _basewirecommands interface.
266 265
267 266 # Begin of peer interface.
268 267
269 268 def iterbatch(self):
270 269 return peer.localiterbatcher(self)
271 270
272 271 # End of peer interface.
273 272
274 273 class locallegacypeer(repository.legacypeer, localpeer):
275 274 '''peer extension which implements legacy methods too; used for tests with
276 275 restricted capabilities'''
277 276
278 277 def __init__(self, repo):
279 278 super(locallegacypeer, self).__init__(repo, caps=legacycaps)
280 279
281 280 # Begin of baselegacywirecommands interface.
282 281
283 282 def between(self, pairs):
284 283 return self._repo.between(pairs)
285 284
286 285 def branches(self, nodes):
287 286 return self._repo.branches(nodes)
288 287
289 288 def changegroup(self, basenodes, source):
290 289 outgoing = discovery.outgoing(self._repo, missingroots=basenodes,
291 290 missingheads=self._repo.heads())
292 291 return changegroup.makechangegroup(self._repo, outgoing, '01', source)
293 292
294 293 def changegroupsubset(self, bases, heads, source):
295 294 outgoing = discovery.outgoing(self._repo, missingroots=bases,
296 295 missingheads=heads)
297 296 return changegroup.makechangegroup(self._repo, outgoing, '01', source)
298 297
299 298 # End of baselegacywirecommands interface.
300 299
301 300 # Increment the sub-version when the revlog v2 format changes to lock out old
302 301 # clients.
303 302 REVLOGV2_REQUIREMENT = 'exp-revlogv2.0'
304 303
305 304 class localrepository(object):
306 305
307 306 supportedformats = {
308 307 'revlogv1',
309 308 'generaldelta',
310 309 'treemanifest',
311 310 'manifestv2',
312 311 REVLOGV2_REQUIREMENT,
313 312 }
314 313 _basesupported = supportedformats | {
315 314 'store',
316 315 'fncache',
317 316 'shared',
318 317 'relshared',
319 318 'dotencode',
320 319 'exp-sparse',
321 320 }
322 321 openerreqs = {
323 322 'revlogv1',
324 323 'generaldelta',
325 324 'treemanifest',
326 325 'manifestv2',
327 326 }
328 327
329 328 # a list of (ui, featureset) functions.
330 329 # only functions defined in module of enabled extensions are invoked
331 330 featuresetupfuncs = set()
332 331
333 332 # list of prefix for file which can be written without 'wlock'
334 333 # Extensions should extend this list when needed
335 334 _wlockfreeprefix = {
336 335 # We migh consider requiring 'wlock' for the next
337 336 # two, but pretty much all the existing code assume
338 337 # wlock is not needed so we keep them excluded for
339 338 # now.
340 339 'hgrc',
341 340 'requires',
342 341 # XXX cache is a complicatged business someone
343 342 # should investigate this in depth at some point
344 343 'cache/',
345 344 # XXX shouldn't be dirstate covered by the wlock?
346 345 'dirstate',
347 346 # XXX bisect was still a bit too messy at the time
348 347 # this changeset was introduced. Someone should fix
349 348 # the remainig bit and drop this line
350 349 'bisect.state',
351 350 }
352 351
353 352 def __init__(self, baseui, path, create=False):
354 353 self.requirements = set()
355 354 self.filtername = None
356 355 # wvfs: rooted at the repository root, used to access the working copy
357 356 self.wvfs = vfsmod.vfs(path, expandpath=True, realpath=True)
358 357 # vfs: rooted at .hg, used to access repo files outside of .hg/store
359 358 self.vfs = None
360 359 # svfs: usually rooted at .hg/store, used to access repository history
361 360 # If this is a shared repository, this vfs may point to another
362 361 # repository's .hg/store directory.
363 362 self.svfs = None
364 363 self.root = self.wvfs.base
365 364 self.path = self.wvfs.join(".hg")
366 365 self.origroot = path
367 366 # This is only used by context.workingctx.match in order to
368 367 # detect files in subrepos.
369 368 self.auditor = pathutil.pathauditor(
370 369 self.root, callback=self._checknested)
371 370 # This is only used by context.basectx.match in order to detect
372 371 # files in subrepos.
373 372 self.nofsauditor = pathutil.pathauditor(
374 373 self.root, callback=self._checknested, realfs=False, cached=True)
375 374 self.baseui = baseui
376 375 self.ui = baseui.copy()
377 376 self.ui.copy = baseui.copy # prevent copying repo configuration
378 377 self.vfs = vfsmod.vfs(self.path, cacheaudited=True)
379 378 if (self.ui.configbool('devel', 'all-warnings') or
380 379 self.ui.configbool('devel', 'check-locks')):
381 380 self.vfs.audit = self._getvfsward(self.vfs.audit)
382 381 # A list of callback to shape the phase if no data were found.
383 382 # Callback are in the form: func(repo, roots) --> processed root.
384 383 # This list it to be filled by extension during repo setup
385 384 self._phasedefaults = []
386 385 try:
387 386 self.ui.readconfig(self.vfs.join("hgrc"), self.root)
388 387 self._loadextensions()
389 388 except IOError:
390 389 pass
391 390
392 391 if self.featuresetupfuncs:
393 392 self.supported = set(self._basesupported) # use private copy
394 393 extmods = set(m.__name__ for n, m
395 394 in extensions.extensions(self.ui))
396 395 for setupfunc in self.featuresetupfuncs:
397 396 if setupfunc.__module__ in extmods:
398 397 setupfunc(self.ui, self.supported)
399 398 else:
400 399 self.supported = self._basesupported
401 400 color.setup(self.ui)
402 401
403 402 # Add compression engines.
404 403 for name in util.compengines:
405 404 engine = util.compengines[name]
406 405 if engine.revlogheader():
407 406 self.supported.add('exp-compression-%s' % name)
408 407
409 408 if not self.vfs.isdir():
410 409 if create:
411 410 self.requirements = newreporequirements(self)
412 411
413 412 if not self.wvfs.exists():
414 413 self.wvfs.makedirs()
415 414 self.vfs.makedir(notindexed=True)
416 415
417 416 if 'store' in self.requirements:
418 417 self.vfs.mkdir("store")
419 418
420 419 # create an invalid changelog
421 420 self.vfs.append(
422 421 "00changelog.i",
423 422 '\0\0\0\2' # represents revlogv2
424 423 ' dummy changelog to prevent using the old repo layout'
425 424 )
426 425 else:
427 426 raise error.RepoError(_("repository %s not found") % path)
428 427 elif create:
429 428 raise error.RepoError(_("repository %s already exists") % path)
430 429 else:
431 430 try:
432 431 self.requirements = scmutil.readrequires(
433 432 self.vfs, self.supported)
434 433 except IOError as inst:
435 434 if inst.errno != errno.ENOENT:
436 435 raise
437 436
438 437 cachepath = self.vfs.join('cache')
439 438 self.sharedpath = self.path
440 439 try:
441 440 sharedpath = self.vfs.read("sharedpath").rstrip('\n')
442 441 if 'relshared' in self.requirements:
443 442 sharedpath = self.vfs.join(sharedpath)
444 443 vfs = vfsmod.vfs(sharedpath, realpath=True)
445 444 cachepath = vfs.join('cache')
446 445 s = vfs.base
447 446 if not vfs.exists():
448 447 raise error.RepoError(
449 448 _('.hg/sharedpath points to nonexistent directory %s') % s)
450 449 self.sharedpath = s
451 450 except IOError as inst:
452 451 if inst.errno != errno.ENOENT:
453 452 raise
454 453
455 454 if 'exp-sparse' in self.requirements and not sparse.enabled:
456 455 raise error.RepoError(_('repository is using sparse feature but '
457 456 'sparse is not enabled; enable the '
458 457 '"sparse" extensions to access'))
459 458
460 459 self.store = store.store(
461 460 self.requirements, self.sharedpath,
462 461 lambda base: vfsmod.vfs(base, cacheaudited=True))
463 462 self.spath = self.store.path
464 463 self.svfs = self.store.vfs
465 464 self.sjoin = self.store.join
466 465 self.vfs.createmode = self.store.createmode
467 466 self.cachevfs = vfsmod.vfs(cachepath, cacheaudited=True)
468 467 self.cachevfs.createmode = self.store.createmode
469 468 if (self.ui.configbool('devel', 'all-warnings') or
470 469 self.ui.configbool('devel', 'check-locks')):
471 470 if util.safehasattr(self.svfs, 'vfs'): # this is filtervfs
472 471 self.svfs.vfs.audit = self._getsvfsward(self.svfs.vfs.audit)
473 472 else: # standard vfs
474 473 self.svfs.audit = self._getsvfsward(self.svfs.audit)
475 474 self._applyopenerreqs()
476 475 if create:
477 476 self._writerequirements()
478 477
479 478 self._dirstatevalidatewarned = False
480 479
481 480 self._branchcaches = {}
482 481 self._revbranchcache = None
483 482 self.filterpats = {}
484 483 self._datafilters = {}
485 484 self._transref = self._lockref = self._wlockref = None
486 485
487 486 # A cache for various files under .hg/ that tracks file changes,
488 487 # (used by the filecache decorator)
489 488 #
490 489 # Maps a property name to its util.filecacheentry
491 490 self._filecache = {}
492 491
493 492 # hold sets of revision to be filtered
494 493 # should be cleared when something might have changed the filter value:
495 494 # - new changesets,
496 495 # - phase change,
497 496 # - new obsolescence marker,
498 497 # - working directory parent change,
499 498 # - bookmark changes
500 499 self.filteredrevcache = {}
501 500
502 501 # post-dirstate-status hooks
503 502 self._postdsstatus = []
504 503
505 504 # generic mapping between names and nodes
506 505 self.names = namespaces.namespaces()
507 506
508 507 # Key to signature value.
509 508 self._sparsesignaturecache = {}
510 509 # Signature to cached matcher instance.
511 510 self._sparsematchercache = {}
512 511
513 512 def _getvfsward(self, origfunc):
514 513 """build a ward for self.vfs"""
515 514 rref = weakref.ref(self)
516 515 def checkvfs(path, mode=None):
517 516 ret = origfunc(path, mode=mode)
518 517 repo = rref()
519 518 if (repo is None
520 519 or not util.safehasattr(repo, '_wlockref')
521 520 or not util.safehasattr(repo, '_lockref')):
522 521 return
523 522 if mode in (None, 'r', 'rb'):
524 523 return
525 524 if path.startswith(repo.path):
526 525 # truncate name relative to the repository (.hg)
527 526 path = path[len(repo.path) + 1:]
528 527 if path.startswith('cache/'):
529 528 msg = 'accessing cache with vfs instead of cachevfs: "%s"'
530 529 repo.ui.develwarn(msg % path, stacklevel=2, config="cache-vfs")
531 530 if path.startswith('journal.'):
532 531 # journal is covered by 'lock'
533 532 if repo._currentlock(repo._lockref) is None:
534 533 repo.ui.develwarn('write with no lock: "%s"' % path,
535 534 stacklevel=2, config='check-locks')
536 535 elif repo._currentlock(repo._wlockref) is None:
537 536 # rest of vfs files are covered by 'wlock'
538 537 #
539 538 # exclude special files
540 539 for prefix in self._wlockfreeprefix:
541 540 if path.startswith(prefix):
542 541 return
543 542 repo.ui.develwarn('write with no wlock: "%s"' % path,
544 543 stacklevel=2, config='check-locks')
545 544 return ret
546 545 return checkvfs
547 546
548 547 def _getsvfsward(self, origfunc):
549 548 """build a ward for self.svfs"""
550 549 rref = weakref.ref(self)
551 550 def checksvfs(path, mode=None):
552 551 ret = origfunc(path, mode=mode)
553 552 repo = rref()
554 553 if repo is None or not util.safehasattr(repo, '_lockref'):
555 554 return
556 555 if mode in (None, 'r', 'rb'):
557 556 return
558 557 if path.startswith(repo.sharedpath):
559 558 # truncate name relative to the repository (.hg)
560 559 path = path[len(repo.sharedpath) + 1:]
561 560 if repo._currentlock(repo._lockref) is None:
562 561 repo.ui.develwarn('write with no lock: "%s"' % path,
563 562 stacklevel=3)
564 563 return ret
565 564 return checksvfs
566 565
567 566 def close(self):
568 567 self._writecaches()
569 568
570 569 def _loadextensions(self):
571 570 extensions.loadall(self.ui)
572 571
573 572 def _writecaches(self):
574 573 if self._revbranchcache:
575 574 self._revbranchcache.write()
576 575
577 576 def _restrictcapabilities(self, caps):
578 577 if self.ui.configbool('experimental', 'bundle2-advertise'):
579 578 caps = set(caps)
580 579 capsblob = bundle2.encodecaps(bundle2.getrepocaps(self,
581 580 role='client'))
582 581 caps.add('bundle2=' + urlreq.quote(capsblob))
583 582 return caps
584 583
585 584 def _applyopenerreqs(self):
586 585 self.svfs.options = dict((r, 1) for r in self.requirements
587 586 if r in self.openerreqs)
588 587 # experimental config: format.chunkcachesize
589 588 chunkcachesize = self.ui.configint('format', 'chunkcachesize')
590 589 if chunkcachesize is not None:
591 590 self.svfs.options['chunkcachesize'] = chunkcachesize
592 591 # experimental config: format.maxchainlen
593 592 maxchainlen = self.ui.configint('format', 'maxchainlen')
594 593 if maxchainlen is not None:
595 594 self.svfs.options['maxchainlen'] = maxchainlen
596 595 # experimental config: format.manifestcachesize
597 596 manifestcachesize = self.ui.configint('format', 'manifestcachesize')
598 597 if manifestcachesize is not None:
599 598 self.svfs.options['manifestcachesize'] = manifestcachesize
600 599 # experimental config: format.aggressivemergedeltas
601 600 aggressivemergedeltas = self.ui.configbool('format',
602 601 'aggressivemergedeltas')
603 602 self.svfs.options['aggressivemergedeltas'] = aggressivemergedeltas
604 603 self.svfs.options['lazydeltabase'] = not scmutil.gddeltaconfig(self.ui)
605 604 chainspan = self.ui.configbytes('experimental', 'maxdeltachainspan')
606 605 if 0 <= chainspan:
607 606 self.svfs.options['maxdeltachainspan'] = chainspan
608 607 mmapindexthreshold = self.ui.configbytes('experimental',
609 608 'mmapindexthreshold')
610 609 if mmapindexthreshold is not None:
611 610 self.svfs.options['mmapindexthreshold'] = mmapindexthreshold
612 611 withsparseread = self.ui.configbool('experimental', 'sparse-read')
613 612 srdensitythres = float(self.ui.config('experimental',
614 613 'sparse-read.density-threshold'))
615 614 srmingapsize = self.ui.configbytes('experimental',
616 615 'sparse-read.min-gap-size')
617 616 self.svfs.options['with-sparse-read'] = withsparseread
618 617 self.svfs.options['sparse-read-density-threshold'] = srdensitythres
619 618 self.svfs.options['sparse-read-min-gap-size'] = srmingapsize
620 619
621 620 for r in self.requirements:
622 621 if r.startswith('exp-compression-'):
623 622 self.svfs.options['compengine'] = r[len('exp-compression-'):]
624 623
625 624 # TODO move "revlogv2" to openerreqs once finalized.
626 625 if REVLOGV2_REQUIREMENT in self.requirements:
627 626 self.svfs.options['revlogv2'] = True
628 627
629 628 def _writerequirements(self):
630 629 scmutil.writerequires(self.vfs, self.requirements)
631 630
632 631 def _checknested(self, path):
633 632 """Determine if path is a legal nested repository."""
634 633 if not path.startswith(self.root):
635 634 return False
636 635 subpath = path[len(self.root) + 1:]
637 636 normsubpath = util.pconvert(subpath)
638 637
639 638 # XXX: Checking against the current working copy is wrong in
640 639 # the sense that it can reject things like
641 640 #
642 641 # $ hg cat -r 10 sub/x.txt
643 642 #
644 643 # if sub/ is no longer a subrepository in the working copy
645 644 # parent revision.
646 645 #
647 646 # However, it can of course also allow things that would have
648 647 # been rejected before, such as the above cat command if sub/
649 648 # is a subrepository now, but was a normal directory before.
650 649 # The old path auditor would have rejected by mistake since it
651 650 # panics when it sees sub/.hg/.
652 651 #
653 652 # All in all, checking against the working copy seems sensible
654 653 # since we want to prevent access to nested repositories on
655 654 # the filesystem *now*.
656 655 ctx = self[None]
657 656 parts = util.splitpath(subpath)
658 657 while parts:
659 658 prefix = '/'.join(parts)
660 659 if prefix in ctx.substate:
661 660 if prefix == normsubpath:
662 661 return True
663 662 else:
664 663 sub = ctx.sub(prefix)
665 664 return sub.checknested(subpath[len(prefix) + 1:])
666 665 else:
667 666 parts.pop()
668 667 return False
669 668
670 669 def peer(self):
671 670 return localpeer(self) # not cached to avoid reference cycle
672 671
673 672 def unfiltered(self):
674 673 """Return unfiltered version of the repository
675 674
676 675 Intended to be overwritten by filtered repo."""
677 676 return self
678 677
679 678 def filtered(self, name, visibilityexceptions=None):
680 679 """Return a filtered version of a repository"""
681 680 cls = repoview.newtype(self.unfiltered().__class__)
682 681 return cls(self, name, visibilityexceptions)
683 682
684 683 @repofilecache('bookmarks', 'bookmarks.current')
685 684 def _bookmarks(self):
686 685 return bookmarks.bmstore(self)
687 686
688 687 @property
689 688 def _activebookmark(self):
690 689 return self._bookmarks.active
691 690
692 691 # _phasesets depend on changelog. what we need is to call
693 692 # _phasecache.invalidate() if '00changelog.i' was changed, but it
694 693 # can't be easily expressed in filecache mechanism.
695 694 @storecache('phaseroots', '00changelog.i')
696 695 def _phasecache(self):
697 696 return phases.phasecache(self, self._phasedefaults)
698 697
699 698 @storecache('obsstore')
700 699 def obsstore(self):
701 700 return obsolete.makestore(self.ui, self)
702 701
703 702 @storecache('00changelog.i')
704 703 def changelog(self):
705 704 return changelog.changelog(self.svfs,
706 705 trypending=txnutil.mayhavepending(self.root))
707 706
708 707 def _constructmanifest(self):
709 708 # This is a temporary function while we migrate from manifest to
710 709 # manifestlog. It allows bundlerepo and unionrepo to intercept the
711 710 # manifest creation.
712 711 return manifest.manifestrevlog(self.svfs)
713 712
714 713 @storecache('00manifest.i')
715 714 def manifestlog(self):
716 715 return manifest.manifestlog(self.svfs, self)
717 716
718 717 @repofilecache('dirstate')
719 718 def dirstate(self):
720 719 sparsematchfn = lambda: sparse.matcher(self)
721 720
722 721 return dirstate.dirstate(self.vfs, self.ui, self.root,
723 722 self._dirstatevalidate, sparsematchfn)
724 723
725 724 def _dirstatevalidate(self, node):
726 725 try:
727 726 self.changelog.rev(node)
728 727 return node
729 728 except error.LookupError:
730 729 if not self._dirstatevalidatewarned:
731 730 self._dirstatevalidatewarned = True
732 731 self.ui.warn(_("warning: ignoring unknown"
733 732 " working parent %s!\n") % short(node))
734 733 return nullid
735 734
736 735 def __getitem__(self, changeid):
737 736 if changeid is None:
738 737 return context.workingctx(self)
739 738 if isinstance(changeid, slice):
740 739 # wdirrev isn't contiguous so the slice shouldn't include it
741 740 return [context.changectx(self, i)
742 741 for i in xrange(*changeid.indices(len(self)))
743 742 if i not in self.changelog.filteredrevs]
744 743 try:
745 744 return context.changectx(self, changeid)
746 745 except error.WdirUnsupported:
747 746 return context.workingctx(self)
748 747
749 748 def __contains__(self, changeid):
750 749 """True if the given changeid exists
751 750
752 751 error.LookupError is raised if an ambiguous node specified.
753 752 """
754 753 try:
755 754 self[changeid]
756 755 return True
757 756 except error.RepoLookupError:
758 757 return False
759 758
760 759 def __nonzero__(self):
761 760 return True
762 761
763 762 __bool__ = __nonzero__
764 763
765 764 def __len__(self):
766 765 # no need to pay the cost of repoview.changelog
767 766 unfi = self.unfiltered()
768 767 return len(unfi.changelog)
769 768
770 769 def __iter__(self):
771 770 return iter(self.changelog)
772 771
773 772 def revs(self, expr, *args):
774 773 '''Find revisions matching a revset.
775 774
776 775 The revset is specified as a string ``expr`` that may contain
777 776 %-formatting to escape certain types. See ``revsetlang.formatspec``.
778 777
779 778 Revset aliases from the configuration are not expanded. To expand
780 779 user aliases, consider calling ``scmutil.revrange()`` or
781 780 ``repo.anyrevs([expr], user=True)``.
782 781
783 782 Returns a revset.abstractsmartset, which is a list-like interface
784 783 that contains integer revisions.
785 784 '''
786 785 expr = revsetlang.formatspec(expr, *args)
787 786 m = revset.match(None, expr)
788 787 return m(self)
789 788
790 789 def set(self, expr, *args):
791 790 '''Find revisions matching a revset and emit changectx instances.
792 791
793 792 This is a convenience wrapper around ``revs()`` that iterates the
794 793 result and is a generator of changectx instances.
795 794
796 795 Revset aliases from the configuration are not expanded. To expand
797 796 user aliases, consider calling ``scmutil.revrange()``.
798 797 '''
799 798 for r in self.revs(expr, *args):
800 799 yield self[r]
801 800
802 801 def anyrevs(self, specs, user=False, localalias=None):
803 802 '''Find revisions matching one of the given revsets.
804 803
805 804 Revset aliases from the configuration are not expanded by default. To
806 805 expand user aliases, specify ``user=True``. To provide some local
807 806 definitions overriding user aliases, set ``localalias`` to
808 807 ``{name: definitionstring}``.
809 808 '''
810 809 if user:
811 810 m = revset.matchany(self.ui, specs, repo=self,
812 811 localalias=localalias)
813 812 else:
814 813 m = revset.matchany(None, specs, localalias=localalias)
815 814 return m(self)
816 815
817 816 def url(self):
818 817 return 'file:' + self.root
819 818
820 819 def hook(self, name, throw=False, **args):
821 820 """Call a hook, passing this repo instance.
822 821
823 822 This a convenience method to aid invoking hooks. Extensions likely
824 823 won't call this unless they have registered a custom hook or are
825 824 replacing code that is expected to call a hook.
826 825 """
827 826 return hook.hook(self.ui, self, name, throw, **args)
828 827
829 828 @filteredpropertycache
830 829 def _tagscache(self):
831 830 '''Returns a tagscache object that contains various tags related
832 831 caches.'''
833 832
834 833 # This simplifies its cache management by having one decorated
835 834 # function (this one) and the rest simply fetch things from it.
836 835 class tagscache(object):
837 836 def __init__(self):
838 837 # These two define the set of tags for this repository. tags
839 838 # maps tag name to node; tagtypes maps tag name to 'global' or
840 839 # 'local'. (Global tags are defined by .hgtags across all
841 840 # heads, and local tags are defined in .hg/localtags.)
842 841 # They constitute the in-memory cache of tags.
843 842 self.tags = self.tagtypes = None
844 843
845 844 self.nodetagscache = self.tagslist = None
846 845
847 846 cache = tagscache()
848 847 cache.tags, cache.tagtypes = self._findtags()
849 848
850 849 return cache
851 850
852 851 def tags(self):
853 852 '''return a mapping of tag to node'''
854 853 t = {}
855 854 if self.changelog.filteredrevs:
856 855 tags, tt = self._findtags()
857 856 else:
858 857 tags = self._tagscache.tags
859 858 for k, v in tags.iteritems():
860 859 try:
861 860 # ignore tags to unknown nodes
862 861 self.changelog.rev(v)
863 862 t[k] = v
864 863 except (error.LookupError, ValueError):
865 864 pass
866 865 return t
867 866
868 867 def _findtags(self):
869 868 '''Do the hard work of finding tags. Return a pair of dicts
870 869 (tags, tagtypes) where tags maps tag name to node, and tagtypes
871 870 maps tag name to a string like \'global\' or \'local\'.
872 871 Subclasses or extensions are free to add their own tags, but
873 872 should be aware that the returned dicts will be retained for the
874 873 duration of the localrepo object.'''
875 874
876 875 # XXX what tagtype should subclasses/extensions use? Currently
877 876 # mq and bookmarks add tags, but do not set the tagtype at all.
878 877 # Should each extension invent its own tag type? Should there
879 878 # be one tagtype for all such "virtual" tags? Or is the status
880 879 # quo fine?
881 880
882 881
883 882 # map tag name to (node, hist)
884 883 alltags = tagsmod.findglobaltags(self.ui, self)
885 884 # map tag name to tag type
886 885 tagtypes = dict((tag, 'global') for tag in alltags)
887 886
888 887 tagsmod.readlocaltags(self.ui, self, alltags, tagtypes)
889 888
890 889 # Build the return dicts. Have to re-encode tag names because
891 890 # the tags module always uses UTF-8 (in order not to lose info
892 891 # writing to the cache), but the rest of Mercurial wants them in
893 892 # local encoding.
894 893 tags = {}
895 894 for (name, (node, hist)) in alltags.iteritems():
896 895 if node != nullid:
897 896 tags[encoding.tolocal(name)] = node
898 897 tags['tip'] = self.changelog.tip()
899 898 tagtypes = dict([(encoding.tolocal(name), value)
900 899 for (name, value) in tagtypes.iteritems()])
901 900 return (tags, tagtypes)
902 901
903 902 def tagtype(self, tagname):
904 903 '''
905 904 return the type of the given tag. result can be:
906 905
907 906 'local' : a local tag
908 907 'global' : a global tag
909 908 None : tag does not exist
910 909 '''
911 910
912 911 return self._tagscache.tagtypes.get(tagname)
913 912
914 913 def tagslist(self):
915 914 '''return a list of tags ordered by revision'''
916 915 if not self._tagscache.tagslist:
917 916 l = []
918 917 for t, n in self.tags().iteritems():
919 918 l.append((self.changelog.rev(n), t, n))
920 919 self._tagscache.tagslist = [(t, n) for r, t, n in sorted(l)]
921 920
922 921 return self._tagscache.tagslist
923 922
924 923 def nodetags(self, node):
925 924 '''return the tags associated with a node'''
926 925 if not self._tagscache.nodetagscache:
927 926 nodetagscache = {}
928 927 for t, n in self._tagscache.tags.iteritems():
929 928 nodetagscache.setdefault(n, []).append(t)
930 929 for tags in nodetagscache.itervalues():
931 930 tags.sort()
932 931 self._tagscache.nodetagscache = nodetagscache
933 932 return self._tagscache.nodetagscache.get(node, [])
934 933
935 934 def nodebookmarks(self, node):
936 935 """return the list of bookmarks pointing to the specified node"""
937 936 marks = []
938 937 for bookmark, n in self._bookmarks.iteritems():
939 938 if n == node:
940 939 marks.append(bookmark)
941 940 return sorted(marks)
942 941
943 942 def branchmap(self):
944 943 '''returns a dictionary {branch: [branchheads]} with branchheads
945 944 ordered by increasing revision number'''
946 945 branchmap.updatecache(self)
947 946 return self._branchcaches[self.filtername]
948 947
949 948 @unfilteredmethod
950 949 def revbranchcache(self):
951 950 if not self._revbranchcache:
952 951 self._revbranchcache = branchmap.revbranchcache(self.unfiltered())
953 952 return self._revbranchcache
954 953
955 954 def branchtip(self, branch, ignoremissing=False):
956 955 '''return the tip node for a given branch
957 956
958 957 If ignoremissing is True, then this method will not raise an error.
959 958 This is helpful for callers that only expect None for a missing branch
960 959 (e.g. namespace).
961 960
962 961 '''
963 962 try:
964 963 return self.branchmap().branchtip(branch)
965 964 except KeyError:
966 965 if not ignoremissing:
967 966 raise error.RepoLookupError(_("unknown branch '%s'") % branch)
968 967 else:
969 968 pass
970 969
971 970 def lookup(self, key):
972 971 return self[key].node()
973 972
974 973 def lookupbranch(self, key, remote=None):
975 974 repo = remote or self
976 975 if key in repo.branchmap():
977 976 return key
978 977
979 978 repo = (remote and remote.local()) and remote or self
980 979 return repo[key].branch()
981 980
982 981 def known(self, nodes):
983 982 cl = self.changelog
984 983 nm = cl.nodemap
985 984 filtered = cl.filteredrevs
986 985 result = []
987 986 for n in nodes:
988 987 r = nm.get(n)
989 988 resp = not (r is None or r in filtered)
990 989 result.append(resp)
991 990 return result
992 991
993 992 def local(self):
994 993 return self
995 994
996 995 def publishing(self):
997 996 # it's safe (and desirable) to trust the publish flag unconditionally
998 997 # so that we don't finalize changes shared between users via ssh or nfs
999 998 return self.ui.configbool('phases', 'publish', untrusted=True)
1000 999
1001 1000 def cancopy(self):
1002 1001 # so statichttprepo's override of local() works
1003 1002 if not self.local():
1004 1003 return False
1005 1004 if not self.publishing():
1006 1005 return True
1007 1006 # if publishing we can't copy if there is filtered content
1008 1007 return not self.filtered('visible').changelog.filteredrevs
1009 1008
1010 1009 def shared(self):
1011 1010 '''the type of shared repository (None if not shared)'''
1012 1011 if self.sharedpath != self.path:
1013 1012 return 'store'
1014 1013 return None
1015 1014
1016 1015 def wjoin(self, f, *insidef):
1017 1016 return self.vfs.reljoin(self.root, f, *insidef)
1018 1017
1019 1018 def file(self, f):
1020 1019 if f[0] == '/':
1021 1020 f = f[1:]
1022 1021 return filelog.filelog(self.svfs, f)
1023 1022
1024 1023 def changectx(self, changeid):
1025 1024 return self[changeid]
1026 1025
1027 1026 def setparents(self, p1, p2=nullid):
1028 1027 with self.dirstate.parentchange():
1029 1028 copies = self.dirstate.setparents(p1, p2)
1030 1029 pctx = self[p1]
1031 1030 if copies:
1032 1031 # Adjust copy records, the dirstate cannot do it, it
1033 1032 # requires access to parents manifests. Preserve them
1034 1033 # only for entries added to first parent.
1035 1034 for f in copies:
1036 1035 if f not in pctx and copies[f] in pctx:
1037 1036 self.dirstate.copy(copies[f], f)
1038 1037 if p2 == nullid:
1039 1038 for f, s in sorted(self.dirstate.copies().items()):
1040 1039 if f not in pctx and s not in pctx:
1041 1040 self.dirstate.copy(None, f)
1042 1041
1043 1042 def filectx(self, path, changeid=None, fileid=None):
1044 1043 """changeid can be a changeset revision, node, or tag.
1045 1044 fileid can be a file revision or node."""
1046 1045 return context.filectx(self, path, changeid, fileid)
1047 1046
1048 1047 def getcwd(self):
1049 1048 return self.dirstate.getcwd()
1050 1049
1051 1050 def pathto(self, f, cwd=None):
1052 1051 return self.dirstate.pathto(f, cwd)
1053 1052
1054 1053 def _loadfilter(self, filter):
1055 1054 if filter not in self.filterpats:
1056 1055 l = []
1057 1056 for pat, cmd in self.ui.configitems(filter):
1058 1057 if cmd == '!':
1059 1058 continue
1060 1059 mf = matchmod.match(self.root, '', [pat])
1061 1060 fn = None
1062 1061 params = cmd
1063 1062 for name, filterfn in self._datafilters.iteritems():
1064 1063 if cmd.startswith(name):
1065 1064 fn = filterfn
1066 1065 params = cmd[len(name):].lstrip()
1067 1066 break
1068 1067 if not fn:
1069 1068 fn = lambda s, c, **kwargs: util.filter(s, c)
1070 1069 # Wrap old filters not supporting keyword arguments
1071 if not inspect.getargspec(fn)[2]:
1070 if not pycompat.getargspec(fn)[2]:
1072 1071 oldfn = fn
1073 1072 fn = lambda s, c, **kwargs: oldfn(s, c)
1074 1073 l.append((mf, fn, params))
1075 1074 self.filterpats[filter] = l
1076 1075 return self.filterpats[filter]
1077 1076
1078 1077 def _filter(self, filterpats, filename, data):
1079 1078 for mf, fn, cmd in filterpats:
1080 1079 if mf(filename):
1081 1080 self.ui.debug("filtering %s through %s\n" % (filename, cmd))
1082 1081 data = fn(data, cmd, ui=self.ui, repo=self, filename=filename)
1083 1082 break
1084 1083
1085 1084 return data
1086 1085
1087 1086 @unfilteredpropertycache
1088 1087 def _encodefilterpats(self):
1089 1088 return self._loadfilter('encode')
1090 1089
1091 1090 @unfilteredpropertycache
1092 1091 def _decodefilterpats(self):
1093 1092 return self._loadfilter('decode')
1094 1093
1095 1094 def adddatafilter(self, name, filter):
1096 1095 self._datafilters[name] = filter
1097 1096
1098 1097 def wread(self, filename):
1099 1098 if self.wvfs.islink(filename):
1100 1099 data = self.wvfs.readlink(filename)
1101 1100 else:
1102 1101 data = self.wvfs.read(filename)
1103 1102 return self._filter(self._encodefilterpats, filename, data)
1104 1103
1105 1104 def wwrite(self, filename, data, flags, backgroundclose=False, **kwargs):
1106 1105 """write ``data`` into ``filename`` in the working directory
1107 1106
1108 1107 This returns length of written (maybe decoded) data.
1109 1108 """
1110 1109 data = self._filter(self._decodefilterpats, filename, data)
1111 1110 if 'l' in flags:
1112 1111 self.wvfs.symlink(data, filename)
1113 1112 else:
1114 1113 self.wvfs.write(filename, data, backgroundclose=backgroundclose,
1115 1114 **kwargs)
1116 1115 if 'x' in flags:
1117 1116 self.wvfs.setflags(filename, False, True)
1118 1117 else:
1119 1118 self.wvfs.setflags(filename, False, False)
1120 1119 return len(data)
1121 1120
1122 1121 def wwritedata(self, filename, data):
1123 1122 return self._filter(self._decodefilterpats, filename, data)
1124 1123
1125 1124 def currenttransaction(self):
1126 1125 """return the current transaction or None if non exists"""
1127 1126 if self._transref:
1128 1127 tr = self._transref()
1129 1128 else:
1130 1129 tr = None
1131 1130
1132 1131 if tr and tr.running():
1133 1132 return tr
1134 1133 return None
1135 1134
1136 1135 def transaction(self, desc, report=None):
1137 1136 if (self.ui.configbool('devel', 'all-warnings')
1138 1137 or self.ui.configbool('devel', 'check-locks')):
1139 1138 if self._currentlock(self._lockref) is None:
1140 1139 raise error.ProgrammingError('transaction requires locking')
1141 1140 tr = self.currenttransaction()
1142 1141 if tr is not None:
1143 1142 return tr.nest()
1144 1143
1145 1144 # abort here if the journal already exists
1146 1145 if self.svfs.exists("journal"):
1147 1146 raise error.RepoError(
1148 1147 _("abandoned transaction found"),
1149 1148 hint=_("run 'hg recover' to clean up transaction"))
1150 1149
1151 1150 idbase = "%.40f#%f" % (random.random(), time.time())
1152 1151 ha = hex(hashlib.sha1(idbase).digest())
1153 1152 txnid = 'TXN:' + ha
1154 1153 self.hook('pretxnopen', throw=True, txnname=desc, txnid=txnid)
1155 1154
1156 1155 self._writejournal(desc)
1157 1156 renames = [(vfs, x, undoname(x)) for vfs, x in self._journalfiles()]
1158 1157 if report:
1159 1158 rp = report
1160 1159 else:
1161 1160 rp = self.ui.warn
1162 1161 vfsmap = {'plain': self.vfs} # root of .hg/
1163 1162 # we must avoid cyclic reference between repo and transaction.
1164 1163 reporef = weakref.ref(self)
1165 1164 # Code to track tag movement
1166 1165 #
1167 1166 # Since tags are all handled as file content, it is actually quite hard
1168 1167 # to track these movement from a code perspective. So we fallback to a
1169 1168 # tracking at the repository level. One could envision to track changes
1170 1169 # to the '.hgtags' file through changegroup apply but that fails to
1171 1170 # cope with case where transaction expose new heads without changegroup
1172 1171 # being involved (eg: phase movement).
1173 1172 #
1174 1173 # For now, We gate the feature behind a flag since this likely comes
1175 1174 # with performance impacts. The current code run more often than needed
1176 1175 # and do not use caches as much as it could. The current focus is on
1177 1176 # the behavior of the feature so we disable it by default. The flag
1178 1177 # will be removed when we are happy with the performance impact.
1179 1178 #
1180 1179 # Once this feature is no longer experimental move the following
1181 1180 # documentation to the appropriate help section:
1182 1181 #
1183 1182 # The ``HG_TAG_MOVED`` variable will be set if the transaction touched
1184 1183 # tags (new or changed or deleted tags). In addition the details of
1185 1184 # these changes are made available in a file at:
1186 1185 # ``REPOROOT/.hg/changes/tags.changes``.
1187 1186 # Make sure you check for HG_TAG_MOVED before reading that file as it
1188 1187 # might exist from a previous transaction even if no tag were touched
1189 1188 # in this one. Changes are recorded in a line base format::
1190 1189 #
1191 1190 # <action> <hex-node> <tag-name>\n
1192 1191 #
1193 1192 # Actions are defined as follow:
1194 1193 # "-R": tag is removed,
1195 1194 # "+A": tag is added,
1196 1195 # "-M": tag is moved (old value),
1197 1196 # "+M": tag is moved (new value),
1198 1197 tracktags = lambda x: None
1199 1198 # experimental config: experimental.hook-track-tags
1200 1199 shouldtracktags = self.ui.configbool('experimental', 'hook-track-tags')
1201 1200 if desc != 'strip' and shouldtracktags:
1202 1201 oldheads = self.changelog.headrevs()
1203 1202 def tracktags(tr2):
1204 1203 repo = reporef()
1205 1204 oldfnodes = tagsmod.fnoderevs(repo.ui, repo, oldheads)
1206 1205 newheads = repo.changelog.headrevs()
1207 1206 newfnodes = tagsmod.fnoderevs(repo.ui, repo, newheads)
1208 1207 # notes: we compare lists here.
1209 1208 # As we do it only once buiding set would not be cheaper
1210 1209 changes = tagsmod.difftags(repo.ui, repo, oldfnodes, newfnodes)
1211 1210 if changes:
1212 1211 tr2.hookargs['tag_moved'] = '1'
1213 1212 with repo.vfs('changes/tags.changes', 'w',
1214 1213 atomictemp=True) as changesfile:
1215 1214 # note: we do not register the file to the transaction
1216 1215 # because we needs it to still exist on the transaction
1217 1216 # is close (for txnclose hooks)
1218 1217 tagsmod.writediff(changesfile, changes)
1219 1218 def validate(tr2):
1220 1219 """will run pre-closing hooks"""
1221 1220 # XXX the transaction API is a bit lacking here so we take a hacky
1222 1221 # path for now
1223 1222 #
1224 1223 # We cannot add this as a "pending" hooks since the 'tr.hookargs'
1225 1224 # dict is copied before these run. In addition we needs the data
1226 1225 # available to in memory hooks too.
1227 1226 #
1228 1227 # Moreover, we also need to make sure this runs before txnclose
1229 1228 # hooks and there is no "pending" mechanism that would execute
1230 1229 # logic only if hooks are about to run.
1231 1230 #
1232 1231 # Fixing this limitation of the transaction is also needed to track
1233 1232 # other families of changes (bookmarks, phases, obsolescence).
1234 1233 #
1235 1234 # This will have to be fixed before we remove the experimental
1236 1235 # gating.
1237 1236 tracktags(tr2)
1238 1237 repo = reporef()
1239 1238 if repo.ui.configbool('experimental', 'single-head-per-branch'):
1240 1239 scmutil.enforcesinglehead(repo, tr2, desc)
1241 1240 if hook.hashook(repo.ui, 'pretxnclose-bookmark'):
1242 1241 for name, (old, new) in sorted(tr.changes['bookmarks'].items()):
1243 1242 args = tr.hookargs.copy()
1244 1243 args.update(bookmarks.preparehookargs(name, old, new))
1245 1244 repo.hook('pretxnclose-bookmark', throw=True,
1246 1245 txnname=desc,
1247 1246 **pycompat.strkwargs(args))
1248 1247 if hook.hashook(repo.ui, 'pretxnclose-phase'):
1249 1248 cl = repo.unfiltered().changelog
1250 1249 for rev, (old, new) in tr.changes['phases'].items():
1251 1250 args = tr.hookargs.copy()
1252 1251 node = hex(cl.node(rev))
1253 1252 args.update(phases.preparehookargs(node, old, new))
1254 1253 repo.hook('pretxnclose-phase', throw=True, txnname=desc,
1255 1254 **pycompat.strkwargs(args))
1256 1255
1257 1256 repo.hook('pretxnclose', throw=True,
1258 1257 txnname=desc, **pycompat.strkwargs(tr.hookargs))
1259 1258 def releasefn(tr, success):
1260 1259 repo = reporef()
1261 1260 if success:
1262 1261 # this should be explicitly invoked here, because
1263 1262 # in-memory changes aren't written out at closing
1264 1263 # transaction, if tr.addfilegenerator (via
1265 1264 # dirstate.write or so) isn't invoked while
1266 1265 # transaction running
1267 1266 repo.dirstate.write(None)
1268 1267 else:
1269 1268 # discard all changes (including ones already written
1270 1269 # out) in this transaction
1271 1270 repo.dirstate.restorebackup(None, 'journal.dirstate')
1272 1271
1273 1272 repo.invalidate(clearfilecache=True)
1274 1273
1275 1274 tr = transaction.transaction(rp, self.svfs, vfsmap,
1276 1275 "journal",
1277 1276 "undo",
1278 1277 aftertrans(renames),
1279 1278 self.store.createmode,
1280 1279 validator=validate,
1281 1280 releasefn=releasefn,
1282 1281 checkambigfiles=_cachedfiles)
1283 1282 tr.changes['revs'] = xrange(0, 0)
1284 1283 tr.changes['obsmarkers'] = set()
1285 1284 tr.changes['phases'] = {}
1286 1285 tr.changes['bookmarks'] = {}
1287 1286
1288 1287 tr.hookargs['txnid'] = txnid
1289 1288 # note: writing the fncache only during finalize mean that the file is
1290 1289 # outdated when running hooks. As fncache is used for streaming clone,
1291 1290 # this is not expected to break anything that happen during the hooks.
1292 1291 tr.addfinalize('flush-fncache', self.store.write)
1293 1292 def txnclosehook(tr2):
1294 1293 """To be run if transaction is successful, will schedule a hook run
1295 1294 """
1296 1295 # Don't reference tr2 in hook() so we don't hold a reference.
1297 1296 # This reduces memory consumption when there are multiple
1298 1297 # transactions per lock. This can likely go away if issue5045
1299 1298 # fixes the function accumulation.
1300 1299 hookargs = tr2.hookargs
1301 1300
1302 1301 def hookfunc():
1303 1302 repo = reporef()
1304 1303 if hook.hashook(repo.ui, 'txnclose-bookmark'):
1305 1304 bmchanges = sorted(tr.changes['bookmarks'].items())
1306 1305 for name, (old, new) in bmchanges:
1307 1306 args = tr.hookargs.copy()
1308 1307 args.update(bookmarks.preparehookargs(name, old, new))
1309 1308 repo.hook('txnclose-bookmark', throw=False,
1310 1309 txnname=desc, **pycompat.strkwargs(args))
1311 1310
1312 1311 if hook.hashook(repo.ui, 'txnclose-phase'):
1313 1312 cl = repo.unfiltered().changelog
1314 1313 phasemv = sorted(tr.changes['phases'].items())
1315 1314 for rev, (old, new) in phasemv:
1316 1315 args = tr.hookargs.copy()
1317 1316 node = hex(cl.node(rev))
1318 1317 args.update(phases.preparehookargs(node, old, new))
1319 1318 repo.hook('txnclose-phase', throw=False, txnname=desc,
1320 1319 **pycompat.strkwargs(args))
1321 1320
1322 1321 repo.hook('txnclose', throw=False, txnname=desc,
1323 1322 **pycompat.strkwargs(hookargs))
1324 1323 reporef()._afterlock(hookfunc)
1325 1324 tr.addfinalize('txnclose-hook', txnclosehook)
1326 1325 # Include a leading "-" to make it happen before the transaction summary
1327 1326 # reports registered via scmutil.registersummarycallback() whose names
1328 1327 # are 00-txnreport etc. That way, the caches will be warm when the
1329 1328 # callbacks run.
1330 1329 tr.addpostclose('-warm-cache', self._buildcacheupdater(tr))
1331 1330 def txnaborthook(tr2):
1332 1331 """To be run if transaction is aborted
1333 1332 """
1334 1333 reporef().hook('txnabort', throw=False, txnname=desc,
1335 1334 **pycompat.strkwargs(tr2.hookargs))
1336 1335 tr.addabort('txnabort-hook', txnaborthook)
1337 1336 # avoid eager cache invalidation. in-memory data should be identical
1338 1337 # to stored data if transaction has no error.
1339 1338 tr.addpostclose('refresh-filecachestats', self._refreshfilecachestats)
1340 1339 self._transref = weakref.ref(tr)
1341 1340 scmutil.registersummarycallback(self, tr, desc)
1342 1341 return tr
1343 1342
1344 1343 def _journalfiles(self):
1345 1344 return ((self.svfs, 'journal'),
1346 1345 (self.vfs, 'journal.dirstate'),
1347 1346 (self.vfs, 'journal.branch'),
1348 1347 (self.vfs, 'journal.desc'),
1349 1348 (self.vfs, 'journal.bookmarks'),
1350 1349 (self.svfs, 'journal.phaseroots'))
1351 1350
1352 1351 def undofiles(self):
1353 1352 return [(vfs, undoname(x)) for vfs, x in self._journalfiles()]
1354 1353
1355 1354 @unfilteredmethod
1356 1355 def _writejournal(self, desc):
1357 1356 self.dirstate.savebackup(None, 'journal.dirstate')
1358 1357 self.vfs.write("journal.branch",
1359 1358 encoding.fromlocal(self.dirstate.branch()))
1360 1359 self.vfs.write("journal.desc",
1361 1360 "%d\n%s\n" % (len(self), desc))
1362 1361 self.vfs.write("journal.bookmarks",
1363 1362 self.vfs.tryread("bookmarks"))
1364 1363 self.svfs.write("journal.phaseroots",
1365 1364 self.svfs.tryread("phaseroots"))
1366 1365
1367 1366 def recover(self):
1368 1367 with self.lock():
1369 1368 if self.svfs.exists("journal"):
1370 1369 self.ui.status(_("rolling back interrupted transaction\n"))
1371 1370 vfsmap = {'': self.svfs,
1372 1371 'plain': self.vfs,}
1373 1372 transaction.rollback(self.svfs, vfsmap, "journal",
1374 1373 self.ui.warn,
1375 1374 checkambigfiles=_cachedfiles)
1376 1375 self.invalidate()
1377 1376 return True
1378 1377 else:
1379 1378 self.ui.warn(_("no interrupted transaction available\n"))
1380 1379 return False
1381 1380
1382 1381 def rollback(self, dryrun=False, force=False):
1383 1382 wlock = lock = dsguard = None
1384 1383 try:
1385 1384 wlock = self.wlock()
1386 1385 lock = self.lock()
1387 1386 if self.svfs.exists("undo"):
1388 1387 dsguard = dirstateguard.dirstateguard(self, 'rollback')
1389 1388
1390 1389 return self._rollback(dryrun, force, dsguard)
1391 1390 else:
1392 1391 self.ui.warn(_("no rollback information available\n"))
1393 1392 return 1
1394 1393 finally:
1395 1394 release(dsguard, lock, wlock)
1396 1395
1397 1396 @unfilteredmethod # Until we get smarter cache management
1398 1397 def _rollback(self, dryrun, force, dsguard):
1399 1398 ui = self.ui
1400 1399 try:
1401 1400 args = self.vfs.read('undo.desc').splitlines()
1402 1401 (oldlen, desc, detail) = (int(args[0]), args[1], None)
1403 1402 if len(args) >= 3:
1404 1403 detail = args[2]
1405 1404 oldtip = oldlen - 1
1406 1405
1407 1406 if detail and ui.verbose:
1408 1407 msg = (_('repository tip rolled back to revision %d'
1409 1408 ' (undo %s: %s)\n')
1410 1409 % (oldtip, desc, detail))
1411 1410 else:
1412 1411 msg = (_('repository tip rolled back to revision %d'
1413 1412 ' (undo %s)\n')
1414 1413 % (oldtip, desc))
1415 1414 except IOError:
1416 1415 msg = _('rolling back unknown transaction\n')
1417 1416 desc = None
1418 1417
1419 1418 if not force and self['.'] != self['tip'] and desc == 'commit':
1420 1419 raise error.Abort(
1421 1420 _('rollback of last commit while not checked out '
1422 1421 'may lose data'), hint=_('use -f to force'))
1423 1422
1424 1423 ui.status(msg)
1425 1424 if dryrun:
1426 1425 return 0
1427 1426
1428 1427 parents = self.dirstate.parents()
1429 1428 self.destroying()
1430 1429 vfsmap = {'plain': self.vfs, '': self.svfs}
1431 1430 transaction.rollback(self.svfs, vfsmap, 'undo', ui.warn,
1432 1431 checkambigfiles=_cachedfiles)
1433 1432 if self.vfs.exists('undo.bookmarks'):
1434 1433 self.vfs.rename('undo.bookmarks', 'bookmarks', checkambig=True)
1435 1434 if self.svfs.exists('undo.phaseroots'):
1436 1435 self.svfs.rename('undo.phaseroots', 'phaseroots', checkambig=True)
1437 1436 self.invalidate()
1438 1437
1439 1438 parentgone = (parents[0] not in self.changelog.nodemap or
1440 1439 parents[1] not in self.changelog.nodemap)
1441 1440 if parentgone:
1442 1441 # prevent dirstateguard from overwriting already restored one
1443 1442 dsguard.close()
1444 1443
1445 1444 self.dirstate.restorebackup(None, 'undo.dirstate')
1446 1445 try:
1447 1446 branch = self.vfs.read('undo.branch')
1448 1447 self.dirstate.setbranch(encoding.tolocal(branch))
1449 1448 except IOError:
1450 1449 ui.warn(_('named branch could not be reset: '
1451 1450 'current branch is still \'%s\'\n')
1452 1451 % self.dirstate.branch())
1453 1452
1454 1453 parents = tuple([p.rev() for p in self[None].parents()])
1455 1454 if len(parents) > 1:
1456 1455 ui.status(_('working directory now based on '
1457 1456 'revisions %d and %d\n') % parents)
1458 1457 else:
1459 1458 ui.status(_('working directory now based on '
1460 1459 'revision %d\n') % parents)
1461 1460 mergemod.mergestate.clean(self, self['.'].node())
1462 1461
1463 1462 # TODO: if we know which new heads may result from this rollback, pass
1464 1463 # them to destroy(), which will prevent the branchhead cache from being
1465 1464 # invalidated.
1466 1465 self.destroyed()
1467 1466 return 0
1468 1467
1469 1468 def _buildcacheupdater(self, newtransaction):
1470 1469 """called during transaction to build the callback updating cache
1471 1470
1472 1471 Lives on the repository to help extension who might want to augment
1473 1472 this logic. For this purpose, the created transaction is passed to the
1474 1473 method.
1475 1474 """
1476 1475 # we must avoid cyclic reference between repo and transaction.
1477 1476 reporef = weakref.ref(self)
1478 1477 def updater(tr):
1479 1478 repo = reporef()
1480 1479 repo.updatecaches(tr)
1481 1480 return updater
1482 1481
1483 1482 @unfilteredmethod
1484 1483 def updatecaches(self, tr=None):
1485 1484 """warm appropriate caches
1486 1485
1487 1486 If this function is called after a transaction closed. The transaction
1488 1487 will be available in the 'tr' argument. This can be used to selectively
1489 1488 update caches relevant to the changes in that transaction.
1490 1489 """
1491 1490 if tr is not None and tr.hookargs.get('source') == 'strip':
1492 1491 # During strip, many caches are invalid but
1493 1492 # later call to `destroyed` will refresh them.
1494 1493 return
1495 1494
1496 1495 if tr is None or tr.changes['revs']:
1497 1496 # updating the unfiltered branchmap should refresh all the others,
1498 1497 self.ui.debug('updating the branch cache\n')
1499 1498 branchmap.updatecache(self.filtered('served'))
1500 1499
1501 1500 def invalidatecaches(self):
1502 1501
1503 1502 if '_tagscache' in vars(self):
1504 1503 # can't use delattr on proxy
1505 1504 del self.__dict__['_tagscache']
1506 1505
1507 1506 self.unfiltered()._branchcaches.clear()
1508 1507 self.invalidatevolatilesets()
1509 1508 self._sparsesignaturecache.clear()
1510 1509
1511 1510 def invalidatevolatilesets(self):
1512 1511 self.filteredrevcache.clear()
1513 1512 obsolete.clearobscaches(self)
1514 1513
1515 1514 def invalidatedirstate(self):
1516 1515 '''Invalidates the dirstate, causing the next call to dirstate
1517 1516 to check if it was modified since the last time it was read,
1518 1517 rereading it if it has.
1519 1518
1520 1519 This is different to dirstate.invalidate() that it doesn't always
1521 1520 rereads the dirstate. Use dirstate.invalidate() if you want to
1522 1521 explicitly read the dirstate again (i.e. restoring it to a previous
1523 1522 known good state).'''
1524 1523 if hasunfilteredcache(self, 'dirstate'):
1525 1524 for k in self.dirstate._filecache:
1526 1525 try:
1527 1526 delattr(self.dirstate, k)
1528 1527 except AttributeError:
1529 1528 pass
1530 1529 delattr(self.unfiltered(), 'dirstate')
1531 1530
1532 1531 def invalidate(self, clearfilecache=False):
1533 1532 '''Invalidates both store and non-store parts other than dirstate
1534 1533
1535 1534 If a transaction is running, invalidation of store is omitted,
1536 1535 because discarding in-memory changes might cause inconsistency
1537 1536 (e.g. incomplete fncache causes unintentional failure, but
1538 1537 redundant one doesn't).
1539 1538 '''
1540 1539 unfiltered = self.unfiltered() # all file caches are stored unfiltered
1541 1540 for k in list(self._filecache.keys()):
1542 1541 # dirstate is invalidated separately in invalidatedirstate()
1543 1542 if k == 'dirstate':
1544 1543 continue
1545 1544 if (k == 'changelog' and
1546 1545 self.currenttransaction() and
1547 1546 self.changelog._delayed):
1548 1547 # The changelog object may store unwritten revisions. We don't
1549 1548 # want to lose them.
1550 1549 # TODO: Solve the problem instead of working around it.
1551 1550 continue
1552 1551
1553 1552 if clearfilecache:
1554 1553 del self._filecache[k]
1555 1554 try:
1556 1555 delattr(unfiltered, k)
1557 1556 except AttributeError:
1558 1557 pass
1559 1558 self.invalidatecaches()
1560 1559 if not self.currenttransaction():
1561 1560 # TODO: Changing contents of store outside transaction
1562 1561 # causes inconsistency. We should make in-memory store
1563 1562 # changes detectable, and abort if changed.
1564 1563 self.store.invalidatecaches()
1565 1564
1566 1565 def invalidateall(self):
1567 1566 '''Fully invalidates both store and non-store parts, causing the
1568 1567 subsequent operation to reread any outside changes.'''
1569 1568 # extension should hook this to invalidate its caches
1570 1569 self.invalidate()
1571 1570 self.invalidatedirstate()
1572 1571
1573 1572 @unfilteredmethod
1574 1573 def _refreshfilecachestats(self, tr):
1575 1574 """Reload stats of cached files so that they are flagged as valid"""
1576 1575 for k, ce in self._filecache.items():
1577 1576 k = pycompat.sysstr(k)
1578 1577 if k == r'dirstate' or k not in self.__dict__:
1579 1578 continue
1580 1579 ce.refresh()
1581 1580
1582 1581 def _lock(self, vfs, lockname, wait, releasefn, acquirefn, desc,
1583 1582 inheritchecker=None, parentenvvar=None):
1584 1583 parentlock = None
1585 1584 # the contents of parentenvvar are used by the underlying lock to
1586 1585 # determine whether it can be inherited
1587 1586 if parentenvvar is not None:
1588 1587 parentlock = encoding.environ.get(parentenvvar)
1589 1588
1590 1589 timeout = 0
1591 1590 warntimeout = 0
1592 1591 if wait:
1593 1592 timeout = self.ui.configint("ui", "timeout")
1594 1593 warntimeout = self.ui.configint("ui", "timeout.warn")
1595 1594
1596 1595 l = lockmod.trylock(self.ui, vfs, lockname, timeout, warntimeout,
1597 1596 releasefn=releasefn,
1598 1597 acquirefn=acquirefn, desc=desc,
1599 1598 inheritchecker=inheritchecker,
1600 1599 parentlock=parentlock)
1601 1600 return l
1602 1601
1603 1602 def _afterlock(self, callback):
1604 1603 """add a callback to be run when the repository is fully unlocked
1605 1604
1606 1605 The callback will be executed when the outermost lock is released
1607 1606 (with wlock being higher level than 'lock')."""
1608 1607 for ref in (self._wlockref, self._lockref):
1609 1608 l = ref and ref()
1610 1609 if l and l.held:
1611 1610 l.postrelease.append(callback)
1612 1611 break
1613 1612 else: # no lock have been found.
1614 1613 callback()
1615 1614
1616 1615 def lock(self, wait=True):
1617 1616 '''Lock the repository store (.hg/store) and return a weak reference
1618 1617 to the lock. Use this before modifying the store (e.g. committing or
1619 1618 stripping). If you are opening a transaction, get a lock as well.)
1620 1619
1621 1620 If both 'lock' and 'wlock' must be acquired, ensure you always acquires
1622 1621 'wlock' first to avoid a dead-lock hazard.'''
1623 1622 l = self._currentlock(self._lockref)
1624 1623 if l is not None:
1625 1624 l.lock()
1626 1625 return l
1627 1626
1628 1627 l = self._lock(self.svfs, "lock", wait, None,
1629 1628 self.invalidate, _('repository %s') % self.origroot)
1630 1629 self._lockref = weakref.ref(l)
1631 1630 return l
1632 1631
1633 1632 def _wlockchecktransaction(self):
1634 1633 if self.currenttransaction() is not None:
1635 1634 raise error.LockInheritanceContractViolation(
1636 1635 'wlock cannot be inherited in the middle of a transaction')
1637 1636
1638 1637 def wlock(self, wait=True):
1639 1638 '''Lock the non-store parts of the repository (everything under
1640 1639 .hg except .hg/store) and return a weak reference to the lock.
1641 1640
1642 1641 Use this before modifying files in .hg.
1643 1642
1644 1643 If both 'lock' and 'wlock' must be acquired, ensure you always acquires
1645 1644 'wlock' first to avoid a dead-lock hazard.'''
1646 1645 l = self._wlockref and self._wlockref()
1647 1646 if l is not None and l.held:
1648 1647 l.lock()
1649 1648 return l
1650 1649
1651 1650 # We do not need to check for non-waiting lock acquisition. Such
1652 1651 # acquisition would not cause dead-lock as they would just fail.
1653 1652 if wait and (self.ui.configbool('devel', 'all-warnings')
1654 1653 or self.ui.configbool('devel', 'check-locks')):
1655 1654 if self._currentlock(self._lockref) is not None:
1656 1655 self.ui.develwarn('"wlock" acquired after "lock"')
1657 1656
1658 1657 def unlock():
1659 1658 if self.dirstate.pendingparentchange():
1660 1659 self.dirstate.invalidate()
1661 1660 else:
1662 1661 self.dirstate.write(None)
1663 1662
1664 1663 self._filecache['dirstate'].refresh()
1665 1664
1666 1665 l = self._lock(self.vfs, "wlock", wait, unlock,
1667 1666 self.invalidatedirstate, _('working directory of %s') %
1668 1667 self.origroot,
1669 1668 inheritchecker=self._wlockchecktransaction,
1670 1669 parentenvvar='HG_WLOCK_LOCKER')
1671 1670 self._wlockref = weakref.ref(l)
1672 1671 return l
1673 1672
1674 1673 def _currentlock(self, lockref):
1675 1674 """Returns the lock if it's held, or None if it's not."""
1676 1675 if lockref is None:
1677 1676 return None
1678 1677 l = lockref()
1679 1678 if l is None or not l.held:
1680 1679 return None
1681 1680 return l
1682 1681
1683 1682 def currentwlock(self):
1684 1683 """Returns the wlock if it's held, or None if it's not."""
1685 1684 return self._currentlock(self._wlockref)
1686 1685
1687 1686 def _filecommit(self, fctx, manifest1, manifest2, linkrev, tr, changelist):
1688 1687 """
1689 1688 commit an individual file as part of a larger transaction
1690 1689 """
1691 1690
1692 1691 fname = fctx.path()
1693 1692 fparent1 = manifest1.get(fname, nullid)
1694 1693 fparent2 = manifest2.get(fname, nullid)
1695 1694 if isinstance(fctx, context.filectx):
1696 1695 node = fctx.filenode()
1697 1696 if node in [fparent1, fparent2]:
1698 1697 self.ui.debug('reusing %s filelog entry\n' % fname)
1699 1698 if manifest1.flags(fname) != fctx.flags():
1700 1699 changelist.append(fname)
1701 1700 return node
1702 1701
1703 1702 flog = self.file(fname)
1704 1703 meta = {}
1705 1704 copy = fctx.renamed()
1706 1705 if copy and copy[0] != fname:
1707 1706 # Mark the new revision of this file as a copy of another
1708 1707 # file. This copy data will effectively act as a parent
1709 1708 # of this new revision. If this is a merge, the first
1710 1709 # parent will be the nullid (meaning "look up the copy data")
1711 1710 # and the second one will be the other parent. For example:
1712 1711 #
1713 1712 # 0 --- 1 --- 3 rev1 changes file foo
1714 1713 # \ / rev2 renames foo to bar and changes it
1715 1714 # \- 2 -/ rev3 should have bar with all changes and
1716 1715 # should record that bar descends from
1717 1716 # bar in rev2 and foo in rev1
1718 1717 #
1719 1718 # this allows this merge to succeed:
1720 1719 #
1721 1720 # 0 --- 1 --- 3 rev4 reverts the content change from rev2
1722 1721 # \ / merging rev3 and rev4 should use bar@rev2
1723 1722 # \- 2 --- 4 as the merge base
1724 1723 #
1725 1724
1726 1725 cfname = copy[0]
1727 1726 crev = manifest1.get(cfname)
1728 1727 newfparent = fparent2
1729 1728
1730 1729 if manifest2: # branch merge
1731 1730 if fparent2 == nullid or crev is None: # copied on remote side
1732 1731 if cfname in manifest2:
1733 1732 crev = manifest2[cfname]
1734 1733 newfparent = fparent1
1735 1734
1736 1735 # Here, we used to search backwards through history to try to find
1737 1736 # where the file copy came from if the source of a copy was not in
1738 1737 # the parent directory. However, this doesn't actually make sense to
1739 1738 # do (what does a copy from something not in your working copy even
1740 1739 # mean?) and it causes bugs (eg, issue4476). Instead, we will warn
1741 1740 # the user that copy information was dropped, so if they didn't
1742 1741 # expect this outcome it can be fixed, but this is the correct
1743 1742 # behavior in this circumstance.
1744 1743
1745 1744 if crev:
1746 1745 self.ui.debug(" %s: copy %s:%s\n" % (fname, cfname, hex(crev)))
1747 1746 meta["copy"] = cfname
1748 1747 meta["copyrev"] = hex(crev)
1749 1748 fparent1, fparent2 = nullid, newfparent
1750 1749 else:
1751 1750 self.ui.warn(_("warning: can't find ancestor for '%s' "
1752 1751 "copied from '%s'!\n") % (fname, cfname))
1753 1752
1754 1753 elif fparent1 == nullid:
1755 1754 fparent1, fparent2 = fparent2, nullid
1756 1755 elif fparent2 != nullid:
1757 1756 # is one parent an ancestor of the other?
1758 1757 fparentancestors = flog.commonancestorsheads(fparent1, fparent2)
1759 1758 if fparent1 in fparentancestors:
1760 1759 fparent1, fparent2 = fparent2, nullid
1761 1760 elif fparent2 in fparentancestors:
1762 1761 fparent2 = nullid
1763 1762
1764 1763 # is the file changed?
1765 1764 text = fctx.data()
1766 1765 if fparent2 != nullid or flog.cmp(fparent1, text) or meta:
1767 1766 changelist.append(fname)
1768 1767 return flog.add(text, meta, tr, linkrev, fparent1, fparent2)
1769 1768 # are just the flags changed during merge?
1770 1769 elif fname in manifest1 and manifest1.flags(fname) != fctx.flags():
1771 1770 changelist.append(fname)
1772 1771
1773 1772 return fparent1
1774 1773
1775 1774 def checkcommitpatterns(self, wctx, vdirs, match, status, fail):
1776 1775 """check for commit arguments that aren't committable"""
1777 1776 if match.isexact() or match.prefix():
1778 1777 matched = set(status.modified + status.added + status.removed)
1779 1778
1780 1779 for f in match.files():
1781 1780 f = self.dirstate.normalize(f)
1782 1781 if f == '.' or f in matched or f in wctx.substate:
1783 1782 continue
1784 1783 if f in status.deleted:
1785 1784 fail(f, _('file not found!'))
1786 1785 if f in vdirs: # visited directory
1787 1786 d = f + '/'
1788 1787 for mf in matched:
1789 1788 if mf.startswith(d):
1790 1789 break
1791 1790 else:
1792 1791 fail(f, _("no match under directory!"))
1793 1792 elif f not in self.dirstate:
1794 1793 fail(f, _("file not tracked!"))
1795 1794
1796 1795 @unfilteredmethod
1797 1796 def commit(self, text="", user=None, date=None, match=None, force=False,
1798 1797 editor=False, extra=None):
1799 1798 """Add a new revision to current repository.
1800 1799
1801 1800 Revision information is gathered from the working directory,
1802 1801 match can be used to filter the committed files. If editor is
1803 1802 supplied, it is called to get a commit message.
1804 1803 """
1805 1804 if extra is None:
1806 1805 extra = {}
1807 1806
1808 1807 def fail(f, msg):
1809 1808 raise error.Abort('%s: %s' % (f, msg))
1810 1809
1811 1810 if not match:
1812 1811 match = matchmod.always(self.root, '')
1813 1812
1814 1813 if not force:
1815 1814 vdirs = []
1816 1815 match.explicitdir = vdirs.append
1817 1816 match.bad = fail
1818 1817
1819 1818 wlock = lock = tr = None
1820 1819 try:
1821 1820 wlock = self.wlock()
1822 1821 lock = self.lock() # for recent changelog (see issue4368)
1823 1822
1824 1823 wctx = self[None]
1825 1824 merge = len(wctx.parents()) > 1
1826 1825
1827 1826 if not force and merge and not match.always():
1828 1827 raise error.Abort(_('cannot partially commit a merge '
1829 1828 '(do not specify files or patterns)'))
1830 1829
1831 1830 status = self.status(match=match, clean=force)
1832 1831 if force:
1833 1832 status.modified.extend(status.clean) # mq may commit clean files
1834 1833
1835 1834 # check subrepos
1836 1835 subs, commitsubs, newstate = subrepoutil.precommit(
1837 1836 self.ui, wctx, status, match, force=force)
1838 1837
1839 1838 # make sure all explicit patterns are matched
1840 1839 if not force:
1841 1840 self.checkcommitpatterns(wctx, vdirs, match, status, fail)
1842 1841
1843 1842 cctx = context.workingcommitctx(self, status,
1844 1843 text, user, date, extra)
1845 1844
1846 1845 # internal config: ui.allowemptycommit
1847 1846 allowemptycommit = (wctx.branch() != wctx.p1().branch()
1848 1847 or extra.get('close') or merge or cctx.files()
1849 1848 or self.ui.configbool('ui', 'allowemptycommit'))
1850 1849 if not allowemptycommit:
1851 1850 return None
1852 1851
1853 1852 if merge and cctx.deleted():
1854 1853 raise error.Abort(_("cannot commit merge with missing files"))
1855 1854
1856 1855 ms = mergemod.mergestate.read(self)
1857 1856 mergeutil.checkunresolved(ms)
1858 1857
1859 1858 if editor:
1860 1859 cctx._text = editor(self, cctx, subs)
1861 1860 edited = (text != cctx._text)
1862 1861
1863 1862 # Save commit message in case this transaction gets rolled back
1864 1863 # (e.g. by a pretxncommit hook). Leave the content alone on
1865 1864 # the assumption that the user will use the same editor again.
1866 1865 msgfn = self.savecommitmessage(cctx._text)
1867 1866
1868 1867 # commit subs and write new state
1869 1868 if subs:
1870 1869 for s in sorted(commitsubs):
1871 1870 sub = wctx.sub(s)
1872 1871 self.ui.status(_('committing subrepository %s\n') %
1873 1872 subrepoutil.subrelpath(sub))
1874 1873 sr = sub.commit(cctx._text, user, date)
1875 1874 newstate[s] = (newstate[s][0], sr)
1876 1875 subrepoutil.writestate(self, newstate)
1877 1876
1878 1877 p1, p2 = self.dirstate.parents()
1879 1878 hookp1, hookp2 = hex(p1), (p2 != nullid and hex(p2) or '')
1880 1879 try:
1881 1880 self.hook("precommit", throw=True, parent1=hookp1,
1882 1881 parent2=hookp2)
1883 1882 tr = self.transaction('commit')
1884 1883 ret = self.commitctx(cctx, True)
1885 1884 except: # re-raises
1886 1885 if edited:
1887 1886 self.ui.write(
1888 1887 _('note: commit message saved in %s\n') % msgfn)
1889 1888 raise
1890 1889 # update bookmarks, dirstate and mergestate
1891 1890 bookmarks.update(self, [p1, p2], ret)
1892 1891 cctx.markcommitted(ret)
1893 1892 ms.reset()
1894 1893 tr.close()
1895 1894
1896 1895 finally:
1897 1896 lockmod.release(tr, lock, wlock)
1898 1897
1899 1898 def commithook(node=hex(ret), parent1=hookp1, parent2=hookp2):
1900 1899 # hack for command that use a temporary commit (eg: histedit)
1901 1900 # temporary commit got stripped before hook release
1902 1901 if self.changelog.hasnode(ret):
1903 1902 self.hook("commit", node=node, parent1=parent1,
1904 1903 parent2=parent2)
1905 1904 self._afterlock(commithook)
1906 1905 return ret
1907 1906
1908 1907 @unfilteredmethod
1909 1908 def commitctx(self, ctx, error=False):
1910 1909 """Add a new revision to current repository.
1911 1910 Revision information is passed via the context argument.
1912 1911 """
1913 1912
1914 1913 tr = None
1915 1914 p1, p2 = ctx.p1(), ctx.p2()
1916 1915 user = ctx.user()
1917 1916
1918 1917 lock = self.lock()
1919 1918 try:
1920 1919 tr = self.transaction("commit")
1921 1920 trp = weakref.proxy(tr)
1922 1921
1923 1922 if ctx.manifestnode():
1924 1923 # reuse an existing manifest revision
1925 1924 mn = ctx.manifestnode()
1926 1925 files = ctx.files()
1927 1926 elif ctx.files():
1928 1927 m1ctx = p1.manifestctx()
1929 1928 m2ctx = p2.manifestctx()
1930 1929 mctx = m1ctx.copy()
1931 1930
1932 1931 m = mctx.read()
1933 1932 m1 = m1ctx.read()
1934 1933 m2 = m2ctx.read()
1935 1934
1936 1935 # check in files
1937 1936 added = []
1938 1937 changed = []
1939 1938 removed = list(ctx.removed())
1940 1939 linkrev = len(self)
1941 1940 self.ui.note(_("committing files:\n"))
1942 1941 for f in sorted(ctx.modified() + ctx.added()):
1943 1942 self.ui.note(f + "\n")
1944 1943 try:
1945 1944 fctx = ctx[f]
1946 1945 if fctx is None:
1947 1946 removed.append(f)
1948 1947 else:
1949 1948 added.append(f)
1950 1949 m[f] = self._filecommit(fctx, m1, m2, linkrev,
1951 1950 trp, changed)
1952 1951 m.setflag(f, fctx.flags())
1953 1952 except OSError as inst:
1954 1953 self.ui.warn(_("trouble committing %s!\n") % f)
1955 1954 raise
1956 1955 except IOError as inst:
1957 1956 errcode = getattr(inst, 'errno', errno.ENOENT)
1958 1957 if error or errcode and errcode != errno.ENOENT:
1959 1958 self.ui.warn(_("trouble committing %s!\n") % f)
1960 1959 raise
1961 1960
1962 1961 # update manifest
1963 1962 self.ui.note(_("committing manifest\n"))
1964 1963 removed = [f for f in sorted(removed) if f in m1 or f in m2]
1965 1964 drop = [f for f in removed if f in m]
1966 1965 for f in drop:
1967 1966 del m[f]
1968 1967 mn = mctx.write(trp, linkrev,
1969 1968 p1.manifestnode(), p2.manifestnode(),
1970 1969 added, drop)
1971 1970 files = changed + removed
1972 1971 else:
1973 1972 mn = p1.manifestnode()
1974 1973 files = []
1975 1974
1976 1975 # update changelog
1977 1976 self.ui.note(_("committing changelog\n"))
1978 1977 self.changelog.delayupdate(tr)
1979 1978 n = self.changelog.add(mn, files, ctx.description(),
1980 1979 trp, p1.node(), p2.node(),
1981 1980 user, ctx.date(), ctx.extra().copy())
1982 1981 xp1, xp2 = p1.hex(), p2 and p2.hex() or ''
1983 1982 self.hook('pretxncommit', throw=True, node=hex(n), parent1=xp1,
1984 1983 parent2=xp2)
1985 1984 # set the new commit is proper phase
1986 1985 targetphase = subrepoutil.newcommitphase(self.ui, ctx)
1987 1986 if targetphase:
1988 1987 # retract boundary do not alter parent changeset.
1989 1988 # if a parent have higher the resulting phase will
1990 1989 # be compliant anyway
1991 1990 #
1992 1991 # if minimal phase was 0 we don't need to retract anything
1993 1992 phases.registernew(self, tr, targetphase, [n])
1994 1993 tr.close()
1995 1994 return n
1996 1995 finally:
1997 1996 if tr:
1998 1997 tr.release()
1999 1998 lock.release()
2000 1999
2001 2000 @unfilteredmethod
2002 2001 def destroying(self):
2003 2002 '''Inform the repository that nodes are about to be destroyed.
2004 2003 Intended for use by strip and rollback, so there's a common
2005 2004 place for anything that has to be done before destroying history.
2006 2005
2007 2006 This is mostly useful for saving state that is in memory and waiting
2008 2007 to be flushed when the current lock is released. Because a call to
2009 2008 destroyed is imminent, the repo will be invalidated causing those
2010 2009 changes to stay in memory (waiting for the next unlock), or vanish
2011 2010 completely.
2012 2011 '''
2013 2012 # When using the same lock to commit and strip, the phasecache is left
2014 2013 # dirty after committing. Then when we strip, the repo is invalidated,
2015 2014 # causing those changes to disappear.
2016 2015 if '_phasecache' in vars(self):
2017 2016 self._phasecache.write()
2018 2017
2019 2018 @unfilteredmethod
2020 2019 def destroyed(self):
2021 2020 '''Inform the repository that nodes have been destroyed.
2022 2021 Intended for use by strip and rollback, so there's a common
2023 2022 place for anything that has to be done after destroying history.
2024 2023 '''
2025 2024 # When one tries to:
2026 2025 # 1) destroy nodes thus calling this method (e.g. strip)
2027 2026 # 2) use phasecache somewhere (e.g. commit)
2028 2027 #
2029 2028 # then 2) will fail because the phasecache contains nodes that were
2030 2029 # removed. We can either remove phasecache from the filecache,
2031 2030 # causing it to reload next time it is accessed, or simply filter
2032 2031 # the removed nodes now and write the updated cache.
2033 2032 self._phasecache.filterunknown(self)
2034 2033 self._phasecache.write()
2035 2034
2036 2035 # refresh all repository caches
2037 2036 self.updatecaches()
2038 2037
2039 2038 # Ensure the persistent tag cache is updated. Doing it now
2040 2039 # means that the tag cache only has to worry about destroyed
2041 2040 # heads immediately after a strip/rollback. That in turn
2042 2041 # guarantees that "cachetip == currenttip" (comparing both rev
2043 2042 # and node) always means no nodes have been added or destroyed.
2044 2043
2045 2044 # XXX this is suboptimal when qrefresh'ing: we strip the current
2046 2045 # head, refresh the tag cache, then immediately add a new head.
2047 2046 # But I think doing it this way is necessary for the "instant
2048 2047 # tag cache retrieval" case to work.
2049 2048 self.invalidate()
2050 2049
2051 2050 def status(self, node1='.', node2=None, match=None,
2052 2051 ignored=False, clean=False, unknown=False,
2053 2052 listsubrepos=False):
2054 2053 '''a convenience method that calls node1.status(node2)'''
2055 2054 return self[node1].status(node2, match, ignored, clean, unknown,
2056 2055 listsubrepos)
2057 2056
2058 2057 def addpostdsstatus(self, ps):
2059 2058 """Add a callback to run within the wlock, at the point at which status
2060 2059 fixups happen.
2061 2060
2062 2061 On status completion, callback(wctx, status) will be called with the
2063 2062 wlock held, unless the dirstate has changed from underneath or the wlock
2064 2063 couldn't be grabbed.
2065 2064
2066 2065 Callbacks should not capture and use a cached copy of the dirstate --
2067 2066 it might change in the meanwhile. Instead, they should access the
2068 2067 dirstate via wctx.repo().dirstate.
2069 2068
2070 2069 This list is emptied out after each status run -- extensions should
2071 2070 make sure it adds to this list each time dirstate.status is called.
2072 2071 Extensions should also make sure they don't call this for statuses
2073 2072 that don't involve the dirstate.
2074 2073 """
2075 2074
2076 2075 # The list is located here for uniqueness reasons -- it is actually
2077 2076 # managed by the workingctx, but that isn't unique per-repo.
2078 2077 self._postdsstatus.append(ps)
2079 2078
2080 2079 def postdsstatus(self):
2081 2080 """Used by workingctx to get the list of post-dirstate-status hooks."""
2082 2081 return self._postdsstatus
2083 2082
2084 2083 def clearpostdsstatus(self):
2085 2084 """Used by workingctx to clear post-dirstate-status hooks."""
2086 2085 del self._postdsstatus[:]
2087 2086
2088 2087 def heads(self, start=None):
2089 2088 if start is None:
2090 2089 cl = self.changelog
2091 2090 headrevs = reversed(cl.headrevs())
2092 2091 return [cl.node(rev) for rev in headrevs]
2093 2092
2094 2093 heads = self.changelog.heads(start)
2095 2094 # sort the output in rev descending order
2096 2095 return sorted(heads, key=self.changelog.rev, reverse=True)
2097 2096
2098 2097 def branchheads(self, branch=None, start=None, closed=False):
2099 2098 '''return a (possibly filtered) list of heads for the given branch
2100 2099
2101 2100 Heads are returned in topological order, from newest to oldest.
2102 2101 If branch is None, use the dirstate branch.
2103 2102 If start is not None, return only heads reachable from start.
2104 2103 If closed is True, return heads that are marked as closed as well.
2105 2104 '''
2106 2105 if branch is None:
2107 2106 branch = self[None].branch()
2108 2107 branches = self.branchmap()
2109 2108 if branch not in branches:
2110 2109 return []
2111 2110 # the cache returns heads ordered lowest to highest
2112 2111 bheads = list(reversed(branches.branchheads(branch, closed=closed)))
2113 2112 if start is not None:
2114 2113 # filter out the heads that cannot be reached from startrev
2115 2114 fbheads = set(self.changelog.nodesbetween([start], bheads)[2])
2116 2115 bheads = [h for h in bheads if h in fbheads]
2117 2116 return bheads
2118 2117
2119 2118 def branches(self, nodes):
2120 2119 if not nodes:
2121 2120 nodes = [self.changelog.tip()]
2122 2121 b = []
2123 2122 for n in nodes:
2124 2123 t = n
2125 2124 while True:
2126 2125 p = self.changelog.parents(n)
2127 2126 if p[1] != nullid or p[0] == nullid:
2128 2127 b.append((t, n, p[0], p[1]))
2129 2128 break
2130 2129 n = p[0]
2131 2130 return b
2132 2131
2133 2132 def between(self, pairs):
2134 2133 r = []
2135 2134
2136 2135 for top, bottom in pairs:
2137 2136 n, l, i = top, [], 0
2138 2137 f = 1
2139 2138
2140 2139 while n != bottom and n != nullid:
2141 2140 p = self.changelog.parents(n)[0]
2142 2141 if i == f:
2143 2142 l.append(n)
2144 2143 f = f * 2
2145 2144 n = p
2146 2145 i += 1
2147 2146
2148 2147 r.append(l)
2149 2148
2150 2149 return r
2151 2150
2152 2151 def checkpush(self, pushop):
2153 2152 """Extensions can override this function if additional checks have
2154 2153 to be performed before pushing, or call it if they override push
2155 2154 command.
2156 2155 """
2157 2156
2158 2157 @unfilteredpropertycache
2159 2158 def prepushoutgoinghooks(self):
2160 2159 """Return util.hooks consists of a pushop with repo, remote, outgoing
2161 2160 methods, which are called before pushing changesets.
2162 2161 """
2163 2162 return util.hooks()
2164 2163
2165 2164 def pushkey(self, namespace, key, old, new):
2166 2165 try:
2167 2166 tr = self.currenttransaction()
2168 2167 hookargs = {}
2169 2168 if tr is not None:
2170 2169 hookargs.update(tr.hookargs)
2171 2170 hookargs['namespace'] = namespace
2172 2171 hookargs['key'] = key
2173 2172 hookargs['old'] = old
2174 2173 hookargs['new'] = new
2175 2174 self.hook('prepushkey', throw=True, **hookargs)
2176 2175 except error.HookAbort as exc:
2177 2176 self.ui.write_err(_("pushkey-abort: %s\n") % exc)
2178 2177 if exc.hint:
2179 2178 self.ui.write_err(_("(%s)\n") % exc.hint)
2180 2179 return False
2181 2180 self.ui.debug('pushing key for "%s:%s"\n' % (namespace, key))
2182 2181 ret = pushkey.push(self, namespace, key, old, new)
2183 2182 def runhook():
2184 2183 self.hook('pushkey', namespace=namespace, key=key, old=old, new=new,
2185 2184 ret=ret)
2186 2185 self._afterlock(runhook)
2187 2186 return ret
2188 2187
2189 2188 def listkeys(self, namespace):
2190 2189 self.hook('prelistkeys', throw=True, namespace=namespace)
2191 2190 self.ui.debug('listing keys for "%s"\n' % namespace)
2192 2191 values = pushkey.list(self, namespace)
2193 2192 self.hook('listkeys', namespace=namespace, values=values)
2194 2193 return values
2195 2194
2196 2195 def debugwireargs(self, one, two, three=None, four=None, five=None):
2197 2196 '''used to test argument passing over the wire'''
2198 2197 return "%s %s %s %s %s" % (one, two, three, four, five)
2199 2198
2200 2199 def savecommitmessage(self, text):
2201 2200 fp = self.vfs('last-message.txt', 'wb')
2202 2201 try:
2203 2202 fp.write(text)
2204 2203 finally:
2205 2204 fp.close()
2206 2205 return self.pathto(fp.name[len(self.root) + 1:])
2207 2206
2208 2207 # used to avoid circular references so destructors work
2209 2208 def aftertrans(files):
2210 2209 renamefiles = [tuple(t) for t in files]
2211 2210 def a():
2212 2211 for vfs, src, dest in renamefiles:
2213 2212 # if src and dest refer to a same file, vfs.rename is a no-op,
2214 2213 # leaving both src and dest on disk. delete dest to make sure
2215 2214 # the rename couldn't be such a no-op.
2216 2215 vfs.tryunlink(dest)
2217 2216 try:
2218 2217 vfs.rename(src, dest)
2219 2218 except OSError: # journal file does not yet exist
2220 2219 pass
2221 2220 return a
2222 2221
2223 2222 def undoname(fn):
2224 2223 base, name = os.path.split(fn)
2225 2224 assert name.startswith('journal')
2226 2225 return os.path.join(base, name.replace('journal', 'undo', 1))
2227 2226
2228 2227 def instance(ui, path, create):
2229 2228 return localrepository(ui, util.urllocalpath(path), create)
2230 2229
2231 2230 def islocal(path):
2232 2231 return True
2233 2232
2234 2233 def newreporequirements(repo):
2235 2234 """Determine the set of requirements for a new local repository.
2236 2235
2237 2236 Extensions can wrap this function to specify custom requirements for
2238 2237 new repositories.
2239 2238 """
2240 2239 ui = repo.ui
2241 2240 requirements = {'revlogv1'}
2242 2241 if ui.configbool('format', 'usestore'):
2243 2242 requirements.add('store')
2244 2243 if ui.configbool('format', 'usefncache'):
2245 2244 requirements.add('fncache')
2246 2245 if ui.configbool('format', 'dotencode'):
2247 2246 requirements.add('dotencode')
2248 2247
2249 2248 compengine = ui.config('experimental', 'format.compression')
2250 2249 if compengine not in util.compengines:
2251 2250 raise error.Abort(_('compression engine %s defined by '
2252 2251 'experimental.format.compression not available') %
2253 2252 compengine,
2254 2253 hint=_('run "hg debuginstall" to list available '
2255 2254 'compression engines'))
2256 2255
2257 2256 # zlib is the historical default and doesn't need an explicit requirement.
2258 2257 if compengine != 'zlib':
2259 2258 requirements.add('exp-compression-%s' % compengine)
2260 2259
2261 2260 if scmutil.gdinitconfig(ui):
2262 2261 requirements.add('generaldelta')
2263 2262 if ui.configbool('experimental', 'treemanifest'):
2264 2263 requirements.add('treemanifest')
2265 2264 if ui.configbool('experimental', 'manifestv2'):
2266 2265 requirements.add('manifestv2')
2267 2266
2268 2267 revlogv2 = ui.config('experimental', 'revlogv2')
2269 2268 if revlogv2 == 'enable-unstable-format-and-corrupt-my-data':
2270 2269 requirements.remove('revlogv1')
2271 2270 # generaldelta is implied by revlogv2.
2272 2271 requirements.discard('generaldelta')
2273 2272 requirements.add(REVLOGV2_REQUIREMENT)
2274 2273
2275 2274 return requirements
@@ -1,348 +1,351
1 1 # pycompat.py - portability shim for python 3
2 2 #
3 3 # This software may be used and distributed according to the terms of the
4 4 # GNU General Public License version 2 or any later version.
5 5
6 6 """Mercurial portability shim for python 3.
7 7
8 8 This contains aliases to hide python version-specific details from the core.
9 9 """
10 10
11 11 from __future__ import absolute_import
12 12
13 13 import getopt
14 import inspect
14 15 import os
15 16 import shlex
16 17 import sys
17 18
18 19 ispy3 = (sys.version_info[0] >= 3)
19 20 ispypy = (r'__pypy__' in sys.builtin_module_names)
20 21
21 22 if not ispy3:
22 23 import cookielib
23 24 import cPickle as pickle
24 25 import httplib
25 26 import Queue as _queue
26 27 import SocketServer as socketserver
27 28 import xmlrpclib
28 29 else:
29 30 import http.cookiejar as cookielib
30 31 import http.client as httplib
31 32 import pickle
32 33 import queue as _queue
33 34 import socketserver
34 35 import xmlrpc.client as xmlrpclib
35 36
36 37 empty = _queue.Empty
37 38 queue = _queue.Queue
38 39
39 40 def identity(a):
40 41 return a
41 42
42 43 if ispy3:
43 44 import builtins
44 45 import functools
45 46 import io
46 47 import struct
47 48
48 49 fsencode = os.fsencode
49 50 fsdecode = os.fsdecode
50 51 oslinesep = os.linesep.encode('ascii')
51 52 osname = os.name.encode('ascii')
52 53 ospathsep = os.pathsep.encode('ascii')
53 54 ossep = os.sep.encode('ascii')
54 55 osaltsep = os.altsep
55 56 if osaltsep:
56 57 osaltsep = osaltsep.encode('ascii')
57 58 # os.getcwd() on Python 3 returns string, but it has os.getcwdb() which
58 59 # returns bytes.
59 60 getcwd = os.getcwdb
60 61 sysplatform = sys.platform.encode('ascii')
61 62 sysexecutable = sys.executable
62 63 if sysexecutable:
63 64 sysexecutable = os.fsencode(sysexecutable)
64 65 stringio = io.BytesIO
65 66 maplist = lambda *args: list(map(*args))
66 67 ziplist = lambda *args: list(zip(*args))
67 68 rawinput = input
69 getargspec = inspect.getfullargspec
68 70
69 71 # TODO: .buffer might not exist if std streams were replaced; we'll need
70 72 # a silly wrapper to make a bytes stream backed by a unicode one.
71 73 stdin = sys.stdin.buffer
72 74 stdout = sys.stdout.buffer
73 75 stderr = sys.stderr.buffer
74 76
75 77 # Since Python 3 converts argv to wchar_t type by Py_DecodeLocale() on Unix,
76 78 # we can use os.fsencode() to get back bytes argv.
77 79 #
78 80 # https://hg.python.org/cpython/file/v3.5.1/Programs/python.c#l55
79 81 #
80 82 # TODO: On Windows, the native argv is wchar_t, so we'll need a different
81 83 # workaround to simulate the Python 2 (i.e. ANSI Win32 API) behavior.
82 84 if getattr(sys, 'argv', None) is not None:
83 85 sysargv = list(map(os.fsencode, sys.argv))
84 86
85 87 bytechr = struct.Struct('>B').pack
86 88
87 89 class bytestr(bytes):
88 90 """A bytes which mostly acts as a Python 2 str
89 91
90 92 >>> bytestr(), bytestr(bytearray(b'foo')), bytestr(u'ascii'), bytestr(1)
91 93 ('', 'foo', 'ascii', '1')
92 94 >>> s = bytestr(b'foo')
93 95 >>> assert s is bytestr(s)
94 96
95 97 __bytes__() should be called if provided:
96 98
97 99 >>> class bytesable(object):
98 100 ... def __bytes__(self):
99 101 ... return b'bytes'
100 102 >>> bytestr(bytesable())
101 103 'bytes'
102 104
103 105 There's no implicit conversion from non-ascii str as its encoding is
104 106 unknown:
105 107
106 108 >>> bytestr(chr(0x80)) # doctest: +ELLIPSIS
107 109 Traceback (most recent call last):
108 110 ...
109 111 UnicodeEncodeError: ...
110 112
111 113 Comparison between bytestr and bytes should work:
112 114
113 115 >>> assert bytestr(b'foo') == b'foo'
114 116 >>> assert b'foo' == bytestr(b'foo')
115 117 >>> assert b'f' in bytestr(b'foo')
116 118 >>> assert bytestr(b'f') in b'foo'
117 119
118 120 Sliced elements should be bytes, not integer:
119 121
120 122 >>> s[1], s[:2]
121 123 (b'o', b'fo')
122 124 >>> list(s), list(reversed(s))
123 125 ([b'f', b'o', b'o'], [b'o', b'o', b'f'])
124 126
125 127 As bytestr type isn't propagated across operations, you need to cast
126 128 bytes to bytestr explicitly:
127 129
128 130 >>> s = bytestr(b'foo').upper()
129 131 >>> t = bytestr(s)
130 132 >>> s[0], t[0]
131 133 (70, b'F')
132 134
133 135 Be careful to not pass a bytestr object to a function which expects
134 136 bytearray-like behavior.
135 137
136 138 >>> t = bytes(t) # cast to bytes
137 139 >>> assert type(t) is bytes
138 140 """
139 141
140 142 def __new__(cls, s=b''):
141 143 if isinstance(s, bytestr):
142 144 return s
143 145 if (not isinstance(s, (bytes, bytearray))
144 146 and not hasattr(s, u'__bytes__')): # hasattr-py3-only
145 147 s = str(s).encode(u'ascii')
146 148 return bytes.__new__(cls, s)
147 149
148 150 def __getitem__(self, key):
149 151 s = bytes.__getitem__(self, key)
150 152 if not isinstance(s, bytes):
151 153 s = bytechr(s)
152 154 return s
153 155
154 156 def __iter__(self):
155 157 return iterbytestr(bytes.__iter__(self))
156 158
157 159 def __repr__(self):
158 160 return bytes.__repr__(self)[1:] # drop b''
159 161
160 162 def iterbytestr(s):
161 163 """Iterate bytes as if it were a str object of Python 2"""
162 164 return map(bytechr, s)
163 165
164 166 def maybebytestr(s):
165 167 """Promote bytes to bytestr"""
166 168 if isinstance(s, bytes):
167 169 return bytestr(s)
168 170 return s
169 171
170 172 def sysbytes(s):
171 173 """Convert an internal str (e.g. keyword, __doc__) back to bytes
172 174
173 175 This never raises UnicodeEncodeError, but only ASCII characters
174 176 can be round-trip by sysstr(sysbytes(s)).
175 177 """
176 178 return s.encode(u'utf-8')
177 179
178 180 def sysstr(s):
179 181 """Return a keyword str to be passed to Python functions such as
180 182 getattr() and str.encode()
181 183
182 184 This never raises UnicodeDecodeError. Non-ascii characters are
183 185 considered invalid and mapped to arbitrary but unique code points
184 186 such that 'sysstr(a) != sysstr(b)' for all 'a != b'.
185 187 """
186 188 if isinstance(s, builtins.str):
187 189 return s
188 190 return s.decode(u'latin-1')
189 191
190 192 def strurl(url):
191 193 """Converts a bytes url back to str"""
192 194 return url.decode(u'ascii')
193 195
194 196 def bytesurl(url):
195 197 """Converts a str url to bytes by encoding in ascii"""
196 198 return url.encode(u'ascii')
197 199
198 200 def raisewithtb(exc, tb):
199 201 """Raise exception with the given traceback"""
200 202 raise exc.with_traceback(tb)
201 203
202 204 def getdoc(obj):
203 205 """Get docstring as bytes; may be None so gettext() won't confuse it
204 206 with _('')"""
205 207 doc = getattr(obj, u'__doc__', None)
206 208 if doc is None:
207 209 return doc
208 210 return sysbytes(doc)
209 211
210 212 def _wrapattrfunc(f):
211 213 @functools.wraps(f)
212 214 def w(object, name, *args):
213 215 return f(object, sysstr(name), *args)
214 216 return w
215 217
216 218 # these wrappers are automagically imported by hgloader
217 219 delattr = _wrapattrfunc(builtins.delattr)
218 220 getattr = _wrapattrfunc(builtins.getattr)
219 221 hasattr = _wrapattrfunc(builtins.hasattr)
220 222 setattr = _wrapattrfunc(builtins.setattr)
221 223 xrange = builtins.range
222 224 unicode = str
223 225
224 226 def open(name, mode='r', buffering=-1):
225 227 return builtins.open(name, sysstr(mode), buffering)
226 228
227 229 def _getoptbwrapper(orig, args, shortlist, namelist):
228 230 """
229 231 Takes bytes arguments, converts them to unicode, pass them to
230 232 getopt.getopt(), convert the returned values back to bytes and then
231 233 return them for Python 3 compatibility as getopt.getopt() don't accepts
232 234 bytes on Python 3.
233 235 """
234 236 args = [a.decode('latin-1') for a in args]
235 237 shortlist = shortlist.decode('latin-1')
236 238 namelist = [a.decode('latin-1') for a in namelist]
237 239 opts, args = orig(args, shortlist, namelist)
238 240 opts = [(a[0].encode('latin-1'), a[1].encode('latin-1'))
239 241 for a in opts]
240 242 args = [a.encode('latin-1') for a in args]
241 243 return opts, args
242 244
243 245 def strkwargs(dic):
244 246 """
245 247 Converts the keys of a python dictonary to str i.e. unicodes so that
246 248 they can be passed as keyword arguments as dictonaries with bytes keys
247 249 can't be passed as keyword arguments to functions on Python 3.
248 250 """
249 251 dic = dict((k.decode('latin-1'), v) for k, v in dic.iteritems())
250 252 return dic
251 253
252 254 def byteskwargs(dic):
253 255 """
254 256 Converts keys of python dictonaries to bytes as they were converted to
255 257 str to pass that dictonary as a keyword argument on Python 3.
256 258 """
257 259 dic = dict((k.encode('latin-1'), v) for k, v in dic.iteritems())
258 260 return dic
259 261
260 262 # TODO: handle shlex.shlex().
261 263 def shlexsplit(s):
262 264 """
263 265 Takes bytes argument, convert it to str i.e. unicodes, pass that into
264 266 shlex.split(), convert the returned value to bytes and return that for
265 267 Python 3 compatibility as shelx.split() don't accept bytes on Python 3.
266 268 """
267 269 ret = shlex.split(s.decode('latin-1'))
268 270 return [a.encode('latin-1') for a in ret]
269 271
270 272 def emailparser(*args, **kwargs):
271 273 import email.parser
272 274 return email.parser.BytesParser(*args, **kwargs)
273 275
274 276 else:
275 277 import cStringIO
276 278
277 279 bytechr = chr
278 280 bytestr = str
279 281 iterbytestr = iter
280 282 maybebytestr = identity
281 283 sysbytes = identity
282 284 sysstr = identity
283 285 strurl = identity
284 286 bytesurl = identity
285 287
286 288 # this can't be parsed on Python 3
287 289 exec('def raisewithtb(exc, tb):\n'
288 290 ' raise exc, None, tb\n')
289 291
290 292 def fsencode(filename):
291 293 """
292 294 Partial backport from os.py in Python 3, which only accepts bytes.
293 295 In Python 2, our paths should only ever be bytes, a unicode path
294 296 indicates a bug.
295 297 """
296 298 if isinstance(filename, str):
297 299 return filename
298 300 else:
299 301 raise TypeError(
300 302 "expect str, not %s" % type(filename).__name__)
301 303
302 304 # In Python 2, fsdecode() has a very chance to receive bytes. So it's
303 305 # better not to touch Python 2 part as it's already working fine.
304 306 fsdecode = identity
305 307
306 308 def getdoc(obj):
307 309 return getattr(obj, '__doc__', None)
308 310
309 311 def _getoptbwrapper(orig, args, shortlist, namelist):
310 312 return orig(args, shortlist, namelist)
311 313
312 314 strkwargs = identity
313 315 byteskwargs = identity
314 316
315 317 oslinesep = os.linesep
316 318 osname = os.name
317 319 ospathsep = os.pathsep
318 320 ossep = os.sep
319 321 osaltsep = os.altsep
320 322 stdin = sys.stdin
321 323 stdout = sys.stdout
322 324 stderr = sys.stderr
323 325 if getattr(sys, 'argv', None) is not None:
324 326 sysargv = sys.argv
325 327 sysplatform = sys.platform
326 328 getcwd = os.getcwd
327 329 sysexecutable = sys.executable
328 330 shlexsplit = shlex.split
329 331 stringio = cStringIO.StringIO
330 332 maplist = map
331 333 ziplist = zip
332 334 rawinput = raw_input
335 getargspec = inspect.getargspec
333 336
334 337 def emailparser(*args, **kwargs):
335 338 import email.parser
336 339 return email.parser.Parser(*args, **kwargs)
337 340
338 341 isjython = sysplatform.startswith('java')
339 342
340 343 isdarwin = sysplatform == 'darwin'
341 344 isposix = osname == 'posix'
342 345 iswindows = osname == 'nt'
343 346
344 347 def getoptb(args, shortlist, namelist):
345 348 return _getoptbwrapper(getopt.getopt, args, shortlist, namelist)
346 349
347 350 def gnugetoptb(args, shortlist, namelist):
348 351 return _getoptbwrapper(getopt.gnu_getopt, args, shortlist, namelist)
General Comments 0
You need to be logged in to leave comments. Login now