# HG changeset patch # User Jun Wu <qu...@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. 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 _______________________________________________ Mercurial-devel mailing list Mercurial-devel@mercurial-scm.org https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel