##// END OF EJS Templates
ui: improve performance for multi-component writes...
Joerg Sonnenberger -
r35907:e49c39ff default
parent child Browse files
Show More
@@ -1,1848 +1,1847
1 1 # ui.py - user interface bits for mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import collections
11 11 import contextlib
12 12 import errno
13 13 import getpass
14 14 import inspect
15 15 import os
16 16 import re
17 17 import signal
18 18 import socket
19 19 import subprocess
20 20 import sys
21 21 import tempfile
22 22 import traceback
23 23
24 24 from .i18n import _
25 25 from .node import hex
26 26
27 27 from . import (
28 28 color,
29 29 config,
30 30 configitems,
31 31 encoding,
32 32 error,
33 33 formatter,
34 34 progress,
35 35 pycompat,
36 36 rcutil,
37 37 scmutil,
38 38 util,
39 39 )
40 40
41 41 urlreq = util.urlreq
42 42
43 43 # for use with str.translate(None, _keepalnum), to keep just alphanumerics
44 44 _keepalnum = ''.join(c for c in map(pycompat.bytechr, range(256))
45 45 if not c.isalnum())
46 46
47 47 # The config knobs that will be altered (if unset) by ui.tweakdefaults.
48 48 tweakrc = """
49 49 [ui]
50 50 # The rollback command is dangerous. As a rule, don't use it.
51 51 rollback = False
52 52 # Make `hg status` report copy information
53 53 statuscopies = yes
54 54 # Prefer curses UIs when available. Revert to plain-text with `text`.
55 55 interface = curses
56 56
57 57 [commands]
58 58 # Make `hg status` emit cwd-relative paths by default.
59 59 status.relative = yes
60 60 # Refuse to perform an `hg update` that would cause a file content merge
61 61 update.check = noconflict
62 62
63 63 [diff]
64 64 git = 1
65 65 showfunc = 1
66 66 """
67 67
68 68 samplehgrcs = {
69 69 'user':
70 70 b"""# example user config (see 'hg help config' for more info)
71 71 [ui]
72 72 # name and email, e.g.
73 73 # username = Jane Doe <jdoe@example.com>
74 74 username =
75 75
76 76 # We recommend enabling tweakdefaults to get slight improvements to
77 77 # the UI over time. Make sure to set HGPLAIN in the environment when
78 78 # writing scripts!
79 79 # tweakdefaults = True
80 80
81 81 # uncomment to disable color in command output
82 82 # (see 'hg help color' for details)
83 83 # color = never
84 84
85 85 # uncomment to disable command output pagination
86 86 # (see 'hg help pager' for details)
87 87 # paginate = never
88 88
89 89 [extensions]
90 90 # uncomment these lines to enable some popular extensions
91 91 # (see 'hg help extensions' for more info)
92 92 #
93 93 # churn =
94 94 """,
95 95
96 96 'cloned':
97 97 b"""# example repository config (see 'hg help config' for more info)
98 98 [paths]
99 99 default = %s
100 100
101 101 # path aliases to other clones of this repo in URLs or filesystem paths
102 102 # (see 'hg help config.paths' for more info)
103 103 #
104 104 # default:pushurl = ssh://jdoe@example.net/hg/jdoes-fork
105 105 # my-fork = ssh://jdoe@example.net/hg/jdoes-fork
106 106 # my-clone = /home/jdoe/jdoes-clone
107 107
108 108 [ui]
109 109 # name and email (local to this repository, optional), e.g.
110 110 # username = Jane Doe <jdoe@example.com>
111 111 """,
112 112
113 113 'local':
114 114 b"""# example repository config (see 'hg help config' for more info)
115 115 [paths]
116 116 # path aliases to other clones of this repo in URLs or filesystem paths
117 117 # (see 'hg help config.paths' for more info)
118 118 #
119 119 # default = http://example.com/hg/example-repo
120 120 # default:pushurl = ssh://jdoe@example.net/hg/jdoes-fork
121 121 # my-fork = ssh://jdoe@example.net/hg/jdoes-fork
122 122 # my-clone = /home/jdoe/jdoes-clone
123 123
124 124 [ui]
125 125 # name and email (local to this repository, optional), e.g.
126 126 # username = Jane Doe <jdoe@example.com>
127 127 """,
128 128
129 129 'global':
130 130 b"""# example system-wide hg config (see 'hg help config' for more info)
131 131
132 132 [ui]
133 133 # uncomment to disable color in command output
134 134 # (see 'hg help color' for details)
135 135 # color = never
136 136
137 137 # uncomment to disable command output pagination
138 138 # (see 'hg help pager' for details)
139 139 # paginate = never
140 140
141 141 [extensions]
142 142 # uncomment these lines to enable some popular extensions
143 143 # (see 'hg help extensions' for more info)
144 144 #
145 145 # blackbox =
146 146 # churn =
147 147 """,
148 148 }
149 149
150 150 def _maybestrurl(maybebytes):
151 151 if maybebytes is None:
152 152 return None
153 153 return pycompat.strurl(maybebytes)
154 154
155 155 def _maybebytesurl(maybestr):
156 156 if maybestr is None:
157 157 return None
158 158 return pycompat.bytesurl(maybestr)
159 159
160 160 class httppasswordmgrdbproxy(object):
161 161 """Delays loading urllib2 until it's needed."""
162 162 def __init__(self):
163 163 self._mgr = None
164 164
165 165 def _get_mgr(self):
166 166 if self._mgr is None:
167 167 self._mgr = urlreq.httppasswordmgrwithdefaultrealm()
168 168 return self._mgr
169 169
170 170 def add_password(self, realm, uris, user, passwd):
171 171 if isinstance(uris, tuple):
172 172 uris = tuple(_maybestrurl(u) for u in uris)
173 173 else:
174 174 uris = _maybestrurl(uris)
175 175 return self._get_mgr().add_password(
176 176 _maybestrurl(realm), uris,
177 177 _maybestrurl(user), _maybestrurl(passwd))
178 178
179 179 def find_user_password(self, realm, uri):
180 180 return tuple(_maybebytesurl(v) for v in
181 181 self._get_mgr().find_user_password(_maybestrurl(realm),
182 182 _maybestrurl(uri)))
183 183
184 184 def _catchterm(*args):
185 185 raise error.SignalInterrupt
186 186
187 187 # unique object used to detect no default value has been provided when
188 188 # retrieving configuration value.
189 189 _unset = object()
190 190
191 191 # _reqexithandlers: callbacks run at the end of a request
192 192 _reqexithandlers = []
193 193
194 194 class ui(object):
195 195 def __init__(self, src=None):
196 196 """Create a fresh new ui object if no src given
197 197
198 198 Use uimod.ui.load() to create a ui which knows global and user configs.
199 199 In most cases, you should use ui.copy() to create a copy of an existing
200 200 ui object.
201 201 """
202 202 # _buffers: used for temporary capture of output
203 203 self._buffers = []
204 204 # 3-tuple describing how each buffer in the stack behaves.
205 205 # Values are (capture stderr, capture subprocesses, apply labels).
206 206 self._bufferstates = []
207 207 # When a buffer is active, defines whether we are expanding labels.
208 208 # This exists to prevent an extra list lookup.
209 209 self._bufferapplylabels = None
210 210 self.quiet = self.verbose = self.debugflag = self.tracebackflag = False
211 211 self._reportuntrusted = True
212 212 self._knownconfig = configitems.coreitems
213 213 self._ocfg = config.config() # overlay
214 214 self._tcfg = config.config() # trusted
215 215 self._ucfg = config.config() # untrusted
216 216 self._trustusers = set()
217 217 self._trustgroups = set()
218 218 self.callhooks = True
219 219 # Insecure server connections requested.
220 220 self.insecureconnections = False
221 221 # Blocked time
222 222 self.logblockedtimes = False
223 223 # color mode: see mercurial/color.py for possible value
224 224 self._colormode = None
225 225 self._terminfoparams = {}
226 226 self._styles = {}
227 227
228 228 if src:
229 229 self.fout = src.fout
230 230 self.ferr = src.ferr
231 231 self.fin = src.fin
232 232 self.pageractive = src.pageractive
233 233 self._disablepager = src._disablepager
234 234 self._tweaked = src._tweaked
235 235
236 236 self._tcfg = src._tcfg.copy()
237 237 self._ucfg = src._ucfg.copy()
238 238 self._ocfg = src._ocfg.copy()
239 239 self._trustusers = src._trustusers.copy()
240 240 self._trustgroups = src._trustgroups.copy()
241 241 self.environ = src.environ
242 242 self.callhooks = src.callhooks
243 243 self.insecureconnections = src.insecureconnections
244 244 self._colormode = src._colormode
245 245 self._terminfoparams = src._terminfoparams.copy()
246 246 self._styles = src._styles.copy()
247 247
248 248 self.fixconfig()
249 249
250 250 self.httppasswordmgrdb = src.httppasswordmgrdb
251 251 self._blockedtimes = src._blockedtimes
252 252 else:
253 253 self.fout = util.stdout
254 254 self.ferr = util.stderr
255 255 self.fin = util.stdin
256 256 self.pageractive = False
257 257 self._disablepager = False
258 258 self._tweaked = False
259 259
260 260 # shared read-only environment
261 261 self.environ = encoding.environ
262 262
263 263 self.httppasswordmgrdb = httppasswordmgrdbproxy()
264 264 self._blockedtimes = collections.defaultdict(int)
265 265
266 266 allowed = self.configlist('experimental', 'exportableenviron')
267 267 if '*' in allowed:
268 268 self._exportableenviron = self.environ
269 269 else:
270 270 self._exportableenviron = {}
271 271 for k in allowed:
272 272 if k in self.environ:
273 273 self._exportableenviron[k] = self.environ[k]
274 274
275 275 @classmethod
276 276 def load(cls):
277 277 """Create a ui and load global and user configs"""
278 278 u = cls()
279 279 # we always trust global config files and environment variables
280 280 for t, f in rcutil.rccomponents():
281 281 if t == 'path':
282 282 u.readconfig(f, trust=True)
283 283 elif t == 'items':
284 284 sections = set()
285 285 for section, name, value, source in f:
286 286 # do not set u._ocfg
287 287 # XXX clean this up once immutable config object is a thing
288 288 u._tcfg.set(section, name, value, source)
289 289 u._ucfg.set(section, name, value, source)
290 290 sections.add(section)
291 291 for section in sections:
292 292 u.fixconfig(section=section)
293 293 else:
294 294 raise error.ProgrammingError('unknown rctype: %s' % t)
295 295 u._maybetweakdefaults()
296 296 return u
297 297
298 298 def _maybetweakdefaults(self):
299 299 if not self.configbool('ui', 'tweakdefaults'):
300 300 return
301 301 if self._tweaked or self.plain('tweakdefaults'):
302 302 return
303 303
304 304 # Note: it is SUPER IMPORTANT that you set self._tweaked to
305 305 # True *before* any calls to setconfig(), otherwise you'll get
306 306 # infinite recursion between setconfig and this method.
307 307 #
308 308 # TODO: We should extract an inner method in setconfig() to
309 309 # avoid this weirdness.
310 310 self._tweaked = True
311 311 tmpcfg = config.config()
312 312 tmpcfg.parse('<tweakdefaults>', tweakrc)
313 313 for section in tmpcfg:
314 314 for name, value in tmpcfg.items(section):
315 315 if not self.hasconfig(section, name):
316 316 self.setconfig(section, name, value, "<tweakdefaults>")
317 317
318 318 def copy(self):
319 319 return self.__class__(self)
320 320
321 321 def resetstate(self):
322 322 """Clear internal state that shouldn't persist across commands"""
323 323 if self._progbar:
324 324 self._progbar.resetstate() # reset last-print time of progress bar
325 325 self.httppasswordmgrdb = httppasswordmgrdbproxy()
326 326
327 327 @contextlib.contextmanager
328 328 def timeblockedsection(self, key):
329 329 # this is open-coded below - search for timeblockedsection to find them
330 330 starttime = util.timer()
331 331 try:
332 332 yield
333 333 finally:
334 334 self._blockedtimes[key + '_blocked'] += \
335 335 (util.timer() - starttime) * 1000
336 336
337 337 def formatter(self, topic, opts):
338 338 return formatter.formatter(self, self, topic, opts)
339 339
340 340 def _trusted(self, fp, f):
341 341 st = util.fstat(fp)
342 342 if util.isowner(st):
343 343 return True
344 344
345 345 tusers, tgroups = self._trustusers, self._trustgroups
346 346 if '*' in tusers or '*' in tgroups:
347 347 return True
348 348
349 349 user = util.username(st.st_uid)
350 350 group = util.groupname(st.st_gid)
351 351 if user in tusers or group in tgroups or user == util.username():
352 352 return True
353 353
354 354 if self._reportuntrusted:
355 355 self.warn(_('not trusting file %s from untrusted '
356 356 'user %s, group %s\n') % (f, user, group))
357 357 return False
358 358
359 359 def readconfig(self, filename, root=None, trust=False,
360 360 sections=None, remap=None):
361 361 try:
362 362 fp = open(filename, u'rb')
363 363 except IOError:
364 364 if not sections: # ignore unless we were looking for something
365 365 return
366 366 raise
367 367
368 368 cfg = config.config()
369 369 trusted = sections or trust or self._trusted(fp, filename)
370 370
371 371 try:
372 372 cfg.read(filename, fp, sections=sections, remap=remap)
373 373 fp.close()
374 374 except error.ConfigError as inst:
375 375 if trusted:
376 376 raise
377 377 self.warn(_("ignored: %s\n") % str(inst))
378 378
379 379 if self.plain():
380 380 for k in ('debug', 'fallbackencoding', 'quiet', 'slash',
381 381 'logtemplate', 'statuscopies', 'style',
382 382 'traceback', 'verbose'):
383 383 if k in cfg['ui']:
384 384 del cfg['ui'][k]
385 385 for k, v in cfg.items('defaults'):
386 386 del cfg['defaults'][k]
387 387 for k, v in cfg.items('commands'):
388 388 del cfg['commands'][k]
389 389 # Don't remove aliases from the configuration if in the exceptionlist
390 390 if self.plain('alias'):
391 391 for k, v in cfg.items('alias'):
392 392 del cfg['alias'][k]
393 393 if self.plain('revsetalias'):
394 394 for k, v in cfg.items('revsetalias'):
395 395 del cfg['revsetalias'][k]
396 396 if self.plain('templatealias'):
397 397 for k, v in cfg.items('templatealias'):
398 398 del cfg['templatealias'][k]
399 399
400 400 if trusted:
401 401 self._tcfg.update(cfg)
402 402 self._tcfg.update(self._ocfg)
403 403 self._ucfg.update(cfg)
404 404 self._ucfg.update(self._ocfg)
405 405
406 406 if root is None:
407 407 root = os.path.expanduser('~')
408 408 self.fixconfig(root=root)
409 409
410 410 def fixconfig(self, root=None, section=None):
411 411 if section in (None, 'paths'):
412 412 # expand vars and ~
413 413 # translate paths relative to root (or home) into absolute paths
414 414 root = root or pycompat.getcwd()
415 415 for c in self._tcfg, self._ucfg, self._ocfg:
416 416 for n, p in c.items('paths'):
417 417 # Ignore sub-options.
418 418 if ':' in n:
419 419 continue
420 420 if not p:
421 421 continue
422 422 if '%%' in p:
423 423 s = self.configsource('paths', n) or 'none'
424 424 self.warn(_("(deprecated '%%' in path %s=%s from %s)\n")
425 425 % (n, p, s))
426 426 p = p.replace('%%', '%')
427 427 p = util.expandpath(p)
428 428 if not util.hasscheme(p) and not os.path.isabs(p):
429 429 p = os.path.normpath(os.path.join(root, p))
430 430 c.set("paths", n, p)
431 431
432 432 if section in (None, 'ui'):
433 433 # update ui options
434 434 self.debugflag = self.configbool('ui', 'debug')
435 435 self.verbose = self.debugflag or self.configbool('ui', 'verbose')
436 436 self.quiet = not self.debugflag and self.configbool('ui', 'quiet')
437 437 if self.verbose and self.quiet:
438 438 self.quiet = self.verbose = False
439 439 self._reportuntrusted = self.debugflag or self.configbool("ui",
440 440 "report_untrusted")
441 441 self.tracebackflag = self.configbool('ui', 'traceback')
442 442 self.logblockedtimes = self.configbool('ui', 'logblockedtimes')
443 443
444 444 if section in (None, 'trusted'):
445 445 # update trust information
446 446 self._trustusers.update(self.configlist('trusted', 'users'))
447 447 self._trustgroups.update(self.configlist('trusted', 'groups'))
448 448
449 449 def backupconfig(self, section, item):
450 450 return (self._ocfg.backup(section, item),
451 451 self._tcfg.backup(section, item),
452 452 self._ucfg.backup(section, item),)
453 453 def restoreconfig(self, data):
454 454 self._ocfg.restore(data[0])
455 455 self._tcfg.restore(data[1])
456 456 self._ucfg.restore(data[2])
457 457
458 458 def setconfig(self, section, name, value, source=''):
459 459 for cfg in (self._ocfg, self._tcfg, self._ucfg):
460 460 cfg.set(section, name, value, source)
461 461 self.fixconfig(section=section)
462 462 self._maybetweakdefaults()
463 463
464 464 def _data(self, untrusted):
465 465 return untrusted and self._ucfg or self._tcfg
466 466
467 467 def configsource(self, section, name, untrusted=False):
468 468 return self._data(untrusted).source(section, name)
469 469
470 470 def config(self, section, name, default=_unset, untrusted=False):
471 471 """return the plain string version of a config"""
472 472 value = self._config(section, name, default=default,
473 473 untrusted=untrusted)
474 474 if value is _unset:
475 475 return None
476 476 return value
477 477
478 478 def _config(self, section, name, default=_unset, untrusted=False):
479 479 value = itemdefault = default
480 480 item = self._knownconfig.get(section, {}).get(name)
481 481 alternates = [(section, name)]
482 482
483 483 if item is not None:
484 484 alternates.extend(item.alias)
485 485 if callable(item.default):
486 486 itemdefault = item.default()
487 487 else:
488 488 itemdefault = item.default
489 489 else:
490 490 msg = ("accessing unregistered config item: '%s.%s'")
491 491 msg %= (section, name)
492 492 self.develwarn(msg, 2, 'warn-config-unknown')
493 493
494 494 if default is _unset:
495 495 if item is None:
496 496 value = default
497 497 elif item.default is configitems.dynamicdefault:
498 498 value = None
499 499 msg = "config item requires an explicit default value: '%s.%s'"
500 500 msg %= (section, name)
501 501 self.develwarn(msg, 2, 'warn-config-default')
502 502 else:
503 503 value = itemdefault
504 504 elif (item is not None
505 505 and item.default is not configitems.dynamicdefault
506 506 and default != itemdefault):
507 507 msg = ("specifying a mismatched default value for a registered "
508 508 "config item: '%s.%s' '%s'")
509 509 msg %= (section, name, default)
510 510 self.develwarn(msg, 2, 'warn-config-default')
511 511
512 512 for s, n in alternates:
513 513 candidate = self._data(untrusted).get(s, n, None)
514 514 if candidate is not None:
515 515 value = candidate
516 516 section = s
517 517 name = n
518 518 break
519 519
520 520 if self.debugflag and not untrusted and self._reportuntrusted:
521 521 for s, n in alternates:
522 522 uvalue = self._ucfg.get(s, n)
523 523 if uvalue is not None and uvalue != value:
524 524 self.debug("ignoring untrusted configuration option "
525 525 "%s.%s = %s\n" % (s, n, uvalue))
526 526 return value
527 527
528 528 def configsuboptions(self, section, name, default=_unset, untrusted=False):
529 529 """Get a config option and all sub-options.
530 530
531 531 Some config options have sub-options that are declared with the
532 532 format "key:opt = value". This method is used to return the main
533 533 option and all its declared sub-options.
534 534
535 535 Returns a 2-tuple of ``(option, sub-options)``, where `sub-options``
536 536 is a dict of defined sub-options where keys and values are strings.
537 537 """
538 538 main = self.config(section, name, default, untrusted=untrusted)
539 539 data = self._data(untrusted)
540 540 sub = {}
541 541 prefix = '%s:' % name
542 542 for k, v in data.items(section):
543 543 if k.startswith(prefix):
544 544 sub[k[len(prefix):]] = v
545 545
546 546 if self.debugflag and not untrusted and self._reportuntrusted:
547 547 for k, v in sub.items():
548 548 uvalue = self._ucfg.get(section, '%s:%s' % (name, k))
549 549 if uvalue is not None and uvalue != v:
550 550 self.debug('ignoring untrusted configuration option '
551 551 '%s:%s.%s = %s\n' % (section, name, k, uvalue))
552 552
553 553 return main, sub
554 554
555 555 def configpath(self, section, name, default=_unset, untrusted=False):
556 556 'get a path config item, expanded relative to repo root or config file'
557 557 v = self.config(section, name, default, untrusted)
558 558 if v is None:
559 559 return None
560 560 if not os.path.isabs(v) or "://" not in v:
561 561 src = self.configsource(section, name, untrusted)
562 562 if ':' in src:
563 563 base = os.path.dirname(src.rsplit(':')[0])
564 564 v = os.path.join(base, os.path.expanduser(v))
565 565 return v
566 566
567 567 def configbool(self, section, name, default=_unset, untrusted=False):
568 568 """parse a configuration element as a boolean
569 569
570 570 >>> u = ui(); s = b'foo'
571 571 >>> u.setconfig(s, b'true', b'yes')
572 572 >>> u.configbool(s, b'true')
573 573 True
574 574 >>> u.setconfig(s, b'false', b'no')
575 575 >>> u.configbool(s, b'false')
576 576 False
577 577 >>> u.configbool(s, b'unknown')
578 578 False
579 579 >>> u.configbool(s, b'unknown', True)
580 580 True
581 581 >>> u.setconfig(s, b'invalid', b'somevalue')
582 582 >>> u.configbool(s, b'invalid')
583 583 Traceback (most recent call last):
584 584 ...
585 585 ConfigError: foo.invalid is not a boolean ('somevalue')
586 586 """
587 587
588 588 v = self._config(section, name, default, untrusted=untrusted)
589 589 if v is None:
590 590 return v
591 591 if v is _unset:
592 592 if default is _unset:
593 593 return False
594 594 return default
595 595 if isinstance(v, bool):
596 596 return v
597 597 b = util.parsebool(v)
598 598 if b is None:
599 599 raise error.ConfigError(_("%s.%s is not a boolean ('%s')")
600 600 % (section, name, v))
601 601 return b
602 602
603 603 def configwith(self, convert, section, name, default=_unset,
604 604 desc=None, untrusted=False):
605 605 """parse a configuration element with a conversion function
606 606
607 607 >>> u = ui(); s = b'foo'
608 608 >>> u.setconfig(s, b'float1', b'42')
609 609 >>> u.configwith(float, s, b'float1')
610 610 42.0
611 611 >>> u.setconfig(s, b'float2', b'-4.25')
612 612 >>> u.configwith(float, s, b'float2')
613 613 -4.25
614 614 >>> u.configwith(float, s, b'unknown', 7)
615 615 7.0
616 616 >>> u.setconfig(s, b'invalid', b'somevalue')
617 617 >>> u.configwith(float, s, b'invalid')
618 618 Traceback (most recent call last):
619 619 ...
620 620 ConfigError: foo.invalid is not a valid float ('somevalue')
621 621 >>> u.configwith(float, s, b'invalid', desc=b'womble')
622 622 Traceback (most recent call last):
623 623 ...
624 624 ConfigError: foo.invalid is not a valid womble ('somevalue')
625 625 """
626 626
627 627 v = self.config(section, name, default, untrusted)
628 628 if v is None:
629 629 return v # do not attempt to convert None
630 630 try:
631 631 return convert(v)
632 632 except (ValueError, error.ParseError):
633 633 if desc is None:
634 634 desc = pycompat.sysbytes(convert.__name__)
635 635 raise error.ConfigError(_("%s.%s is not a valid %s ('%s')")
636 636 % (section, name, desc, v))
637 637
638 638 def configint(self, section, name, default=_unset, untrusted=False):
639 639 """parse a configuration element as an integer
640 640
641 641 >>> u = ui(); s = b'foo'
642 642 >>> u.setconfig(s, b'int1', b'42')
643 643 >>> u.configint(s, b'int1')
644 644 42
645 645 >>> u.setconfig(s, b'int2', b'-42')
646 646 >>> u.configint(s, b'int2')
647 647 -42
648 648 >>> u.configint(s, b'unknown', 7)
649 649 7
650 650 >>> u.setconfig(s, b'invalid', b'somevalue')
651 651 >>> u.configint(s, b'invalid')
652 652 Traceback (most recent call last):
653 653 ...
654 654 ConfigError: foo.invalid is not a valid integer ('somevalue')
655 655 """
656 656
657 657 return self.configwith(int, section, name, default, 'integer',
658 658 untrusted)
659 659
660 660 def configbytes(self, section, name, default=_unset, untrusted=False):
661 661 """parse a configuration element as a quantity in bytes
662 662
663 663 Units can be specified as b (bytes), k or kb (kilobytes), m or
664 664 mb (megabytes), g or gb (gigabytes).
665 665
666 666 >>> u = ui(); s = b'foo'
667 667 >>> u.setconfig(s, b'val1', b'42')
668 668 >>> u.configbytes(s, b'val1')
669 669 42
670 670 >>> u.setconfig(s, b'val2', b'42.5 kb')
671 671 >>> u.configbytes(s, b'val2')
672 672 43520
673 673 >>> u.configbytes(s, b'unknown', b'7 MB')
674 674 7340032
675 675 >>> u.setconfig(s, b'invalid', b'somevalue')
676 676 >>> u.configbytes(s, b'invalid')
677 677 Traceback (most recent call last):
678 678 ...
679 679 ConfigError: foo.invalid is not a byte quantity ('somevalue')
680 680 """
681 681
682 682 value = self._config(section, name, default, untrusted)
683 683 if value is _unset:
684 684 if default is _unset:
685 685 default = 0
686 686 value = default
687 687 if not isinstance(value, bytes):
688 688 return value
689 689 try:
690 690 return util.sizetoint(value)
691 691 except error.ParseError:
692 692 raise error.ConfigError(_("%s.%s is not a byte quantity ('%s')")
693 693 % (section, name, value))
694 694
695 695 def configlist(self, section, name, default=_unset, untrusted=False):
696 696 """parse a configuration element as a list of comma/space separated
697 697 strings
698 698
699 699 >>> u = ui(); s = b'foo'
700 700 >>> u.setconfig(s, b'list1', b'this,is "a small" ,test')
701 701 >>> u.configlist(s, b'list1')
702 702 ['this', 'is', 'a small', 'test']
703 703 >>> u.setconfig(s, b'list2', b'this, is "a small" , test ')
704 704 >>> u.configlist(s, b'list2')
705 705 ['this', 'is', 'a small', 'test']
706 706 """
707 707 # default is not always a list
708 708 v = self.configwith(config.parselist, section, name, default,
709 709 'list', untrusted)
710 710 if isinstance(v, bytes):
711 711 return config.parselist(v)
712 712 elif v is None:
713 713 return []
714 714 return v
715 715
716 716 def configdate(self, section, name, default=_unset, untrusted=False):
717 717 """parse a configuration element as a tuple of ints
718 718
719 719 >>> u = ui(); s = b'foo'
720 720 >>> u.setconfig(s, b'date', b'0 0')
721 721 >>> u.configdate(s, b'date')
722 722 (0, 0)
723 723 """
724 724 if self.config(section, name, default, untrusted):
725 725 return self.configwith(util.parsedate, section, name, default,
726 726 'date', untrusted)
727 727 if default is _unset:
728 728 return None
729 729 return default
730 730
731 731 def hasconfig(self, section, name, untrusted=False):
732 732 return self._data(untrusted).hasitem(section, name)
733 733
734 734 def has_section(self, section, untrusted=False):
735 735 '''tell whether section exists in config.'''
736 736 return section in self._data(untrusted)
737 737
738 738 def configitems(self, section, untrusted=False, ignoresub=False):
739 739 items = self._data(untrusted).items(section)
740 740 if ignoresub:
741 741 newitems = {}
742 742 for k, v in items:
743 743 if ':' not in k:
744 744 newitems[k] = v
745 745 items = newitems.items()
746 746 if self.debugflag and not untrusted and self._reportuntrusted:
747 747 for k, v in self._ucfg.items(section):
748 748 if self._tcfg.get(section, k) != v:
749 749 self.debug("ignoring untrusted configuration option "
750 750 "%s.%s = %s\n" % (section, k, v))
751 751 return items
752 752
753 753 def walkconfig(self, untrusted=False):
754 754 cfg = self._data(untrusted)
755 755 for section in cfg.sections():
756 756 for name, value in self.configitems(section, untrusted):
757 757 yield section, name, value
758 758
759 759 def plain(self, feature=None):
760 760 '''is plain mode active?
761 761
762 762 Plain mode means that all configuration variables which affect
763 763 the behavior and output of Mercurial should be
764 764 ignored. Additionally, the output should be stable,
765 765 reproducible and suitable for use in scripts or applications.
766 766
767 767 The only way to trigger plain mode is by setting either the
768 768 `HGPLAIN' or `HGPLAINEXCEPT' environment variables.
769 769
770 770 The return value can either be
771 771 - False if HGPLAIN is not set, or feature is in HGPLAINEXCEPT
772 772 - False if feature is disabled by default and not included in HGPLAIN
773 773 - True otherwise
774 774 '''
775 775 if ('HGPLAIN' not in encoding.environ and
776 776 'HGPLAINEXCEPT' not in encoding.environ):
777 777 return False
778 778 exceptions = encoding.environ.get('HGPLAINEXCEPT',
779 779 '').strip().split(',')
780 780 # TODO: add support for HGPLAIN=+feature,-feature syntax
781 781 if '+strictflags' not in encoding.environ.get('HGPLAIN', '').split(','):
782 782 exceptions.append('strictflags')
783 783 if feature and exceptions:
784 784 return feature not in exceptions
785 785 return True
786 786
787 787 def username(self, acceptempty=False):
788 788 """Return default username to be used in commits.
789 789
790 790 Searched in this order: $HGUSER, [ui] section of hgrcs, $EMAIL
791 791 and stop searching if one of these is set.
792 792 If not found and acceptempty is True, returns None.
793 793 If not found and ui.askusername is True, ask the user, else use
794 794 ($LOGNAME or $USER or $LNAME or $USERNAME) + "@full.hostname".
795 795 If no username could be found, raise an Abort error.
796 796 """
797 797 user = encoding.environ.get("HGUSER")
798 798 if user is None:
799 799 user = self.config("ui", "username")
800 800 if user is not None:
801 801 user = os.path.expandvars(user)
802 802 if user is None:
803 803 user = encoding.environ.get("EMAIL")
804 804 if user is None and acceptempty:
805 805 return user
806 806 if user is None and self.configbool("ui", "askusername"):
807 807 user = self.prompt(_("enter a commit username:"), default=None)
808 808 if user is None and not self.interactive():
809 809 try:
810 810 user = '%s@%s' % (util.getuser(), socket.getfqdn())
811 811 self.warn(_("no username found, using '%s' instead\n") % user)
812 812 except KeyError:
813 813 pass
814 814 if not user:
815 815 raise error.Abort(_('no username supplied'),
816 816 hint=_("use 'hg config --edit' "
817 817 'to set your username'))
818 818 if "\n" in user:
819 819 raise error.Abort(_("username %s contains a newline\n")
820 820 % repr(user))
821 821 return user
822 822
823 823 def shortuser(self, user):
824 824 """Return a short representation of a user name or email address."""
825 825 if not self.verbose:
826 826 user = util.shortuser(user)
827 827 return user
828 828
829 829 def expandpath(self, loc, default=None):
830 830 """Return repository location relative to cwd or from [paths]"""
831 831 try:
832 832 p = self.paths.getpath(loc)
833 833 if p:
834 834 return p.rawloc
835 835 except error.RepoError:
836 836 pass
837 837
838 838 if default:
839 839 try:
840 840 p = self.paths.getpath(default)
841 841 if p:
842 842 return p.rawloc
843 843 except error.RepoError:
844 844 pass
845 845
846 846 return loc
847 847
848 848 @util.propertycache
849 849 def paths(self):
850 850 return paths(self)
851 851
852 852 def pushbuffer(self, error=False, subproc=False, labeled=False):
853 853 """install a buffer to capture standard output of the ui object
854 854
855 855 If error is True, the error output will be captured too.
856 856
857 857 If subproc is True, output from subprocesses (typically hooks) will be
858 858 captured too.
859 859
860 860 If labeled is True, any labels associated with buffered
861 861 output will be handled. By default, this has no effect
862 862 on the output returned, but extensions and GUI tools may
863 863 handle this argument and returned styled output. If output
864 864 is being buffered so it can be captured and parsed or
865 865 processed, labeled should not be set to True.
866 866 """
867 867 self._buffers.append([])
868 868 self._bufferstates.append((error, subproc, labeled))
869 869 self._bufferapplylabels = labeled
870 870
871 871 def popbuffer(self):
872 872 '''pop the last buffer and return the buffered output'''
873 873 self._bufferstates.pop()
874 874 if self._bufferstates:
875 875 self._bufferapplylabels = self._bufferstates[-1][2]
876 876 else:
877 877 self._bufferapplylabels = None
878 878
879 879 return "".join(self._buffers.pop())
880 880
881 881 def write(self, *args, **opts):
882 882 '''write args to output
883 883
884 884 By default, this method simply writes to the buffer or stdout.
885 885 Color mode can be set on the UI class to have the output decorated
886 886 with color modifier before being written to stdout.
887 887
888 888 The color used is controlled by an optional keyword argument, "label".
889 889 This should be a string containing label names separated by space.
890 890 Label names take the form of "topic.type". For example, ui.debug()
891 891 issues a label of "ui.debug".
892 892
893 893 When labeling output for a specific command, a label of
894 894 "cmdname.type" is recommended. For example, status issues
895 895 a label of "status.modified" for modified files.
896 896 '''
897 897 if self._buffers and not opts.get(r'prompt', False):
898 898 if self._bufferapplylabels:
899 899 label = opts.get(r'label', '')
900 900 self._buffers[-1].extend(self.label(a, label) for a in args)
901 901 else:
902 902 self._buffers[-1].extend(args)
903 903 elif self._colormode == 'win32':
904 904 # windows color printing is its own can of crab, defer to
905 905 # the color module and that is it.
906 906 color.win32print(self, self._write, *args, **opts)
907 907 else:
908 908 msgs = args
909 909 if self._colormode is not None:
910 910 label = opts.get(r'label', '')
911 911 msgs = [self.label(a, label) for a in args]
912 912 self._write(*msgs, **opts)
913 913
914 914 def _write(self, *msgs, **opts):
915 915 self._progclear()
916 916 # opencode timeblockedsection because this is a critical path
917 917 starttime = util.timer()
918 918 try:
919 for a in msgs:
920 self.fout.write(a)
919 self.fout.write(''.join(msgs))
921 920 except IOError as err:
922 921 raise error.StdioError(err)
923 922 finally:
924 923 self._blockedtimes['stdio_blocked'] += \
925 924 (util.timer() - starttime) * 1000
926 925
927 926 def write_err(self, *args, **opts):
928 927 self._progclear()
929 928 if self._bufferstates and self._bufferstates[-1][0]:
930 929 self.write(*args, **opts)
931 930 elif self._colormode == 'win32':
932 931 # windows color printing is its own can of crab, defer to
933 932 # the color module and that is it.
934 933 color.win32print(self, self._write_err, *args, **opts)
935 934 else:
936 935 msgs = args
937 936 if self._colormode is not None:
938 937 label = opts.get(r'label', '')
939 938 msgs = [self.label(a, label) for a in args]
940 939 self._write_err(*msgs, **opts)
941 940
942 941 def _write_err(self, *msgs, **opts):
943 942 try:
944 943 with self.timeblockedsection('stdio'):
945 944 if not getattr(self.fout, 'closed', False):
946 945 self.fout.flush()
947 946 for a in msgs:
948 947 self.ferr.write(a)
949 948 # stderr may be buffered under win32 when redirected to files,
950 949 # including stdout.
951 950 if not getattr(self.ferr, 'closed', False):
952 951 self.ferr.flush()
953 952 except IOError as inst:
954 953 if inst.errno not in (errno.EPIPE, errno.EIO, errno.EBADF):
955 954 raise error.StdioError(inst)
956 955
957 956 def flush(self):
958 957 # opencode timeblockedsection because this is a critical path
959 958 starttime = util.timer()
960 959 try:
961 960 try:
962 961 self.fout.flush()
963 962 except IOError as err:
964 963 if err.errno not in (errno.EPIPE, errno.EIO, errno.EBADF):
965 964 raise error.StdioError(err)
966 965 finally:
967 966 try:
968 967 self.ferr.flush()
969 968 except IOError as err:
970 969 if err.errno not in (errno.EPIPE, errno.EIO, errno.EBADF):
971 970 raise error.StdioError(err)
972 971 finally:
973 972 self._blockedtimes['stdio_blocked'] += \
974 973 (util.timer() - starttime) * 1000
975 974
976 975 def _isatty(self, fh):
977 976 if self.configbool('ui', 'nontty'):
978 977 return False
979 978 return util.isatty(fh)
980 979
981 980 def disablepager(self):
982 981 self._disablepager = True
983 982
984 983 def pager(self, command):
985 984 """Start a pager for subsequent command output.
986 985
987 986 Commands which produce a long stream of output should call
988 987 this function to activate the user's preferred pagination
989 988 mechanism (which may be no pager). Calling this function
990 989 precludes any future use of interactive functionality, such as
991 990 prompting the user or activating curses.
992 991
993 992 Args:
994 993 command: The full, non-aliased name of the command. That is, "log"
995 994 not "history, "summary" not "summ", etc.
996 995 """
997 996 if (self._disablepager
998 997 or self.pageractive):
999 998 # how pager should do is already determined
1000 999 return
1001 1000
1002 1001 if not command.startswith('internal-always-') and (
1003 1002 # explicit --pager=on (= 'internal-always-' prefix) should
1004 1003 # take precedence over disabling factors below
1005 1004 command in self.configlist('pager', 'ignore')
1006 1005 or not self.configbool('ui', 'paginate')
1007 1006 or not self.configbool('pager', 'attend-' + command, True)
1008 1007 # TODO: if we want to allow HGPLAINEXCEPT=pager,
1009 1008 # formatted() will need some adjustment.
1010 1009 or not self.formatted()
1011 1010 or self.plain()
1012 1011 or self._buffers
1013 1012 # TODO: expose debugger-enabled on the UI object
1014 1013 or '--debugger' in pycompat.sysargv):
1015 1014 # We only want to paginate if the ui appears to be
1016 1015 # interactive, the user didn't say HGPLAIN or
1017 1016 # HGPLAINEXCEPT=pager, and the user didn't specify --debug.
1018 1017 return
1019 1018
1020 1019 pagercmd = self.config('pager', 'pager', rcutil.fallbackpager)
1021 1020 if not pagercmd:
1022 1021 return
1023 1022
1024 1023 pagerenv = {}
1025 1024 for name, value in rcutil.defaultpagerenv().items():
1026 1025 if name not in encoding.environ:
1027 1026 pagerenv[name] = value
1028 1027
1029 1028 self.debug('starting pager for command %r\n' % command)
1030 1029 self.flush()
1031 1030
1032 1031 wasformatted = self.formatted()
1033 1032 if util.safehasattr(signal, "SIGPIPE"):
1034 1033 signal.signal(signal.SIGPIPE, _catchterm)
1035 1034 if self._runpager(pagercmd, pagerenv):
1036 1035 self.pageractive = True
1037 1036 # Preserve the formatted-ness of the UI. This is important
1038 1037 # because we mess with stdout, which might confuse
1039 1038 # auto-detection of things being formatted.
1040 1039 self.setconfig('ui', 'formatted', wasformatted, 'pager')
1041 1040 self.setconfig('ui', 'interactive', False, 'pager')
1042 1041
1043 1042 # If pagermode differs from color.mode, reconfigure color now that
1044 1043 # pageractive is set.
1045 1044 cm = self._colormode
1046 1045 if cm != self.config('color', 'pagermode', cm):
1047 1046 color.setup(self)
1048 1047 else:
1049 1048 # If the pager can't be spawned in dispatch when --pager=on is
1050 1049 # given, don't try again when the command runs, to avoid a duplicate
1051 1050 # warning about a missing pager command.
1052 1051 self.disablepager()
1053 1052
1054 1053 def _runpager(self, command, env=None):
1055 1054 """Actually start the pager and set up file descriptors.
1056 1055
1057 1056 This is separate in part so that extensions (like chg) can
1058 1057 override how a pager is invoked.
1059 1058 """
1060 1059 if command == 'cat':
1061 1060 # Save ourselves some work.
1062 1061 return False
1063 1062 # If the command doesn't contain any of these characters, we
1064 1063 # assume it's a binary and exec it directly. This means for
1065 1064 # simple pager command configurations, we can degrade
1066 1065 # gracefully and tell the user about their broken pager.
1067 1066 shell = any(c in command for c in "|&;<>()$`\\\"' \t\n*?[#~=%")
1068 1067
1069 1068 if pycompat.iswindows and not shell:
1070 1069 # Window's built-in `more` cannot be invoked with shell=False, but
1071 1070 # its `more.com` can. Hide this implementation detail from the
1072 1071 # user so we can also get sane bad PAGER behavior. MSYS has
1073 1072 # `more.exe`, so do a cmd.exe style resolution of the executable to
1074 1073 # determine which one to use.
1075 1074 fullcmd = util.findexe(command)
1076 1075 if not fullcmd:
1077 1076 self.warn(_("missing pager command '%s', skipping pager\n")
1078 1077 % command)
1079 1078 return False
1080 1079
1081 1080 command = fullcmd
1082 1081
1083 1082 try:
1084 1083 pager = subprocess.Popen(
1085 1084 command, shell=shell, bufsize=-1,
1086 1085 close_fds=util.closefds, stdin=subprocess.PIPE,
1087 1086 stdout=util.stdout, stderr=util.stderr,
1088 1087 env=util.shellenviron(env))
1089 1088 except OSError as e:
1090 1089 if e.errno == errno.ENOENT and not shell:
1091 1090 self.warn(_("missing pager command '%s', skipping pager\n")
1092 1091 % command)
1093 1092 return False
1094 1093 raise
1095 1094
1096 1095 # back up original file descriptors
1097 1096 stdoutfd = os.dup(util.stdout.fileno())
1098 1097 stderrfd = os.dup(util.stderr.fileno())
1099 1098
1100 1099 os.dup2(pager.stdin.fileno(), util.stdout.fileno())
1101 1100 if self._isatty(util.stderr):
1102 1101 os.dup2(pager.stdin.fileno(), util.stderr.fileno())
1103 1102
1104 1103 @self.atexit
1105 1104 def killpager():
1106 1105 if util.safehasattr(signal, "SIGINT"):
1107 1106 signal.signal(signal.SIGINT, signal.SIG_IGN)
1108 1107 # restore original fds, closing pager.stdin copies in the process
1109 1108 os.dup2(stdoutfd, util.stdout.fileno())
1110 1109 os.dup2(stderrfd, util.stderr.fileno())
1111 1110 pager.stdin.close()
1112 1111 pager.wait()
1113 1112
1114 1113 return True
1115 1114
1116 1115 @property
1117 1116 def _exithandlers(self):
1118 1117 return _reqexithandlers
1119 1118
1120 1119 def atexit(self, func, *args, **kwargs):
1121 1120 '''register a function to run after dispatching a request
1122 1121
1123 1122 Handlers do not stay registered across request boundaries.'''
1124 1123 self._exithandlers.append((func, args, kwargs))
1125 1124 return func
1126 1125
1127 1126 def interface(self, feature):
1128 1127 """what interface to use for interactive console features?
1129 1128
1130 1129 The interface is controlled by the value of `ui.interface` but also by
1131 1130 the value of feature-specific configuration. For example:
1132 1131
1133 1132 ui.interface.histedit = text
1134 1133 ui.interface.chunkselector = curses
1135 1134
1136 1135 Here the features are "histedit" and "chunkselector".
1137 1136
1138 1137 The configuration above means that the default interfaces for commands
1139 1138 is curses, the interface for histedit is text and the interface for
1140 1139 selecting chunk is crecord (the best curses interface available).
1141 1140
1142 1141 Consider the following example:
1143 1142 ui.interface = curses
1144 1143 ui.interface.histedit = text
1145 1144
1146 1145 Then histedit will use the text interface and chunkselector will use
1147 1146 the default curses interface (crecord at the moment).
1148 1147 """
1149 1148 alldefaults = frozenset(["text", "curses"])
1150 1149
1151 1150 featureinterfaces = {
1152 1151 "chunkselector": [
1153 1152 "text",
1154 1153 "curses",
1155 1154 ]
1156 1155 }
1157 1156
1158 1157 # Feature-specific interface
1159 1158 if feature not in featureinterfaces.keys():
1160 1159 # Programming error, not user error
1161 1160 raise ValueError("Unknown feature requested %s" % feature)
1162 1161
1163 1162 availableinterfaces = frozenset(featureinterfaces[feature])
1164 1163 if alldefaults > availableinterfaces:
1165 1164 # Programming error, not user error. We need a use case to
1166 1165 # define the right thing to do here.
1167 1166 raise ValueError(
1168 1167 "Feature %s does not handle all default interfaces" %
1169 1168 feature)
1170 1169
1171 1170 if self.plain():
1172 1171 return "text"
1173 1172
1174 1173 # Default interface for all the features
1175 1174 defaultinterface = "text"
1176 1175 i = self.config("ui", "interface")
1177 1176 if i in alldefaults:
1178 1177 defaultinterface = i
1179 1178
1180 1179 choseninterface = defaultinterface
1181 1180 f = self.config("ui", "interface.%s" % feature)
1182 1181 if f in availableinterfaces:
1183 1182 choseninterface = f
1184 1183
1185 1184 if i is not None and defaultinterface != i:
1186 1185 if f is not None:
1187 1186 self.warn(_("invalid value for ui.interface: %s\n") %
1188 1187 (i,))
1189 1188 else:
1190 1189 self.warn(_("invalid value for ui.interface: %s (using %s)\n") %
1191 1190 (i, choseninterface))
1192 1191 if f is not None and choseninterface != f:
1193 1192 self.warn(_("invalid value for ui.interface.%s: %s (using %s)\n") %
1194 1193 (feature, f, choseninterface))
1195 1194
1196 1195 return choseninterface
1197 1196
1198 1197 def interactive(self):
1199 1198 '''is interactive input allowed?
1200 1199
1201 1200 An interactive session is a session where input can be reasonably read
1202 1201 from `sys.stdin'. If this function returns false, any attempt to read
1203 1202 from stdin should fail with an error, unless a sensible default has been
1204 1203 specified.
1205 1204
1206 1205 Interactiveness is triggered by the value of the `ui.interactive'
1207 1206 configuration variable or - if it is unset - when `sys.stdin' points
1208 1207 to a terminal device.
1209 1208
1210 1209 This function refers to input only; for output, see `ui.formatted()'.
1211 1210 '''
1212 1211 i = self.configbool("ui", "interactive")
1213 1212 if i is None:
1214 1213 # some environments replace stdin without implementing isatty
1215 1214 # usually those are non-interactive
1216 1215 return self._isatty(self.fin)
1217 1216
1218 1217 return i
1219 1218
1220 1219 def termwidth(self):
1221 1220 '''how wide is the terminal in columns?
1222 1221 '''
1223 1222 if 'COLUMNS' in encoding.environ:
1224 1223 try:
1225 1224 return int(encoding.environ['COLUMNS'])
1226 1225 except ValueError:
1227 1226 pass
1228 1227 return scmutil.termsize(self)[0]
1229 1228
1230 1229 def formatted(self):
1231 1230 '''should formatted output be used?
1232 1231
1233 1232 It is often desirable to format the output to suite the output medium.
1234 1233 Examples of this are truncating long lines or colorizing messages.
1235 1234 However, this is not often not desirable when piping output into other
1236 1235 utilities, e.g. `grep'.
1237 1236
1238 1237 Formatted output is triggered by the value of the `ui.formatted'
1239 1238 configuration variable or - if it is unset - when `sys.stdout' points
1240 1239 to a terminal device. Please note that `ui.formatted' should be
1241 1240 considered an implementation detail; it is not intended for use outside
1242 1241 Mercurial or its extensions.
1243 1242
1244 1243 This function refers to output only; for input, see `ui.interactive()'.
1245 1244 This function always returns false when in plain mode, see `ui.plain()'.
1246 1245 '''
1247 1246 if self.plain():
1248 1247 return False
1249 1248
1250 1249 i = self.configbool("ui", "formatted")
1251 1250 if i is None:
1252 1251 # some environments replace stdout without implementing isatty
1253 1252 # usually those are non-interactive
1254 1253 return self._isatty(self.fout)
1255 1254
1256 1255 return i
1257 1256
1258 1257 def _readline(self, prompt=''):
1259 1258 if self._isatty(self.fin):
1260 1259 try:
1261 1260 # magically add command line editing support, where
1262 1261 # available
1263 1262 import readline
1264 1263 # force demandimport to really load the module
1265 1264 readline.read_history_file
1266 1265 # windows sometimes raises something other than ImportError
1267 1266 except Exception:
1268 1267 pass
1269 1268
1270 1269 # call write() so output goes through subclassed implementation
1271 1270 # e.g. color extension on Windows
1272 1271 self.write(prompt, prompt=True)
1273 1272 self.flush()
1274 1273
1275 1274 # prompt ' ' must exist; otherwise readline may delete entire line
1276 1275 # - http://bugs.python.org/issue12833
1277 1276 with self.timeblockedsection('stdio'):
1278 1277 line = util.bytesinput(self.fin, self.fout, r' ')
1279 1278
1280 1279 # When stdin is in binary mode on Windows, it can cause
1281 1280 # raw_input() to emit an extra trailing carriage return
1282 1281 if pycompat.oslinesep == '\r\n' and line and line[-1] == '\r':
1283 1282 line = line[:-1]
1284 1283 return line
1285 1284
1286 1285 def prompt(self, msg, default="y"):
1287 1286 """Prompt user with msg, read response.
1288 1287 If ui is not interactive, the default is returned.
1289 1288 """
1290 1289 if not self.interactive():
1291 1290 self.write(msg, ' ', default or '', "\n")
1292 1291 return default
1293 1292 try:
1294 1293 r = self._readline(self.label(msg, 'ui.prompt'))
1295 1294 if not r:
1296 1295 r = default
1297 1296 if self.configbool('ui', 'promptecho'):
1298 1297 self.write(r, "\n")
1299 1298 return r
1300 1299 except EOFError:
1301 1300 raise error.ResponseExpected()
1302 1301
1303 1302 @staticmethod
1304 1303 def extractchoices(prompt):
1305 1304 """Extract prompt message and list of choices from specified prompt.
1306 1305
1307 1306 This returns tuple "(message, choices)", and "choices" is the
1308 1307 list of tuple "(response character, text without &)".
1309 1308
1310 1309 >>> ui.extractchoices(b"awake? $$ &Yes $$ &No")
1311 1310 ('awake? ', [('y', 'Yes'), ('n', 'No')])
1312 1311 >>> ui.extractchoices(b"line\\nbreak? $$ &Yes $$ &No")
1313 1312 ('line\\nbreak? ', [('y', 'Yes'), ('n', 'No')])
1314 1313 >>> ui.extractchoices(b"want lots of $$money$$?$$Ye&s$$N&o")
1315 1314 ('want lots of $$money$$?', [('s', 'Yes'), ('o', 'No')])
1316 1315 """
1317 1316
1318 1317 # Sadly, the prompt string may have been built with a filename
1319 1318 # containing "$$" so let's try to find the first valid-looking
1320 1319 # prompt to start parsing. Sadly, we also can't rely on
1321 1320 # choices containing spaces, ASCII, or basically anything
1322 1321 # except an ampersand followed by a character.
1323 1322 m = re.match(br'(?s)(.+?)\$\$([^\$]*&[^ \$].*)', prompt)
1324 1323 msg = m.group(1)
1325 1324 choices = [p.strip(' ') for p in m.group(2).split('$$')]
1326 1325 def choicetuple(s):
1327 1326 ampidx = s.index('&')
1328 1327 return s[ampidx + 1:ampidx + 2].lower(), s.replace('&', '', 1)
1329 1328 return (msg, [choicetuple(s) for s in choices])
1330 1329
1331 1330 def promptchoice(self, prompt, default=0):
1332 1331 """Prompt user with a message, read response, and ensure it matches
1333 1332 one of the provided choices. The prompt is formatted as follows:
1334 1333
1335 1334 "would you like fries with that (Yn)? $$ &Yes $$ &No"
1336 1335
1337 1336 The index of the choice is returned. Responses are case
1338 1337 insensitive. If ui is not interactive, the default is
1339 1338 returned.
1340 1339 """
1341 1340
1342 1341 msg, choices = self.extractchoices(prompt)
1343 1342 resps = [r for r, t in choices]
1344 1343 while True:
1345 1344 r = self.prompt(msg, resps[default])
1346 1345 if r.lower() in resps:
1347 1346 return resps.index(r.lower())
1348 1347 self.write(_("unrecognized response\n"))
1349 1348
1350 1349 def getpass(self, prompt=None, default=None):
1351 1350 if not self.interactive():
1352 1351 return default
1353 1352 try:
1354 1353 self.write_err(self.label(prompt or _('password: '), 'ui.prompt'))
1355 1354 # disable getpass() only if explicitly specified. it's still valid
1356 1355 # to interact with tty even if fin is not a tty.
1357 1356 with self.timeblockedsection('stdio'):
1358 1357 if self.configbool('ui', 'nontty'):
1359 1358 l = self.fin.readline()
1360 1359 if not l:
1361 1360 raise EOFError
1362 1361 return l.rstrip('\n')
1363 1362 else:
1364 1363 return getpass.getpass('')
1365 1364 except EOFError:
1366 1365 raise error.ResponseExpected()
1367 1366 def status(self, *msg, **opts):
1368 1367 '''write status message to output (if ui.quiet is False)
1369 1368
1370 1369 This adds an output label of "ui.status".
1371 1370 '''
1372 1371 if not self.quiet:
1373 1372 opts[r'label'] = opts.get(r'label', '') + ' ui.status'
1374 1373 self.write(*msg, **opts)
1375 1374 def warn(self, *msg, **opts):
1376 1375 '''write warning message to output (stderr)
1377 1376
1378 1377 This adds an output label of "ui.warning".
1379 1378 '''
1380 1379 opts[r'label'] = opts.get(r'label', '') + ' ui.warning'
1381 1380 self.write_err(*msg, **opts)
1382 1381 def note(self, *msg, **opts):
1383 1382 '''write note to output (if ui.verbose is True)
1384 1383
1385 1384 This adds an output label of "ui.note".
1386 1385 '''
1387 1386 if self.verbose:
1388 1387 opts[r'label'] = opts.get(r'label', '') + ' ui.note'
1389 1388 self.write(*msg, **opts)
1390 1389 def debug(self, *msg, **opts):
1391 1390 '''write debug message to output (if ui.debugflag is True)
1392 1391
1393 1392 This adds an output label of "ui.debug".
1394 1393 '''
1395 1394 if self.debugflag:
1396 1395 opts[r'label'] = opts.get(r'label', '') + ' ui.debug'
1397 1396 self.write(*msg, **opts)
1398 1397
1399 1398 def edit(self, text, user, extra=None, editform=None, pending=None,
1400 1399 repopath=None, action=None):
1401 1400 if action is None:
1402 1401 self.develwarn('action is None but will soon be a required '
1403 1402 'parameter to ui.edit()')
1404 1403 extra_defaults = {
1405 1404 'prefix': 'editor',
1406 1405 'suffix': '.txt',
1407 1406 }
1408 1407 if extra is not None:
1409 1408 if extra.get('suffix') is not None:
1410 1409 self.develwarn('extra.suffix is not None but will soon be '
1411 1410 'ignored by ui.edit()')
1412 1411 extra_defaults.update(extra)
1413 1412 extra = extra_defaults
1414 1413
1415 1414 if action == 'diff':
1416 1415 suffix = '.diff'
1417 1416 elif action:
1418 1417 suffix = '.%s.hg.txt' % action
1419 1418 else:
1420 1419 suffix = extra['suffix']
1421 1420
1422 1421 rdir = None
1423 1422 if self.configbool('experimental', 'editortmpinhg'):
1424 1423 rdir = repopath
1425 1424 (fd, name) = tempfile.mkstemp(prefix='hg-' + extra['prefix'] + '-',
1426 1425 suffix=suffix,
1427 1426 dir=rdir)
1428 1427 try:
1429 1428 f = os.fdopen(fd, r'wb')
1430 1429 f.write(util.tonativeeol(text))
1431 1430 f.close()
1432 1431
1433 1432 environ = {'HGUSER': user}
1434 1433 if 'transplant_source' in extra:
1435 1434 environ.update({'HGREVISION': hex(extra['transplant_source'])})
1436 1435 for label in ('intermediate-source', 'source', 'rebase_source'):
1437 1436 if label in extra:
1438 1437 environ.update({'HGREVISION': extra[label]})
1439 1438 break
1440 1439 if editform:
1441 1440 environ.update({'HGEDITFORM': editform})
1442 1441 if pending:
1443 1442 environ.update({'HG_PENDING': pending})
1444 1443
1445 1444 editor = self.geteditor()
1446 1445
1447 1446 self.system("%s \"%s\"" % (editor, name),
1448 1447 environ=environ,
1449 1448 onerr=error.Abort, errprefix=_("edit failed"),
1450 1449 blockedtag='editor')
1451 1450
1452 1451 f = open(name, r'rb')
1453 1452 t = util.fromnativeeol(f.read())
1454 1453 f.close()
1455 1454 finally:
1456 1455 os.unlink(name)
1457 1456
1458 1457 return t
1459 1458
1460 1459 def system(self, cmd, environ=None, cwd=None, onerr=None, errprefix=None,
1461 1460 blockedtag=None):
1462 1461 '''execute shell command with appropriate output stream. command
1463 1462 output will be redirected if fout is not stdout.
1464 1463
1465 1464 if command fails and onerr is None, return status, else raise onerr
1466 1465 object as exception.
1467 1466 '''
1468 1467 if blockedtag is None:
1469 1468 # Long cmds tend to be because of an absolute path on cmd. Keep
1470 1469 # the tail end instead
1471 1470 cmdsuffix = cmd.translate(None, _keepalnum)[-85:]
1472 1471 blockedtag = 'unknown_system_' + cmdsuffix
1473 1472 out = self.fout
1474 1473 if any(s[1] for s in self._bufferstates):
1475 1474 out = self
1476 1475 with self.timeblockedsection(blockedtag):
1477 1476 rc = self._runsystem(cmd, environ=environ, cwd=cwd, out=out)
1478 1477 if rc and onerr:
1479 1478 errmsg = '%s %s' % (os.path.basename(cmd.split(None, 1)[0]),
1480 1479 util.explainexit(rc)[0])
1481 1480 if errprefix:
1482 1481 errmsg = '%s: %s' % (errprefix, errmsg)
1483 1482 raise onerr(errmsg)
1484 1483 return rc
1485 1484
1486 1485 def _runsystem(self, cmd, environ, cwd, out):
1487 1486 """actually execute the given shell command (can be overridden by
1488 1487 extensions like chg)"""
1489 1488 return util.system(cmd, environ=environ, cwd=cwd, out=out)
1490 1489
1491 1490 def traceback(self, exc=None, force=False):
1492 1491 '''print exception traceback if traceback printing enabled or forced.
1493 1492 only to call in exception handler. returns true if traceback
1494 1493 printed.'''
1495 1494 if self.tracebackflag or force:
1496 1495 if exc is None:
1497 1496 exc = sys.exc_info()
1498 1497 cause = getattr(exc[1], 'cause', None)
1499 1498
1500 1499 if cause is not None:
1501 1500 causetb = traceback.format_tb(cause[2])
1502 1501 exctb = traceback.format_tb(exc[2])
1503 1502 exconly = traceback.format_exception_only(cause[0], cause[1])
1504 1503
1505 1504 # exclude frame where 'exc' was chained and rethrown from exctb
1506 1505 self.write_err('Traceback (most recent call last):\n',
1507 1506 ''.join(exctb[:-1]),
1508 1507 ''.join(causetb),
1509 1508 ''.join(exconly))
1510 1509 else:
1511 1510 output = traceback.format_exception(exc[0], exc[1], exc[2])
1512 1511 data = r''.join(output)
1513 1512 if pycompat.ispy3:
1514 1513 enc = pycompat.sysstr(encoding.encoding)
1515 1514 data = data.encode(enc, errors=r'replace')
1516 1515 self.write_err(data)
1517 1516 return self.tracebackflag or force
1518 1517
1519 1518 def geteditor(self):
1520 1519 '''return editor to use'''
1521 1520 if pycompat.sysplatform == 'plan9':
1522 1521 # vi is the MIPS instruction simulator on Plan 9. We
1523 1522 # instead default to E to plumb commit messages to
1524 1523 # avoid confusion.
1525 1524 editor = 'E'
1526 1525 else:
1527 1526 editor = 'vi'
1528 1527 return (encoding.environ.get("HGEDITOR") or
1529 1528 self.config("ui", "editor", editor))
1530 1529
1531 1530 @util.propertycache
1532 1531 def _progbar(self):
1533 1532 """setup the progbar singleton to the ui object"""
1534 1533 if (self.quiet or self.debugflag
1535 1534 or self.configbool('progress', 'disable')
1536 1535 or not progress.shouldprint(self)):
1537 1536 return None
1538 1537 return getprogbar(self)
1539 1538
1540 1539 def _progclear(self):
1541 1540 """clear progress bar output if any. use it before any output"""
1542 1541 if not haveprogbar(): # nothing loaded yet
1543 1542 return
1544 1543 if self._progbar is not None and self._progbar.printed:
1545 1544 self._progbar.clear()
1546 1545
1547 1546 def progress(self, topic, pos, item="", unit="", total=None):
1548 1547 '''show a progress message
1549 1548
1550 1549 By default a textual progress bar will be displayed if an operation
1551 1550 takes too long. 'topic' is the current operation, 'item' is a
1552 1551 non-numeric marker of the current position (i.e. the currently
1553 1552 in-process file), 'pos' is the current numeric position (i.e.
1554 1553 revision, bytes, etc.), unit is a corresponding unit label,
1555 1554 and total is the highest expected pos.
1556 1555
1557 1556 Multiple nested topics may be active at a time.
1558 1557
1559 1558 All topics should be marked closed by setting pos to None at
1560 1559 termination.
1561 1560 '''
1562 1561 if self._progbar is not None:
1563 1562 self._progbar.progress(topic, pos, item=item, unit=unit,
1564 1563 total=total)
1565 1564 if pos is None or not self.configbool('progress', 'debug'):
1566 1565 return
1567 1566
1568 1567 if unit:
1569 1568 unit = ' ' + unit
1570 1569 if item:
1571 1570 item = ' ' + item
1572 1571
1573 1572 if total:
1574 1573 pct = 100.0 * pos / total
1575 1574 self.debug('%s:%s %d/%d%s (%4.2f%%)\n'
1576 1575 % (topic, item, pos, total, unit, pct))
1577 1576 else:
1578 1577 self.debug('%s:%s %d%s\n' % (topic, item, pos, unit))
1579 1578
1580 1579 def log(self, service, *msg, **opts):
1581 1580 '''hook for logging facility extensions
1582 1581
1583 1582 service should be a readily-identifiable subsystem, which will
1584 1583 allow filtering.
1585 1584
1586 1585 *msg should be a newline-terminated format string to log, and
1587 1586 then any values to %-format into that format string.
1588 1587
1589 1588 **opts currently has no defined meanings.
1590 1589 '''
1591 1590
1592 1591 def label(self, msg, label):
1593 1592 '''style msg based on supplied label
1594 1593
1595 1594 If some color mode is enabled, this will add the necessary control
1596 1595 characters to apply such color. In addition, 'debug' color mode adds
1597 1596 markup showing which label affects a piece of text.
1598 1597
1599 1598 ui.write(s, 'label') is equivalent to
1600 1599 ui.write(ui.label(s, 'label')).
1601 1600 '''
1602 1601 if self._colormode is not None:
1603 1602 return color.colorlabel(self, msg, label)
1604 1603 return msg
1605 1604
1606 1605 def develwarn(self, msg, stacklevel=1, config=None):
1607 1606 """issue a developer warning message
1608 1607
1609 1608 Use 'stacklevel' to report the offender some layers further up in the
1610 1609 stack.
1611 1610 """
1612 1611 if not self.configbool('devel', 'all-warnings'):
1613 1612 if config is None or not self.configbool('devel', config):
1614 1613 return
1615 1614 msg = 'devel-warn: ' + msg
1616 1615 stacklevel += 1 # get in develwarn
1617 1616 if self.tracebackflag:
1618 1617 util.debugstacktrace(msg, stacklevel, self.ferr, self.fout)
1619 1618 self.log('develwarn', '%s at:\n%s' %
1620 1619 (msg, ''.join(util.getstackframes(stacklevel))))
1621 1620 else:
1622 1621 curframe = inspect.currentframe()
1623 1622 calframe = inspect.getouterframes(curframe, 2)
1624 1623 self.write_err('%s at: %s:%s (%s)\n'
1625 1624 % ((msg,) + calframe[stacklevel][1:4]))
1626 1625 self.log('develwarn', '%s at: %s:%s (%s)\n',
1627 1626 msg, *calframe[stacklevel][1:4])
1628 1627 curframe = calframe = None # avoid cycles
1629 1628
1630 1629 def deprecwarn(self, msg, version):
1631 1630 """issue a deprecation warning
1632 1631
1633 1632 - msg: message explaining what is deprecated and how to upgrade,
1634 1633 - version: last version where the API will be supported,
1635 1634 """
1636 1635 if not (self.configbool('devel', 'all-warnings')
1637 1636 or self.configbool('devel', 'deprec-warn')):
1638 1637 return
1639 1638 msg += ("\n(compatibility will be dropped after Mercurial-%s,"
1640 1639 " update your code.)") % version
1641 1640 self.develwarn(msg, stacklevel=2, config='deprec-warn')
1642 1641
1643 1642 def exportableenviron(self):
1644 1643 """The environment variables that are safe to export, e.g. through
1645 1644 hgweb.
1646 1645 """
1647 1646 return self._exportableenviron
1648 1647
1649 1648 @contextlib.contextmanager
1650 1649 def configoverride(self, overrides, source=""):
1651 1650 """Context manager for temporary config overrides
1652 1651 `overrides` must be a dict of the following structure:
1653 1652 {(section, name) : value}"""
1654 1653 backups = {}
1655 1654 try:
1656 1655 for (section, name), value in overrides.items():
1657 1656 backups[(section, name)] = self.backupconfig(section, name)
1658 1657 self.setconfig(section, name, value, source)
1659 1658 yield
1660 1659 finally:
1661 1660 for __, backup in backups.items():
1662 1661 self.restoreconfig(backup)
1663 1662 # just restoring ui.quiet config to the previous value is not enough
1664 1663 # as it does not update ui.quiet class member
1665 1664 if ('ui', 'quiet') in overrides:
1666 1665 self.fixconfig(section='ui')
1667 1666
1668 1667 class paths(dict):
1669 1668 """Represents a collection of paths and their configs.
1670 1669
1671 1670 Data is initially derived from ui instances and the config files they have
1672 1671 loaded.
1673 1672 """
1674 1673 def __init__(self, ui):
1675 1674 dict.__init__(self)
1676 1675
1677 1676 for name, loc in ui.configitems('paths', ignoresub=True):
1678 1677 # No location is the same as not existing.
1679 1678 if not loc:
1680 1679 continue
1681 1680 loc, sub = ui.configsuboptions('paths', name)
1682 1681 self[name] = path(ui, name, rawloc=loc, suboptions=sub)
1683 1682
1684 1683 def getpath(self, name, default=None):
1685 1684 """Return a ``path`` from a string, falling back to default.
1686 1685
1687 1686 ``name`` can be a named path or locations. Locations are filesystem
1688 1687 paths or URIs.
1689 1688
1690 1689 Returns None if ``name`` is not a registered path, a URI, or a local
1691 1690 path to a repo.
1692 1691 """
1693 1692 # Only fall back to default if no path was requested.
1694 1693 if name is None:
1695 1694 if not default:
1696 1695 default = ()
1697 1696 elif not isinstance(default, (tuple, list)):
1698 1697 default = (default,)
1699 1698 for k in default:
1700 1699 try:
1701 1700 return self[k]
1702 1701 except KeyError:
1703 1702 continue
1704 1703 return None
1705 1704
1706 1705 # Most likely empty string.
1707 1706 # This may need to raise in the future.
1708 1707 if not name:
1709 1708 return None
1710 1709
1711 1710 try:
1712 1711 return self[name]
1713 1712 except KeyError:
1714 1713 # Try to resolve as a local path or URI.
1715 1714 try:
1716 1715 # We don't pass sub-options in, so no need to pass ui instance.
1717 1716 return path(None, None, rawloc=name)
1718 1717 except ValueError:
1719 1718 raise error.RepoError(_('repository %s does not exist') %
1720 1719 name)
1721 1720
1722 1721 _pathsuboptions = {}
1723 1722
1724 1723 def pathsuboption(option, attr):
1725 1724 """Decorator used to declare a path sub-option.
1726 1725
1727 1726 Arguments are the sub-option name and the attribute it should set on
1728 1727 ``path`` instances.
1729 1728
1730 1729 The decorated function will receive as arguments a ``ui`` instance,
1731 1730 ``path`` instance, and the string value of this option from the config.
1732 1731 The function should return the value that will be set on the ``path``
1733 1732 instance.
1734 1733
1735 1734 This decorator can be used to perform additional verification of
1736 1735 sub-options and to change the type of sub-options.
1737 1736 """
1738 1737 def register(func):
1739 1738 _pathsuboptions[option] = (attr, func)
1740 1739 return func
1741 1740 return register
1742 1741
1743 1742 @pathsuboption('pushurl', 'pushloc')
1744 1743 def pushurlpathoption(ui, path, value):
1745 1744 u = util.url(value)
1746 1745 # Actually require a URL.
1747 1746 if not u.scheme:
1748 1747 ui.warn(_('(paths.%s:pushurl not a URL; ignoring)\n') % path.name)
1749 1748 return None
1750 1749
1751 1750 # Don't support the #foo syntax in the push URL to declare branch to
1752 1751 # push.
1753 1752 if u.fragment:
1754 1753 ui.warn(_('("#fragment" in paths.%s:pushurl not supported; '
1755 1754 'ignoring)\n') % path.name)
1756 1755 u.fragment = None
1757 1756
1758 1757 return str(u)
1759 1758
1760 1759 @pathsuboption('pushrev', 'pushrev')
1761 1760 def pushrevpathoption(ui, path, value):
1762 1761 return value
1763 1762
1764 1763 class path(object):
1765 1764 """Represents an individual path and its configuration."""
1766 1765
1767 1766 def __init__(self, ui, name, rawloc=None, suboptions=None):
1768 1767 """Construct a path from its config options.
1769 1768
1770 1769 ``ui`` is the ``ui`` instance the path is coming from.
1771 1770 ``name`` is the symbolic name of the path.
1772 1771 ``rawloc`` is the raw location, as defined in the config.
1773 1772 ``pushloc`` is the raw locations pushes should be made to.
1774 1773
1775 1774 If ``name`` is not defined, we require that the location be a) a local
1776 1775 filesystem path with a .hg directory or b) a URL. If not,
1777 1776 ``ValueError`` is raised.
1778 1777 """
1779 1778 if not rawloc:
1780 1779 raise ValueError('rawloc must be defined')
1781 1780
1782 1781 # Locations may define branches via syntax <base>#<branch>.
1783 1782 u = util.url(rawloc)
1784 1783 branch = None
1785 1784 if u.fragment:
1786 1785 branch = u.fragment
1787 1786 u.fragment = None
1788 1787
1789 1788 self.url = u
1790 1789 self.branch = branch
1791 1790
1792 1791 self.name = name
1793 1792 self.rawloc = rawloc
1794 1793 self.loc = '%s' % u
1795 1794
1796 1795 # When given a raw location but not a symbolic name, validate the
1797 1796 # location is valid.
1798 1797 if not name and not u.scheme and not self._isvalidlocalpath(self.loc):
1799 1798 raise ValueError('location is not a URL or path to a local '
1800 1799 'repo: %s' % rawloc)
1801 1800
1802 1801 suboptions = suboptions or {}
1803 1802
1804 1803 # Now process the sub-options. If a sub-option is registered, its
1805 1804 # attribute will always be present. The value will be None if there
1806 1805 # was no valid sub-option.
1807 1806 for suboption, (attr, func) in _pathsuboptions.iteritems():
1808 1807 if suboption not in suboptions:
1809 1808 setattr(self, attr, None)
1810 1809 continue
1811 1810
1812 1811 value = func(ui, self, suboptions[suboption])
1813 1812 setattr(self, attr, value)
1814 1813
1815 1814 def _isvalidlocalpath(self, path):
1816 1815 """Returns True if the given path is a potentially valid repository.
1817 1816 This is its own function so that extensions can change the definition of
1818 1817 'valid' in this case (like when pulling from a git repo into a hg
1819 1818 one)."""
1820 1819 return os.path.isdir(os.path.join(path, '.hg'))
1821 1820
1822 1821 @property
1823 1822 def suboptions(self):
1824 1823 """Return sub-options and their values for this path.
1825 1824
1826 1825 This is intended to be used for presentation purposes.
1827 1826 """
1828 1827 d = {}
1829 1828 for subopt, (attr, _func) in _pathsuboptions.iteritems():
1830 1829 value = getattr(self, attr)
1831 1830 if value is not None:
1832 1831 d[subopt] = value
1833 1832 return d
1834 1833
1835 1834 # we instantiate one globally shared progress bar to avoid
1836 1835 # competing progress bars when multiple UI objects get created
1837 1836 _progresssingleton = None
1838 1837
1839 1838 def getprogbar(ui):
1840 1839 global _progresssingleton
1841 1840 if _progresssingleton is None:
1842 1841 # passing 'ui' object to the singleton is fishy,
1843 1842 # this is how the extension used to work but feel free to rework it.
1844 1843 _progresssingleton = progress.progbar(ui)
1845 1844 return _progresssingleton
1846 1845
1847 1846 def haveprogbar():
1848 1847 return _progresssingleton is not None
General Comments 0
You need to be logged in to leave comments. Login now