Repository: pantsbuild/pants

Issue:
Overlaid Config Files are applied in reverse order
http://www.pantsbuild.org/options.html#overlaying-config-files documents that one can do:

    $ ./pants --pants-config-files=a.ini --pants-config-files=b.ini options --options-name="level"
    level = info (from CONFIG in a.ini)

    $ cat a.ini
    [GLOBAL]
    level: info

    $ cat b.ini
    [GLOBAL]
    level: debug

According to the docs, the second --pants-config-files should overlay the earlier values, but this is not happening :/


Discussion:
A quick check to make sure the docs had not drifted from list-option CLI syntax:
```
$ ./pants --pants-config-files=a.ini --pants-config-files=b.ini options --name=pants-config-files 2>/dev/null
pants_config_files = ['/home/jsirois/dev/pantsbuild/pants/pants.ini', 'a.ini', 'b.ini'] (from FLAG)
```
So pants is seeing the correct list of 3 config files.

Resolving patch:
diff --git a/src/python/pants/option/config.py b/src/python/pants/option/config.py
--- a/src/python/pants/option/config.py
+++ b/src/python/pants/option/config.py
@@ -218,7 +218,8 @@ def configs(self):
     return self._configs
 
   def sources(self):
-    return list(itertools.chain.from_iterable(cfg.sources() for cfg in self._configs))
+    # NB: Present the sources in the order we were given them.
+    return list(itertools.chain.from_iterable(cfg.sources() for cfg in reversed(self._configs)))
 
   def sections(self):
     ret = OrderedSet()


Test patch:
diff --git a/tests/python/pants_test/option/test_config.py b/tests/python/pants_test/option/test_config.py
--- a/tests/python/pants_test/option/test_config.py
+++ b/tests/python/pants_test/option/test_config.py
@@ -56,6 +56,7 @@ def setUp(self):
           """))
         ini2.close()
         self.config = Config.load(configpaths=[ini1.name, ini2.name])
+        self.assertEqual([ini1.name, ini2.name], self.config.sources())
 
   def test_getstring(self):
     self.assertEquals('/a/b/42', self.config.get('a', 'path'))
diff --git a/tests/python/pants_test/option/test_options_bootstrapper.py b/tests/python/pants_test/option/test_options_bootstrapper.py
--- a/tests/python/pants_test/option/test_options_bootstrapper.py
+++ b/tests/python/pants_test/option/test_options_bootstrapper.py
@@ -50,7 +50,7 @@ def test_bootstrap_option_values(self):
 
     def br(path):
       # Returns the full path of the given path under the buildroot.
-      return '{}/{}'.format(buildroot, path)
+      return os.path.join(buildroot, path)
 
     self._do_test([br('.pants.d'), br('build-support'), br('dist')],
                   config=None, env={}, args=[])
@@ -134,7 +134,7 @@ def test_create_bootstrapped_options(self):
     self.assertEquals('/qux/baz', opts.for_scope('foo').bar)
     self.assertEquals('/pear/banana', opts.for_scope('fruit').apple)
 
-  def test_create_bootstrapped_multiple_config_override(self):
+  def do_test_create_bootstrapped_multiple_config(self, create_options_bootstrapper):
     # check with multiple config files, the latest values always get taken
     # in this case worker_count will be overwritten, while fruit stays the same
     with temporary_file() as fp:
@@ -147,16 +147,15 @@ def test_create_bootstrapped_multiple_config_override(self):
       """))
       fp.close()
 
-      args = ['--config-override={}'.format(fp.name)] + self._config_path(fp.name)
-      bootstrapper_single_config = OptionsBootstrapper(args=args)
+      bootstrapper_single_config = create_options_bootstrapper(fp.name)
 
-      opts_single_config  = bootstrapper_single_config.get_full_options(known_scope_infos=[
+      opts_single_config = bootstrapper_single_config.get_full_options(known_scope_infos=[
           ScopeInfo('', ScopeInfo.GLOBAL),
           ScopeInfo('compile.apt', ScopeInfo.TASK),
           ScopeInfo('fruit', ScopeInfo.TASK),
       ])
       # So we don't choke on these on the cmd line.
-      opts_single_config.register('', '--pants-config-files')
+      opts_single_config.register('', '--pants-config-files', type=list)
       opts_single_config.register('', '--config-override', type=list)
 
       opts_single_config.register('compile.apt', '--worker-count')
@@ -172,10 +171,7 @@ def test_create_bootstrapped_multiple_config_override(self):
         """))
         fp2.close()
 
-        args = ['--config-override={}'.format(fp.name),
-                '--config-override={}'.format(fp2.name)] + self._config_path(fp.name)
-
-        bootstrapper_double_config = OptionsBootstrapper(args=args)
+        bootstrapper_double_config = create_options_bootstrapper(fp.name, fp2.name)
 
         opts_double_config = bootstrapper_double_config.get_full_options(known_scope_infos=[
           ScopeInfo('', ScopeInfo.GLOBAL),
@@ -183,7 +179,7 @@ def test_create_bootstrapped_multiple_config_override(self):
           ScopeInfo('fruit', ScopeInfo.TASK),
         ])
         # So we don't choke on these on the cmd line.
-        opts_double_config.register('', '--pants-config-files')
+        opts_double_config.register('', '--pants-config-files', type=list)
         opts_double_config.register('', '--config-override', type=list)
         opts_double_config.register('compile.apt', '--worker-count')
         opts_double_config.register('fruit', '--apple')
@@ -191,6 +187,18 @@ def test_create_bootstrapped_multiple_config_override(self):
         self.assertEquals('2', opts_double_config.for_scope('compile.apt').worker_count)
         self.assertEquals('red', opts_double_config.for_scope('fruit').apple)
 
+  def test_create_bootstrapped_multiple_config_override(self):
+    def create_options_bootstrapper(*config_paths):
+      return OptionsBootstrapper(args=['--config-override={}'.format(cp) for cp in config_paths])
+
+    self.do_test_create_bootstrapped_multiple_config(create_options_bootstrapper)
+
+  def test_create_bootstrapped_multiple_pants_config_files(self):
+    def create_options_bootstrapper(*config_paths):
+      return OptionsBootstrapper(args=['--pants-config-files={}'.format(cp) for cp in config_paths])
+
+    self.do_test_create_bootstrapped_multiple_config(create_options_bootstrapper)
+
   def test_full_options_caching(self):
     with temporary_file_path() as config:
       args = self._config_path(config)