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
=== modified file 'bzrlib/builtins.py'
--- bzrlib/builtins.py 2010-10-12 09:46:37 +0000
+++ bzrlib/builtins.py 2010-10-15 09:37:41 +0000
@@ -6070,6 +6070,7 @@
6070 # be only called once.6070 # be only called once.
6071 for (name, aliases, module_name) in [6071 for (name, aliases, module_name) in [
6072 ('cmd_bundle_info', [], 'bzrlib.bundle.commands'),6072 ('cmd_bundle_info', [], 'bzrlib.bundle.commands'),
6073 ('cmd_config', [], 'bzrlib.config'),
6073 ('cmd_dpush', [], 'bzrlib.foreign'),6074 ('cmd_dpush', [], 'bzrlib.foreign'),
6074 ('cmd_version_info', [], 'bzrlib.cmd_version_info'),6075 ('cmd_version_info', [], 'bzrlib.cmd_version_info'),
6075 ('cmd_resolve', ['resolved'], 'bzrlib.conflicts'),6076 ('cmd_resolve', ['resolved'], 'bzrlib.conflicts'),
60766077
=== modified file 'bzrlib/config.py'
--- bzrlib/config.py 2010-10-13 15:28:45 +0000
+++ bzrlib/config.py 2010-10-15 09:37:41 +0000
@@ -65,17 +65,19 @@
65import os65import os
66import sys66import sys
6767
68from bzrlib import commands
68from bzrlib.decorators import needs_write_lock69from bzrlib.decorators import needs_write_lock
69from bzrlib.lazy_import import lazy_import70from bzrlib.lazy_import import lazy_import
70lazy_import(globals(), """71lazy_import(globals(), """
71import errno72import errno
72from fnmatch import fnmatch73import fnmatch
73import re74import re
74from cStringIO import StringIO75from cStringIO import StringIO
7576
76import bzrlib77import bzrlib
77from bzrlib import (78from bzrlib import (
78 atomicfile,79 atomicfile,
80 bzrdir,
79 debug,81 debug,
80 errors,82 errors,
81 lockdir,83 lockdir,
@@ -153,6 +155,10 @@
153 def __init__(self):155 def __init__(self):
154 super(Config, self).__init__()156 super(Config, self).__init__()
155157
158 def config_id(self):
159 """Returns a unique ID for the config."""
160 raise NotImplementedError(self.config_id)
161
156 def get_editor(self):162 def get_editor(self):
157 """Get the users pop up editor."""163 """Get the users pop up editor."""
158 raise NotImplementedError164 raise NotImplementedError
@@ -444,6 +450,54 @@
444 """Override this to define the section used by the config."""450 """Override this to define the section used by the config."""
445 return "DEFAULT"451 return "DEFAULT"
446452
453 def _get_sections(self, name=None):
454 """Returns an iterator of the sections specified by ``name``.
455
456 :param name: The section name. If None is supplied, the default
457 configurations are yielded.
458
459 :return: A tuple (name, section, config_id) for all sections that will
460 be walked by user_get_option() in the 'right' order. The first one
461 is where set_user_option() will update the value.
462 """
463 parser = self._get_parser()
464 if name is not None:
465 yield (name, parser[name], self.config_id())
466 else:
467 # No section name has been given so we fallback to the configobj
468 # itself which holds the variables defined outside of any section.
469 yield (None, parser, self.config_id())
470
471 def _get_options(self, sections=None):
472 """Return an ordered list of (name, value, section, config_id) tuples.
473
474 All options are returned with their associated value and the section
475 they appeared in. ``config_id`` is a unique identifier for the
476 configuration file the option is defined in.
477
478 :param sections: Default to ``_get_matching_sections`` if not
479 specified. This gives a better control to daughter classes about
480 which sections should be searched. This is a list of (name,
481 configobj) tuples.
482 """
483 opts = []
484 if sections is None:
485 parser = self._get_parser()
486 sections = []
487 for (section_name, _) in self._get_matching_sections():
488 try:
489 section = parser[section_name]
490 except KeyError:
491 # This could happen for an empty file for which we define a
492 # DEFAULT section. FIXME: Force callers to provide sections
493 # instead ? -- vila 20100930
494 continue
495 sections.append((section_name, section))
496 config_id = self.config_id()
497 for (section_name, section) in sections:
498 for (name, value) in section.iteritems():
499 yield (name, value, section_name, config_id)
500
447 def _get_option_policy(self, section, option_name):501 def _get_option_policy(self, section, option_name):
448 """Return the policy for the given (section, option_name) pair."""502 """Return the policy for the given (section, option_name) pair."""
449 return POLICY_NONE503 return POLICY_NONE
@@ -536,6 +590,26 @@
536 def _get_nickname(self):590 def _get_nickname(self):
537 return self.get_user_option('nickname')591 return self.get_user_option('nickname')
538592
593 def remove_user_option(self, option_name, section_name=None):
594 """Remove a user option and save the configuration file.
595
596 :param option_name: The option to be removed.
597
598 :param section_name: The section the option is defined in, default to
599 the default section.
600 """
601 self.reload()
602 parser = self._get_parser()
603 if section_name is None:
604 section = parser
605 else:
606 section = parser[section_name]
607 try:
608 del section[option_name]
609 except KeyError:
610 raise errors.NoSuchConfigOption(option_name)
611 self._write_config_file()
612
539 def _write_config_file(self):613 def _write_config_file(self):
540 if self.file_name is None:614 if self.file_name is None:
541 raise AssertionError('We cannot save, self.file_name is None')615 raise AssertionError('We cannot save, self.file_name is None')
@@ -604,6 +678,11 @@
604 def break_lock(self):678 def break_lock(self):
605 self._lock.break_lock()679 self._lock.break_lock()
606680
681 @needs_write_lock
682 def remove_user_option(self, option_name, section_name=None):
683 super(LockableConfig, self).remove_user_option(option_name,
684 section_name)
685
607 def _write_config_file(self):686 def _write_config_file(self):
608 if self._lock is None or not self._lock.is_held:687 if self._lock is None or not self._lock.is_held:
609 # NB: if the following exception is raised it probably means a688 # NB: if the following exception is raised it probably means a
@@ -618,6 +697,9 @@
618 def __init__(self):697 def __init__(self):
619 super(GlobalConfig, self).__init__(file_name=config_filename())698 super(GlobalConfig, self).__init__(file_name=config_filename())
620699
700 def config_id(self):
701 return 'bazaar'
702
621 @classmethod703 @classmethod
622 def from_string(cls, str_or_unicode, save=False):704 def from_string(cls, str_or_unicode, save=False):
623 """Create a config object from a string.705 """Create a config object from a string.
@@ -667,6 +749,30 @@
667 self._write_config_file()749 self._write_config_file()
668750
669751
752 def _get_sections(self, name=None):
753 """See IniBasedConfig._get_sections()."""
754 parser = self._get_parser()
755 # We don't give access to options defined outside of any section, we
756 # used the DEFAULT section by... default.
757 if name in (None, 'DEFAULT'):
758 # This could happen for an empty file where the DEFAULT section
759 # doesn't exist yet. So we force DEFAULT when yielding
760 name = 'DEFAULT'
761 if 'DEFAULT' not in parser:
762 parser['DEFAULT']= {}
763 yield (name, parser[name], self.config_id())
764
765 @needs_write_lock
766 def remove_user_option(self, option_name, section_name=None):
767 if section_name is None:
768 # We need to force the default section.
769 section_name = 'DEFAULT'
770 # We need to avoid the LockableConfig implementation or we'll lock
771 # twice
772 super(LockableConfig, self).remove_user_option(option_name,
773 section_name)
774
775
670class LocationConfig(LockableConfig):776class LocationConfig(LockableConfig):
671 """A configuration object that gives the policy for a location."""777 """A configuration object that gives the policy for a location."""
672778
@@ -680,6 +786,9 @@
680 location = urlutils.local_path_from_url(location)786 location = urlutils.local_path_from_url(location)
681 self.location = location787 self.location = location
682788
789 def config_id(self):
790 return 'locations'
791
683 @classmethod792 @classmethod
684 def from_string(cls, str_or_unicode, location, save=False):793 def from_string(cls, str_or_unicode, location, save=False):
685 """Create a config object from a string.794 """Create a config object from a string.
@@ -717,7 +826,7 @@
717 names = zip(location_names, section_names)826 names = zip(location_names, section_names)
718 matched = True827 matched = True
719 for name in names:828 for name in names:
720 if not fnmatch(name[0], name[1]):829 if not fnmatch.fnmatch(name[0], name[1]):
721 matched = False830 matched = False
722 break831 break
723 if not matched:832 if not matched:
@@ -728,6 +837,7 @@
728 continue837 continue
729 matches.append((len(section_names), section,838 matches.append((len(section_names), section,
730 '/'.join(location_names[len(section_names):])))839 '/'.join(location_names[len(section_names):])))
840 # put the longest (aka more specific) locations first
731 matches.sort(reverse=True)841 matches.sort(reverse=True)
732 sections = []842 sections = []
733 for (length, section, extra_path) in matches:843 for (length, section, extra_path) in matches:
@@ -740,6 +850,14 @@
740 pass850 pass
741 return sections851 return sections
742852
853 def _get_sections(self, name=None):
854 """See IniBasedConfig._get_sections()."""
855 # We ignore the name here as the only sections handled are named with
856 # the location path and we don't expose embedded sections either.
857 parser = self._get_parser()
858 for name, extra_path in self._get_matching_sections():
859 yield (name, parser[name], self.config_id())
860
743 def _get_option_policy(self, section, option_name):861 def _get_option_policy(self, section, option_name):
744 """Return the policy for the given (section, option_name) pair."""862 """Return the policy for the given (section, option_name) pair."""
745 # check for the old 'recurse=False' flag863 # check for the old 'recurse=False' flag
@@ -824,9 +942,13 @@
824 self._get_branch_data_config,942 self._get_branch_data_config,
825 self._get_global_config)943 self._get_global_config)
826944
945 def config_id(self):
946 return 'branch'
947
827 def _get_branch_data_config(self):948 def _get_branch_data_config(self):
828 if self._branch_data_config is None:949 if self._branch_data_config is None:
829 self._branch_data_config = TreeConfig(self.branch)950 self._branch_data_config = TreeConfig(self.branch)
951 self._branch_data_config.config_id = self.config_id
830 return self._branch_data_config952 return self._branch_data_config
831953
832 def _get_location_config(self):954 def _get_location_config(self):
@@ -900,6 +1022,31 @@
900 return value1022 return value
901 return None1023 return None
9021024
1025 def _get_sections(self, name=None):
1026 """See IniBasedConfig.get_sections()."""
1027 for source in self.option_sources:
1028 for section in source()._get_sections(name):
1029 yield section
1030
1031 def _get_options(self, sections=None):
1032 opts = []
1033 # First the locations options
1034 for option in self._get_location_config()._get_options():
1035 yield option
1036 # Then the branch options
1037 branch_config = self._get_branch_data_config()
1038 if sections is None:
1039 sections = [('DEFAULT', branch_config._get_parser())]
1040 # FIXME: We shouldn't have to duplicate the code in IniBasedConfig but
1041 # Config itself has no notion of sections :( -- vila 20101001
1042 config_id = self.config_id()
1043 for (section_name, section) in sections:
1044 for (name, value) in section.iteritems():
1045 yield (name, value, section_name, config_id)
1046 # Then the global options
1047 for option in self._get_global_config()._get_options():
1048 yield option
1049
903 def set_user_option(self, name, value, store=STORE_BRANCH,1050 def set_user_option(self, name, value, store=STORE_BRANCH,
904 warn_masked=False):1051 warn_masked=False):
905 if store == STORE_BRANCH:1052 if store == STORE_BRANCH:
@@ -923,6 +1070,9 @@
923 trace.warning('Value "%s" is masked by "%s" from'1070 trace.warning('Value "%s" is masked by "%s" from'
924 ' branch.conf', value, mask_value)1071 ' branch.conf', value, mask_value)
9251072
1073 def remove_user_option(self, option_name, section_name=None):
1074 self._get_branch_data_config().remove_option(option_name, section_name)
1075
926 def _gpg_signing_command(self):1076 def _gpg_signing_command(self):
927 """See Config.gpg_signing_command."""1077 """See Config.gpg_signing_command."""
928 return self._get_safe_value('_gpg_signing_command')1078 return self._get_safe_value('_gpg_signing_command')
@@ -1086,12 +1236,23 @@
10861236
1087 def set_option(self, value, name, section=None):1237 def set_option(self, value, name, section=None):
1088 """Set a per-branch configuration option"""1238 """Set a per-branch configuration option"""
1239 # FIXME: We shouldn't need to lock explicitly here but rather rely on
1240 # higher levels providing the right lock -- vila 20101004
1089 self.branch.lock_write()1241 self.branch.lock_write()
1090 try:1242 try:
1091 self._config.set_option(value, name, section)1243 self._config.set_option(value, name, section)
1092 finally:1244 finally:
1093 self.branch.unlock()1245 self.branch.unlock()
10941246
1247 def remove_option(self, option_name, section_name=None):
1248 # FIXME: We shouldn't need to lock explicitly here but rather rely on
1249 # higher levels providing the right lock -- vila 20101004
1250 self.branch.lock_write()
1251 try:
1252 self._config.remove_option(option_name, section_name)
1253 finally:
1254 self.branch.unlock()
1255
10951256
1096class AuthenticationConfig(object):1257class AuthenticationConfig(object):
1097 """The authentication configuration file based on a ini file.1258 """The authentication configuration file based on a ini file.
@@ -1540,8 +1701,8 @@
1540 """A Config that reads/writes a config file on a Transport.1701 """A Config that reads/writes a config file on a Transport.
15411702
1542 It is a low-level object that considers config data to be name/value pairs1703 It is a low-level object that considers config data to be name/value pairs
1543 that may be associated with a section. Assigning meaning to the these1704 that may be associated with a section. Assigning meaning to these values
1544 values is done at higher levels like TreeConfig.1705 is done at higher levels like TreeConfig.
1545 """1706 """
15461707
1547 def __init__(self, transport, filename):1708 def __init__(self, transport, filename):
@@ -1580,6 +1741,14 @@
1580 configobj.setdefault(section, {})[name] = value1741 configobj.setdefault(section, {})[name] = value
1581 self._set_configobj(configobj)1742 self._set_configobj(configobj)
15821743
1744 def remove_option(self, option_name, section_name=None):
1745 configobj = self._get_configobj()
1746 if section_name is None:
1747 del configobj[option_name]
1748 else:
1749 del configobj[section_name][option_name]
1750 self._set_configobj(configobj)
1751
1583 def _get_config_file(self):1752 def _get_config_file(self):
1584 try:1753 try:
1585 return StringIO(self._transport.get_bytes(self._filename))1754 return StringIO(self._transport.get_bytes(self._filename))
@@ -1598,3 +1767,113 @@
1598 configobj.write(out_file)1767 configobj.write(out_file)
1599 out_file.seek(0)1768 out_file.seek(0)
1600 self._transport.put_file(self._filename, out_file)1769 self._transport.put_file(self._filename, out_file)
1770
1771
1772class cmd_config(commands.Command):
1773 __doc__ = """Display, set or remove a configuration option.
1774
1775 Display the MATCHING configuration options mentioning their scope (the
1776 configuration file they are defined in). The active value that bzr will
1777 take into account is the first one displayed.
1778
1779 Setting a value is achieved by using name=value without spaces. The value
1780 is set in the most relevant scope and can be checked by displaying the
1781 option again.
1782 """
1783
1784 aliases = ['conf']
1785 takes_args = ['matching?']
1786
1787 takes_options = [
1788 'directory',
1789 # FIXME: This should be a registry option so that plugins can register
1790 # their own config files (or not) -- vila 20101002
1791 commands.Option('scope', help='Reduce the scope to the specified'
1792 ' configuration file',
1793 type=unicode),
1794 commands.Option('remove', help='Remove the option from'
1795 ' the configuration file'),
1796 ]
1797
1798 @commands.display_command
1799 def run(self, matching=None, directory=None, scope=None, remove=False):
1800 if directory is None:
1801 directory = '.'
1802 directory = urlutils.normalize_url(directory)
1803 if matching is None:
1804 self._show_config('*', directory)
1805 else:
1806 if remove:
1807 self._remove_config_option(matching, directory, scope)
1808 else:
1809 pos = matching.find('=')
1810 if pos == -1:
1811 self._show_config(matching, directory)
1812 else:
1813 self._set_config_option(matching[:pos], matching[pos+1:],
1814 directory, scope)
1815
1816 def _get_configs(self, directory, scope=None):
1817 """Iterate the configurations specified by ``directory`` and ``scope``.
1818
1819 :param directory: Where the configurations are derived from.
1820
1821 :param scope: A specific config to start from.
1822 """
1823 if scope is not None:
1824 if scope == 'bazaar':
1825 yield GlobalConfig()
1826 elif scope == 'locations':
1827 yield LocationConfig(directory)
1828 elif scope == 'branch':
1829 (_, br, _) = bzrdir.BzrDir.open_containing_tree_or_branch(
1830 directory)
1831 yield br.get_config()
1832 else:
1833 try:
1834 (_, br, _) = bzrdir.BzrDir.open_containing_tree_or_branch(
1835 directory)
1836 yield br.get_config()
1837 except errors.NotBranchError:
1838 yield LocationConfig(directory)
1839 yield GlobalConfig()
1840
1841 def _show_config(self, matching, directory):
1842 # Turn the glob into a regexp
1843 matching_re = re.compile(fnmatch.translate(matching))
1844 cur_conf_id = None
1845 for c in self._get_configs(directory):
1846 for (name, value, section, conf_id) in c._get_options():
1847 if matching_re.search(name):
1848 if cur_conf_id != conf_id:
1849 self.outf.write('%s:\n' % (conf_id,))
1850 cur_conf_id = conf_id
1851 self.outf.write(' %s = %s\n' % (name, value))
1852
1853 def _set_config_option(self, name, value, directory, scope):
1854 for conf in self._get_configs(directory, scope):
1855 conf.set_user_option(name, value)
1856 break
1857 else:
1858 raise errors.NoSuchConfig(scope)
1859
1860 def _remove_config_option(self, name, directory, scope):
1861 removed = False
1862 for conf in self._get_configs(directory, scope):
1863 for (section_name, section, conf_id) in conf._get_sections():
1864 if scope is not None and conf_id != scope:
1865 # Not the right configuration file
1866 continue
1867 if name in section:
1868 if conf_id != conf.config_id():
1869 conf = self._get_configs(directory, conf_id).next()
1870 # We use the first section in the first config where the
1871 # option is defined to remove it
1872 conf.remove_user_option(name, section_name)
1873 removed = True
1874 break
1875 break
1876 else:
1877 raise errors.NoSuchConfig(scope)
1878 if not removed:
1879 raise errors.NoSuchConfigOption(name)
16011880
=== modified file 'bzrlib/errors.py'
--- bzrlib/errors.py 2010-09-28 18:51:47 +0000
+++ bzrlib/errors.py 2010-10-15 09:37:41 +0000
@@ -2945,6 +2945,22 @@
2945 self.user_encoding = osutils.get_user_encoding()2945 self.user_encoding = osutils.get_user_encoding()
29462946
29472947
2948class NoSuchConfig(BzrError):
2949
2950 _fmt = ('The "%(config_id)s" configuration does not exist.')
2951
2952 def __init__(self, config_id):
2953 BzrError.__init__(self, config_id=config_id)
2954
2955
2956class NoSuchConfigOption(BzrError):
2957
2958 _fmt = ('The "%(option_name)s" configuration option does not exist.')
2959
2960 def __init__(self, option_name):
2961 BzrError.__init__(self, option_name=option_name)
2962
2963
2948class NoSuchAlias(BzrError):2964class NoSuchAlias(BzrError):
29492965
2950 _fmt = ('The alias "%(alias_name)s" does not exist.')2966 _fmt = ('The alias "%(alias_name)s" does not exist.')
29512967
=== modified file 'bzrlib/tests/blackbox/__init__.py'
--- bzrlib/tests/blackbox/__init__.py 2010-07-28 07:05:19 +0000
+++ bzrlib/tests/blackbox/__init__.py 2010-10-15 09:37:41 +0000
@@ -54,6 +54,7 @@
54 'bzrlib.tests.blackbox.test_clean_tree',54 'bzrlib.tests.blackbox.test_clean_tree',
55 'bzrlib.tests.blackbox.test_command_encoding',55 'bzrlib.tests.blackbox.test_command_encoding',
56 'bzrlib.tests.blackbox.test_commit',56 'bzrlib.tests.blackbox.test_commit',
57 'bzrlib.tests.blackbox.test_config',
57 'bzrlib.tests.blackbox.test_conflicts',58 'bzrlib.tests.blackbox.test_conflicts',
58 'bzrlib.tests.blackbox.test_debug',59 'bzrlib.tests.blackbox.test_debug',
59 'bzrlib.tests.blackbox.test_deleted',60 'bzrlib.tests.blackbox.test_deleted',
6061
=== added file 'bzrlib/tests/blackbox/test_config.py'
--- bzrlib/tests/blackbox/test_config.py 1970-01-01 00:00:00 +0000
+++ bzrlib/tests/blackbox/test_config.py 2010-10-15 09:37:41 +0000
@@ -0,0 +1,204 @@
1# Copyright (C) 2010 Canonical Ltd
2#
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation; either version 2 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program; if not, write to the Free Software
15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
17
18"""Black-box tests for bzr config."""
19
20import os
21
22from bzrlib import (
23 config,
24 errors,
25 tests,
26 )
27from bzrlib.tests import (
28 script,
29 test_config as _t_config,
30 )
31
32class TestWithoutConfig(tests.TestCaseWithTransport):
33
34 def test_no_config(self):
35 out, err = self.run_bzr(['config'])
36 self.assertEquals('', out)
37 self.assertEquals('', err)
38
39 def test_all_variables_no_config(self):
40 out, err = self.run_bzr(['config', '*'])
41 self.assertEquals('', out)
42 self.assertEquals('', err)
43
44 def test_unknown_option(self):
45 self.run_bzr_error(['The "file" configuration option does not exist',],
46 ['config', '--remove', 'file'])
47
48class TestConfigDisplay(tests.TestCaseWithTransport):
49
50 def setUp(self):
51 super(TestConfigDisplay, self).setUp()
52 _t_config.create_configs(self)
53
54 def test_bazaar_config(self):
55 self.bazaar_config.set_user_option('hello', 'world')
56 script.run_script(self, '''\
57 $ bzr config -d tree
58 bazaar:
59 hello = world
60 ''')
61
62 def test_locations_config_for_branch(self):
63 self.locations_config.set_user_option('hello', 'world')
64 self.branch_config.set_user_option('hello', 'you')
65 script.run_script(self, '''\
66 $ bzr config -d tree
67 locations:
68 hello = world
69 branch:
70 hello = you
71 ''')
72
73 def test_locations_config_outside_branch(self):
74 self.bazaar_config.set_user_option('hello', 'world')
75 self.locations_config.set_user_option('hello', 'world')
76 script.run_script(self, '''\
77 $ bzr config
78 bazaar:
79 hello = world
80 ''')
81
82
83class TestConfigSetOption(tests.TestCaseWithTransport):
84
85 def setUp(self):
86 super(TestConfigSetOption, self).setUp()
87 _t_config.create_configs(self)
88
89 def test_unknown_config(self):
90 self.run_bzr_error(['The "moon" configuration does not exist'],
91 ['config', '--scope', 'moon', 'hello=world'])
92
93 def test_bazaar_config_outside_branch(self):
94 script.run_script(self, '''\
95 $ bzr config --scope bazaar hello=world
96 $ bzr config -d tree hello
97 bazaar:
98 hello = world
99 ''')
100
101 def test_bazaar_config_inside_branch(self):
102 script.run_script(self, '''\
103 $ bzr config -d tree --scope bazaar hello=world
104 $ bzr config -d tree hello
105 bazaar:
106 hello = world
107 ''')
108
109 def test_locations_config_inside_branch(self):
110 script.run_script(self, '''\
111 $ bzr config -d tree --scope locations hello=world
112 $ bzr config -d tree hello
113 locations:
114 hello = world
115 ''')
116
117 def test_branch_config_default(self):
118 script.run_script(self, '''\
119 $ bzr config -d tree hello=world
120 $ bzr config -d tree hello
121 branch:
122 hello = world
123 ''')
124
125 def test_branch_config_forcing_branch(self):
126 script.run_script(self, '''\
127 $ bzr config -d tree --scope branch hello=world
128 $ bzr config -d tree hello
129 branch:
130 hello = world
131 ''')
132
133
134class TestConfigRemoveOption(tests.TestCaseWithTransport):
135
136 def setUp(self):
137 super(TestConfigRemoveOption, self).setUp()
138 _t_config.create_configs_with_file_option(self)
139
140 def test_unknown_config(self):
141 self.run_bzr_error(['The "moon" configuration does not exist'],
142 ['config', '--scope', 'moon', '--remove', 'file'])
143
144 def test_bazaar_config_outside_branch(self):
145 script.run_script(self, '''\
146 $ bzr config --scope bazaar --remove file
147 $ bzr config -d tree file
148 locations:
149 file = locations
150 branch:
151 file = branch
152 ''')
153
154 def test_bazaar_config_inside_branch(self):
155 script.run_script(self, '''\
156 $ bzr config -d tree --scope bazaar --remove file
157 $ bzr config -d tree file
158 locations:
159 file = locations
160 branch:
161 file = branch
162 ''')
163
164 def test_locations_config_inside_branch(self):
165 script.run_script(self, '''\
166 $ bzr config -d tree --scope locations --remove file
167 $ bzr config -d tree file
168 branch:
169 file = branch
170 bazaar:
171 file = bazaar
172 ''')
173
174 def test_branch_config_default(self):
175 script.run_script(self, '''\
176 $ bzr config -d tree --remove file
177 $ bzr config -d tree file
178 branch:
179 file = branch
180 bazaar:
181 file = bazaar
182 ''')
183 script.run_script(self, '''\
184 $ bzr config -d tree --remove file
185 $ bzr config -d tree file
186 bazaar:
187 file = bazaar
188 ''')
189
190 def test_branch_config_forcing_branch(self):
191 script.run_script(self, '''\
192 $ bzr config -d tree --scope branch --remove file
193 $ bzr config -d tree file
194 locations:
195 file = locations
196 bazaar:
197 file = bazaar
198 ''')
199 script.run_script(self, '''\
200 $ bzr config -d tree --remove file
201 $ bzr config -d tree file
202 bazaar:
203 file = bazaar
204 ''')
0205
=== modified file 'bzrlib/tests/test_config.py'
--- bzrlib/tests/test_config.py 2010-09-28 16:28:45 +0000
+++ bzrlib/tests/test_config.py 2010-10-15 09:37:41 +0000
@@ -1471,6 +1471,200 @@
1471 self.assertIs(None, bzrdir_config.get_default_stack_on())1471 self.assertIs(None, bzrdir_config.get_default_stack_on())
14721472
14731473
1474def create_configs(test):
1475 """Create configuration files for a given test.
1476
1477 This requires creating a tree (and populate the ``test.tree`` attribute and
1478 its associated branch and will populate the following attributes:
1479
1480 - branch_config: A BranchConfig for the associated branch.
1481
1482 - locations_config : A LocationConfig for the associated branch
1483
1484 - bazaar_config: A GlobalConfig.
1485
1486 The tree and branch are created in a 'tree' subdirectory so the tests can
1487 still use the test directory to stay outside of the branch.
1488 """
1489 tree = test.make_branch_and_tree('tree')
1490 test.tree = tree
1491 test.branch_config = config.BranchConfig(tree.branch)
1492 test.locations_config = config.LocationConfig(tree.basedir)
1493 test.bazaar_config = config.GlobalConfig()
1494
1495
1496def create_configs_with_file_option(test):
1497 """Create configuration files with a ``file`` option set in each.
1498
1499 This builds on ``create_configs`` and add one ``file`` option in each
1500 configuration with a value which allows identifying the configuration file.
1501 """
1502 create_configs(test)
1503 test.bazaar_config.set_user_option('file', 'bazaar')
1504 test.locations_config.set_user_option('file', 'locations')
1505 test.branch_config.set_user_option('file', 'branch')
1506
1507
1508class TestConfigGetOptions(tests.TestCaseWithTransport):
1509
1510 def setUp(self):
1511 super(TestConfigGetOptions, self).setUp()
1512 create_configs(self)
1513
1514 def assertOptions(self, expected, conf):
1515 actual = list(conf._get_options())
1516 self.assertEqual(expected, actual)
1517
1518 # One variable in none of the above
1519 def test_no_variable(self):
1520 # Using branch should query branch, locations and bazaar
1521 self.assertOptions([], self.branch_config)
1522
1523 def test_option_in_bazaar(self):
1524 self.bazaar_config.set_user_option('file', 'bazaar')
1525 self.assertOptions([('file', 'bazaar', 'DEFAULT', 'bazaar')],
1526 self.bazaar_config)
1527
1528 def test_option_in_locations(self):
1529 self.locations_config.set_user_option('file', 'locations')
1530 self.assertOptions(
1531 [('file', 'locations', self.tree.basedir, 'locations')],
1532 self.locations_config)
1533
1534 def test_option_in_branch(self):
1535 self.branch_config.set_user_option('file', 'branch')
1536 self.assertOptions([('file', 'branch', 'DEFAULT', 'branch')],
1537 self.branch_config)
1538
1539 def test_option_in_bazaar_and_branch(self):
1540 self.bazaar_config.set_user_option('file', 'bazaar')
1541 self.branch_config.set_user_option('file', 'branch')
1542 self.assertOptions([('file', 'branch', 'DEFAULT', 'branch'),
1543 ('file', 'bazaar', 'DEFAULT', 'bazaar'),],
1544 self.branch_config)
1545
1546 def test_option_in_branch_and_locations(self):
1547 # Hmm, locations override branch :-/
1548 self.locations_config.set_user_option('file', 'locations')
1549 self.branch_config.set_user_option('file', 'branch')
1550 self.assertOptions(
1551 [('file', 'locations', self.tree.basedir, 'locations'),
1552 ('file', 'branch', 'DEFAULT', 'branch'),],
1553 self.branch_config)
1554
1555 def test_option_in_bazaar_locations_and_branch(self):
1556 self.bazaar_config.set_user_option('file', 'bazaar')
1557 self.locations_config.set_user_option('file', 'locations')
1558 self.branch_config.set_user_option('file', 'branch')
1559 self.assertOptions(
1560 [('file', 'locations', self.tree.basedir, 'locations'),
1561 ('file', 'branch', 'DEFAULT', 'branch'),
1562 ('file', 'bazaar', 'DEFAULT', 'bazaar'),],
1563 self.branch_config)
1564
1565
1566class TestConfigRemoveOption(tests.TestCaseWithTransport):
1567
1568 def setUp(self):
1569 super(TestConfigRemoveOption, self).setUp()
1570 create_configs_with_file_option(self)
1571
1572 def assertOptions(self, expected, conf):
1573 actual = list(conf._get_options())
1574 self.assertEqual(expected, actual)
1575
1576 def test_remove_in_locations(self):
1577 self.locations_config.remove_user_option('file', self.tree.basedir)
1578 self.assertOptions(
1579 [('file', 'branch', 'DEFAULT', 'branch'),
1580 ('file', 'bazaar', 'DEFAULT', 'bazaar'),],
1581 self.branch_config)
1582
1583 def test_remove_in_branch(self):
1584 self.branch_config.remove_user_option('file')
1585 self.assertOptions(
1586 [('file', 'locations', self.tree.basedir, 'locations'),
1587 ('file', 'bazaar', 'DEFAULT', 'bazaar'),],
1588 self.branch_config)
1589
1590 def test_remove_in_bazaar(self):
1591 self.bazaar_config.remove_user_option('file')
1592 self.assertOptions(
1593 [('file', 'locations', self.tree.basedir, 'locations'),
1594 ('file', 'branch', 'DEFAULT', 'branch'),],
1595 self.branch_config)
1596
1597
1598class TestConfigGetSections(tests.TestCaseWithTransport):
1599
1600 def setUp(self):
1601 super(TestConfigGetSections, self).setUp()
1602 create_configs(self)
1603
1604 def assertSectionNames(self, expected, conf, name=None):
1605 """Check which sections are returned for a given config.
1606
1607 If fallback configurations exist their sections can be included.
1608
1609 :param expected: A list of section names.
1610
1611 :param conf: The configuration that will be queried.
1612
1613 :param name: An optional section name that will be passed to
1614 get_sections().
1615 """
1616 sections = list(conf._get_sections(name))
1617 self.assertLength(len(expected), sections)
1618 self.assertEqual(expected, [name for name, _, _ in sections])
1619
1620 def test_bazaar_default_section(self):
1621 self.assertSectionNames(['DEFAULT'], self.bazaar_config)
1622
1623 def test_locations_default_section(self):
1624 # No sections are defined in an empty file
1625 self.assertSectionNames([], self.locations_config)
1626
1627 def test_locations_named_section(self):
1628 self.locations_config.set_user_option('file', 'locations')
1629 self.assertSectionNames([self.tree.basedir], self.locations_config)
1630
1631 def test_locations_matching_sections(self):
1632 loc_config = self.locations_config
1633 loc_config.set_user_option('file', 'locations')
1634 # We need to cheat a bit here to create an option in sections above and
1635 # below the 'location' one.
1636 parser = loc_config._get_parser()
1637 # locations.cong deals with '/' ignoring native os.sep
1638 location_names = self.tree.basedir.split('/')
1639 parent = '/'.join(location_names[:-1])
1640 child = '/'.join(location_names + ['child'])
1641 parser[parent] = {}
1642 parser[parent]['file'] = 'parent'
1643 parser[child] = {}
1644 parser[child]['file'] = 'child'
1645 self.assertSectionNames([self.tree.basedir, parent], loc_config)
1646
1647 def test_branch_data_default_section(self):
1648 self.assertSectionNames([None],
1649 self.branch_config._get_branch_data_config())
1650
1651 def test_branch_default_sections(self):
1652 # No sections are defined in an empty locations file
1653 self.assertSectionNames([None, 'DEFAULT'],
1654 self.branch_config)
1655 # Unless we define an option
1656 self.branch_config._get_location_config().set_user_option(
1657 'file', 'locations')
1658 self.assertSectionNames([self.tree.basedir, None, 'DEFAULT'],
1659 self.branch_config)
1660
1661 def test_bazaar_named_section(self):
1662 # We need to cheat as the API doesn't give direct access to sections
1663 # other than DEFAULT.
1664 self.bazaar_config.set_alias('bazaar', 'bzr')
1665 self.assertSectionNames(['ALIASES'], self.bazaar_config, 'ALIASES')
1666
1667
1474class TestAuthenticationConfigFile(tests.TestCase):1668class TestAuthenticationConfigFile(tests.TestCase):
1475 """Test the authentication.conf file matching"""1669 """Test the authentication.conf file matching"""
14761670
14771671
=== modified file 'doc/en/release-notes/bzr-2.3.txt'
--- doc/en/release-notes/bzr-2.3.txt 2010-10-15 07:36:59 +0000
+++ doc/en/release-notes/bzr-2.3.txt 2010-10-15 09:37:41 +0000
@@ -22,6 +22,11 @@
22 new or mirrored branch without working trees.22 new or mirrored branch without working trees.
23 (Matthew Gordon, #506730)23 (Matthew Gordon, #506730)
2424
25* ``bzr config`` is a new command that displays the configuration options for
26 a given directory. It accepts a glob to match against multiple options at
27 once. It can also be used to set or delete a configuration option in any
28 configuration file. (Vincent Ladeuil)
29
25* New shortcut url schemes ``ubuntu:`` and ``debianlp:`` access source30* New shortcut url schemes ``ubuntu:`` and ``debianlp:`` access source
26 branches on Launchpad. E.g. ``bzr branch ubuntu:foo`` gives you the source31 branches on Launchpad. E.g. ``bzr branch ubuntu:foo`` gives you the source
27 branch for project ``foo`` in the current distroseries for Ubuntu while32 branch for project ``foo`` in the current distroseries for Ubuntu while
2833
=== modified file 'doc/en/user-guide/configuring_bazaar.txt'
--- doc/en/user-guide/configuring_bazaar.txt 2010-10-08 10:50:51 +0000
+++ doc/en/user-guide/configuring_bazaar.txt 2010-10-15 09:37:41 +0000
@@ -70,6 +70,59 @@
70in the Bazaar User Reference.70in the Bazaar User Reference.
7171
7272
73Looking at the active configuration
74-----------------------------------
75
76To look at all the currently defined options, you can use the following
77command::
78
79 bzr config
80
81``bzr`` implements some rules to decide where to get the value of a
82configuration option.
83
84The current policy is to examine the existing configurations files in a
85given order for matching definitions.
86
87 * ``locations.conf`` is searched first for a section whose name matches the
88 location considered (working tree, branch or remote branch),
89
90 * the current ``branch.conf`` is searched next,
91
92 * ``bazaar.conf`` is searched next,
93
94 * finally, some options can have default values generally defined in the
95 code itself and not displayed by ``bzr config`` (see `Configuration
96 Settings <../user-reference/index.html#configuration-settings>`_).
97
98This is better understood by using ```bzr config`` with no arguments, which
99will display some output of the form::
100
101 locations:
102 post_commit_to = commits@example.com
103 news_merge_files = NEWS
104 branch:
105 parent_location = bzr+ssh://bazaar.launchpad.net/+branch/bzr/
106 nickname = config-modify
107 push_location = bzr+ssh://bazaar.launchpad.net/~vila/bzr/config-modify/
108 bazaar:
109 debug_flags = hpss,
110
111Each configuration file is associated with a given scope whose name is
112displayed before each set of defined options.
113
114Modifying the active configuration
115----------------------------------
116
117To set an option to a given value use::
118
119 bzr config opt=value
120
121To remove an option use::
122
123 bzr config --remove opt
124
125
73Rule-based preferences126Rule-based preferences
74----------------------127----------------------
75128
76129
=== modified file 'doc/en/whats-new/whats-new-in-2.3.txt'
--- doc/en/whats-new/whats-new-in-2.3.txt 2010-10-13 07:04:50 +0000
+++ doc/en/whats-new/whats-new-in-2.3.txt 2010-10-15 09:37:41 +0000
@@ -97,6 +97,14 @@
97 format, used by emacs and the standalone ``info`` reader.97 format, used by emacs and the standalone ``info`` reader.
98 (Vincent Ladeuil, #219334)98 (Vincent Ladeuil, #219334)
9999
100Configuration
101*************
102
103``bzr`` can be configure via environment variables, command-line options
104and configurations files. We've started working on unifying this and give
105access to more options. The first step is a new ``bzr config`` command that
106can be used to display the active configuration options in the current
107working tree or branch as well as the ability to set or remove an option.
100108
101Expected releases for the 2.3 series109Expected releases for the 2.3 series
102************************************110************************************