Merge lp:~vila/bzr/config-modify into lp:bzr

Proposed by Vincent Ladeuil
Status: Merged
Approved by: Vincent Ladeuil
Approved revision: no longer in the source branch.
Merged at revision: 5499
Proposed branch: lp:~vila/bzr/config-modify
Merge into: lp:bzr
Diff against target: 996 lines (+765/-4)
9 files modified
bzrlib/builtins.py (+1/-0)
bzrlib/config.py (+283/-4)
bzrlib/errors.py (+16/-0)
bzrlib/tests/blackbox/__init__.py (+1/-0)
bzrlib/tests/blackbox/test_config.py (+204/-0)
bzrlib/tests/test_config.py (+194/-0)
doc/en/release-notes/bzr-2.3.txt (+5/-0)
doc/en/user-guide/configuring_bazaar.txt (+53/-0)
doc/en/whats-new/whats-new-in-2.3.txt (+8/-0)
To merge this branch: bzr merge lp:~vila/bzr/config-modify
Reviewer Review Type Date Requested Status
Martin Pool Needs Fixing
Review via email: mp+37513@code.launchpad.net

Commit message

Add ``bzr config` command.

Description of the change

This proposal implements a new ``bzr config`` new command that can:

 * displays the configuration options for a given directory. It
   accepts a glob to match against multiple options at once,

 * set a new value for a given option in any configuration file,

 * remove an option from any configuration file.

There are some awkward hacks mostly caused by the BranchConfig
implementation that doesn't help sharing code and other
limitations of our current section handling in the configuration
files.

Branchconfig
============

BranchConfig is actually implemented by delegating the config
variables handling to TreeConfig (badly named as noted in a
comment) and the file containing the text to TransportConfig.

As such it implements a minimal API to get or set a config
variable.

Surprisingly it also implements its own locking mechanism. This
doesn't seem necessary since the config is always accessed via
the branch itself which is already locked.

Numerous related problems here, but luckily I didn't have to
support RemoteBranchConfig...

Section handling
================

For historical reasons, each of bazaar.conf, locations.conf and
branch.conf use sections in a different way:

- bazaar.conf: Defining options outside of any config is
  supported by the underlying configobj but hard to do while
  using the official API. Instead, all options should be in a
  given section and for bzr itself, we use DEFAULTS and
  ALIASES. Plugins can (and some do, at least lp:bookmarks)
  define and use their own sections.

- locations.conf: Each location defines its own section. This
  conflicts with section use in bazaar.conf since you can't
  define aliases or bookmarks by location.

- branch.conf: No sections are used here because BranchConfig
  (and its composites) doesn't provide access to them. The
  bookmarks plugin works around this limitation by prefixing the
  bookmarks with 'bookmarks_' (hint, hint, we can avoid using
  sections in bazaar.conf by using a proper name space for
  options).

Option removal
==============

In the actual implementation, only aliases can be removed with
unset_alias().

With the proposed implementation all options can be removed
(well, except the ones that are not in DEFAULT section in
bazaar.conf for reasons explained above).

Referring to config files
=========================

Using absolute paths when referring to config files is user
hostile, so I went with introducing an id for each config file
(anticipating the implementation of a registry for them).

While it may seem acceptable to display and require the user to
use ${HOME}/.bazaar/bazaar.conf and
${HOME}/.bazaar/locations.conf, xxxxxx/.bzr/branch/branch.conf
reveal an implementation detail and that's bad :)

So I used 'bazaar', 'locations' and 'branch' instead, clean and
short. We may have to provide something else for bound branches
though... But one can argue that ``bzr unbind`` will allow
playing with both configuration files already.

Exceptions
==========

I didn't declare the new exceptions in config.py because it would
have meant error.py couldn't be lazy imported any more... I can't
see how to work around that which isn't great since we want to
declare exceptions closer to where they are needed.

Command naming
==============

I chose 'config' over 'configuration' as the later is already an
help topic. Cheap shot :)

Tests
=====

Not tests were harmed during this proposal development.

35 tests were added and became part of the 197 tests running in
<2s and ensuring against regression with:

  ./bzr selftest -s bb.test_config -s bt.test_config

Running a full test suite before submitting didn't reveal any
fallout.

Summary
=======

Yeah, I know, that's pretty long for a cover letter, yet, there
is more to say about configs and I will start a thread on the
mailing list on future plans.

This proposal is one step to help us better figure out how we use
our configuration files today without changing any existing
implementation (have a look at 'bzr config -d lp:bzr', yeah, no
harm done, but still...) and make it easier to play with various
configuration schemes with a simple way to look at the results.

To post a comment you must log in.
Revision history for this message
Martin Pool (mbp) wrote :
Download full text (4.2 KiB)

Epic cover letter. What a shame it's not updating the user documentation. :-) Perhaps you could paste and update some of it into there: we need the documentation, and checking how it's explained to the user is a good way to find out if the ui is actually sane.

This should also be mentioned in whatsnew.

And, I don't want to hijack your mp, but perhaps some of the other text could go into a developer document giving an overview of configurations?

> Section handling

That seems like a good description of the situation. I think locations.conf is on the right track by reserving the section headers to match _which_ configuration applies, not as part of the settings.

> I chose 'config' over 'configuration' as the later is already an help topic. Cheap shot :)

Probably the topic needs to be updated then.

> + ('cmd_config', ['conf'], 'bzrlib.config'),

I don't think people will type this often enough that saving two characters is worth it.

-from fnmatch import fnmatch
+import fnmatch

Not this again :-)

+ def id(self):
+ return 'bazaar'

'id' is of course a Python builtin. It's legal and not shadowing anything to put it as an instance attribute but I wonder if it's a good idea; maybe config_id would be better.

+ commands.Option('force', help='Force the configuration file',
+ short_name='f', type=unicode),

Force it to do what? :)

If it's going to be "config --force ~/.bazaar/bazaar.conf" that's a bit strange and inconsistent with other uses of --force, which take no argument. Maybe just --file?

OK, I see later on this is actually passing a kind of configuration scope.

+ if matching is None:
+ matching = '*'
+ self._show_config('*', directory)

Why assign to 'matching' in here?

+ else:
+ if remove:
+ self._remove_config_option(matching, directory, force)
+ else:
+ pos = matching.find('=')
+ if pos == -1:
+ self._show_config(matching, directory)
+ else:
+ self._set_config_option(matching[:pos], matching[pos+1:],
+ directory, force)
+

I would unfold the 'remove' to one if/elif/else.

This seems like a very good case for wanting both a cli command and another layer that's just below the cli but not specific to that, that can be reused by other front ends or people scripting bzrlib in Python. I'd like to have a standard pattern for this. This code is actually fairly close, and seems to indicate perhaps we should keep that on Command objects and just make sure they're easy to use other than through run_bzr.

+ 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
+''')
+

Now that scripts automatically strip consistent indentation, I think we should normally keep them indented inside the block, so that they don't mess up the Python structure.

I wonder if the output from config when showing more ...

Read more...

review: Needs Fixing
Revision history for this message
Vincent Ladeuil (vila) wrote :
Download full text (6.8 KiB)

> Epic cover letter. What a shame it's not updating the user documentation. :-)
> Perhaps you could paste and update some of it into there: we need the
> documentation, and checking how it's explained to the user is a good way to
> find out if the ui is actually sane.

Well, basically I'm raising problems I want to address but since I don't have answers so far, I can't (yet) update the user documentation.

>
> This should also be mentioned in whatsnew.

Right, I can do that.

>
> And, I don't want to hijack your mp, but perhaps some of the other text could
> go into a developer document giving an overview of configurations?

Please, let's not hijack it indeed, nothing has changed in this regard, the problems are still here and we don't provide (yet) a clearly defined set of features.

>
> > Section handling
>
> That seems like a good description of the situation. I think locations.conf
> is on the right track by reserving the section headers to match _which_
> configuration applies, not as part of the settings.

Right, and from there, do you agree that sections should not be used *at all* in the other configuration files.

I realise we will need to address the ALIASES and BOOKMARKS cases, but we must first agree on that.

>
>
> > I chose 'config' over 'configuration' as the later is already an help topic.
> Cheap shot :)
>
> Probably the topic needs to be updated then.
>
> > + ('cmd_config', ['conf'], 'bzrlib.config'),
>
> I don't think people will type this often enough that saving two characters is
> worth it.
>
> -from fnmatch import fnmatch
> +import fnmatch
>
> Not this again :-)

That's a different case, the former makes it *impossible* to use fnmatch.translate !

>
> + def id(self):
> + return 'bazaar'
>
> 'id' is of course a Python builtin. It's legal and not shadowing anything to
> put it as an instance attribute but I wonder if it's a good idea; maybe
> config_id would be better.
>

I can do that, it will probably go away in the end and be controlled via the registry (when it's implemented).

> + commands.Option('force', help='Force the configuration file',
> + short_name='f', type=unicode),
>
> Force it to do what? :)
>
> If it's going to be "config --force ~/.bazaar/bazaar.conf" that's a bit
> strange and inconsistent with other uses of --force, which take no argument.
> Maybe just --file?
>
> OK, I see later on this is actually passing a kind of configuration scope.
>
> + if matching is None:
> + matching = '*'
> + self._show_config('*', directory)
>
> Why assign to 'matching' in here?

Left over, good catch.

>
> + else:
> + if remove:
> + self._remove_config_option(matching, directory, force)
> + else:
> + pos = matching.find('=')
> + if pos == -1:
> + self._show_config(matching, directory)
> + else:
> + self._set_config_option(matching[:pos], matching[pos+1:],
> + directory, force)
> +
>
> I would unfold the 'remove' to one if/elif/else.

And unconditionally do ...

Read more...

Revision history for this message
Martin Pool (mbp) wrote :
Download full text (4.3 KiB)

On 12 October 2010 19:06, Vincent Ladeuil <email address hidden> wrote:
>> Epic cover letter.  What a shame it's not updating the user documentation. :-)
>> Perhaps you could paste and update some of it into there: we need the
>> documentation, and checking how it's explained to the user is a good way to
>> find out if the ui is actually sane.
>
> Well, basically I'm raising problems I want to address but since I don't have answers so far, I can't (yet) update the user documentation.

I think it should at least address the new command, even if the
description is imperfect or if some uncertainty remains. For example
the user guide ought to mention the existence of this command.

>
>>
>> > Section handling
>>
>> That seems like a good description of the situation.  I think locations.conf
>> is on the right track by reserving the section headers to match _which_
>> configuration applies, not as part of the settings.
>
> Right, and from there, do you agree that sections should not be used *at all* in the other configuration files.
>
> I realise we will need to address the ALIASES and BOOKMARKS cases, but we must first agree on that.

I almost agree. I think they should only be used in ways that are
parallel to locations. For instance I think the authentication.conf
thing is basically ok, because parallel to locations.conf the sections
are used to find which settings are relevant.

>> +    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
>> +''')
>> +
>>
>> Now that scripts automatically strip consistent indentation, I think we should
>> normally keep them indented inside the block, so that they don't mess up the
>> Python structure.
>
> Well, I'm never comfortable with inlined files that differ from what they would be on disk. I forgot the '\' at the end of the first line for that to be true though.
>
> Should we bikeshed on that or is it ok to have different povs there ?

Maybe we can bikeshed a bit :-). I don't see these as inlined files.
I know you just added test-script but I don't think they normally are
files on disk; they're primarily bits of a Python program. In all the
editors we use it's easy to indent/dedent things.

>> I wonder if the output from config when showing more than one variable should
>> be something that looks like a config file?
>
> This output is a *valid* content for a config file.
>
> At least for the actual tests ;)
>
> Things may become more complicated if we want to ensure that tough:
> - comments are not displayed,
> - strings are not quoted,
> - multi-lines strings are not even considered.
>
> And why would you use such output to put into a config file ?

I wouldn't necessarily send it into a file, I just don't think it
should be gratuituously different.

So is

  locations:
     file = locations

a valid configobject file? It's not the form we tend to use, which
has [] sections.

>> istm that if you ask just for the value of a variable, you should by default
>> just get that one variable, a...

Read more...

Revision history for this message
Vincent Ladeuil (vila) wrote :
Download full text (4.1 KiB)

> On 12 October 2010 19:06, Vincent Ladeuil <email address hidden> wrote:
> >> Epic cover letter.  What a shame it's not updating the user documentation.
> :-)
> >> Perhaps you could paste and update some of it into there: we need the
> >> documentation, and checking how it's explained to the user is a good way to
> >> find out if the ui is actually sane.
> >
> > Well, basically I'm raising problems I want to address but since I don't
> have answers so far, I can't (yet) update the user documentation.
>
> I think it should at least address the new command, even if the
> description is imperfect or if some uncertainty remains. For example
> the user guide ought to mention the existence of this command.

Ok.

> > Right, and from there, do you agree that sections should not be used *at
> all* in the other configuration files.
> >
> > I realise we will need to address the ALIASES and BOOKMARKS cases, but we
> must first agree on that.
>
> I almost agree. I think they should only be used in ways that are
> parallel to locations. For instance I think the authentication.conf
> thing is basically ok, because parallel to locations.conf the sections
> are used to find which settings are relevant.

Exactly, I didn't mention authentication.conf yet, but it re-implements too much things already in IniConfig and I did that at the time because I felt it wasn't fitting well there.

> Maybe we can bikeshed a bit :-). I don't see these as inlined files.
> I know you just added test-script but I don't think they normally are
> files on disk; they're primarily bits of a Python program.

Hmm, I'd argue that their syntax has nothing to do with python.

> In all the
> editors we use it's easy to indent/dedent things.

But even in python the indentation is meaningful (I agree that in this case the initial indentations are meaningless but we are still using dedent as a syntactic sugar and mostly because we can and because we apply a python rule).

>
> >> I wonder if the output from config when showing more than one variable
> should
> >> be something that looks like a config file?
> >
> > This output is a *valid* content for a config file.
> >
> > At least for the actual tests ;)
> >
> > Things may become more complicated if we want to ensure that tough:
> > - comments are not displayed,
> > - strings are not quoted,
> > - multi-lines strings are not even considered.
> >
> > And why would you use such output to put into a config file ?
>
> I wouldn't necessarily send it into a file, I just don't think it
> should be gratuituously different.
>
> So is
>
> locations:
> file = locations
>
> a valid configobject file? It's not the form we tend to use, which
> has [] sections.

Rhaaa, of course not, I was referring to ' file = locations' not the config id line.

>
> >> istm that if you ask just for the value of a variable, you should by
> default
> >> just get that one variable, and only with say -v be told where it comes
> from.
> >
> > Well, that would quickly be confusing since you want to see the multiple
> definitions as soon as there is more than one without having to specify -v,
> remember that you can use 'bzr config' without specifying any v...

Read more...

Revision history for this message
Martin Pool (mbp) wrote :
Download full text (4.9 KiB)

> > On 12 October 2010 19:06, Vincent Ladeuil <email address hidden> wrote:
> > >> Epic cover letter.  What a shame it's not updating the user
> documentation.
> > :-)
> > >> Perhaps you could paste and update some of it into there: we need the
> > >> documentation, and checking how it's explained to the user is a good way
> to
> > >> find out if the ui is actually sane.
> > >
> > > Well, basically I'm raising problems I want to address but since I don't
> > have answers so far, I can't (yet) update the user documentation.
> >
> > I think it should at least address the new command, even if the
> > description is imperfect or if some uncertainty remains. For example
> > the user guide ought to mention the existence of this command.
>
> Ok.

That looks nice, and it makes this easier to review.

My main question from what you have documented there is, when you set a value, in what scope is it set?

(It would be really cool, later, to actually do scriptrunner tests within the user guide; somehow we would need to make sure the setup/check code was not too intrusive to people reading the document.)

> > Maybe we can bikeshed a bit :-). I don't see these as inlined files.
> > I know you just added test-script but I don't think they normally are
> > files on disk; they're primarily bits of a Python program.
>
> Hmm, I'd argue that their syntax has nothing to do with python.

That's true the test itself is not Python syntax. However, it is embedded within a Python file, and the rule there is that things are indented by scope. Even though technically the indent whitespace appears within the triplequoted string, the effect we want to achieve is that the whole thing is shifted right.

docstrings or (in other programs) inline sql are not Python either but they're normally indented; <https://dev.launchpad.net/PythonStyleGuide#Multi-line%20SQL> gives a weak precedent.

>
> > In all the
> > editors we use it's easy to indent/dedent things.
>
> But even in python the indentation is meaningful (I agree that in this case
> the initial indentations are meaningless but we are still using dedent as a
> syntactic sugar and mostly because we can and because we apply a python rule).

The main reason I want them indented is so that scanning over the file, I can easily see the classes and the test modules within them. Having internals of the tests flush left messes that up.

> > So is
> >
> > locations:
> > file = locations
> >
> > a valid configobject file? It's not the form we tend to use, which
> > has [] sections.
>
> Rhaaa, of course not, I was referring to ' file = locations' not the config
> id line.

I think the output needs to make some things more clear to the user:

1- wtf is 'locations'? :-)
2- which value is actually active? which of these is most important?
3- which location section is matching that this matters?

We could perhaps make that more obvious with

# per-location-config from /home/mbp/.bazaar/locations.conf

(I'm open to tweaking this after landing)

> > >> istm that if you ask just for the value of a variable, you should by
> > default
> > >> just get that one variable, and only with say -v be told where it comes
> > from...

Read more...

review: Needs Fixing
Revision history for this message
Vincent Ladeuil (vila) wrote :

sent to pqm by email

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'bzrlib/builtins.py'
2--- bzrlib/builtins.py 2010-10-12 09:46:37 +0000
3+++ bzrlib/builtins.py 2010-10-15 09:37:41 +0000
4@@ -6070,6 +6070,7 @@
5 # be only called once.
6 for (name, aliases, module_name) in [
7 ('cmd_bundle_info', [], 'bzrlib.bundle.commands'),
8+ ('cmd_config', [], 'bzrlib.config'),
9 ('cmd_dpush', [], 'bzrlib.foreign'),
10 ('cmd_version_info', [], 'bzrlib.cmd_version_info'),
11 ('cmd_resolve', ['resolved'], 'bzrlib.conflicts'),
12
13=== modified file 'bzrlib/config.py'
14--- bzrlib/config.py 2010-10-13 15:28:45 +0000
15+++ bzrlib/config.py 2010-10-15 09:37:41 +0000
16@@ -65,17 +65,19 @@
17 import os
18 import sys
19
20+from bzrlib import commands
21 from bzrlib.decorators import needs_write_lock
22 from bzrlib.lazy_import import lazy_import
23 lazy_import(globals(), """
24 import errno
25-from fnmatch import fnmatch
26+import fnmatch
27 import re
28 from cStringIO import StringIO
29
30 import bzrlib
31 from bzrlib import (
32 atomicfile,
33+ bzrdir,
34 debug,
35 errors,
36 lockdir,
37@@ -153,6 +155,10 @@
38 def __init__(self):
39 super(Config, self).__init__()
40
41+ def config_id(self):
42+ """Returns a unique ID for the config."""
43+ raise NotImplementedError(self.config_id)
44+
45 def get_editor(self):
46 """Get the users pop up editor."""
47 raise NotImplementedError
48@@ -444,6 +450,54 @@
49 """Override this to define the section used by the config."""
50 return "DEFAULT"
51
52+ def _get_sections(self, name=None):
53+ """Returns an iterator of the sections specified by ``name``.
54+
55+ :param name: The section name. If None is supplied, the default
56+ configurations are yielded.
57+
58+ :return: A tuple (name, section, config_id) for all sections that will
59+ be walked by user_get_option() in the 'right' order. The first one
60+ is where set_user_option() will update the value.
61+ """
62+ parser = self._get_parser()
63+ if name is not None:
64+ yield (name, parser[name], self.config_id())
65+ else:
66+ # No section name has been given so we fallback to the configobj
67+ # itself which holds the variables defined outside of any section.
68+ yield (None, parser, self.config_id())
69+
70+ def _get_options(self, sections=None):
71+ """Return an ordered list of (name, value, section, config_id) tuples.
72+
73+ All options are returned with their associated value and the section
74+ they appeared in. ``config_id`` is a unique identifier for the
75+ configuration file the option is defined in.
76+
77+ :param sections: Default to ``_get_matching_sections`` if not
78+ specified. This gives a better control to daughter classes about
79+ which sections should be searched. This is a list of (name,
80+ configobj) tuples.
81+ """
82+ opts = []
83+ if sections is None:
84+ parser = self._get_parser()
85+ sections = []
86+ for (section_name, _) in self._get_matching_sections():
87+ try:
88+ section = parser[section_name]
89+ except KeyError:
90+ # This could happen for an empty file for which we define a
91+ # DEFAULT section. FIXME: Force callers to provide sections
92+ # instead ? -- vila 20100930
93+ continue
94+ sections.append((section_name, section))
95+ config_id = self.config_id()
96+ for (section_name, section) in sections:
97+ for (name, value) in section.iteritems():
98+ yield (name, value, section_name, config_id)
99+
100 def _get_option_policy(self, section, option_name):
101 """Return the policy for the given (section, option_name) pair."""
102 return POLICY_NONE
103@@ -536,6 +590,26 @@
104 def _get_nickname(self):
105 return self.get_user_option('nickname')
106
107+ def remove_user_option(self, option_name, section_name=None):
108+ """Remove a user option and save the configuration file.
109+
110+ :param option_name: The option to be removed.
111+
112+ :param section_name: The section the option is defined in, default to
113+ the default section.
114+ """
115+ self.reload()
116+ parser = self._get_parser()
117+ if section_name is None:
118+ section = parser
119+ else:
120+ section = parser[section_name]
121+ try:
122+ del section[option_name]
123+ except KeyError:
124+ raise errors.NoSuchConfigOption(option_name)
125+ self._write_config_file()
126+
127 def _write_config_file(self):
128 if self.file_name is None:
129 raise AssertionError('We cannot save, self.file_name is None')
130@@ -604,6 +678,11 @@
131 def break_lock(self):
132 self._lock.break_lock()
133
134+ @needs_write_lock
135+ def remove_user_option(self, option_name, section_name=None):
136+ super(LockableConfig, self).remove_user_option(option_name,
137+ section_name)
138+
139 def _write_config_file(self):
140 if self._lock is None or not self._lock.is_held:
141 # NB: if the following exception is raised it probably means a
142@@ -618,6 +697,9 @@
143 def __init__(self):
144 super(GlobalConfig, self).__init__(file_name=config_filename())
145
146+ def config_id(self):
147+ return 'bazaar'
148+
149 @classmethod
150 def from_string(cls, str_or_unicode, save=False):
151 """Create a config object from a string.
152@@ -667,6 +749,30 @@
153 self._write_config_file()
154
155
156+ def _get_sections(self, name=None):
157+ """See IniBasedConfig._get_sections()."""
158+ parser = self._get_parser()
159+ # We don't give access to options defined outside of any section, we
160+ # used the DEFAULT section by... default.
161+ if name in (None, 'DEFAULT'):
162+ # This could happen for an empty file where the DEFAULT section
163+ # doesn't exist yet. So we force DEFAULT when yielding
164+ name = 'DEFAULT'
165+ if 'DEFAULT' not in parser:
166+ parser['DEFAULT']= {}
167+ yield (name, parser[name], self.config_id())
168+
169+ @needs_write_lock
170+ def remove_user_option(self, option_name, section_name=None):
171+ if section_name is None:
172+ # We need to force the default section.
173+ section_name = 'DEFAULT'
174+ # We need to avoid the LockableConfig implementation or we'll lock
175+ # twice
176+ super(LockableConfig, self).remove_user_option(option_name,
177+ section_name)
178+
179+
180 class LocationConfig(LockableConfig):
181 """A configuration object that gives the policy for a location."""
182
183@@ -680,6 +786,9 @@
184 location = urlutils.local_path_from_url(location)
185 self.location = location
186
187+ def config_id(self):
188+ return 'locations'
189+
190 @classmethod
191 def from_string(cls, str_or_unicode, location, save=False):
192 """Create a config object from a string.
193@@ -717,7 +826,7 @@
194 names = zip(location_names, section_names)
195 matched = True
196 for name in names:
197- if not fnmatch(name[0], name[1]):
198+ if not fnmatch.fnmatch(name[0], name[1]):
199 matched = False
200 break
201 if not matched:
202@@ -728,6 +837,7 @@
203 continue
204 matches.append((len(section_names), section,
205 '/'.join(location_names[len(section_names):])))
206+ # put the longest (aka more specific) locations first
207 matches.sort(reverse=True)
208 sections = []
209 for (length, section, extra_path) in matches:
210@@ -740,6 +850,14 @@
211 pass
212 return sections
213
214+ def _get_sections(self, name=None):
215+ """See IniBasedConfig._get_sections()."""
216+ # We ignore the name here as the only sections handled are named with
217+ # the location path and we don't expose embedded sections either.
218+ parser = self._get_parser()
219+ for name, extra_path in self._get_matching_sections():
220+ yield (name, parser[name], self.config_id())
221+
222 def _get_option_policy(self, section, option_name):
223 """Return the policy for the given (section, option_name) pair."""
224 # check for the old 'recurse=False' flag
225@@ -824,9 +942,13 @@
226 self._get_branch_data_config,
227 self._get_global_config)
228
229+ def config_id(self):
230+ return 'branch'
231+
232 def _get_branch_data_config(self):
233 if self._branch_data_config is None:
234 self._branch_data_config = TreeConfig(self.branch)
235+ self._branch_data_config.config_id = self.config_id
236 return self._branch_data_config
237
238 def _get_location_config(self):
239@@ -900,6 +1022,31 @@
240 return value
241 return None
242
243+ def _get_sections(self, name=None):
244+ """See IniBasedConfig.get_sections()."""
245+ for source in self.option_sources:
246+ for section in source()._get_sections(name):
247+ yield section
248+
249+ def _get_options(self, sections=None):
250+ opts = []
251+ # First the locations options
252+ for option in self._get_location_config()._get_options():
253+ yield option
254+ # Then the branch options
255+ branch_config = self._get_branch_data_config()
256+ if sections is None:
257+ sections = [('DEFAULT', branch_config._get_parser())]
258+ # FIXME: We shouldn't have to duplicate the code in IniBasedConfig but
259+ # Config itself has no notion of sections :( -- vila 20101001
260+ config_id = self.config_id()
261+ for (section_name, section) in sections:
262+ for (name, value) in section.iteritems():
263+ yield (name, value, section_name, config_id)
264+ # Then the global options
265+ for option in self._get_global_config()._get_options():
266+ yield option
267+
268 def set_user_option(self, name, value, store=STORE_BRANCH,
269 warn_masked=False):
270 if store == STORE_BRANCH:
271@@ -923,6 +1070,9 @@
272 trace.warning('Value "%s" is masked by "%s" from'
273 ' branch.conf', value, mask_value)
274
275+ def remove_user_option(self, option_name, section_name=None):
276+ self._get_branch_data_config().remove_option(option_name, section_name)
277+
278 def _gpg_signing_command(self):
279 """See Config.gpg_signing_command."""
280 return self._get_safe_value('_gpg_signing_command')
281@@ -1086,12 +1236,23 @@
282
283 def set_option(self, value, name, section=None):
284 """Set a per-branch configuration option"""
285+ # FIXME: We shouldn't need to lock explicitly here but rather rely on
286+ # higher levels providing the right lock -- vila 20101004
287 self.branch.lock_write()
288 try:
289 self._config.set_option(value, name, section)
290 finally:
291 self.branch.unlock()
292
293+ def remove_option(self, option_name, section_name=None):
294+ # FIXME: We shouldn't need to lock explicitly here but rather rely on
295+ # higher levels providing the right lock -- vila 20101004
296+ self.branch.lock_write()
297+ try:
298+ self._config.remove_option(option_name, section_name)
299+ finally:
300+ self.branch.unlock()
301+
302
303 class AuthenticationConfig(object):
304 """The authentication configuration file based on a ini file.
305@@ -1540,8 +1701,8 @@
306 """A Config that reads/writes a config file on a Transport.
307
308 It is a low-level object that considers config data to be name/value pairs
309- that may be associated with a section. Assigning meaning to the these
310- values is done at higher levels like TreeConfig.
311+ that may be associated with a section. Assigning meaning to these values
312+ is done at higher levels like TreeConfig.
313 """
314
315 def __init__(self, transport, filename):
316@@ -1580,6 +1741,14 @@
317 configobj.setdefault(section, {})[name] = value
318 self._set_configobj(configobj)
319
320+ def remove_option(self, option_name, section_name=None):
321+ configobj = self._get_configobj()
322+ if section_name is None:
323+ del configobj[option_name]
324+ else:
325+ del configobj[section_name][option_name]
326+ self._set_configobj(configobj)
327+
328 def _get_config_file(self):
329 try:
330 return StringIO(self._transport.get_bytes(self._filename))
331@@ -1598,3 +1767,113 @@
332 configobj.write(out_file)
333 out_file.seek(0)
334 self._transport.put_file(self._filename, out_file)
335+
336+
337+class cmd_config(commands.Command):
338+ __doc__ = """Display, set or remove a configuration option.
339+
340+ Display the MATCHING configuration options mentioning their scope (the
341+ configuration file they are defined in). The active value that bzr will
342+ take into account is the first one displayed.
343+
344+ Setting a value is achieved by using name=value without spaces. The value
345+ is set in the most relevant scope and can be checked by displaying the
346+ option again.
347+ """
348+
349+ aliases = ['conf']
350+ takes_args = ['matching?']
351+
352+ takes_options = [
353+ 'directory',
354+ # FIXME: This should be a registry option so that plugins can register
355+ # their own config files (or not) -- vila 20101002
356+ commands.Option('scope', help='Reduce the scope to the specified'
357+ ' configuration file',
358+ type=unicode),
359+ commands.Option('remove', help='Remove the option from'
360+ ' the configuration file'),
361+ ]
362+
363+ @commands.display_command
364+ def run(self, matching=None, directory=None, scope=None, remove=False):
365+ if directory is None:
366+ directory = '.'
367+ directory = urlutils.normalize_url(directory)
368+ if matching is None:
369+ self._show_config('*', directory)
370+ else:
371+ if remove:
372+ self._remove_config_option(matching, directory, scope)
373+ else:
374+ pos = matching.find('=')
375+ if pos == -1:
376+ self._show_config(matching, directory)
377+ else:
378+ self._set_config_option(matching[:pos], matching[pos+1:],
379+ directory, scope)
380+
381+ def _get_configs(self, directory, scope=None):
382+ """Iterate the configurations specified by ``directory`` and ``scope``.
383+
384+ :param directory: Where the configurations are derived from.
385+
386+ :param scope: A specific config to start from.
387+ """
388+ if scope is not None:
389+ if scope == 'bazaar':
390+ yield GlobalConfig()
391+ elif scope == 'locations':
392+ yield LocationConfig(directory)
393+ elif scope == 'branch':
394+ (_, br, _) = bzrdir.BzrDir.open_containing_tree_or_branch(
395+ directory)
396+ yield br.get_config()
397+ else:
398+ try:
399+ (_, br, _) = bzrdir.BzrDir.open_containing_tree_or_branch(
400+ directory)
401+ yield br.get_config()
402+ except errors.NotBranchError:
403+ yield LocationConfig(directory)
404+ yield GlobalConfig()
405+
406+ def _show_config(self, matching, directory):
407+ # Turn the glob into a regexp
408+ matching_re = re.compile(fnmatch.translate(matching))
409+ cur_conf_id = None
410+ for c in self._get_configs(directory):
411+ for (name, value, section, conf_id) in c._get_options():
412+ if matching_re.search(name):
413+ if cur_conf_id != conf_id:
414+ self.outf.write('%s:\n' % (conf_id,))
415+ cur_conf_id = conf_id
416+ self.outf.write(' %s = %s\n' % (name, value))
417+
418+ def _set_config_option(self, name, value, directory, scope):
419+ for conf in self._get_configs(directory, scope):
420+ conf.set_user_option(name, value)
421+ break
422+ else:
423+ raise errors.NoSuchConfig(scope)
424+
425+ def _remove_config_option(self, name, directory, scope):
426+ removed = False
427+ for conf in self._get_configs(directory, scope):
428+ for (section_name, section, conf_id) in conf._get_sections():
429+ if scope is not None and conf_id != scope:
430+ # Not the right configuration file
431+ continue
432+ if name in section:
433+ if conf_id != conf.config_id():
434+ conf = self._get_configs(directory, conf_id).next()
435+ # We use the first section in the first config where the
436+ # option is defined to remove it
437+ conf.remove_user_option(name, section_name)
438+ removed = True
439+ break
440+ break
441+ else:
442+ raise errors.NoSuchConfig(scope)
443+ if not removed:
444+ raise errors.NoSuchConfigOption(name)
445
446=== modified file 'bzrlib/errors.py'
447--- bzrlib/errors.py 2010-09-28 18:51:47 +0000
448+++ bzrlib/errors.py 2010-10-15 09:37:41 +0000
449@@ -2945,6 +2945,22 @@
450 self.user_encoding = osutils.get_user_encoding()
451
452
453+class NoSuchConfig(BzrError):
454+
455+ _fmt = ('The "%(config_id)s" configuration does not exist.')
456+
457+ def __init__(self, config_id):
458+ BzrError.__init__(self, config_id=config_id)
459+
460+
461+class NoSuchConfigOption(BzrError):
462+
463+ _fmt = ('The "%(option_name)s" configuration option does not exist.')
464+
465+ def __init__(self, option_name):
466+ BzrError.__init__(self, option_name=option_name)
467+
468+
469 class NoSuchAlias(BzrError):
470
471 _fmt = ('The alias "%(alias_name)s" does not exist.')
472
473=== modified file 'bzrlib/tests/blackbox/__init__.py'
474--- bzrlib/tests/blackbox/__init__.py 2010-07-28 07:05:19 +0000
475+++ bzrlib/tests/blackbox/__init__.py 2010-10-15 09:37:41 +0000
476@@ -54,6 +54,7 @@
477 'bzrlib.tests.blackbox.test_clean_tree',
478 'bzrlib.tests.blackbox.test_command_encoding',
479 'bzrlib.tests.blackbox.test_commit',
480+ 'bzrlib.tests.blackbox.test_config',
481 'bzrlib.tests.blackbox.test_conflicts',
482 'bzrlib.tests.blackbox.test_debug',
483 'bzrlib.tests.blackbox.test_deleted',
484
485=== added file 'bzrlib/tests/blackbox/test_config.py'
486--- bzrlib/tests/blackbox/test_config.py 1970-01-01 00:00:00 +0000
487+++ bzrlib/tests/blackbox/test_config.py 2010-10-15 09:37:41 +0000
488@@ -0,0 +1,204 @@
489+# Copyright (C) 2010 Canonical Ltd
490+#
491+# This program is free software; you can redistribute it and/or modify
492+# it under the terms of the GNU General Public License as published by
493+# the Free Software Foundation; either version 2 of the License, or
494+# (at your option) any later version.
495+#
496+# This program is distributed in the hope that it will be useful,
497+# but WITHOUT ANY WARRANTY; without even the implied warranty of
498+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
499+# GNU General Public License for more details.
500+#
501+# You should have received a copy of the GNU General Public License
502+# along with this program; if not, write to the Free Software
503+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
504+
505+
506+"""Black-box tests for bzr config."""
507+
508+import os
509+
510+from bzrlib import (
511+ config,
512+ errors,
513+ tests,
514+ )
515+from bzrlib.tests import (
516+ script,
517+ test_config as _t_config,
518+ )
519+
520+class TestWithoutConfig(tests.TestCaseWithTransport):
521+
522+ def test_no_config(self):
523+ out, err = self.run_bzr(['config'])
524+ self.assertEquals('', out)
525+ self.assertEquals('', err)
526+
527+ def test_all_variables_no_config(self):
528+ out, err = self.run_bzr(['config', '*'])
529+ self.assertEquals('', out)
530+ self.assertEquals('', err)
531+
532+ def test_unknown_option(self):
533+ self.run_bzr_error(['The "file" configuration option does not exist',],
534+ ['config', '--remove', 'file'])
535+
536+class TestConfigDisplay(tests.TestCaseWithTransport):
537+
538+ def setUp(self):
539+ super(TestConfigDisplay, self).setUp()
540+ _t_config.create_configs(self)
541+
542+ def test_bazaar_config(self):
543+ self.bazaar_config.set_user_option('hello', 'world')
544+ script.run_script(self, '''\
545+ $ bzr config -d tree
546+ bazaar:
547+ hello = world
548+ ''')
549+
550+ def test_locations_config_for_branch(self):
551+ self.locations_config.set_user_option('hello', 'world')
552+ self.branch_config.set_user_option('hello', 'you')
553+ script.run_script(self, '''\
554+ $ bzr config -d tree
555+ locations:
556+ hello = world
557+ branch:
558+ hello = you
559+ ''')
560+
561+ def test_locations_config_outside_branch(self):
562+ self.bazaar_config.set_user_option('hello', 'world')
563+ self.locations_config.set_user_option('hello', 'world')
564+ script.run_script(self, '''\
565+ $ bzr config
566+ bazaar:
567+ hello = world
568+ ''')
569+
570+
571+class TestConfigSetOption(tests.TestCaseWithTransport):
572+
573+ def setUp(self):
574+ super(TestConfigSetOption, self).setUp()
575+ _t_config.create_configs(self)
576+
577+ def test_unknown_config(self):
578+ self.run_bzr_error(['The "moon" configuration does not exist'],
579+ ['config', '--scope', 'moon', 'hello=world'])
580+
581+ def test_bazaar_config_outside_branch(self):
582+ script.run_script(self, '''\
583+ $ bzr config --scope bazaar hello=world
584+ $ bzr config -d tree hello
585+ bazaar:
586+ hello = world
587+ ''')
588+
589+ def test_bazaar_config_inside_branch(self):
590+ script.run_script(self, '''\
591+ $ bzr config -d tree --scope bazaar hello=world
592+ $ bzr config -d tree hello
593+ bazaar:
594+ hello = world
595+ ''')
596+
597+ def test_locations_config_inside_branch(self):
598+ script.run_script(self, '''\
599+ $ bzr config -d tree --scope locations hello=world
600+ $ bzr config -d tree hello
601+ locations:
602+ hello = world
603+ ''')
604+
605+ def test_branch_config_default(self):
606+ script.run_script(self, '''\
607+ $ bzr config -d tree hello=world
608+ $ bzr config -d tree hello
609+ branch:
610+ hello = world
611+ ''')
612+
613+ def test_branch_config_forcing_branch(self):
614+ script.run_script(self, '''\
615+ $ bzr config -d tree --scope branch hello=world
616+ $ bzr config -d tree hello
617+ branch:
618+ hello = world
619+ ''')
620+
621+
622+class TestConfigRemoveOption(tests.TestCaseWithTransport):
623+
624+ def setUp(self):
625+ super(TestConfigRemoveOption, self).setUp()
626+ _t_config.create_configs_with_file_option(self)
627+
628+ def test_unknown_config(self):
629+ self.run_bzr_error(['The "moon" configuration does not exist'],
630+ ['config', '--scope', 'moon', '--remove', 'file'])
631+
632+ def test_bazaar_config_outside_branch(self):
633+ script.run_script(self, '''\
634+ $ bzr config --scope bazaar --remove file
635+ $ bzr config -d tree file
636+ locations:
637+ file = locations
638+ branch:
639+ file = branch
640+ ''')
641+
642+ def test_bazaar_config_inside_branch(self):
643+ script.run_script(self, '''\
644+ $ bzr config -d tree --scope bazaar --remove file
645+ $ bzr config -d tree file
646+ locations:
647+ file = locations
648+ branch:
649+ file = branch
650+ ''')
651+
652+ def test_locations_config_inside_branch(self):
653+ script.run_script(self, '''\
654+ $ bzr config -d tree --scope locations --remove file
655+ $ bzr config -d tree file
656+ branch:
657+ file = branch
658+ bazaar:
659+ file = bazaar
660+ ''')
661+
662+ def test_branch_config_default(self):
663+ script.run_script(self, '''\
664+ $ bzr config -d tree --remove file
665+ $ bzr config -d tree file
666+ branch:
667+ file = branch
668+ bazaar:
669+ file = bazaar
670+ ''')
671+ script.run_script(self, '''\
672+ $ bzr config -d tree --remove file
673+ $ bzr config -d tree file
674+ bazaar:
675+ file = bazaar
676+ ''')
677+
678+ def test_branch_config_forcing_branch(self):
679+ script.run_script(self, '''\
680+ $ bzr config -d tree --scope branch --remove file
681+ $ bzr config -d tree file
682+ locations:
683+ file = locations
684+ bazaar:
685+ file = bazaar
686+ ''')
687+ script.run_script(self, '''\
688+ $ bzr config -d tree --remove file
689+ $ bzr config -d tree file
690+ bazaar:
691+ file = bazaar
692+ ''')
693
694=== modified file 'bzrlib/tests/test_config.py'
695--- bzrlib/tests/test_config.py 2010-09-28 16:28:45 +0000
696+++ bzrlib/tests/test_config.py 2010-10-15 09:37:41 +0000
697@@ -1471,6 +1471,200 @@
698 self.assertIs(None, bzrdir_config.get_default_stack_on())
699
700
701+def create_configs(test):
702+ """Create configuration files for a given test.
703+
704+ This requires creating a tree (and populate the ``test.tree`` attribute and
705+ its associated branch and will populate the following attributes:
706+
707+ - branch_config: A BranchConfig for the associated branch.
708+
709+ - locations_config : A LocationConfig for the associated branch
710+
711+ - bazaar_config: A GlobalConfig.
712+
713+ The tree and branch are created in a 'tree' subdirectory so the tests can
714+ still use the test directory to stay outside of the branch.
715+ """
716+ tree = test.make_branch_and_tree('tree')
717+ test.tree = tree
718+ test.branch_config = config.BranchConfig(tree.branch)
719+ test.locations_config = config.LocationConfig(tree.basedir)
720+ test.bazaar_config = config.GlobalConfig()
721+
722+
723+def create_configs_with_file_option(test):
724+ """Create configuration files with a ``file`` option set in each.
725+
726+ This builds on ``create_configs`` and add one ``file`` option in each
727+ configuration with a value which allows identifying the configuration file.
728+ """
729+ create_configs(test)
730+ test.bazaar_config.set_user_option('file', 'bazaar')
731+ test.locations_config.set_user_option('file', 'locations')
732+ test.branch_config.set_user_option('file', 'branch')
733+
734+
735+class TestConfigGetOptions(tests.TestCaseWithTransport):
736+
737+ def setUp(self):
738+ super(TestConfigGetOptions, self).setUp()
739+ create_configs(self)
740+
741+ def assertOptions(self, expected, conf):
742+ actual = list(conf._get_options())
743+ self.assertEqual(expected, actual)
744+
745+ # One variable in none of the above
746+ def test_no_variable(self):
747+ # Using branch should query branch, locations and bazaar
748+ self.assertOptions([], self.branch_config)
749+
750+ def test_option_in_bazaar(self):
751+ self.bazaar_config.set_user_option('file', 'bazaar')
752+ self.assertOptions([('file', 'bazaar', 'DEFAULT', 'bazaar')],
753+ self.bazaar_config)
754+
755+ def test_option_in_locations(self):
756+ self.locations_config.set_user_option('file', 'locations')
757+ self.assertOptions(
758+ [('file', 'locations', self.tree.basedir, 'locations')],
759+ self.locations_config)
760+
761+ def test_option_in_branch(self):
762+ self.branch_config.set_user_option('file', 'branch')
763+ self.assertOptions([('file', 'branch', 'DEFAULT', 'branch')],
764+ self.branch_config)
765+
766+ def test_option_in_bazaar_and_branch(self):
767+ self.bazaar_config.set_user_option('file', 'bazaar')
768+ self.branch_config.set_user_option('file', 'branch')
769+ self.assertOptions([('file', 'branch', 'DEFAULT', 'branch'),
770+ ('file', 'bazaar', 'DEFAULT', 'bazaar'),],
771+ self.branch_config)
772+
773+ def test_option_in_branch_and_locations(self):
774+ # Hmm, locations override branch :-/
775+ self.locations_config.set_user_option('file', 'locations')
776+ self.branch_config.set_user_option('file', 'branch')
777+ self.assertOptions(
778+ [('file', 'locations', self.tree.basedir, 'locations'),
779+ ('file', 'branch', 'DEFAULT', 'branch'),],
780+ self.branch_config)
781+
782+ def test_option_in_bazaar_locations_and_branch(self):
783+ self.bazaar_config.set_user_option('file', 'bazaar')
784+ self.locations_config.set_user_option('file', 'locations')
785+ self.branch_config.set_user_option('file', 'branch')
786+ self.assertOptions(
787+ [('file', 'locations', self.tree.basedir, 'locations'),
788+ ('file', 'branch', 'DEFAULT', 'branch'),
789+ ('file', 'bazaar', 'DEFAULT', 'bazaar'),],
790+ self.branch_config)
791+
792+
793+class TestConfigRemoveOption(tests.TestCaseWithTransport):
794+
795+ def setUp(self):
796+ super(TestConfigRemoveOption, self).setUp()
797+ create_configs_with_file_option(self)
798+
799+ def assertOptions(self, expected, conf):
800+ actual = list(conf._get_options())
801+ self.assertEqual(expected, actual)
802+
803+ def test_remove_in_locations(self):
804+ self.locations_config.remove_user_option('file', self.tree.basedir)
805+ self.assertOptions(
806+ [('file', 'branch', 'DEFAULT', 'branch'),
807+ ('file', 'bazaar', 'DEFAULT', 'bazaar'),],
808+ self.branch_config)
809+
810+ def test_remove_in_branch(self):
811+ self.branch_config.remove_user_option('file')
812+ self.assertOptions(
813+ [('file', 'locations', self.tree.basedir, 'locations'),
814+ ('file', 'bazaar', 'DEFAULT', 'bazaar'),],
815+ self.branch_config)
816+
817+ def test_remove_in_bazaar(self):
818+ self.bazaar_config.remove_user_option('file')
819+ self.assertOptions(
820+ [('file', 'locations', self.tree.basedir, 'locations'),
821+ ('file', 'branch', 'DEFAULT', 'branch'),],
822+ self.branch_config)
823+
824+
825+class TestConfigGetSections(tests.TestCaseWithTransport):
826+
827+ def setUp(self):
828+ super(TestConfigGetSections, self).setUp()
829+ create_configs(self)
830+
831+ def assertSectionNames(self, expected, conf, name=None):
832+ """Check which sections are returned for a given config.
833+
834+ If fallback configurations exist their sections can be included.
835+
836+ :param expected: A list of section names.
837+
838+ :param conf: The configuration that will be queried.
839+
840+ :param name: An optional section name that will be passed to
841+ get_sections().
842+ """
843+ sections = list(conf._get_sections(name))
844+ self.assertLength(len(expected), sections)
845+ self.assertEqual(expected, [name for name, _, _ in sections])
846+
847+ def test_bazaar_default_section(self):
848+ self.assertSectionNames(['DEFAULT'], self.bazaar_config)
849+
850+ def test_locations_default_section(self):
851+ # No sections are defined in an empty file
852+ self.assertSectionNames([], self.locations_config)
853+
854+ def test_locations_named_section(self):
855+ self.locations_config.set_user_option('file', 'locations')
856+ self.assertSectionNames([self.tree.basedir], self.locations_config)
857+
858+ def test_locations_matching_sections(self):
859+ loc_config = self.locations_config
860+ loc_config.set_user_option('file', 'locations')
861+ # We need to cheat a bit here to create an option in sections above and
862+ # below the 'location' one.
863+ parser = loc_config._get_parser()
864+ # locations.cong deals with '/' ignoring native os.sep
865+ location_names = self.tree.basedir.split('/')
866+ parent = '/'.join(location_names[:-1])
867+ child = '/'.join(location_names + ['child'])
868+ parser[parent] = {}
869+ parser[parent]['file'] = 'parent'
870+ parser[child] = {}
871+ parser[child]['file'] = 'child'
872+ self.assertSectionNames([self.tree.basedir, parent], loc_config)
873+
874+ def test_branch_data_default_section(self):
875+ self.assertSectionNames([None],
876+ self.branch_config._get_branch_data_config())
877+
878+ def test_branch_default_sections(self):
879+ # No sections are defined in an empty locations file
880+ self.assertSectionNames([None, 'DEFAULT'],
881+ self.branch_config)
882+ # Unless we define an option
883+ self.branch_config._get_location_config().set_user_option(
884+ 'file', 'locations')
885+ self.assertSectionNames([self.tree.basedir, None, 'DEFAULT'],
886+ self.branch_config)
887+
888+ def test_bazaar_named_section(self):
889+ # We need to cheat as the API doesn't give direct access to sections
890+ # other than DEFAULT.
891+ self.bazaar_config.set_alias('bazaar', 'bzr')
892+ self.assertSectionNames(['ALIASES'], self.bazaar_config, 'ALIASES')
893+
894+
895 class TestAuthenticationConfigFile(tests.TestCase):
896 """Test the authentication.conf file matching"""
897
898
899=== modified file 'doc/en/release-notes/bzr-2.3.txt'
900--- doc/en/release-notes/bzr-2.3.txt 2010-10-15 07:36:59 +0000
901+++ doc/en/release-notes/bzr-2.3.txt 2010-10-15 09:37:41 +0000
902@@ -22,6 +22,11 @@
903 new or mirrored branch without working trees.
904 (Matthew Gordon, #506730)
905
906+* ``bzr config`` is a new command that displays the configuration options for
907+ a given directory. It accepts a glob to match against multiple options at
908+ once. It can also be used to set or delete a configuration option in any
909+ configuration file. (Vincent Ladeuil)
910+
911 * New shortcut url schemes ``ubuntu:`` and ``debianlp:`` access source
912 branches on Launchpad. E.g. ``bzr branch ubuntu:foo`` gives you the source
913 branch for project ``foo`` in the current distroseries for Ubuntu while
914
915=== modified file 'doc/en/user-guide/configuring_bazaar.txt'
916--- doc/en/user-guide/configuring_bazaar.txt 2010-10-08 10:50:51 +0000
917+++ doc/en/user-guide/configuring_bazaar.txt 2010-10-15 09:37:41 +0000
918@@ -70,6 +70,59 @@
919 in the Bazaar User Reference.
920
921
922+Looking at the active configuration
923+-----------------------------------
924+
925+To look at all the currently defined options, you can use the following
926+command::
927+
928+ bzr config
929+
930+``bzr`` implements some rules to decide where to get the value of a
931+configuration option.
932+
933+The current policy is to examine the existing configurations files in a
934+given order for matching definitions.
935+
936+ * ``locations.conf`` is searched first for a section whose name matches the
937+ location considered (working tree, branch or remote branch),
938+
939+ * the current ``branch.conf`` is searched next,
940+
941+ * ``bazaar.conf`` is searched next,
942+
943+ * finally, some options can have default values generally defined in the
944+ code itself and not displayed by ``bzr config`` (see `Configuration
945+ Settings <../user-reference/index.html#configuration-settings>`_).
946+
947+This is better understood by using ```bzr config`` with no arguments, which
948+will display some output of the form::
949+
950+ locations:
951+ post_commit_to = commits@example.com
952+ news_merge_files = NEWS
953+ branch:
954+ parent_location = bzr+ssh://bazaar.launchpad.net/+branch/bzr/
955+ nickname = config-modify
956+ push_location = bzr+ssh://bazaar.launchpad.net/~vila/bzr/config-modify/
957+ bazaar:
958+ debug_flags = hpss,
959+
960+Each configuration file is associated with a given scope whose name is
961+displayed before each set of defined options.
962+
963+Modifying the active configuration
964+----------------------------------
965+
966+To set an option to a given value use::
967+
968+ bzr config opt=value
969+
970+To remove an option use::
971+
972+ bzr config --remove opt
973+
974+
975 Rule-based preferences
976 ----------------------
977
978
979=== modified file 'doc/en/whats-new/whats-new-in-2.3.txt'
980--- doc/en/whats-new/whats-new-in-2.3.txt 2010-10-13 07:04:50 +0000
981+++ doc/en/whats-new/whats-new-in-2.3.txt 2010-10-15 09:37:41 +0000
982@@ -97,6 +97,14 @@
983 format, used by emacs and the standalone ``info`` reader.
984 (Vincent Ladeuil, #219334)
985
986+Configuration
987+*************
988+
989+``bzr`` can be configure via environment variables, command-line options
990+and configurations files. We've started working on unifying this and give
991+access to more options. The first step is a new ``bzr config`` command that
992+can be used to display the active configuration options in the current
993+working tree or branch as well as the ability to set or remove an option.
994
995 Expected releases for the 2.3 series
996 ************************************