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