Rev 5499: (vila) Add ``bzr config` command. (Vincent Ladeuil) in file:///home/pqm/archives/thelove/bzr/%2Btrunk/
Canonical.com Patch Queue Manager
pqm at pqm.ubuntu.com
Fri Oct 15 11:14:55 BST 2010
At file:///home/pqm/archives/thelove/bzr/%2Btrunk/
------------------------------------------------------------
revno: 5499 [merge]
revision-id: pqm at pqm.ubuntu.com-20101015101453-ran88oqq3a5qb7jw
parent: pqm at pqm.ubuntu.com-20101015073659-hes51hpjncxrezuq
parent: v.ladeuil+lp at free.fr-20101015093433-3rtdmpq8pdkc7reg
committer: Canonical.com Patch Queue Manager <pqm at pqm.ubuntu.com>
branch nick: +trunk
timestamp: Fri 2010-10-15 11:14:53 +0100
message:
(vila) Add ``bzr config` command. (Vincent Ladeuil)
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
=== modified file 'bzrlib/builtins.py'
--- a/bzrlib/builtins.py 2010-10-12 09:46:37 +0000
+++ b/bzrlib/builtins.py 2010-10-13 14:06:32 +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-07-28 07:05:19 +0000
+++ b/bzrlib/tests/blackbox/__init__.py 2010-09-30 17:15:15 +0000
@@ -54,6 +54,7 @@
'bzrlib.tests.blackbox.test_clean_tree',
'bzrlib.tests.blackbox.test_command_encoding',
'bzrlib.tests.blackbox.test_commit',
+ 'bzrlib.tests.blackbox.test_config',
'bzrlib.tests.blackbox.test_conflicts',
'bzrlib.tests.blackbox.test_debug',
'bzrlib.tests.blackbox.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 07:36:59 +0000
+++ b/doc/en/release-notes/bzr-2.3.txt 2010-10-15 09:34:33 +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:04:50 +0000
+++ b/doc/en/whats-new/whats-new-in-2.3.txt 2010-10-15 07:35:21 +0000
@@ -97,6 +97,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