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