Patchwork [2,of,2,RFC] RFC: switch to immutable configs

login
register
mail settings
Submitter Jun Wu
Date March 27, 2017, 6:38 p.m.
Message ID <13bee3e959f04f970f2f.1490639887@localhost.localdomain>
Download mbox | patch
Permalink /patch/19761/
State Changes Requested
Headers show

Comments

Jun Wu - March 27, 2017, 6:38 p.m.
# HG changeset patch
# User Jun Wu <quark@fb.com>
# Date 1490639836 25200
#      Mon Mar 27 11:37:16 2017 -0700
# Node ID 13bee3e959f04f970f2fc0a01120f0b30d725b84
# Parent  4eb7c76340791f379a34f9df4ec42e0c8b9b2a2f
RFC: switch to immutable configs

This replaces "ui" so it's based on immutable config. Tests pass.

The config hierarchy is currently like:

    mergedconfig (root config)
     \_ mergedconfig (self.__class__._gcfgs, global configs across repos)
     |   \_ atomicconfig (added by setconfig(priority=3))
     |   \_ atomicconfig (added by setconfig(priority=3))
     |   \_ ...
     \_ mergedconfig (self._ocfgs)
     |   \_ atomicconfig (added by setconfig)
     |   \_ atomicconfig (added by setconfig)
     |   \_ filteredconfig (if the subconfig has [paths], needs fix)
     |   |   \_ atomicconfig (added by setconfig)
     |   \_ ...
     \_ filteredconfig (filters out HGPLAIN stuff)
     |   \_ mergedconfig (self._tcfgs, or self._ucfgs, config files)
     |       \_ filteredconfig (if the subconfig has [paths], needs xi)
     |       |   \_ fileconfig
     |       \_ fileconfig (added by readconfig)
     |       \_ fileconfig (added by readconfig)
     |       \_ ...
     \_ atomicconfig (handles ui.compat, dos not exists yet, replaceable)

In the future we may want to let rcutil.py construct part of the configs,
and make systemrc and userrc separately accessible.

As a RFC, the patch is not split intentionally, to give an overview of what
needed to be done for switching to immutable configs. The current code is
done in a relatively quick & dirty way. If we feel good about the direction,
I'll clean things up into smaller patches.
David Soria Parra - March 29, 2017, 10:56 p.m.
On Mon, Mar 27, 2017 at 11:38:07AM -0700, Jun Wu wrote:
> # HG changeset patch
> # User Jun Wu <quark@fb.com>
> # Date 1490639836 25200
> #      Mon Mar 27 11:37:16 2017 -0700
> # Node ID 13bee3e959f04f970f2fc0a01120f0b30d725b84
> # Parent  4eb7c76340791f379a34f9df4ec42e0c8b9b2a2f
> RFC: switch to immutable configs

I personally like the overall direction
> +def _filterconfig(subconfig):
> +    '''remove configs according to HGPLAIN and HGPLAINEXCEPT
> +
> +    subconfig is an immutable config object. Returns an immutable config object
> +    with related fields filtered.
> +    '''
> +    filters = {}
> +    if _isplain():
> +        def filterui(items):
> +            result = util.sortdict(items)
> +            for k in ('debug', 'fallbackencoding', 'quiet', 'slash',
> +                      'logtemplate', 'statuscopies', 'style',
> +                      'traceback', 'verbose'):
> +                if k in result:
> +                    del result[k]
> +            return result
> +
> +        filters['ui'] = filterui
> +        filters['defaults'] = {}
> +    if _isplain('alias'):
> +        filters['alias'] = {}
> +    if _isplain('revsetalias'):
> +        filters['revsetalias'] = {}
> +    if _isplain('templatealias'):
> +        filters['templatealias'] = {}
> +    if _isplain('commands'):
> +        filters['commands'] = {}
> +
> +    if filters:
> +        return config.filteredconfig('filter', subconfig, filters)
> +    else:
> +        return subconfig
> +
> +def _getconfig(configroot, section, name, default=None, index=0):
> +    '''get value (index=0) or source (index=-1) from an immutable config'''
> +    value = configroot.getsection(section).get(name, (None,))[index]
> +    if value is None:
> +        value = default
> +    return value
> +
> +def _buildconfigroot(cfg, ocfg, gcfg):
> +    return config.mergedconfig('root', [cfg, ocfg, gcfg])
> +
> +def dependson(*fields):
> +    '''cache result which gets invalidates if any field changes'''
> +
> +    def decorator(oldfunc):
> +        cached = [[None], None] # cache key, result
> +        def getcachekey(self):
> +            return [getattr(self, f, None) for f in fields]
> +
> +        def newfunc(self):
> +            newkey = getcachekey(self)
> +            oldkey = cached[0]
> +            if oldkey == newkey:
> +                return cached[1]
> +            result = oldfunc(self)
> +            cached[:] = [newkey, result]
> +            return result
> +        newfunc.__name__ = oldfunc.__name__
> +        newfunc.__doc__ = oldfunc.__doc__
> +        return newfunc
> +    return decorator
> +
>  class ui(object):
>      def __init__(self, src=None):
> @@ -146,16 +257,7 @@ class ui(object):
>          # This exists to prevent an extra list lookup.
>          self._bufferapplylabels = None
> -        self.quiet = self.verbose = self.debugflag = self.tracebackflag = False
> -        self._reportuntrusted = True
> -        self._ocfg = config.config() # overlay
> -        self._tcfg = config.config() # trusted
> -        self._ucfg = config.config() # untrusted
> -        self._trustusers = set()
> -        self._trustgroups = set()
>          self.callhooks = True
>          # Insecure server connections requested.
>          self.insecureconnections = False
> -        # Blocked time
> -        self.logblockedtimes = False
>          # color mode: see mercurial/color.py for possible value
>          self._colormode = None
> @@ -170,9 +272,9 @@ class ui(object):
>              self._disablepager = src._disablepager
>  
> -            self._tcfg = src._tcfg.copy()
> -            self._ucfg = src._ucfg.copy()
> -            self._ocfg = src._ocfg.copy()
> -            self._trustusers = src._trustusers.copy()
> -            self._trustgroups = src._trustgroups.copy()
> +            # immutable configs can be reused without copying
> +            self._ocfgs = src._ocfgs
> +            self._tcfgs = src._tcfgs
> +            self._ucfgs = src._ucfgs
> +
>              self.environ = src.environ
>              self.callhooks = src.callhooks
> @@ -182,6 +284,4 @@ class ui(object):
>              self._styles = src._styles.copy()
>  
> -            self.fixconfig()
> -
>              self.httppasswordmgrdb = src.httppasswordmgrdb
>              self._blockedtimes = src._blockedtimes
> @@ -199,4 +299,9 @@ class ui(object):
>              self._blockedtimes = collections.defaultdict(int)
>  
> +            # immutable configs
> +            self._ocfgs = config.mergedconfig('setconfig', []) # overlay
> +            self._tcfgs = config.mergedconfig('loaded', []) # trusted
i would probably actually call them 'trusted' instead of 'loaded'
> +            self._ucfgs = config.mergedconfig('loaded', []) # trusted+untrusted
with merge configs in, we would split up trusted+untrusted into trusted and
untrusted?

> +    def setconfig(self, section, name, value, source='', priority=None):
I think we can do better by using the title, e.g. 'untrusted', 'setconfig' to
access the right layer. a numeric priority seems to be very hard to use and I
think we could improve this interface a bit.
> +        title = source or 'setconfig'
> +        acfg = config.atomicconfig(title, [(section, name, (value, source))])
> +        try:
> +            cwd = pycompat.getcwd()
> +        except OSError:
> +            pass
> +        else:
> +            acfg = _fixpathsection(acfg, cwd)
> +        if priority == 3:
> +            # global overlay
> +            self.__class__._gcfgs = self.__class__._gcfgs.append(acfg)
> +        elif priority == 2:
> +            # change overlay in this ui
> +            self._ocfgs = self._ocfgs.append(acfg)
> +        elif priority == 1:
> +            # change overlay in this ui, do not override existing overlays
> +            self._ocfgs = self._ocfgs.prepend(acfg)
> +        else:
> +            self._tcfgs = self._tcfgs.append(acfg)
> +            self._ucfgs = self._ucfgs.append(acfg)
>
Jun Wu - March 29, 2017, 11 p.m.
Excerpts from David Soria Parra's message of 2017-03-29 15:56:21 -0700:
> > +            # immutable configs
> > +            self._ocfgs = config.mergedconfig('setconfig', []) # overlay
> > +            self._tcfgs = config.mergedconfig('loaded', []) # trusted
> i would probably actually call them 'trusted' instead of 'loaded'
> > +            self._ucfgs = config.mergedconfig('loaded', []) # trusted+untrusted
> with merge configs in, we would split up trusted+untrusted into trusted and
> untrusted?
> 
> > +    def setconfig(self, section, name, value, source='', priority=None):
> I think we can do better by using the title, e.g. 'untrusted', 'setconfig' to
> access the right layer. a numeric priority seems to be very hard to use and I
> think we could improve this interface a bit.

Yeah, the "priority" is added in the last hour. It was "overlay=True/False",
until I met "ui.commitsubrepos", which seems to have to be a global
affecting multiple uis. Basically we need at least 3 priorities: ('normal',
'overlay', 'global').

> > +        title = source or 'setconfig'
> > +        acfg = config.atomicconfig(title, [(section, name, (value, source))])
> > +        try:
> > +            cwd = pycompat.getcwd()
> > +        except OSError:
> > +            pass
> > +        else:
> > +            acfg = _fixpathsection(acfg, cwd)
> > +        if priority == 3:
> > +            # global overlay
> > +            self.__class__._gcfgs = self.__class__._gcfgs.append(acfg)
> > +        elif priority == 2:
> > +            # change overlay in this ui
> > +            self._ocfgs = self._ocfgs.append(acfg)
> > +        elif priority == 1:
> > +            # change overlay in this ui, do not override existing overlays
> > +            self._ocfgs = self._ocfgs.prepend(acfg)
> > +        else:
> > +            self._tcfgs = self._tcfgs.append(acfg)
> > +            self._ucfgs = self._ucfgs.append(acfg)
> >

Patch

diff --git a/mercurial/commands.py b/mercurial/commands.py
--- a/mercurial/commands.py
+++ b/mercurial/commands.py
@@ -1652,5 +1652,5 @@  def _docommit(ui, repo, *pats, **opts):
             raise error.Abort(_('cannot amend with --subrepos'))
         # Let --subrepos on the command line override config setting.
-        ui.setconfig('ui', 'commitsubrepos', True, 'commit')
+        ui.setconfig('ui', 'commitsubrepos', True, 'commit', priority=3)
 
     cmdutil.checkunfinished(repo, commit=True)
diff --git a/mercurial/dispatch.py b/mercurial/dispatch.py
--- a/mercurial/dispatch.py
+++ b/mercurial/dispatch.py
@@ -109,5 +109,6 @@  def dispatch(req):
             req.ui = uimod.ui.load()
         if '--traceback' in req.args:
-            req.ui.setconfig('ui', 'traceback', 'on', '--traceback')
+            req.ui.setconfig('ui', 'traceback', 'on', '--traceback',
+                             priority=2)
 
         # set ui streams from the request
@@ -180,5 +181,6 @@  def _runcatch(req):
                 # the repo ui
                 for sec, name, val in cfgs:
-                    req.repo.ui.setconfig(sec, name, val, source='--config')
+                    req.repo.ui.setconfig(sec, name, val, source='--config',
+                                          priority=2)
 
             # developer config: ui.debugger
@@ -511,5 +513,5 @@  def _parseconfig(ui, config):
             if not section or not name:
                 raise IndexError
-            ui.setconfig(section, name, value, '--config')
+            ui.setconfig(section, name, value, '--config', priority=2)
             configs.append((section, name, value))
         except (IndexError, ValueError):
@@ -683,5 +685,6 @@  def _dispatch(req):
     if '--profile' in args:
         for ui_ in uis:
-            ui_.setconfig('profiling', 'enabled', 'true', '--profile')
+            ui_.setconfig('profiling', 'enabled', 'true', '--profile',
+                          priority=2)
 
     with profiling.maybeprofile(lui):
@@ -755,13 +758,14 @@  def _dispatch(req):
                     val = val.encode('ascii')
                 for ui_ in uis:
-                    ui_.setconfig('ui', opt, val, '--' + opt)
+                    ui_.setconfig('ui', opt, val, '--' + opt, priority=2)
 
         if options['traceback']:
             for ui_ in uis:
-                ui_.setconfig('ui', 'traceback', 'on', '--traceback')
+                ui_.setconfig('ui', 'traceback', 'on', '--traceback',
+                              priority=2)
 
         if options['noninteractive']:
             for ui_ in uis:
-                ui_.setconfig('ui', 'interactive', 'off', '-y')
+                ui_.setconfig('ui', 'interactive', 'off', '-y', priority=2)
 
         if util.parsebool(options['pager']):
@@ -778,5 +782,5 @@  def _dispatch(req):
         for ui_ in uis:
             if coloropt:
-                ui_.setconfig('ui', 'color', coloropt, '--color')
+                ui_.setconfig('ui', 'color', coloropt, '--color', priority=2)
             color.setup(ui_)
 
diff --git a/mercurial/ui.py b/mercurial/ui.py
--- a/mercurial/ui.py
+++ b/mercurial/ui.py
@@ -130,4 +130,115 @@  def _catchterm(*args):
     raise error.SignalInterrupt
 
+def _fixpathsection(fileconfig, root):
+    """normalize paths in the [paths] section
+
+    fileconfig is an immutable config object. Return the fileconfig if it does
+    not have a "paths" section, or a "filteredconfig" if paths are normalized.
+    """
+    if 'paths' not in fileconfig.sections():
+        return fileconfig
+
+    def filterpaths(items):
+        result = util.sortdict()
+        for name, (path, source) in items.items():
+            # Only normalize it if path is non-empty and is not a sub-option
+            if ':' not in name and path:
+                path = util.expandpath(path)
+                if not util.hasscheme(path) and not os.path.isabs(path):
+                    path = os.path.normpath(os.path.join(root, path))
+            result[name] = (path, source)
+        return result
+
+    filters = {'paths': filterpaths}
+    return config.filteredconfig(fileconfig.title, fileconfig, filters)
+
+def _isplain(feature=None):
+    '''is plain mode active?
+
+    Plain mode means that all configuration variables which affect
+    the behavior and output of Mercurial should be
+    ignored. Additionally, the output should be stable,
+    reproducible and suitable for use in scripts or applications.
+
+    The only way to trigger plain mode is by setting either the
+    `HGPLAIN' or `HGPLAINEXCEPT' environment variables.
+
+    The return value can either be
+    - False if HGPLAIN is not set, or feature is in HGPLAINEXCEPT
+    - True otherwise
+    '''
+    if ('HGPLAIN' not in encoding.environ and
+            'HGPLAINEXCEPT' not in encoding.environ):
+        return False
+    exceptions = encoding.environ.get('HGPLAINEXCEPT',
+            '').strip().split(',')
+    if feature and exceptions:
+        return feature not in exceptions
+    return True
+
+def _filterconfig(subconfig):
+    '''remove configs according to HGPLAIN and HGPLAINEXCEPT
+
+    subconfig is an immutable config object. Returns an immutable config object
+    with related fields filtered.
+    '''
+    filters = {}
+    if _isplain():
+        def filterui(items):
+            result = util.sortdict(items)
+            for k in ('debug', 'fallbackencoding', 'quiet', 'slash',
+                      'logtemplate', 'statuscopies', 'style',
+                      'traceback', 'verbose'):
+                if k in result:
+                    del result[k]
+            return result
+
+        filters['ui'] = filterui
+        filters['defaults'] = {}
+    if _isplain('alias'):
+        filters['alias'] = {}
+    if _isplain('revsetalias'):
+        filters['revsetalias'] = {}
+    if _isplain('templatealias'):
+        filters['templatealias'] = {}
+    if _isplain('commands'):
+        filters['commands'] = {}
+
+    if filters:
+        return config.filteredconfig('filter', subconfig, filters)
+    else:
+        return subconfig
+
+def _getconfig(configroot, section, name, default=None, index=0):
+    '''get value (index=0) or source (index=-1) from an immutable config'''
+    value = configroot.getsection(section).get(name, (None,))[index]
+    if value is None:
+        value = default
+    return value
+
+def _buildconfigroot(cfg, ocfg, gcfg):
+    return config.mergedconfig('root', [cfg, ocfg, gcfg])
+
+def dependson(*fields):
+    '''cache result which gets invalidates if any field changes'''
+
+    def decorator(oldfunc):
+        cached = [[None], None] # cache key, result
+        def getcachekey(self):
+            return [getattr(self, f, None) for f in fields]
+
+        def newfunc(self):
+            newkey = getcachekey(self)
+            oldkey = cached[0]
+            if oldkey == newkey:
+                return cached[1]
+            result = oldfunc(self)
+            cached[:] = [newkey, result]
+            return result
+        newfunc.__name__ = oldfunc.__name__
+        newfunc.__doc__ = oldfunc.__doc__
+        return newfunc
+    return decorator
+
 class ui(object):
     def __init__(self, src=None):
@@ -146,16 +257,7 @@  class ui(object):
         # This exists to prevent an extra list lookup.
         self._bufferapplylabels = None
-        self.quiet = self.verbose = self.debugflag = self.tracebackflag = False
-        self._reportuntrusted = True
-        self._ocfg = config.config() # overlay
-        self._tcfg = config.config() # trusted
-        self._ucfg = config.config() # untrusted
-        self._trustusers = set()
-        self._trustgroups = set()
         self.callhooks = True
         # Insecure server connections requested.
         self.insecureconnections = False
-        # Blocked time
-        self.logblockedtimes = False
         # color mode: see mercurial/color.py for possible value
         self._colormode = None
@@ -170,9 +272,9 @@  class ui(object):
             self._disablepager = src._disablepager
 
-            self._tcfg = src._tcfg.copy()
-            self._ucfg = src._ucfg.copy()
-            self._ocfg = src._ocfg.copy()
-            self._trustusers = src._trustusers.copy()
-            self._trustgroups = src._trustgroups.copy()
+            # immutable configs can be reused without copying
+            self._ocfgs = src._ocfgs
+            self._tcfgs = src._tcfgs
+            self._ucfgs = src._ucfgs
+
             self.environ = src.environ
             self.callhooks = src.callhooks
@@ -182,6 +284,4 @@  class ui(object):
             self._styles = src._styles.copy()
 
-            self.fixconfig()
-
             self.httppasswordmgrdb = src.httppasswordmgrdb
             self._blockedtimes = src._blockedtimes
@@ -199,4 +299,9 @@  class ui(object):
             self._blockedtimes = collections.defaultdict(int)
 
+            # immutable configs
+            self._ocfgs = config.mergedconfig('setconfig', []) # overlay
+            self._tcfgs = config.mergedconfig('loaded', []) # trusted
+            self._ucfgs = config.mergedconfig('loaded', []) # trusted+untrusted
+
         allowed = self.configlist('experimental', 'exportableenviron')
         if '*' in allowed:
@@ -208,4 +313,7 @@  class ui(object):
                     self._exportableenviron[k] = self.environ[k]
 
+    # global overlay, for things like ui.commitsubrepos
+    _gcfgs = config.mergedconfig('global', [])
+
     @classmethod
     def load(cls):
@@ -217,13 +325,9 @@  class ui(object):
                 u.readconfig(f, trust=True)
             elif t == 'items':
-                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)
+                acfg = config.atomicconfig(
+                    'load', ((s, n, (v, src)) for s, n, v, src in f))
+                u._tcfgs = u._tcfgs.append(acfg)
+                u._ucfgs = u._ucfgs.append(acfg)
+                u.readconfigitems(f)
             else:
                 raise error.ProgrammingError('unknown rctype: %s' % t)
@@ -280,104 +384,141 @@  class ui(object):
             raise
 
-        cfg = config.config()
         trusted = sections or trust or self._trusted(fp, filename)
 
         try:
-            cfg.read(filename, fp, sections=sections, remap=remap)
-            fp.close()
+            fcfg = config.fileconfig(filename, fp, sections, remap)
         except error.ConfigError as inst:
             if trusted:
                 raise
             self.warn(_("ignored: %s\n") % str(inst))
+        else:
+            fcfg = _fixpathsection(fcfg, root or os.path.expanduser('~'))
+            fcfg = _filterconfig(fcfg)
+            if trusted:
+                self._tcfgs = self._tcfgs.append(fcfg)
+            self._ucfgs = self._ucfgs.append(fcfg)
+        finally:
+            fp.close()
 
-        if self.plain():
-            for k in ('debug', 'fallbackencoding', 'quiet', 'slash',
-                      'logtemplate', 'statuscopies', 'style',
-                      'traceback', 'verbose'):
-                if k in cfg['ui']:
-                    del cfg['ui'][k]
-            for k, v in cfg.items('defaults'):
-                del cfg['defaults'][k]
-            for k, v in cfg.items('commands'):
-                del cfg['commands'][k]
-        # Don't remove aliases from the configuration if in the exceptionlist
-        if self.plain('alias'):
-            for k, v in cfg.items('alias'):
-                del cfg['alias'][k]
-        if self.plain('revsetalias'):
-            for k, v in cfg.items('revsetalias'):
-                del cfg['revsetalias'][k]
-        if self.plain('templatealias'):
-            for k, v in cfg.items('templatealias'):
-                del cfg['templatealias'][k]
+    def readconfigitems(self, items):
+        acfg = config.atomicconfig(
+            'load', ((s, n, (v, src)) for s, n, v, src in items))
+        self._tcfgs = self._tcfgs.append(acfg)
+        self._ucfgs = self._ucfgs.append(acfg)
+
+    @property
+    @dependson('_roottcfg')
+    def debugflag(self):
+        return util.parsebool(_getconfig(self._roottcfg, 'ui', 'debug', '0'))
 
-        if trusted:
-            self._tcfg.update(cfg)
-            self._tcfg.update(self._ocfg)
-        self._ucfg.update(cfg)
-        self._ucfg.update(self._ocfg)
+    @property
+    @dependson('_roottcfg')
+    def _verbosequiet(self):
+        verbose = self.debugflag or self.configbool('ui', 'verbose')
+        quiet = not self.debugflag and self.configbool('ui', 'quiet')
+        # they could cancel each other
+        if verbose and quiet:
+            verbose = quiet = False
+        return (verbose, quiet)
 
-        if root is None:
-            root = os.path.expanduser('~')
-        self.fixconfig(root=root)
+    @property
+    def verbose(self):
+        return self._verbosequiet[0]
+
+    @property
+    def quiet(self):
+        return self._verbosequiet[1]
+
+    @quiet.setter
+    def quiet(self, value):
+        # this is needed by perf.py; new code shouldn't depend on this
+        assert value in (True, False)
+        if self.quiet == value:
+            return
+        self.setconfig('ui', 'quiet', str(value), 'quiet=')
+        assert self.quiet == value
 
-    def fixconfig(self, root=None, section=None):
-        if section in (None, 'paths'):
-            # expand vars and ~
-            # translate paths relative to root (or home) into absolute paths
-            root = root or pycompat.getcwd()
-            for c in self._tcfg, self._ucfg, self._ocfg:
-                for n, p in c.items('paths'):
-                    # Ignore sub-options.
-                    if ':' in n:
-                        continue
-                    if not p:
-                        continue
-                    if '%%' in p:
-                        s = self.configsource('paths', n) or 'none'
-                        self.warn(_("(deprecated '%%' in path %s=%s from %s)\n")
-                                  % (n, p, s))
-                        p = p.replace('%%', '%')
-                    p = util.expandpath(p)
-                    if not util.hasscheme(p) and not os.path.isabs(p):
-                        p = os.path.normpath(os.path.join(root, p))
-                    c.set("paths", n, p)
+    @verbose.setter
+    def verbose(self, value):
+        assert value in (True, False)
+        if self.verbose == value:
+            return
+        # this is needed by hgweb
+        self.setconfig('ui', 'verbose', str(value), 'verbose=')
+        if self.quiet:
+            self.setconfig('ui', 'quiet', '0', 'verbose=')
+        assert self.verbose == value
+
+    @property
+    @dependson('_roottcfg')
+    def tracebackflag(self):
+        return self.configbool('ui', 'traceback', False)
+
+    @property
+    @dependson('_roottcfg')
+    def logblockedtimes(self):
+        return self.configbool('ui', 'logblockedtimes')
+
+    @property
+    @dependson('_roottcfg')
+    def _reportuntrusted(self):
+        return self.debugflag or self.configbool("ui", "report_untrusted", True)
 
-        if section in (None, 'ui'):
-            # update ui options
-            self.debugflag = self.configbool('ui', 'debug')
-            self.verbose = self.debugflag or self.configbool('ui', 'verbose')
-            self.quiet = not self.debugflag and self.configbool('ui', 'quiet')
-            if self.verbose and self.quiet:
-                self.quiet = self.verbose = False
-            self._reportuntrusted = self.debugflag or self.configbool("ui",
-                "report_untrusted", True)
-            self.tracebackflag = self.configbool('ui', 'traceback', False)
-            self.logblockedtimes = self.configbool('ui', 'logblockedtimes')
+    @property
+    @dependson('_tcfgs')
+    def _trustusers(self):
+        trustedusers = set()
+        for cfg in self._tcfgs.subconfigs('trusted'):
+            users = config.parselist(_getconfig(cfg, 'trusted', 'users', []))
+            trustedusers.update(users)
+        return trustedusers
+
+    @property
+    @dependson('_tcfgs')
+    def _trustgroups(self):
+        trustedgroups = set()
+        for cfg in self._tcfgs.subconfigs('trusted'):
+            groups = config.parselist(_getconfig(cfg, 'trusted', 'groups', []))
+            trustedgroups.update(groups)
+        return trustedgroups
 
-        if section in (None, 'trusted'):
-            # update trust information
-            self._trustusers.update(self.configlist('trusted', 'users'))
-            self._trustgroups.update(self.configlist('trusted', 'groups'))
-
-    def backupconfig(self, section, item):
-        return (self._ocfg.backup(section, item),
-                self._tcfg.backup(section, item),
-                self._ucfg.backup(section, item),)
-    def restoreconfig(self, data):
-        self._ocfg.restore(data[0])
-        self._tcfg.restore(data[1])
-        self._ucfg.restore(data[2])
-
-    def setconfig(self, section, name, value, source=''):
-        for cfg in (self._ocfg, self._tcfg, self._ucfg):
-            cfg.set(section, name, value, source)
-        self.fixconfig(section=section)
+    def setconfig(self, section, name, value, source='', priority=None):
+        title = source or 'setconfig'
+        acfg = config.atomicconfig(title, [(section, name, (value, source))])
+        try:
+            cwd = pycompat.getcwd()
+        except OSError:
+            pass
+        else:
+            acfg = _fixpathsection(acfg, cwd)
+        if priority == 3:
+            # global overlay
+            self.__class__._gcfgs = self.__class__._gcfgs.append(acfg)
+        elif priority == 2:
+            # change overlay in this ui
+            self._ocfgs = self._ocfgs.append(acfg)
+        elif priority == 1:
+            # change overlay in this ui, do not override existing overlays
+            self._ocfgs = self._ocfgs.prepend(acfg)
+        else:
+            self._tcfgs = self._tcfgs.append(acfg)
+            self._ucfgs = self._ucfgs.append(acfg)
 
     def _data(self, untrusted):
-        return untrusted and self._ucfg or self._tcfg
+        return untrusted and self._rootucfg or self._roottcfg
+
+    @property
+    @dependson('_ucfgs', '_ocfgs', '_gcfgs')
+    def _rootucfg(self):
+        return _buildconfigroot(self._ucfgs, self._ocfgs, self._gcfgs)
+
+    @property
+    @dependson('_tcfgs', '_ocfgs', '_gcfgs')
+    def _roottcfg(self):
+        return _buildconfigroot(self._tcfgs, self._ocfgs, self._gcfgs)
 
     def configsource(self, section, name, untrusted=False):
-        return self._data(untrusted).source(section, name)
+        root = self._data(untrusted)
+        return _getconfig(root, section, name, '', index=-1)
 
     def config(self, section, name, default=None, untrusted=False):
@@ -387,6 +528,7 @@  class ui(object):
             alternates = [name]
 
+        root = self._data(untrusted)
         for n in alternates:
-            value = self._data(untrusted).get(section, n, None)
+            value = _getconfig(root, section, n, default)
             if value is not None:
                 name = n
@@ -396,6 +538,7 @@  class ui(object):
 
         if self.debugflag and not untrusted and self._reportuntrusted:
+            uroot = self._data(untrusted=True)
             for n in alternates:
-                uvalue = self._ucfg.get(section, n)
+                uvalue = _getconfig(uroot, section, n)
                 if uvalue is not None and uvalue != value:
                     self.debug("ignoring untrusted configuration option "
@@ -413,25 +556,12 @@  class ui(object):
         is a dict of defined sub-options where keys and values are strings.
         """
-        data = self._data(untrusted)
-        main = data.get(section, name, default)
-        if self.debugflag and not untrusted and self._reportuntrusted:
-            uvalue = self._ucfg.get(section, name)
-            if uvalue is not None and uvalue != main:
-                self.debug('ignoring untrusted configuration option '
-                           '%s.%s = %s\n' % (section, name, uvalue))
-
+        root = self._data(untrusted)
+        main = _getconfig(root, section, name, default)
         sub = {}
         prefix = '%s:' % name
-        for k, v in data.items(section):
-            if k.startswith(prefix):
-                sub[k[len(prefix):]] = v
-
-        if self.debugflag and not untrusted and self._reportuntrusted:
-            for k, v in sub.items():
-                uvalue = self._ucfg.get(section, '%s:%s' % (name, k))
-                if uvalue is not None and uvalue != v:
-                    self.debug('ignoring untrusted configuration option '
-                               '%s:%s.%s = %s\n' % (section, name, k, uvalue))
-
+        if section in root.sections():
+            for k, (v, src) in root.getsection(section).iteritems():
+                if k.startswith(prefix):
+                    sub[k[len(prefix):]] = v
         return main, sub
 
@@ -586,54 +716,30 @@  class ui(object):
 
     def hasconfig(self, section, name, untrusted=False):
-        return self._data(untrusted).hasitem(section, name)
+        return _getconfig(self._data(untrusted), section, name) is not None
 
     def has_section(self, section, untrusted=False):
         '''tell whether section exists in config.'''
-        return section in self._data(untrusted)
+        return section in self._data(untrusted).sections()
 
     def configitems(self, section, untrusted=False, ignoresub=False):
-        items = self._data(untrusted).items(section)
-        if ignoresub:
-            newitems = {}
-            for k, v in items:
-                if ':' not in k:
-                    newitems[k] = v
-            items = newitems.items()
-        if self.debugflag and not untrusted and self._reportuntrusted:
-            for k, v in self._ucfg.items(section):
-                if self._tcfg.get(section, k) != v:
-                    self.debug("ignoring untrusted configuration option "
-                               "%s.%s = %s\n" % (section, k, v))
-        return items
+        items = self._data(untrusted).getsection(section)
+        result = []
+        for k, (v, s) in items.iteritems():
+            if ignoresub and ':' in k:
+                continue
+            if v is not None:
+                result.append((k, v))
+        return result
 
     def walkconfig(self, untrusted=False):
         cfg = self._data(untrusted)
-        for section in cfg.sections():
-            for name, value in self.configitems(section, untrusted):
+        for section in sorted(cfg.sections()):
+            for name, (value, source) in cfg.getsection(section).iteritems():
+                if value is None:
+                    continue
                 yield section, name, value
 
     def plain(self, feature=None):
-        '''is plain mode active?
-
-        Plain mode means that all configuration variables which affect
-        the behavior and output of Mercurial should be
-        ignored. Additionally, the output should be stable,
-        reproducible and suitable for use in scripts or applications.
-
-        The only way to trigger plain mode is by setting either the
-        `HGPLAIN' or `HGPLAINEXCEPT' environment variables.
-
-        The return value can either be
-        - False if HGPLAIN is not set, or feature is in HGPLAINEXCEPT
-        - True otherwise
-        '''
-        if ('HGPLAIN' not in encoding.environ and
-                'HGPLAINEXCEPT' not in encoding.environ):
-            return False
-        exceptions = encoding.environ.get('HGPLAINEXCEPT',
-                '').strip().split(',')
-        if feature and exceptions:
-            return feature not in exceptions
-        return True
+        return _isplain(feature)
 
     def username(self):
@@ -1446,17 +1552,12 @@  class ui(object):
         `overrides` must be a dict of the following structure:
         {(section, name) : value}"""
-        backups = {}
+        bakocfgs = self._ocfgs
         try:
-            for (section, name), value in overrides.items():
-                backups[(section, name)] = self.backupconfig(section, name)
-                self.setconfig(section, name, value, source)
+            acfg = config.atomicconfig('configoverride',
+                ((s, n, (v, source)) for (s, n), v in overrides.items()))
+            self._ocfgs = self._ocfgs.append(acfg)
             yield
         finally:
-            for __, backup in backups.items():
-                self.restoreconfig(backup)
-            # just restoring ui.quiet config to the previous value is not enough
-            # as it does not update ui.quiet class member
-            if ('ui', 'quiet') in overrides:
-                self.fixconfig(section='ui')
+            self._ocfgs = bakocfgs
 
 class paths(dict):
diff --git a/tests/test-config.t b/tests/test-config.t
--- a/tests/test-config.t
+++ b/tests/test-config.t
@@ -157,6 +157,6 @@  sub-options in [paths] aren't expanded
 
   $ hg showconfig paths
+  paths.foo=$TESTTMP/foo
   paths.foo:suboption=~/foo
-  paths.foo=$TESTTMP/foo
 
 edit failure
diff --git a/tests/test-trusted.py.out b/tests/test-trusted.py.out
--- a/tests/test-trusted.py.out
+++ b/tests/test-trusted.py.out
@@ -130,5 +130,4 @@  untrusted
 not trusting file .hg/hgrc from untrusted user abc, group def
 trusted
-ignoring untrusted configuration option paths.local = /another/path
     global = /some/path
 untrusted
@@ -149,5 +148,4 @@  untrusted
 not trusting file .hg/hgrc from untrusted user abc, group def
 trusted
-ignoring untrusted configuration option paths.local = /another/path
     global = /some/path
 untrusted