This instance will be upgraded to Heptapod 0.21.0rc2 on 2021-04-16 at 14:00 UTC+2 (a few minutes of down time)

ui.py 82.6 KB
Newer Older
1 2
# ui.py - user interface bits for mercurial
#
3
# Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4
#
5
# This software may be used and distributed according to the terms of the
Matt Mackall's avatar
Matt Mackall committed
6
# GNU General Public License version 2 or any later version.
7

Gregory Szorc's avatar
Gregory Szorc committed
8 9
from __future__ import absolute_import

10
import collections
11
import contextlib
12
import datetime
Gregory Szorc's avatar
Gregory Szorc committed
13 14
import errno
import getpass
15
import inspect
Gregory Szorc's avatar
Gregory Szorc committed
16
import os
17
import re
18
import signal
Gregory Szorc's avatar
Gregory Szorc committed
19
import socket
20
import subprocess
Gregory Szorc's avatar
Gregory Szorc committed
21 22 23 24 25
import sys
import traceback

from .i18n import _
from .node import hex
26
from .pycompat import (
27
    getattr,
28 29 30
    open,
    setattr,
)
Gregory Szorc's avatar
Gregory Szorc committed
31 32

from . import (
33
    color,
Gregory Szorc's avatar
Gregory Szorc committed
34
    config,
35
    configitems,
36
    encoding,
Gregory Szorc's avatar
Gregory Szorc committed
37 38
    error,
    formatter,
39
    loggingutil,
Gregory Szorc's avatar
Gregory Szorc committed
40
    progress,
41
    pycompat,
42
    rcutil,
Gregory Szorc's avatar
Gregory Szorc committed
43 44 45
    scmutil,
    util,
)
46 47
from .utils import (
    dateutil,
48
    procutil,
49
    resourceutil,
50 51
    stringutil,
)
Matt Mackall's avatar
Matt Mackall committed
52

53 54
urlreq = util.urlreq

Simon Farnsworth's avatar
Simon Farnsworth committed
55
# for use with str.translate(None, _keepalnum), to keep just alphanumerics
56
_keepalnum = b''.join(
57 58
    c for c in map(pycompat.bytechr, range(256)) if not c.isalnum()
)
Simon Farnsworth's avatar
Simon Farnsworth committed
59

60
# The config knobs that will be altered (if unset) by ui.tweakdefaults.
61
tweakrc = b"""
62
[ui]
63 64
# Gives detailed exit codes for input/user errors, config errors, etc.
detailed-exit-code = True
65 66
# The rollback command is dangerous. As a rule, don't use it.
rollback = False
67 68
# Make `hg status` report copy information
statuscopies = yes
69 70
# Prefer curses UIs when available. Revert to plain-text with `text`.
interface = curses
71 72
# Make compatible commands emit cwd-relative paths by default.
relative-paths = yes
73 74

[commands]
75 76
# Grep working directory by default.
grep.all-files = True
77 78
# Refuse to perform an `hg update` that would cause a file content merge
update.check = noconflict
79 80
# Show conflicts information in `hg status`
status.verbose = True
81 82
# Make `hg resolve` with no action (like `-m`) fail instead of re-merging.
resolve.explicit-re-merge = True
83 84 85

[diff]
git = 1
86
showfunc = 1
87
word-diff = 1
88 89
"""

90
samplehgrcs = {
91
    b'user': b"""# example user config (see 'hg help config' for more info)
92 93 94 95 96
[ui]
# name and email, e.g.
# username = Jane Doe <jdoe@example.com>
username =

97 98 99 100 101
# We recommend enabling tweakdefaults to get slight improvements to
# the UI over time. Make sure to set HGPLAIN in the environment when
# writing scripts!
# tweakdefaults = True

102
# uncomment to disable color in command output
103
# (see 'hg help color' for details)
104
# color = never
105

106 107
# uncomment to disable command output pagination
# (see 'hg help pager' for details)
108
# paginate = never
109

110
[extensions]
111
# uncomment the lines below to enable some popular extensions
112
# (see 'hg help extensions' for more info)
113
#
114 115 116
# histedit =
# rebase =
# uncommit =
117
""",
118
    b'cloned': b"""# example repository config (see 'hg help config' for more info)
119 120 121 122
[paths]
default = %s

# path aliases to other clones of this repo in URLs or filesystem paths
123
# (see 'hg help config.paths' for more info)
124
#
125 126 127
# default:pushurl = ssh://jdoe@example.net/hg/jdoes-fork
# my-fork         = ssh://jdoe@example.net/hg/jdoes-fork
# my-clone        = /home/jdoe/jdoes-clone
128 129 130 131 132

[ui]
# name and email (local to this repository, optional), e.g.
# username = Jane Doe <jdoe@example.com>
""",
133
    b'local': b"""# example repository config (see 'hg help config' for more info)
134 135
[paths]
# path aliases to other clones of this repo in URLs or filesystem paths
136
# (see 'hg help config.paths' for more info)
137
#
138 139 140 141
# default         = http://example.com/hg/example-repo
# default:pushurl = ssh://jdoe@example.net/hg/jdoes-fork
# my-fork         = ssh://jdoe@example.net/hg/jdoes-fork
# my-clone        = /home/jdoe/jdoes-clone
142 143 144 145

[ui]
# name and email (local to this repository, optional), e.g.
# username = Jane Doe <jdoe@example.com>
146
""",
147
    b'global': b"""# example system-wide hg config (see 'hg help config' for more info)
148

149
[ui]
150
# uncomment to disable color in command output
151
# (see 'hg help color' for details)
152
# color = never
153

154 155
# uncomment to disable command output pagination
# (see 'hg help pager' for details)
156
# paginate = never
157

158
[extensions]
159
# uncomment the lines below to enable some popular extensions
160
# (see 'hg help extensions' for more info)
161 162
#
# blackbox =
163
# churn =
164
""",
165 166
}

167

168
def _maybestrurl(maybebytes):
169
    return pycompat.rapply(pycompat.strurl, maybebytes)
170

171

172
def _maybebytesurl(maybestr):
173
    return pycompat.rapply(pycompat.bytesurl, maybestr)
174

175

176 177
class httppasswordmgrdbproxy(object):
    """Delays loading urllib2 until it's needed."""
178

179 180 181 182 183 184 185 186
    def __init__(self):
        self._mgr = None

    def _get_mgr(self):
        if self._mgr is None:
            self._mgr = urlreq.httppasswordmgrwithdefaultrealm()
        return self._mgr

187
    def add_password(self, realm, uris, user, passwd):
188
        return self._get_mgr().add_password(
189 190 191 192 193
            _maybestrurl(realm),
            _maybestrurl(uris),
            _maybestrurl(user),
            _maybestrurl(passwd),
        )
194

195
    def find_user_password(self, realm, uri):
196
        mgr = self._get_mgr()
197 198 199 200
        return _maybebytesurl(
            mgr.find_user_password(_maybestrurl(realm), _maybestrurl(uri))
        )

201

202 203
def _catchterm(*args):
    raise error.SignalInterrupt
204

205

206 207 208 209
# unique object used to detect no default value has been provided when
# retrieving configuration value.
_unset = object()

210 211 212
# _reqexithandlers: callbacks run at the end of a request
_reqexithandlers = []

213

214
class ui(object):
215
    def __init__(self, src=None):
216 217 218 219 220 221
        """Create a fresh new ui object if no src given

        Use uimod.ui.load() to create a ui which knows global and user configs.
        In most cases, you should use ui.copy() to create a copy of an existing
        ui object.
        """
222
        # _buffers: used for temporary capture of output
Matt Mackall's avatar
Matt Mackall committed
223
        self._buffers = []
224 225
        # 3-tuple describing how each buffer in the stack behaves.
        # Values are (capture stderr, capture subprocesses, apply labels).
226
        self._bufferstates = []
227 228 229
        # When a buffer is active, defines whether we are expanding labels.
        # This exists to prevent an extra list lookup.
        self._bufferapplylabels = None
230
        self.quiet = self.verbose = self.debugflag = self.tracebackflag = False
Matt Mackall's avatar
Matt Mackall committed
231
        self._reportuntrusted = True
232
        self._knownconfig = configitems.coreitems
233 234 235
        self._ocfg = config.config()  # overlay
        self._tcfg = config.config()  # trusted
        self._ucfg = config.config()  # untrusted
Martin Geisler's avatar
Martin Geisler committed
236 237
        self._trustusers = set()
        self._trustgroups = set()
238
        self.callhooks = True
239 240
        # Insecure server connections requested.
        self.insecureconnections = False
241 242
        # Blocked time
        self.logblockedtimes = False
243 244
        # color mode: see mercurial/color.py for possible value
        self._colormode = None
245
        self._terminfoparams = {}
246
        self._styles = {}
247
        self._uninterruptible = False
248
        self.showtimestamp = False
249

250
        if src:
251 252 253
            self._fout = src._fout
            self._ferr = src._ferr
            self._fin = src._fin
254
            self._fmsg = src._fmsg
255 256
            self._fmsgout = src._fmsgout
            self._fmsgerr = src._fmsgerr
257
            self._finoutredirected = src._finoutredirected
258
            self._loggers = src._loggers.copy()
259
            self.pageractive = src.pageractive
260
            self._disablepager = src._disablepager
261
            self._tweaked = src._tweaked
Idan Kamara's avatar
Idan Kamara committed
262

Matt Mackall's avatar
Matt Mackall committed
263 264 265
            self._tcfg = src._tcfg.copy()
            self._ucfg = src._ucfg.copy()
            self._ocfg = src._ocfg.copy()
266 267
            self._trustusers = src._trustusers.copy()
            self._trustgroups = src._trustgroups.copy()
268
            self.environ = src.environ
269
            self.callhooks = src.callhooks
270
            self.insecureconnections = src.insecureconnections
271
            self._colormode = src._colormode
272
            self._terminfoparams = src._terminfoparams.copy()
273
            self._styles = src._styles.copy()
274

275
            self.fixconfig()
276 277

            self.httppasswordmgrdb = src.httppasswordmgrdb
278
            self._blockedtimes = src._blockedtimes
279
        else:
280 281 282
            self._fout = procutil.stdout
            self._ferr = procutil.stderr
            self._fin = procutil.stdin
283
            self._fmsg = None
284 285
            self._fmsgout = self.fout  # configurable
            self._fmsgerr = self.ferr  # configurable
286
            self._finoutredirected = False
287
            self._loggers = {}
288
            self.pageractive = False
289
            self._disablepager = False
290
            self._tweaked = False
Idan Kamara's avatar
Idan Kamara committed
291

292
            # shared read-only environment
293
            self.environ = encoding.environ
294

295
            self.httppasswordmgrdb = httppasswordmgrdbproxy()
296
            self._blockedtimes = collections.defaultdict(int)
297

298 299
        allowed = self.configlist(b'experimental', b'exportableenviron')
        if b'*' in allowed:
300 301 302 303 304 305 306
            self._exportableenviron = self.environ
        else:
            self._exportableenviron = {}
            for k in allowed:
                if k in self.environ:
                    self._exportableenviron[k] = self.environ[k]

307 308 309 310
    @classmethod
    def load(cls):
        """Create a ui and load global and user configs"""
        u = cls()
311
        # we always trust global config files and environment variables
312
        for t, f in rcutil.rccomponents():
313
            if t == b'path':
314
                u.readconfig(f, trust=True)
315 316
            elif t == b'resource':
                u.read_resource_config(f, trust=True)
317
            elif t == b'items':
318 319 320 321 322 323 324 325 326
                sections = set()
                for section, name, value, source in f:
                    # do not set u._ocfg
                    # XXX clean this up once immutable config object is a thing
                    u._tcfg.set(section, name, value, source)
                    u._ucfg.set(section, name, value, source)
                    sections.add(section)
                for section in sections:
                    u.fixconfig(section=section)
327
            else:
328
                raise error.ProgrammingError(b'unknown rctype: %s' % t)
329
        u._maybetweakdefaults()
330 331
        return u

332
    def _maybetweakdefaults(self):
333
        if not self.configbool(b'ui', b'tweakdefaults'):
334
            return
335
        if self._tweaked or self.plain(b'tweakdefaults'):
336 337 338 339 340 341 342 343 344 345
            return

        # Note: it is SUPER IMPORTANT that you set self._tweaked to
        # True *before* any calls to setconfig(), otherwise you'll get
        # infinite recursion between setconfig and this method.
        #
        # TODO: We should extract an inner method in setconfig() to
        # avoid this weirdness.
        self._tweaked = True
        tmpcfg = config.config()
346
        tmpcfg.parse(b'<tweakdefaults>', tweakrc)
347 348 349
        for section in tmpcfg:
            for name, value in tmpcfg.items(section):
                if not self.hasconfig(section, name):
350
                    self.setconfig(section, name, value, b"<tweakdefaults>")
351

352
    def copy(self):
353
        return self.__class__(self)
354

355 356 357 358
    def resetstate(self):
        """Clear internal state that shouldn't persist across commands"""
        if self._progbar:
            self._progbar.resetstate()  # reset last-print time of progress bar
359
        self.httppasswordmgrdb = httppasswordmgrdbproxy()
360

361 362
    @contextlib.contextmanager
    def timeblockedsection(self, key):
363
        # this is open-coded below - search for timeblockedsection to find them
364 365 366 367
        starttime = util.timer()
        try:
            yield
        finally:
368
            self._blockedtimes[key + b'_blocked'] += (
369 370
                util.timer() - starttime
            ) * 1000
371

372
    @contextlib.contextmanager
373
    def uninterruptible(self):
374 375 376 377 378 379 380
        """Mark an operation as unsafe.

        Most operations on a repository are safe to interrupt, but a
        few are risky (for example repair.strip). This context manager
        lets you advise Mercurial that something risky is happening so
        that control-C etc can be blocked if desired.
        """
381
        enabled = self.configbool(b'experimental', b'nointerrupt')
382
        if enabled and self.configbool(
383
            b'experimental', b'nointerrupt-interactiveonly'
384
        ):
385 386 387
            enabled = self.interactive()
        if self._uninterruptible or not enabled:
            # if nointerrupt support is turned off, the process isn't
388
            # interactive, or we're already in an uninterruptible
389 390 391
            # block, do nothing.
            yield
            return
392

393
        def warn():
394
            self.warn(_(b"shutting down cleanly\n"))
395
            self.warn(
396
                _(b"press ^C again to terminate immediately (dangerous)\n")
397
            )
398
            return True
399

400
        with procutil.uninterruptible(warn):
401 402 403 404 405 406
            try:
                self._uninterruptible = True
                yield
            finally:
                self._uninterruptible = False

Matt Mackall's avatar
Matt Mackall committed
407
    def formatter(self, topic, opts):
408
        return formatter.formatter(self, self, topic, opts)
Matt Mackall's avatar
Matt Mackall committed
409

410
    def _trusted(self, fp, f):
411
        st = util.fstat(fp)
412
        if util.isowner(st):
413
            return True
Matt Mackall's avatar
Matt Mackall committed
414

415
        tusers, tgroups = self._trustusers, self._trustgroups
416
        if b'*' in tusers or b'*' in tgroups:
Matt Mackall's avatar
Matt Mackall committed
417 418 419 420 421 422 423
            return True

        user = util.username(st.st_uid)
        group = util.groupname(st.st_gid)
        if user in tusers or group in tgroups or user == util.username():
            return True

Matt Mackall's avatar
Matt Mackall committed
424
        if self._reportuntrusted:
425
            self.warn(
426 427 428 429
                _(
                    b'not trusting file %s from untrusted '
                    b'user %s, group %s\n'
                )
430 431
                % (f, user, group)
            )
Matt Mackall's avatar
Matt Mackall committed
432
        return False
433

434 435 436 437 438 439 440 441 442 443 444 445 446 447
    def read_resource_config(
        self, name, root=None, trust=False, sections=None, remap=None
    ):
        try:
            fp = resourceutil.open_resource(name[0], name[1])
        except IOError:
            if not sections:  # ignore unless we were looking for something
                return
            raise

        self._readconfig(
            b'resource:%s.%s' % name, fp, root, trust, sections, remap
        )

448 449 450
    def readconfig(
        self, filename, root=None, trust=False, sections=None, remap=None
    ):
451
        try:
452
            fp = open(filename, 'rb')
453
        except IOError:
454
            if not sections:  # ignore unless we were looking for something
455 456
                return
            raise
457

458 459 460 461 462
        self._readconfig(filename, fp, root, trust, sections, remap)

    def _readconfig(
        self, filename, fp, root=None, trust=False, sections=None, remap=None
    ):
463 464 465
        with fp:
            cfg = config.config()
            trusted = sections or trust or self._trusted(fp, filename)
466

467 468 469 470 471 472
            try:
                cfg.read(filename, fp, sections=sections, remap=remap)
            except error.ParseError as inst:
                if trusted:
                    raise
                self.warn(_(b'ignored: %s\n') % stringutil.forcebytestr(inst))
473

474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492
        self._applyconfig(cfg, trusted, root)

    def applyconfig(self, configitems, source=b"", root=None):
        """Add configitems from a non-file source.  Unlike with ``setconfig()``,
        they can be overridden by subsequent config file reads.  The items are
        in the same format as ``configoverride()``, namely a dict of the
        following structures: {(section, name) : value}

        Typically this is used by extensions that inject themselves into the
        config file load procedure by monkeypatching ``localrepo.loadhgrc()``.
        """
        cfg = config.config()

        for (section, name), value in configitems.items():
            cfg.set(section, name, value, source)

        self._applyconfig(cfg, True, root)

    def _applyconfig(self, cfg, trusted, root):
493
        if self.plain():
494
            for k in (
495 496 497 498 499 500 501 502 503 504
                b'debug',
                b'fallbackencoding',
                b'quiet',
                b'slash',
                b'logtemplate',
                b'message-output',
                b'statuscopies',
                b'style',
                b'traceback',
                b'verbose',
505
            ):
506 507 508 509 510 511
                if k in cfg[b'ui']:
                    del cfg[b'ui'][k]
            for k, v in cfg.items(b'defaults'):
                del cfg[b'defaults'][k]
            for k, v in cfg.items(b'commands'):
                del cfg[b'commands'][k]
512 513
            for k, v in cfg.items(b'command-templates'):
                del cfg[b'command-templates'][k]
514
        # Don't remove aliases from the configuration if in the exceptionlist
515 516 517 518 519 520 521 522 523
        if self.plain(b'alias'):
            for k, v in cfg.items(b'alias'):
                del cfg[b'alias'][k]
        if self.plain(b'revsetalias'):
            for k, v in cfg.items(b'revsetalias'):
                del cfg[b'revsetalias'][k]
        if self.plain(b'templatealias'):
            for k, v in cfg.items(b'templatealias'):
                del cfg[b'templatealias'][k]
524

525
        if trusted:
Matt Mackall's avatar
Matt Mackall committed
526 527 528 529
            self._tcfg.update(cfg)
            self._tcfg.update(self._ocfg)
        self._ucfg.update(cfg)
        self._ucfg.update(self._ocfg)
Matt Mackall's avatar
Matt Mackall committed
530

531
        if root is None:
532
            root = os.path.expanduser(b'~')
533
        self.fixconfig(root=root)
534

535
    def fixconfig(self, root=None, section=None):
536
        if section in (None, b'paths'):
537 538
            # expand vars and ~
            # translate paths relative to root (or home) into absolute paths
539
            root = root or encoding.getcwd()
540
            for c in self._tcfg, self._ucfg, self._ocfg:
541
                for n, p in c.items(b'paths'):
542
                    # Ignore sub-options.
543
                    if b':' in n:
544
                        continue
545 546
                    if not p:
                        continue
547 548
                    if b'%%' in p:
                        s = self.configsource(b'paths', n) or b'none'
549
                        self.warn(
550
                            _(b"(deprecated '%%' in path %s=%s from %s)\n")
551 552
                            % (n, p, s)
                        )
553
                        p = p.replace(b'%%', b'%')
554
                    p = util.expandpath(p)
555
                    if not util.hasscheme(p) and not os.path.isabs(p):
556
                        p = os.path.normpath(os.path.join(root, p))
557
                    c.set(b"paths", n, p)
558

559
        if section in (None, b'ui'):
560
            # update ui options
561
            self._fmsgout, self._fmsgerr = _selectmsgdests(self)
562 563 564
            self.debugflag = self.configbool(b'ui', b'debug')
            self.verbose = self.debugflag or self.configbool(b'ui', b'verbose')
            self.quiet = not self.debugflag and self.configbool(b'ui', b'quiet')
565 566
            if self.verbose and self.quiet:
                self.quiet = self.verbose = False
567
            self._reportuntrusted = self.debugflag or self.configbool(
568
                b"ui", b"report_untrusted"
569
            )
570
            self.showtimestamp = self.configbool(b'ui', b'timestamp-output')
571 572
            self.tracebackflag = self.configbool(b'ui', b'traceback')
            self.logblockedtimes = self.configbool(b'ui', b'logblockedtimes')
573

574
        if section in (None, b'trusted'):
575
            # update trust information
576 577
            self._trustusers.update(self.configlist(b'trusted', b'users'))
            self._trustgroups.update(self.configlist(b'trusted', b'groups'))
578

579 580 581 582 583 584 585 586
        if section in (None, b'devel', b'ui') and self.debugflag:
            tracked = set()
            if self.configbool(b'devel', b'debug.extensions'):
                tracked.add(b'extension')
            if tracked:
                logger = loggingutil.fileobjectlogger(self._ferr, tracked)
                self.setlogger(b'debug', logger)

587
    def backupconfig(self, section, item):
588 589 590 591 592 593
        return (
            self._ocfg.backup(section, item),
            self._tcfg.backup(section, item),
            self._ucfg.backup(section, item),
        )

594 595 596 597 598
    def restoreconfig(self, data):
        self._ocfg.restore(data[0])
        self._tcfg.restore(data[1])
        self._ucfg.restore(data[2])

599
    def setconfig(self, section, name, value, source=b''):
600
        for cfg in (self._ocfg, self._tcfg, self._ucfg):
601
            cfg.set(section, name, value, source)
602
        self.fixconfig(section=section)
603
        self._maybetweakdefaults()
mpm@selenic.com's avatar
mpm@selenic.com committed
604

Matt Mackall's avatar
Matt Mackall committed
605
    def _data(self, untrusted):
Matt Mackall's avatar
Matt Mackall committed
606
        return untrusted and self._ucfg or self._tcfg
607

608
    def configsource(self, section, name, untrusted=False):
609
        return self._data(untrusted).source(section, name)
610

611
    def config(self, section, name, default=_unset, untrusted=False):
612
        """return the plain string version of a config"""
613 614 615
        value = self._config(
            section, name, default=default, untrusted=untrusted
        )
616 617 618 619 620
        if value is _unset:
            return None
        return value

    def _config(self, section, name, default=_unset, untrusted=False):
621
        value = itemdefault = default
622 623 624 625 626
        item = self._knownconfig.get(section, {}).get(name)
        alternates = [(section, name)]

        if item is not None:
            alternates.extend(item.alias)
627 628 629 630
            if callable(item.default):
                itemdefault = item.default()
            else:
                itemdefault = item.default
631
        else:
632
            msg = b"accessing unregistered config item: '%s.%s'"
633
            msg %= (section, name)
634
            self.develwarn(msg, 2, b'warn-config-unknown')
635 636 637 638

        if default is _unset:
            if item is None:
                value = default
639 640
            elif item.default is configitems.dynamicdefault:
                value = None
641
                msg = b"config item requires an explicit default value: '%s.%s'"
642
                msg %= (section, name)
643
                self.develwarn(msg, 2, b'warn-config-default')
644
            else:
645
                value = itemdefault
646 647 648 649 650 651
        elif (
            item is not None
            and item.default is not configitems.dynamicdefault
            and default != itemdefault
        ):
            msg = (
652 653
                b"specifying a mismatched default value for a registered "
                b"config item: '%s.%s' '%s'"
654
            )
655
            msg %= (section, name, pycompat.bytestr(default))
656
            self.develwarn(msg, 2, b'warn-config-default')
657 658 659

        for s, n in alternates:
            candidate = self._data(untrusted).get(s, n, None)
660 661
            if candidate is not None:
                value = candidate
662 663
                break

Matt Mackall's avatar
Matt Mackall committed
664
        if self.debugflag and not untrusted and self._reportuntrusted:
665 666
            for s, n in alternates:
                uvalue = self._ucfg.get(s, n)
667
                if uvalue is not None and uvalue != value:
668
                    self.debug(
669 670
                        b"ignoring untrusted configuration option "
                        b"%s.%s = %s\n" % (s, n, uvalue)
671
                    )
672 673
        return value

674
    def configsuboptions(self, section, name, default=_unset, untrusted=False):
675 676 677 678 679 680 681 682 683
        """Get a config option and all sub-options.

        Some config options have sub-options that are declared with the
        format "key:opt = value". This method is used to return the main
        option and all its declared sub-options.

        Returns a 2-tuple of ``(option, sub-options)``, where `sub-options``
        is a dict of defined sub-options where keys and values are strings.
        """
684
        main = self.config(section, name, default, untrusted=untrusted)
685 686
        data = self._data(untrusted)
        sub = {}
687
        prefix = b'%s:' % name
688 689
        for k, v in data.items(section):
            if k.startswith(prefix):
690
                sub[k[len(prefix) :]] = v
691 692 693

        if self.debugflag and not untrusted and self._reportuntrusted:
            for k, v in sub.items():
694
                uvalue = self._ucfg.get(section, b'%s:%s' % (name, k))
695
                if uvalue is not None and uvalue != v:
696
                    self.debug(
697 698
                        b'ignoring untrusted configuration option '
                        b'%s:%s.%s = %s\n' % (section, name, k, uvalue)
699
                    )
700 701 702

        return main, sub

703
    def configpath(self, section, name, default=_unset, untrusted=False):
704 705
        """get a path config item, expanded relative to repo root or config
        file"""
Matt Mackall's avatar
Matt Mackall committed
706
        v = self.config(section, name, default, untrusted)
707 708
        if v is None:
            return None
709
        if not os.path.isabs(v) or b"://" not in v:
Matt Mackall's avatar
Matt Mackall committed
710
            src = self.configsource(section, name, untrusted)
711 712
            if b':' in src:
                base = os.path.dirname(src.rsplit(b':')[0])
Matt Mackall's avatar
Matt Mackall committed
713 714 715
                v = os.path.join(base, os.path.expanduser(v))
        return v

716
    def configbool(self, section, name, default=_unset, untrusted=False):
717 718
        """parse a configuration element as a boolean

719 720 721
        >>> u = ui(); s = b'foo'
        >>> u.setconfig(s, b'true', b'yes')
        >>> u.configbool(s, b'true')
722
        True
723 724
        >>> u.setconfig(s, b'false', b'no')
        >>> u.configbool(s, b'false')
725
        False
726
        >>> u.configbool(s, b'unknown')
727
        False
728
        >>> u.configbool(s, b'unknown', True)
729
        True
730 731
        >>> u.setconfig(s, b'invalid', b'somevalue')
        >>> u.configbool(s, b'invalid')
732 733 734 735 736
        Traceback (most recent call last):
            ...
        ConfigError: foo.invalid is not a boolean ('somevalue')
        """

737
        v = self._config(section, name, default, untrusted=untrusted)
738
        if v is None:
739