Merge lp:~vila/bzr/config-modify into lp:bzr
- config-modify
- Merge into bzr.dev
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 |
Related bugs: |
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 RemoteBranchCon
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}
${HOME}
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.
Vincent Ladeuil (vila) 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.
>
> 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.
> + short_name='f', type=unicode),
>
> Force it to do what? :)
>
> If it's going to be "config --force ~/.bazaar/
> 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_
>
> Why assign to 'matching' in here?
Left over, good catch.
>
> + else:
> + if remove:
> + self._remove_
> + else:
> + pos = matching.find('=')
> + if pos == -1:
> + self._show_
> + else:
> + self._set_
> + directory, force)
> +
>
> I would unfold the 'remove' to one if/elif/else.
And unconditionally do ...
Martin Pool (mbp) wrote : | # |
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_
>> + self.bazaar_
>> + self.locations_
>> + script.
>> +$ 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...
Vincent Ladeuil (vila) wrote : | # |
> 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...
Martin Pool (mbp) wrote : | # |
> > 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:/
>
> > 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/
(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...
Vincent Ladeuil (vila) wrote : | # |
sent to pqm by email
Preview Diff
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 | ************************************ |
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: config( '*', directory)
+ matching = '*'
+ self._show_
Why assign to 'matching' in here?
+ else: config_ option( matching, directory, force) config( matching, directory) config_ option( matching[ :pos], matching[pos+1:],
+ if remove:
+ self._remove_
+ else:
+ pos = matching.find('=')
+ if pos == -1:
+ self._show_
+ else:
+ self._set_
+ 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): config. set_user_ option( 'hello' , 'world') config. set_user_ option( 'hello' , 'world') run_script( self, '''
+ self.bazaar_
+ self.locations_
+ script.
+$ 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 ...