Submitter | Jun Wu |
---|---|
Date | March 27, 2017, 6:38 p.m. |
Message ID | <4eb7c76340791f379a34.1490639886@localhost.localdomain> |
Download | mbox | patch |
Permalink | /patch/19760/ |
State | Changes Requested |
Headers | show |
Comments
On Mon, Mar 27, 2017 at 11:38:06AM -0700, Jun Wu wrote: > # HG changeset patch > # User Jun Wu <quark@fb.com> > # Date 1490635856 25200 > # Mon Mar 27 10:30:56 2017 -0700 > # Node ID 4eb7c76340791f379a34f9df4ec42e0c8b9b2a2f > # Parent 4a8d065bbad80d3b3401010375bc80165404aa87 > RFC: implement immutable config objects I like the overall approach and that it's parallel to config.config() and ui.config. We might in later patches combine config.config into this. > +class abstractimmutableconfig(object): > + """minimal interface defined for a read-only config accessor > + > + The immutable config object should get its state set and frozen during > + __init__ and should not have any setconfig-like method. > + > + The immutable config layer knows about sections, and should probably uses > + ordered dict for each section. To simplify things, this layer does not care > + about "source", "unset" or any filesystem IO. They're up to the upper layer > + to deal with. For example, the upper layer could store "None" as "unset"s, > + and store (value, source) as a tuple together. > + """ > + > + def __init__(self, title): > + """title: useful to identify the config""" > + self.title = title > + > + def subconfigs(self, section=None): > + """return a list-ish of child config objects filtered by section""" > + raise NotImplementedError > + > + def get(self, section, item): > + """return value or None""" > + return self.getsection(section).get(item) > + > + def getsection(self, section): > + """return a dict-ish""" > + raise NotImplementedError > + > + def sections(self): > + """return an iter-able""" > + raise NotImplementedError > + > +class atomicconfig(abstractimmutableconfig): > + """immutable config that converted from a list-ish""" > + > + def __init__(self, title, entries=None): > + """ > + title: a title used to identify the atomicconfig > + entries: a list-ish of (section, item, value) > + """ > + super(atomicconfig, self).__init__(title) > + self._sections = util.sortdict() Can we use collections.defaultdict(util.sortdict) here? > + for entry in entries: > + section, item, value = entry > + if section not in self._sections: > + self._sections[section] = util.sortdict() > + if item is not None: > + self._sections[section][item] = value > + > + def subconfigs(self, section=None): > + return () > + > + def getsection(self, section): > + return self._sections.get(section, {}) > + > + def sections(self): > + return self._sections.keys() > + > +class mergedconfig(abstractimmutableconfig): > + """immutable config that is a merge of a list of immutable configs""" > + > + def __init__(self, title, subconfigs): > + super(mergedconfig, self).__init__(title) > + self._subconfigs = tuple(subconfigs) # make it immutable > + self._cachedsections = {} > + > + def subconfigs(self, section=None): I am not sure about this API. We already have getsection() from abstractimmutableconfig. I think this method does two things depending on the arguments. > + if section is None: > + return self._subconfigs > + else: > + return self._sectionconfigs.get(section, ()) > + > + def sections(self): > + return self._sectionconfigs.keys() > + > + @util.propertycache > + def _sectionconfigs(self): > + """{section: [subconfig]}""" > + sectionconfigs = {} > + for subconfig in self._subconfigs: > + for section in subconfig.sections(): > + sectionconfigs.setdefault(section, []).append(subconfig) > + return sectionconfigs > + > + def getsection(self, section): > + items = self._cachedsections.get(section, None) > + if items is None: > + subconfigs = self._sectionconfigs.get(section, []) > + if len(subconfigs) == 1: > + # no need to merge configs > + items = subconfigs[0].getsection(section) > + else: > + # merge configs > + items = util.sortdict() > + for subconfig in subconfigs: > + subconfigitems = subconfig.getsection(section).items() Could we do an `items.update(subconfig.getsection(section))`? > + for item, value in subconfigitems: > + items[item] = value > + self._cachedsections[section] = items > + return items > + > + def append(self, subconfig): > + """return a new mergedconfig with the new subconfig appended""" > + return mergedconfig(self.title, list(self._subconfigs) + [subconfig]) > + > + def prepend(self, subconfig): > + """return a new mergedconfig with the new subconfig prepended""" > + return mergedconfig(self.title, [subconfig] + list(self._subconfigs)) > + > + def filter(self, func): > + """return a new mergedconfig with only selected subconfigs > + func: subconfig -> bool > + """ > + return mergedconfig(self.title, filter(func, self._subconfigs)) > + > +class fileconfig(atomicconfig): > + """immutable config constructed from config files""" > + > + def __init__(self, title, fp, sections=None, remap=None): > + # Currently, just use the legacy, non-immutable "config" object to do > + # the parsing, remap stuff. In the future we may want to detach from it > + cfg = config() > + cfg.read(title, fp, sections=sections, remap=remap) > + > + def cfgwalker(): > + # visible config, with source > + for section in cfg: > + emptysection = True > + for item, value in cfg.items(section): > + emptysection = False > + source = cfg.source(section, item) > + yield (section, item, (value, source)) > + if emptysection: > + # create the section > + yield (section, None, (None, None)) > + # "%unset" configs > + for (section, item) in cfg._unset: > + # TODO "%unset" could have line numbers > + if cfg.get(section, item) is None: > + yield (section, item, (None, '%s:(unset)' % title)) > + > + super(fileconfig, self).__init__(title, cfgwalker()) > + > +class filteredconfig(abstractimmutableconfig): > + """immutable config that changes other configs""" > + > + def __init__(self, title, subconfig, filters=None): > + """ > + filters: a dict-ish, {section: filterfunc or sortdict-ish} > + a filterfunc takes sortdict-ish and returns sortdict-ish > + a sortdict-ish will replace the section directly > + """ > + super(filteredconfig, self).__init__(title) > + self._filters = filters or {} > + self._subconfig = subconfig > + self._cachedsections = {} > + > + def subconfigs(self, section=None): I think this is actually not needed except for dumpconfig, which might just be a helper that knows about the internals. > + return (self._subconfig,) > + > + def sections(self): > + return self._subconfig.sections() > + > + def getsection(self, section): > + if section not in self._filters: > + return self._subconfig.getsection(section) > + items = self._cachedsections.get(section, None) > + if items is None: > + filter = self._filters[section] > + if util.safehasattr(filter, '__call__'): this should be `callable` > + items = filter(self._subconfig.getsection(section)) > + else: > + items = filter > + self._cachedsections[section] = items > + return items
Excerpts from David Soria Parra's message of 2017-03-29 15:25:26 -0700: > On Mon, Mar 27, 2017 at 11:38:06AM -0700, Jun Wu wrote: > > # HG changeset patch > > # User Jun Wu <quark@fb.com> > > # Date 1490635856 25200 > > # Mon Mar 27 10:30:56 2017 -0700 > > # Node ID 4eb7c76340791f379a34f9df4ec42e0c8b9b2a2f > > # Parent 4a8d065bbad80d3b3401010375bc80165404aa87 > > RFC: implement immutable config objects > > I like the overall approach and that it's parallel to config.config() and > ui.config. We might in later patches combine config.config into this. If we can replace "x.setconfig(...)" to "x = x.setconfig(...)", and make "config.config" immutable too, the path is worth a try. Practically, that may lead to BCs. > > + super(atomicconfig, self).__init__(title) > > + self._sections = util.sortdict() > Can we use collections.defaultdict(util.sortdict) here? Good advice. > > + > > + def subconfigs(self, section=None): > I am not sure about this API. We already have getsection() from > abstractimmutableconfig. I think this method does two things depending > on the arguments. It was "subconfigs(self)", the "section" was added for performance. I'll revisit if it's still needed. I think eventually we want "debugconfig / config" to be able to get the layered information too. So it can show "config X" set by "file Y" is shallowed by "file Z". Or it can draw a tree. Initially, I was thinking this like a React DOM, a diff algorithm to compare the DOM tree, and replace elements in it. That makes "subconfigs" useful. Later I decided the diff algorithm is a overkill. The same reason applies to why I make "title" a required parameter. So things like "get-subconfig-by-title" is possible. Since we now control the subconfigs directly and build the root config on demand, we never need to get subconfigs from the root config, so the API is unnecessary. I'll make them private in the next version. > > + items = util.sortdict() > > + for subconfig in subconfigs: > > + subconfigitems = subconfig.getsection(section).items() > Could we do an `items.update(subconfig.getsection(section))`? Sure. > > + def subconfigs(self, section=None): > I think this is actually not needed except for dumpconfig, which might just be a > helper that knows about the internals. > > > + return (self._subconfig,) > > + > > + def sections(self): > > + return self._subconfig.sections() > > + > > + def getsection(self, section): > > + if section not in self._filters: > > + return self._subconfig.getsection(section) > > + items = self._cachedsections.get(section, None) > > + if items is None: > > + filter = self._filters[section] > > + if util.safehasattr(filter, '__call__'): > this should be `callable` > > > + items = filter(self._subconfig.getsection(section)) > > + else: > > + items = filter > > + self._cachedsections[section] = items > > + return items
On Wed, 29 Mar 2017 15:25:26 -0700, David Soria Parra wrote: > On Mon, Mar 27, 2017 at 11:38:06AM -0700, Jun Wu wrote: > > # HG changeset patch > > # User Jun Wu <quark@fb.com> > > # Date 1490635856 25200 > > # Mon Mar 27 10:30:56 2017 -0700 > > # Node ID 4eb7c76340791f379a34f9df4ec42e0c8b9b2a2f > > # Parent 4a8d065bbad80d3b3401010375bc80165404aa87 > > RFC: implement immutable config objects > > I like the overall approach and that it's parallel to config.config() and > ui.config. We might in later patches combine config.config into this. Yea, the idea sounds good and the next patch proves that this actually works. Nice. Random nitpicks in line. > > +class fileconfig(atomicconfig): > > + """immutable config constructed from config files""" > > + > > + def __init__(self, title, fp, sections=None, remap=None): > > + # Currently, just use the legacy, non-immutable "config" object to do > > + # the parsing, remap stuff. In the future we may want to detach from it > > + cfg = config() > > + cfg.read(title, fp, sections=sections, remap=remap) > > + > > + def cfgwalker(): > > + # visible config, with source > > + for section in cfg: > > + emptysection = True > > + for item, value in cfg.items(section): > > + emptysection = False > > + source = cfg.source(section, item) > > + yield (section, item, (value, source)) We might want to keep filename and line number separately in future version. > > +class filteredconfig(abstractimmutableconfig): > > + """immutable config that changes other configs""" > > + > > + def __init__(self, title, subconfig, filters=None): > > + """ > > + filters: a dict-ish, {section: filterfunc or sortdict-ish} > > + a filterfunc takes sortdict-ish and returns sortdict-ish > > + a sortdict-ish will replace the section directly > > + """ So this is more like "map" operation? I thought filterfunc would return booleans. > > + super(filteredconfig, self).__init__(title) > > + self._filters = filters or {} > > + self._subconfig = subconfig > > + self._cachedsections = {} > > + > > + def subconfigs(self, section=None): > I think this is actually not needed except for dumpconfig, which might just be a > helper that knows about the internals. > > > + return (self._subconfig,) > > + > > + def sections(self): > > + return self._subconfig.sections() > > + > > + def getsection(self, section): > > + if section not in self._filters: > > + return self._subconfig.getsection(section) > > + items = self._cachedsections.get(section, None) > > + if items is None: > > + filter = self._filters[section] > > + if util.safehasattr(filter, '__call__'): > this should be `callable` > > > + items = filter(self._subconfig.getsection(section)) > > + else: > > + items = filter Do we really need to overload the type of filter?
Excerpts from Yuya Nishihara's message of 2017-04-01 23:39:34 +0900: > Yea, the idea sounds good and the next patch proves that this actually works. > Nice. Thanks. > > Random nitpicks in line. > > > > + yield (section, item, (value, source)) > > We might want to keep filename and line number separately in future version. Since there are other sources like "setconfig", "--config", "--verbose", and the new "$ENVNAME", maybe "source" needs to a tuple or something more complex / typed. But it's probably another series. The immutable configs (other than fileconfig) do not assume the format of the tuple "(section, item, value)". So if we do change it to "(section, item, (value, ('file', path, line)))", other objects won't need change. > > > +class filteredconfig(abstractimmutableconfig): > > > + """immutable config that changes other configs""" > > > + > > > + def __init__(self, title, subconfig, filters=None): > > > + """ > > > + filters: a dict-ish, {section: filterfunc or sortdict-ish} > > > + a filterfunc takes sortdict-ish and returns sortdict-ish > > > + a sortdict-ish will replace the section directly > > > + """ > > So this is more like "map" operation? I thought filterfunc would return > booleans. It should probably be renamed to "transformfunc". > > > + if util.safehasattr(filter, '__call__'): > > this should be `callable` > > > > > + items = filter(self._subconfig.getsection(section)) > > > + else: > > > + items = filter > > Do we really need to overload the type of filter? Good point. The "transform" function could take a section: def transform(section, items): .... So the "static items" type is no longer needed. I wrote that mainly for performance consideration, and it seems unnecessary.
On Sat, 1 Apr 2017 15:56:25 -0700, Jun Wu wrote: > Excerpts from Yuya Nishihara's message of 2017-04-01 23:39:34 +0900: > > > > + yield (section, item, (value, source)) > > > > We might want to keep filename and line number separately in future version. > > Since there are other sources like "setconfig", "--config", "--verbose", and > the new "$ENVNAME", maybe "source" needs to a tuple or something more > complex / typed. But it's probably another series. Yeah, that's kinda unrelated topic to the core functions in this series. I just recalled someone (Mathias?) wanted to get source filenames. > The immutable configs (other than fileconfig) do not assume the format of > the tuple "(section, item, value)". So if we do change it to "(section, > item, (value, ('file', path, line)))", other objects won't need change. > > > > +class filteredconfig(abstractimmutableconfig): > > > > + """immutable config that changes other configs""" > > > > + > > > > + def __init__(self, title, subconfig, filters=None): > > > > + """ > > > > + filters: a dict-ish, {section: filterfunc or sortdict-ish} > > > > + a filterfunc takes sortdict-ish and returns sortdict-ish > > > > + a sortdict-ish will replace the section directly > > > > + """ > > > > So this is more like "map" operation? I thought filterfunc would return > > booleans. > > It should probably be renamed to "transformfunc". and "transformedconfig" respectively, maybe.
On 02-04-17 07:08, Yuya Nishihara wrote: > On Sat, 1 Apr 2017 15:56:25 -0700, Jun Wu wrote: >> Excerpts from Yuya Nishihara's message of 2017-04-01 23:39:34 +0900: >>>>> + yield (section, item, (value, source)) >>> We might want to keep filename and line number separately in future version. >> Since there are other sources like "setconfig", "--config", "--verbose", and >> the new "$ENVNAME", maybe "source" needs to a tuple or something more >> complex / typed. But it's probably another series. > Yeah, that's kinda unrelated topic to the core functions in this series. > I just recalled someone (Mathias?) wanted to get source filenames. That's correct, I would be quite interested in that for hg-configexpress.
Patch
diff --git a/mercurial/config.py b/mercurial/config.py --- a/mercurial/config.py +++ b/mercurial/config.py @@ -263,2 +263,179 @@ def parselist(value): result = value return result or [] + +class abstractimmutableconfig(object): + """minimal interface defined for a read-only config accessor + + The immutable config object should get its state set and frozen during + __init__ and should not have any setconfig-like method. + + The immutable config layer knows about sections, and should probably uses + ordered dict for each section. To simplify things, this layer does not care + about "source", "unset" or any filesystem IO. They're up to the upper layer + to deal with. For example, the upper layer could store "None" as "unset"s, + and store (value, source) as a tuple together. + """ + + def __init__(self, title): + """title: useful to identify the config""" + self.title = title + + def subconfigs(self, section=None): + """return a list-ish of child config objects filtered by section""" + raise NotImplementedError + + def get(self, section, item): + """return value or None""" + return self.getsection(section).get(item) + + def getsection(self, section): + """return a dict-ish""" + raise NotImplementedError + + def sections(self): + """return an iter-able""" + raise NotImplementedError + +class atomicconfig(abstractimmutableconfig): + """immutable config that converted from a list-ish""" + + def __init__(self, title, entries=None): + """ + title: a title used to identify the atomicconfig + entries: a list-ish of (section, item, value) + """ + super(atomicconfig, self).__init__(title) + self._sections = util.sortdict() + for entry in entries: + section, item, value = entry + if section not in self._sections: + self._sections[section] = util.sortdict() + if item is not None: + self._sections[section][item] = value + + def subconfigs(self, section=None): + return () + + def getsection(self, section): + return self._sections.get(section, {}) + + def sections(self): + return self._sections.keys() + +class mergedconfig(abstractimmutableconfig): + """immutable config that is a merge of a list of immutable configs""" + + def __init__(self, title, subconfigs): + super(mergedconfig, self).__init__(title) + self._subconfigs = tuple(subconfigs) # make it immutable + self._cachedsections = {} + + def subconfigs(self, section=None): + if section is None: + return self._subconfigs + else: + return self._sectionconfigs.get(section, ()) + + def sections(self): + return self._sectionconfigs.keys() + + @util.propertycache + def _sectionconfigs(self): + """{section: [subconfig]}""" + sectionconfigs = {} + for subconfig in self._subconfigs: + for section in subconfig.sections(): + sectionconfigs.setdefault(section, []).append(subconfig) + return sectionconfigs + + def getsection(self, section): + items = self._cachedsections.get(section, None) + if items is None: + subconfigs = self._sectionconfigs.get(section, []) + if len(subconfigs) == 1: + # no need to merge configs + items = subconfigs[0].getsection(section) + else: + # merge configs + items = util.sortdict() + for subconfig in subconfigs: + subconfigitems = subconfig.getsection(section).items() + for item, value in subconfigitems: + items[item] = value + self._cachedsections[section] = items + return items + + def append(self, subconfig): + """return a new mergedconfig with the new subconfig appended""" + return mergedconfig(self.title, list(self._subconfigs) + [subconfig]) + + def prepend(self, subconfig): + """return a new mergedconfig with the new subconfig prepended""" + return mergedconfig(self.title, [subconfig] + list(self._subconfigs)) + + def filter(self, func): + """return a new mergedconfig with only selected subconfigs + func: subconfig -> bool + """ + return mergedconfig(self.title, filter(func, self._subconfigs)) + +class fileconfig(atomicconfig): + """immutable config constructed from config files""" + + def __init__(self, title, fp, sections=None, remap=None): + # Currently, just use the legacy, non-immutable "config" object to do + # the parsing, remap stuff. In the future we may want to detach from it + cfg = config() + cfg.read(title, fp, sections=sections, remap=remap) + + def cfgwalker(): + # visible config, with source + for section in cfg: + emptysection = True + for item, value in cfg.items(section): + emptysection = False + source = cfg.source(section, item) + yield (section, item, (value, source)) + if emptysection: + # create the section + yield (section, None, (None, None)) + # "%unset" configs + for (section, item) in cfg._unset: + # TODO "%unset" could have line numbers + if cfg.get(section, item) is None: + yield (section, item, (None, '%s:(unset)' % title)) + + super(fileconfig, self).__init__(title, cfgwalker()) + +class filteredconfig(abstractimmutableconfig): + """immutable config that changes other configs""" + + def __init__(self, title, subconfig, filters=None): + """ + filters: a dict-ish, {section: filterfunc or sortdict-ish} + a filterfunc takes sortdict-ish and returns sortdict-ish + a sortdict-ish will replace the section directly + """ + super(filteredconfig, self).__init__(title) + self._filters = filters or {} + self._subconfig = subconfig + self._cachedsections = {} + + def subconfigs(self, section=None): + return (self._subconfig,) + + def sections(self): + return self._subconfig.sections() + + def getsection(self, section): + if section not in self._filters: + return self._subconfig.getsection(section) + items = self._cachedsections.get(section, None) + if items is None: + filter = self._filters[section] + if util.safehasattr(filter, '__call__'): + items = filter(self._subconfig.getsection(section)) + else: + items = filter + self._cachedsections[section] = items + return items diff --git a/tests/test-config-immutable.py b/tests/test-config-immutable.py new file mode 100644 --- /dev/null +++ b/tests/test-config-immutable.py @@ -0,0 +1,94 @@ +# Test the config layer generated by environment variables + +from __future__ import absolute_import, print_function + +from mercurial import ( + config, + util, +) + +def dumpconfig(config, indent=0): + print('%s%s (%s)' % (' ' * indent, config.title, config.__class__.__name__)) + sections = sorted(config.sections()) + for section in sections: + items = config.getsection(section) + for name, (value, source) in items.iteritems(): + print('%s %s.%s=%s # %s' + % (' ' * indent, section, name, value, source)) + subconfigs = config.subconfigs() + if subconfigs: + for subc in subconfigs: + dumpconfig(subc, indent + 2) + if indent == 0: + print('') + +# atomicconfig + +c1 = config.atomicconfig('c1', [ + ['ui', 'editor', ('notepad', 'rc1:1')], + ['ui', 'editor', ('vim', 'rc1:2')], + ['ui', 'traceback', ('1', 'rc1:3')], + ['paths', 'default', ('none', 'rc2:1')], + ['pager', 'pager', (None, 'rc3:1')], + ['diff', 'git', ('1', 'rc3:2')], +]) +dumpconfig(c1) + +c2 = config.atomicconfig('c2', [ + ['pager', 'pager', ('more', 'rc5:1')], + ['smtp', 'tls', ('True', 'rc5:2')], + ['ui', 'traceback', (None, 'rc4:1')], + ['paths', 'remote', ('ssh://foo/bar', 'rc6:1')], + ['ui', 'editor', ('emacs', 'rc4:2')], +]) +dumpconfig(c2) + +# mergedconfig + +m1 = config.mergedconfig('c1 + c2', [c1, c2]) +dumpconfig(m1) + +m2 = config.mergedconfig('c2 + c1', [c2, c1]) +dumpconfig(m2) + +# filteredconfig + +def filterpath(items): + result = util.sortdict() + for name, (value, source) in items.items(): + result[name] = (value.replace('ssh', 'http'), source) + return result + +filters = {'paths': filterpath, + 'diff': {'git': ('0', 'f')}, + 'smtp': {'tls': ('False', 'f')}, + 'ui': {}} + +f1 = config.filteredconfig('f1', c2, filters) +dumpconfig(f1) + +# fileconfig + +sio = util.stringio() +rc = ''' +[ui] +editor = nano +debug = 1 + +[pager] +%unset pager + +[diff] +unified = 2 +''' +sio.write(rc) +sio.reset() + +r1 = config.fileconfig('hgrc', sio) +dumpconfig(r1) + +# complex example + +root = config.mergedconfig('root', [m1, f1, r1]) +dumpconfig(root) + diff --git a/tests/test-config-immutable.py.out b/tests/test-config-immutable.py.out new file mode 100644 --- /dev/null +++ b/tests/test-config-immutable.py.out @@ -0,0 +1,119 @@ +c1 (atomicconfig) + diff.git=1 # rc3:2 + pager.pager=None # rc3:1 + paths.default=none # rc2:1 + ui.editor=vim # rc1:2 + ui.traceback=1 # rc1:3 + +c2 (atomicconfig) + pager.pager=more # rc5:1 + paths.remote=ssh://foo/bar # rc6:1 + smtp.tls=True # rc5:2 + ui.traceback=None # rc4:1 + ui.editor=emacs # rc4:2 + +c1 + c2 (mergedconfig) + diff.git=1 # rc3:2 + pager.pager=more # rc5:1 + paths.default=none # rc2:1 + paths.remote=ssh://foo/bar # rc6:1 + smtp.tls=True # rc5:2 + ui.traceback=None # rc4:1 + ui.editor=emacs # rc4:2 + c1 (atomicconfig) + diff.git=1 # rc3:2 + pager.pager=None # rc3:1 + paths.default=none # rc2:1 + ui.editor=vim # rc1:2 + ui.traceback=1 # rc1:3 + c2 (atomicconfig) + pager.pager=more # rc5:1 + paths.remote=ssh://foo/bar # rc6:1 + smtp.tls=True # rc5:2 + ui.traceback=None # rc4:1 + ui.editor=emacs # rc4:2 + +c2 + c1 (mergedconfig) + diff.git=1 # rc3:2 + pager.pager=None # rc3:1 + paths.remote=ssh://foo/bar # rc6:1 + paths.default=none # rc2:1 + smtp.tls=True # rc5:2 + ui.editor=vim # rc1:2 + ui.traceback=1 # rc1:3 + c2 (atomicconfig) + pager.pager=more # rc5:1 + paths.remote=ssh://foo/bar # rc6:1 + smtp.tls=True # rc5:2 + ui.traceback=None # rc4:1 + ui.editor=emacs # rc4:2 + c1 (atomicconfig) + diff.git=1 # rc3:2 + pager.pager=None # rc3:1 + paths.default=none # rc2:1 + ui.editor=vim # rc1:2 + ui.traceback=1 # rc1:3 + +f1 (filteredconfig) + pager.pager=more # rc5:1 + paths.remote=http://foo/bar # rc6:1 + smtp.tls=False # f + c2 (atomicconfig) + pager.pager=more # rc5:1 + paths.remote=ssh://foo/bar # rc6:1 + smtp.tls=True # rc5:2 + ui.traceback=None # rc4:1 + ui.editor=emacs # rc4:2 + +hgrc (fileconfig) + diff.unified=2 # hgrc:10 + pager.pager=None # hgrc:(unset) + ui.editor=nano # hgrc:3 + ui.debug=1 # hgrc:4 + +root (mergedconfig) + diff.git=1 # rc3:2 + diff.unified=2 # hgrc:10 + pager.pager=None # hgrc:(unset) + paths.default=none # rc2:1 + paths.remote=http://foo/bar # rc6:1 + smtp.tls=False # f + ui.traceback=None # rc4:1 + ui.editor=nano # hgrc:3 + ui.debug=1 # hgrc:4 + c1 + c2 (mergedconfig) + diff.git=1 # rc3:2 + pager.pager=more # rc5:1 + paths.default=none # rc2:1 + paths.remote=ssh://foo/bar # rc6:1 + smtp.tls=True # rc5:2 + ui.traceback=None # rc4:1 + ui.editor=emacs # rc4:2 + c1 (atomicconfig) + diff.git=1 # rc3:2 + pager.pager=None # rc3:1 + paths.default=none # rc2:1 + ui.editor=vim # rc1:2 + ui.traceback=1 # rc1:3 + c2 (atomicconfig) + pager.pager=more # rc5:1 + paths.remote=ssh://foo/bar # rc6:1 + smtp.tls=True # rc5:2 + ui.traceback=None # rc4:1 + ui.editor=emacs # rc4:2 + f1 (filteredconfig) + pager.pager=more # rc5:1 + paths.remote=http://foo/bar # rc6:1 + smtp.tls=False # f + c2 (atomicconfig) + pager.pager=more # rc5:1 + paths.remote=ssh://foo/bar # rc6:1 + smtp.tls=True # rc5:2 + ui.traceback=None # rc4:1 + ui.editor=emacs # rc4:2 + hgrc (fileconfig) + diff.unified=2 # hgrc:10 + pager.pager=None # hgrc:(unset) + ui.editor=nano # hgrc:3 + ui.debug=1 # hgrc:4 +