Rev 5461: Merge bzr.dev in file:///home/vila/src/bzr/experimental/test-script/

Vincent Ladeuil v.ladeuil+lp at free.fr
Fri Oct 15 12:30:55 BST 2010


At file:///home/vila/src/bzr/experimental/test-script/

------------------------------------------------------------
revno: 5461 [merge]
revision-id: v.ladeuil+lp at free.fr-20101015113054-a3nd1xnb3ro8c44c
parent: v.ladeuil+lp at free.fr-20101015094131-rwm3f0a10wwomaj2
parent: pqm at pqm.ubuntu.com-20101015101453-ran88oqq3a5qb7jw
committer: Vincent Ladeuil <v.ladeuil+lp at free.fr>
branch nick: test-script
timestamp: Fri 2010-10-15 13:30:54 +0200
message:
  Merge bzr.dev
added:
  bzrlib/tests/blackbox/test_config.py test_config.py-20100927150753-x6rf54uibd08r636-1
modified:
  bzrlib/builtins.py             builtins.py-20050830033751-fc01482b9ca23183
  bzrlib/config.py               config.py-20051011043216-070c74f4e9e338e8
  bzrlib/errors.py               errors.py-20050309040759-20512168c4e14fbd
  bzrlib/tests/blackbox/__init__.py __init__.py-20051128053524-eba30d8255e08dc3
  bzrlib/tests/test_config.py    testconfig.py-20051011041908-742d0c15d8d8c8eb
  doc/en/release-notes/bzr-2.3.txt NEWS-20050323055033-4e00b5db738777ff
  doc/en/user-guide/configuring_bazaar.txt configuring_bazaar.t-20071128000722-ncxiua259xwbdbg7-1
  doc/en/whats-new/whats-new-in-2.3.txt whatsnewin2.3.txt-20100818072501-x2h25r7jbnknvy30-1
-------------- next part --------------
=== modified file 'bzrlib/builtins.py'
--- a/bzrlib/builtins.py	2010-10-13 07:55:13 +0000
+++ b/bzrlib/builtins.py	2010-10-15 11:30:54 +0000
@@ -6070,6 +6070,7 @@
     # be only called once.
     for (name, aliases, module_name) in [
         ('cmd_bundle_info', [], 'bzrlib.bundle.commands'),
+        ('cmd_config', [], 'bzrlib.config'),
         ('cmd_dpush', [], 'bzrlib.foreign'),
         ('cmd_version_info', [], 'bzrlib.cmd_version_info'),
         ('cmd_resolve', ['resolved'], 'bzrlib.conflicts'),

=== modified file 'bzrlib/config.py'
--- a/bzrlib/config.py	2010-10-13 15:28:45 +0000
+++ b/bzrlib/config.py	2010-10-15 09:34:33 +0000
@@ -65,17 +65,19 @@
 import os
 import sys
 
+from bzrlib import commands
 from bzrlib.decorators import needs_write_lock
 from bzrlib.lazy_import import lazy_import
 lazy_import(globals(), """
 import errno
-from fnmatch import fnmatch
+import fnmatch
 import re
 from cStringIO import StringIO
 
 import bzrlib
 from bzrlib import (
     atomicfile,
+    bzrdir,
     debug,
     errors,
     lockdir,
@@ -153,6 +155,10 @@
     def __init__(self):
         super(Config, self).__init__()
 
+    def config_id(self):
+        """Returns a unique ID for the config."""
+        raise NotImplementedError(self.config_id)
+
     def get_editor(self):
         """Get the users pop up editor."""
         raise NotImplementedError
@@ -444,6 +450,54 @@
         """Override this to define the section used by the config."""
         return "DEFAULT"
 
+    def _get_sections(self, name=None):
+        """Returns an iterator of the sections specified by ``name``.
+
+        :param name: The section name. If None is supplied, the default
+            configurations are yielded.
+
+        :return: A tuple (name, section, config_id) for all sections that will
+            be walked by user_get_option() in the 'right' order. The first one
+            is where set_user_option() will update the value.
+        """
+        parser = self._get_parser()
+        if name is not None:
+            yield (name, parser[name], self.config_id())
+        else:
+            # No section name has been given so we fallback to the configobj
+            # itself which holds the variables defined outside of any section.
+            yield (None, parser, self.config_id())
+
+    def _get_options(self, sections=None):
+        """Return an ordered list of (name, value, section, config_id) tuples.
+
+        All options are returned with their associated value and the section
+        they appeared in. ``config_id`` is a unique identifier for the
+        configuration file the option is defined in.
+
+        :param sections: Default to ``_get_matching_sections`` if not
+            specified. This gives a better control to daughter classes about
+            which sections should be searched. This is a list of (name,
+            configobj) tuples.
+        """
+        opts = []
+        if sections is None:
+            parser = self._get_parser()
+            sections = []
+            for (section_name, _) in self._get_matching_sections():
+                try:
+                    section = parser[section_name]
+                except KeyError:
+                    # This could happen for an empty file for which we define a
+                    # DEFAULT section. FIXME: Force callers to provide sections
+                    # instead ? -- vila 20100930
+                    continue
+                sections.append((section_name, section))
+        config_id = self.config_id()
+        for (section_name, section) in sections:
+            for (name, value) in section.iteritems():
+                yield (name, value, section_name, config_id)
+
     def _get_option_policy(self, section, option_name):
         """Return the policy for the given (section, option_name) pair."""
         return POLICY_NONE
@@ -536,6 +590,26 @@
     def _get_nickname(self):
         return self.get_user_option('nickname')
 
+    def remove_user_option(self, option_name, section_name=None):
+        """Remove a user option and save the configuration file.
+
+        :param option_name: The option to be removed.
+
+        :param section_name: The section the option is defined in, default to
+            the default section.
+        """
+        self.reload()
+        parser = self._get_parser()
+        if section_name is None:
+            section = parser
+        else:
+            section = parser[section_name]
+        try:
+            del section[option_name]
+        except KeyError:
+            raise errors.NoSuchConfigOption(option_name)
+        self._write_config_file()
+
     def _write_config_file(self):
         if self.file_name is None:
             raise AssertionError('We cannot save, self.file_name is None')
@@ -604,6 +678,11 @@
     def break_lock(self):
         self._lock.break_lock()
 
+    @needs_write_lock
+    def remove_user_option(self, option_name, section_name=None):
+        super(LockableConfig, self).remove_user_option(option_name,
+                                                       section_name)
+
     def _write_config_file(self):
         if self._lock is None or not self._lock.is_held:
             # NB: if the following exception is raised it probably means a
@@ -618,6 +697,9 @@
     def __init__(self):
         super(GlobalConfig, self).__init__(file_name=config_filename())
 
+    def config_id(self):
+        return 'bazaar'
+
     @classmethod
     def from_string(cls, str_or_unicode, save=False):
         """Create a config object from a string.
@@ -667,6 +749,30 @@
         self._write_config_file()
 
 
+    def _get_sections(self, name=None):
+        """See IniBasedConfig._get_sections()."""
+        parser = self._get_parser()
+        # We don't give access to options defined outside of any section, we
+        # used the DEFAULT section by... default.
+        if name in (None, 'DEFAULT'):
+            # This could happen for an empty file where the DEFAULT section
+            # doesn't exist yet. So we force DEFAULT when yielding
+            name = 'DEFAULT'
+            if 'DEFAULT' not in parser:
+               parser['DEFAULT']= {}
+        yield (name, parser[name], self.config_id())
+
+    @needs_write_lock
+    def remove_user_option(self, option_name, section_name=None):
+        if section_name is None:
+            # We need to force the default section.
+            section_name = 'DEFAULT'
+        # We need to avoid the LockableConfig implementation or we'll lock
+        # twice
+        super(LockableConfig, self).remove_user_option(option_name,
+                                                       section_name)
+
+
 class LocationConfig(LockableConfig):
     """A configuration object that gives the policy for a location."""
 
@@ -680,6 +786,9 @@
             location = urlutils.local_path_from_url(location)
         self.location = location
 
+    def config_id(self):
+        return 'locations'
+
     @classmethod
     def from_string(cls, str_or_unicode, location, save=False):
         """Create a config object from a string.
@@ -717,7 +826,7 @@
             names = zip(location_names, section_names)
             matched = True
             for name in names:
-                if not fnmatch(name[0], name[1]):
+                if not fnmatch.fnmatch(name[0], name[1]):
                     matched = False
                     break
             if not matched:
@@ -728,6 +837,7 @@
                 continue
             matches.append((len(section_names), section,
                             '/'.join(location_names[len(section_names):])))
+        # put the longest (aka more specific) locations first
         matches.sort(reverse=True)
         sections = []
         for (length, section, extra_path) in matches:
@@ -740,6 +850,14 @@
                 pass
         return sections
 
+    def _get_sections(self, name=None):
+        """See IniBasedConfig._get_sections()."""
+        # We ignore the name here as the only sections handled are named with
+        # the location path and we don't expose embedded sections either.
+        parser = self._get_parser()
+        for name, extra_path in self._get_matching_sections():
+            yield (name, parser[name], self.config_id())
+
     def _get_option_policy(self, section, option_name):
         """Return the policy for the given (section, option_name) pair."""
         # check for the old 'recurse=False' flag
@@ -824,9 +942,13 @@
                                self._get_branch_data_config,
                                self._get_global_config)
 
+    def config_id(self):
+        return 'branch'
+
     def _get_branch_data_config(self):
         if self._branch_data_config is None:
             self._branch_data_config = TreeConfig(self.branch)
+            self._branch_data_config.config_id = self.config_id
         return self._branch_data_config
 
     def _get_location_config(self):
@@ -900,6 +1022,31 @@
                 return value
         return None
 
+    def _get_sections(self, name=None):
+        """See IniBasedConfig.get_sections()."""
+        for source in self.option_sources:
+            for section in source()._get_sections(name):
+                yield section
+
+    def _get_options(self, sections=None):
+        opts = []
+        # First the locations options
+        for option in self._get_location_config()._get_options():
+            yield option
+        # Then the branch options
+        branch_config = self._get_branch_data_config()
+        if sections is None:
+            sections = [('DEFAULT', branch_config._get_parser())]
+        # FIXME: We shouldn't have to duplicate the code in IniBasedConfig but
+        # Config itself has no notion of sections :( -- vila 20101001
+        config_id = self.config_id()
+        for (section_name, section) in sections:
+            for (name, value) in section.iteritems():
+                yield (name, value, section_name, config_id)
+        # Then the global options
+        for option in self._get_global_config()._get_options():
+            yield option
+
     def set_user_option(self, name, value, store=STORE_BRANCH,
         warn_masked=False):
         if store == STORE_BRANCH:
@@ -923,6 +1070,9 @@
                         trace.warning('Value "%s" is masked by "%s" from'
                                       ' branch.conf', value, mask_value)
 
+    def remove_user_option(self, option_name, section_name=None):
+        self._get_branch_data_config().remove_option(option_name, section_name)
+
     def _gpg_signing_command(self):
         """See Config.gpg_signing_command."""
         return self._get_safe_value('_gpg_signing_command')
@@ -1086,12 +1236,23 @@
 
     def set_option(self, value, name, section=None):
         """Set a per-branch configuration option"""
+        # FIXME: We shouldn't need to lock explicitly here but rather rely on
+        # higher levels providing the right lock -- vila 20101004
         self.branch.lock_write()
         try:
             self._config.set_option(value, name, section)
         finally:
             self.branch.unlock()
 
+    def remove_option(self, option_name, section_name=None):
+        # FIXME: We shouldn't need to lock explicitly here but rather rely on
+        # higher levels providing the right lock -- vila 20101004
+        self.branch.lock_write()
+        try:
+            self._config.remove_option(option_name, section_name)
+        finally:
+            self.branch.unlock()
+
 
 class AuthenticationConfig(object):
     """The authentication configuration file based on a ini file.
@@ -1540,8 +1701,8 @@
     """A Config that reads/writes a config file on a Transport.
 
     It is a low-level object that considers config data to be name/value pairs
-    that may be associated with a section.  Assigning meaning to the these
-    values is done at higher levels like TreeConfig.
+    that may be associated with a section.  Assigning meaning to these values
+    is done at higher levels like TreeConfig.
     """
 
     def __init__(self, transport, filename):
@@ -1580,6 +1741,14 @@
             configobj.setdefault(section, {})[name] = value
         self._set_configobj(configobj)
 
+    def remove_option(self, option_name, section_name=None):
+        configobj = self._get_configobj()
+        if section_name is None:
+            del configobj[option_name]
+        else:
+            del configobj[section_name][option_name]
+        self._set_configobj(configobj)
+
     def _get_config_file(self):
         try:
             return StringIO(self._transport.get_bytes(self._filename))
@@ -1598,3 +1767,113 @@
         configobj.write(out_file)
         out_file.seek(0)
         self._transport.put_file(self._filename, out_file)
+
+
+class cmd_config(commands.Command):
+    __doc__ = """Display, set or remove a configuration option.
+
+    Display the MATCHING configuration options mentioning their scope (the
+    configuration file they are defined in). The active value that bzr will
+    take into account is the first one displayed.
+
+    Setting a value is achieved by using name=value without spaces. The value
+    is set in the most relevant scope and can be checked by displaying the
+    option again.
+    """
+
+    aliases = ['conf']
+    takes_args = ['matching?']
+
+    takes_options = [
+        'directory',
+        # FIXME: This should be a registry option so that plugins can register
+        # their own config files (or not) -- vila 20101002
+        commands.Option('scope', help='Reduce the scope to the specified'
+                        ' configuration file',
+                        type=unicode),
+        commands.Option('remove', help='Remove the option from'
+                        ' the configuration file'),
+        ]
+
+    @commands.display_command
+    def run(self, matching=None, directory=None, scope=None, remove=False):
+        if directory is None:
+            directory = '.'
+        directory = urlutils.normalize_url(directory)
+        if matching is None:
+            self._show_config('*', directory)
+        else:
+            if remove:
+                self._remove_config_option(matching, directory, scope)
+            else:
+                pos = matching.find('=')
+                if pos == -1:
+                    self._show_config(matching, directory)
+                else:
+                    self._set_config_option(matching[:pos], matching[pos+1:],
+                                            directory, scope)
+
+    def _get_configs(self, directory, scope=None):
+        """Iterate the configurations specified by ``directory`` and ``scope``.
+
+        :param directory: Where the configurations are derived from.
+
+        :param scope: A specific config to start from.
+        """
+        if scope is not None:
+            if scope == 'bazaar':
+                yield GlobalConfig()
+            elif scope == 'locations':
+                yield LocationConfig(directory)
+            elif scope == 'branch':
+                (_, br, _) = bzrdir.BzrDir.open_containing_tree_or_branch(
+                    directory)
+                yield br.get_config()
+        else:
+            try:
+                (_, br, _) = bzrdir.BzrDir.open_containing_tree_or_branch(
+                    directory)
+                yield br.get_config()
+            except errors.NotBranchError:
+                yield LocationConfig(directory)
+                yield GlobalConfig()
+
+    def _show_config(self, matching, directory):
+        # Turn the glob into a regexp
+        matching_re = re.compile(fnmatch.translate(matching))
+        cur_conf_id = None
+        for c in self._get_configs(directory):
+            for (name, value, section, conf_id) in c._get_options():
+                if matching_re.search(name):
+                    if cur_conf_id != conf_id:
+                        self.outf.write('%s:\n' % (conf_id,))
+                        cur_conf_id = conf_id
+                    self.outf.write('  %s = %s\n' % (name, value))
+
+    def _set_config_option(self, name, value, directory, scope):
+        for conf in self._get_configs(directory, scope):
+            conf.set_user_option(name, value)
+            break
+        else:
+            raise errors.NoSuchConfig(scope)
+
+    def _remove_config_option(self, name, directory, scope):
+        removed = False
+        for conf in self._get_configs(directory, scope):
+            for (section_name, section, conf_id) in conf._get_sections():
+                if scope is not None and conf_id != scope:
+                    # Not the right configuration file
+                    continue
+                if name in section:
+                    if conf_id != conf.config_id():
+                        conf = self._get_configs(directory, conf_id).next()
+                    # We use the first section in the first config where the
+                    # option is defined to remove it
+                    conf.remove_user_option(name, section_name)
+                    removed = True
+                    break
+            break
+        else:
+            raise errors.NoSuchConfig(scope)
+        if not removed:
+            raise errors.NoSuchConfigOption(name)

=== modified file 'bzrlib/errors.py'
--- a/bzrlib/errors.py	2010-09-28 18:51:47 +0000
+++ b/bzrlib/errors.py	2010-10-13 08:01:36 +0000
@@ -2945,6 +2945,22 @@
         self.user_encoding = osutils.get_user_encoding()
 
 
+class NoSuchConfig(BzrError):
+
+    _fmt = ('The "%(config_id)s" configuration does not exist.')
+
+    def __init__(self, config_id):
+        BzrError.__init__(self, config_id=config_id)
+
+
+class NoSuchConfigOption(BzrError):
+
+    _fmt = ('The "%(option_name)s" configuration option does not exist.')
+
+    def __init__(self, option_name):
+        BzrError.__init__(self, option_name=option_name)
+
+
 class NoSuchAlias(BzrError):
 
     _fmt = ('The alias "%(alias_name)s" does not exist.')

=== modified file 'bzrlib/tests/blackbox/__init__.py'
--- a/bzrlib/tests/blackbox/__init__.py	2010-10-01 13:45:42 +0000
+++ b/bzrlib/tests/blackbox/__init__.py	2010-10-15 11:30:54 +0000
@@ -55,6 +55,7 @@
                      'test_clean_tree',
                      'test_command_encoding',
                      'test_commit',
+                     'test_config',
                      'test_conflicts',
                      'test_debug',
                      'test_deleted',

=== added file 'bzrlib/tests/blackbox/test_config.py'
--- a/bzrlib/tests/blackbox/test_config.py	1970-01-01 00:00:00 +0000
+++ b/bzrlib/tests/blackbox/test_config.py	2010-10-15 07:41:12 +0000
@@ -0,0 +1,204 @@
+# Copyright (C) 2010 Canonical Ltd
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+
+"""Black-box tests for bzr config."""
+
+import os
+
+from bzrlib import (
+    config,
+    errors,
+    tests,
+    )
+from bzrlib.tests import (
+    script,
+    test_config as _t_config,
+    )
+
+class TestWithoutConfig(tests.TestCaseWithTransport):
+
+    def test_no_config(self):
+        out, err = self.run_bzr(['config'])
+        self.assertEquals('', out)
+        self.assertEquals('', err)
+
+    def test_all_variables_no_config(self):
+        out, err = self.run_bzr(['config', '*'])
+        self.assertEquals('', out)
+        self.assertEquals('', err)
+
+    def test_unknown_option(self):
+        self.run_bzr_error(['The "file" configuration option does not exist',],
+                           ['config', '--remove', 'file'])
+
+class TestConfigDisplay(tests.TestCaseWithTransport):
+
+    def setUp(self):
+        super(TestConfigDisplay, self).setUp()
+        _t_config.create_configs(self)
+
+    def test_bazaar_config(self):
+        self.bazaar_config.set_user_option('hello', 'world')
+        script.run_script(self, '''\
+            $ bzr config -d tree
+            bazaar:
+              hello = world
+            ''')
+
+    def test_locations_config_for_branch(self):
+        self.locations_config.set_user_option('hello', 'world')
+        self.branch_config.set_user_option('hello', 'you')
+        script.run_script(self, '''\
+            $ bzr config -d tree
+            locations:
+              hello = world
+            branch:
+              hello = you
+            ''')
+
+    def test_locations_config_outside_branch(self):
+        self.bazaar_config.set_user_option('hello', 'world')
+        self.locations_config.set_user_option('hello', 'world')
+        script.run_script(self, '''\
+            $ bzr config
+            bazaar:
+              hello = world
+            ''')
+
+
+class TestConfigSetOption(tests.TestCaseWithTransport):
+
+    def setUp(self):
+        super(TestConfigSetOption, self).setUp()
+        _t_config.create_configs(self)
+
+    def test_unknown_config(self):
+        self.run_bzr_error(['The "moon" configuration does not exist'],
+                           ['config', '--scope', 'moon', 'hello=world'])
+
+    def test_bazaar_config_outside_branch(self):
+        script.run_script(self, '''\
+            $ bzr config --scope bazaar hello=world
+            $ bzr config -d tree hello
+            bazaar:
+              hello = world
+            ''')
+
+    def test_bazaar_config_inside_branch(self):
+        script.run_script(self, '''\
+            $ bzr config -d tree --scope bazaar hello=world
+            $ bzr config -d tree hello
+            bazaar:
+              hello = world
+            ''')
+
+    def test_locations_config_inside_branch(self):
+        script.run_script(self, '''\
+            $ bzr config -d tree --scope locations hello=world
+            $ bzr config -d tree hello
+            locations:
+              hello = world
+            ''')
+
+    def test_branch_config_default(self):
+        script.run_script(self, '''\
+            $ bzr config -d tree hello=world
+            $ bzr config -d tree hello
+            branch:
+              hello = world
+            ''')
+
+    def test_branch_config_forcing_branch(self):
+        script.run_script(self, '''\
+            $ bzr config -d tree --scope branch hello=world
+            $ bzr config -d tree hello
+            branch:
+              hello = world
+            ''')
+
+
+class TestConfigRemoveOption(tests.TestCaseWithTransport):
+
+    def setUp(self):
+        super(TestConfigRemoveOption, self).setUp()
+        _t_config.create_configs_with_file_option(self)
+
+    def test_unknown_config(self):
+        self.run_bzr_error(['The "moon" configuration does not exist'],
+                           ['config', '--scope', 'moon', '--remove', 'file'])
+
+    def test_bazaar_config_outside_branch(self):
+        script.run_script(self, '''\
+            $ bzr config --scope bazaar --remove file
+            $ bzr config -d tree file
+            locations:
+              file = locations
+            branch:
+              file = branch
+            ''')
+
+    def test_bazaar_config_inside_branch(self):
+        script.run_script(self, '''\
+            $ bzr config -d tree --scope bazaar --remove file
+            $ bzr config -d tree file
+            locations:
+              file = locations
+            branch:
+              file = branch
+            ''')
+
+    def test_locations_config_inside_branch(self):
+        script.run_script(self, '''\
+            $ bzr config -d tree --scope locations --remove file
+            $ bzr config -d tree file
+            branch:
+              file = branch
+            bazaar:
+              file = bazaar
+            ''')
+
+    def test_branch_config_default(self):
+        script.run_script(self, '''\
+            $ bzr config -d tree --remove file
+            $ bzr config -d tree file
+            branch:
+              file = branch
+            bazaar:
+              file = bazaar
+            ''')
+        script.run_script(self, '''\
+            $ bzr config -d tree --remove file
+            $ bzr config -d tree file
+            bazaar:
+              file = bazaar
+            ''')
+
+    def test_branch_config_forcing_branch(self):
+        script.run_script(self, '''\
+            $ bzr config -d tree --scope branch --remove file
+            $ bzr config -d tree file
+            locations:
+              file = locations
+            bazaar:
+              file = bazaar
+            ''')
+        script.run_script(self, '''\
+            $ bzr config -d tree --remove file
+            $ bzr config -d tree file
+            bazaar:
+              file = bazaar
+            ''')

=== modified file 'bzrlib/tests/test_config.py'
--- a/bzrlib/tests/test_config.py	2010-09-28 16:28:45 +0000
+++ b/bzrlib/tests/test_config.py	2010-10-13 08:01:36 +0000
@@ -1471,6 +1471,200 @@
         self.assertIs(None, bzrdir_config.get_default_stack_on())
 
 
+def create_configs(test):
+    """Create configuration files for a given test.
+
+    This requires creating a tree (and populate the ``test.tree`` attribute and
+    its associated branch and will populate the following attributes:
+
+    - branch_config: A BranchConfig for the associated branch.
+
+    - locations_config : A LocationConfig for the associated branch
+
+    - bazaar_config: A GlobalConfig.
+
+    The tree and branch are created in a 'tree' subdirectory so the tests can
+    still use the test directory to stay outside of the branch.
+    """
+    tree = test.make_branch_and_tree('tree')
+    test.tree = tree
+    test.branch_config = config.BranchConfig(tree.branch)
+    test.locations_config = config.LocationConfig(tree.basedir)
+    test.bazaar_config = config.GlobalConfig()
+
+
+def create_configs_with_file_option(test):
+    """Create configuration files with a ``file`` option set in each.
+
+    This builds on ``create_configs`` and add one ``file`` option in each
+    configuration with a value which allows identifying the configuration file.
+    """
+    create_configs(test)
+    test.bazaar_config.set_user_option('file', 'bazaar')
+    test.locations_config.set_user_option('file', 'locations')
+    test.branch_config.set_user_option('file', 'branch')
+
+
+class TestConfigGetOptions(tests.TestCaseWithTransport):
+
+    def setUp(self):
+        super(TestConfigGetOptions, self).setUp()
+        create_configs(self)
+
+    def assertOptions(self, expected, conf):
+        actual = list(conf._get_options())
+        self.assertEqual(expected, actual)
+
+    # One variable in none of the above
+    def test_no_variable(self):
+        # Using branch should query branch, locations and bazaar
+        self.assertOptions([], self.branch_config)
+
+    def test_option_in_bazaar(self):
+        self.bazaar_config.set_user_option('file', 'bazaar')
+        self.assertOptions([('file', 'bazaar', 'DEFAULT', 'bazaar')],
+                           self.bazaar_config)
+
+    def test_option_in_locations(self):
+        self.locations_config.set_user_option('file', 'locations')
+        self.assertOptions(
+            [('file', 'locations', self.tree.basedir, 'locations')],
+            self.locations_config)
+
+    def test_option_in_branch(self):
+        self.branch_config.set_user_option('file', 'branch')
+        self.assertOptions([('file', 'branch', 'DEFAULT', 'branch')],
+                           self.branch_config)
+
+    def test_option_in_bazaar_and_branch(self):
+        self.bazaar_config.set_user_option('file', 'bazaar')
+        self.branch_config.set_user_option('file', 'branch')
+        self.assertOptions([('file', 'branch', 'DEFAULT', 'branch'),
+                            ('file', 'bazaar', 'DEFAULT', 'bazaar'),],
+                           self.branch_config)
+
+    def test_option_in_branch_and_locations(self):
+        # Hmm, locations override branch :-/
+        self.locations_config.set_user_option('file', 'locations')
+        self.branch_config.set_user_option('file', 'branch')
+        self.assertOptions(
+            [('file', 'locations', self.tree.basedir, 'locations'),
+             ('file', 'branch', 'DEFAULT', 'branch'),],
+            self.branch_config)
+
+    def test_option_in_bazaar_locations_and_branch(self):
+        self.bazaar_config.set_user_option('file', 'bazaar')
+        self.locations_config.set_user_option('file', 'locations')
+        self.branch_config.set_user_option('file', 'branch')
+        self.assertOptions(
+            [('file', 'locations', self.tree.basedir, 'locations'),
+             ('file', 'branch', 'DEFAULT', 'branch'),
+             ('file', 'bazaar', 'DEFAULT', 'bazaar'),],
+            self.branch_config)
+
+
+class TestConfigRemoveOption(tests.TestCaseWithTransport):
+
+    def setUp(self):
+        super(TestConfigRemoveOption, self).setUp()
+        create_configs_with_file_option(self)
+
+    def assertOptions(self, expected, conf):
+        actual = list(conf._get_options())
+        self.assertEqual(expected, actual)
+
+    def test_remove_in_locations(self):
+        self.locations_config.remove_user_option('file', self.tree.basedir)
+        self.assertOptions(
+            [('file', 'branch', 'DEFAULT', 'branch'),
+             ('file', 'bazaar', 'DEFAULT', 'bazaar'),],
+            self.branch_config)
+
+    def test_remove_in_branch(self):
+        self.branch_config.remove_user_option('file')
+        self.assertOptions(
+            [('file', 'locations', self.tree.basedir, 'locations'),
+             ('file', 'bazaar', 'DEFAULT', 'bazaar'),],
+            self.branch_config)
+
+    def test_remove_in_bazaar(self):
+        self.bazaar_config.remove_user_option('file')
+        self.assertOptions(
+            [('file', 'locations', self.tree.basedir, 'locations'),
+             ('file', 'branch', 'DEFAULT', 'branch'),],
+            self.branch_config)
+
+
+class TestConfigGetSections(tests.TestCaseWithTransport):
+
+    def setUp(self):
+        super(TestConfigGetSections, self).setUp()
+        create_configs(self)
+
+    def assertSectionNames(self, expected, conf, name=None):
+        """Check which sections are returned for a given config.
+
+        If fallback configurations exist their sections can be included.
+
+        :param expected: A list of section names.
+
+        :param conf: The configuration that will be queried.
+
+        :param name: An optional section name that will be passed to
+            get_sections().
+        """
+        sections = list(conf._get_sections(name))
+        self.assertLength(len(expected), sections)
+        self.assertEqual(expected, [name for name, _, _ in sections])
+
+    def test_bazaar_default_section(self):
+        self.assertSectionNames(['DEFAULT'], self.bazaar_config)
+
+    def test_locations_default_section(self):
+        # No sections are defined in an empty file
+        self.assertSectionNames([], self.locations_config)
+
+    def test_locations_named_section(self):
+        self.locations_config.set_user_option('file', 'locations')
+        self.assertSectionNames([self.tree.basedir], self.locations_config)
+
+    def test_locations_matching_sections(self):
+        loc_config = self.locations_config
+        loc_config.set_user_option('file', 'locations')
+        # We need to cheat a bit here to create an option in sections above and
+        # below the 'location' one.
+        parser = loc_config._get_parser()
+        # locations.cong deals with '/' ignoring native os.sep
+        location_names = self.tree.basedir.split('/')
+        parent = '/'.join(location_names[:-1])
+        child = '/'.join(location_names + ['child'])
+        parser[parent] = {}
+        parser[parent]['file'] = 'parent'
+        parser[child] = {}
+        parser[child]['file'] = 'child'
+        self.assertSectionNames([self.tree.basedir, parent], loc_config)
+
+    def test_branch_data_default_section(self):
+        self.assertSectionNames([None],
+                                self.branch_config._get_branch_data_config())
+
+    def test_branch_default_sections(self):
+        # No sections are defined in an empty locations file
+        self.assertSectionNames([None, 'DEFAULT'],
+                                self.branch_config)
+        # Unless we define an option
+        self.branch_config._get_location_config().set_user_option(
+            'file', 'locations')
+        self.assertSectionNames([self.tree.basedir, None, 'DEFAULT'],
+                                self.branch_config)
+
+    def test_bazaar_named_section(self):
+        # We need to cheat as the API doesn't give direct access to sections
+        # other than DEFAULT.
+        self.bazaar_config.set_alias('bazaar', 'bzr')
+        self.assertSectionNames(['ALIASES'], self.bazaar_config, 'ALIASES')
+
+
 class TestAuthenticationConfigFile(tests.TestCase):
     """Test the authentication.conf file matching"""
 

=== modified file 'doc/en/release-notes/bzr-2.3.txt'
--- a/doc/en/release-notes/bzr-2.3.txt	2010-10-15 09:41:31 +0000
+++ b/doc/en/release-notes/bzr-2.3.txt	2010-10-15 11:30:54 +0000
@@ -22,6 +22,11 @@
   new or mirrored branch without working trees.
   (Matthew Gordon, #506730)
 
+* ``bzr config`` is a new command that displays the configuration options for
+  a given directory. It accepts a glob to match against multiple options at
+  once. It can also be used to set or delete a configuration option in any
+  configuration file. (Vincent Ladeuil)
+
 * New shortcut url schemes ``ubuntu:`` and ``debianlp:`` access source
   branches on Launchpad.  E.g. ``bzr branch ubuntu:foo`` gives you the source
   branch for project ``foo`` in the current distroseries for Ubuntu while

=== modified file 'doc/en/user-guide/configuring_bazaar.txt'
--- a/doc/en/user-guide/configuring_bazaar.txt	2010-10-08 10:50:51 +0000
+++ b/doc/en/user-guide/configuring_bazaar.txt	2010-10-13 13:59:44 +0000
@@ -70,6 +70,59 @@
 in the Bazaar User Reference.
 
 
+Looking at the active configuration
+-----------------------------------
+
+To look at all the currently defined options, you can use the following
+command::
+
+  bzr config
+
+``bzr`` implements some rules to decide where to get the value of a
+configuration option.
+
+The current policy is to examine the existing configurations files in a
+given order for matching definitions.
+
+  * ``locations.conf`` is searched first for a section whose name matches the
+    location considered (working tree, branch or remote branch),
+
+  * the current ``branch.conf`` is searched next,
+
+  * ``bazaar.conf`` is searched next,
+
+  * finally, some options can have default values generally defined in the
+    code itself and not displayed by ``bzr config`` (see `Configuration
+    Settings <../user-reference/index.html#configuration-settings>`_).
+
+This is better understood by using ```bzr config`` with no arguments, which
+will display some output of the form::
+
+  locations:
+    post_commit_to = commits at example.com
+    news_merge_files = NEWS
+  branch:
+    parent_location = bzr+ssh://bazaar.launchpad.net/+branch/bzr/
+    nickname = config-modify
+    push_location = bzr+ssh://bazaar.launchpad.net/~vila/bzr/config-modify/
+  bazaar:
+    debug_flags = hpss,
+
+Each configuration file is associated with a given scope whose name is
+displayed before each set of defined options.
+
+Modifying the active configuration
+----------------------------------
+
+To set an option to a given value use::
+
+  bzr config opt=value
+
+To remove an option use::
+
+  bzr config --remove opt
+
+
 Rule-based preferences
 ----------------------
 

=== modified file 'doc/en/whats-new/whats-new-in-2.3.txt'
--- a/doc/en/whats-new/whats-new-in-2.3.txt	2010-10-13 07:55:13 +0000
+++ b/doc/en/whats-new/whats-new-in-2.3.txt	2010-10-15 11:30:54 +0000
@@ -105,6 +105,14 @@
   format, used by emacs and the standalone ``info`` reader.
   (Vincent Ladeuil, #219334)
 
+Configuration
+*************
+
+``bzr`` can be configure via environment variables, command-line options
+and configurations files. We've started working on unifying this and give
+access to more options. The first step is a new ``bzr config`` command that
+can be used to display the active configuration options in the current
+working tree or branch as well as the ability to set or remove an option.
 
 Expected releases for the 2.3 series
 ************************************



More information about the bazaar-commits mailing list