summaryrefslogtreecommitdiff
path: root/git/config.py
diff options
context:
space:
mode:
authorSebastian Thiel <byronimo@gmail.com>2015-01-14 12:46:51 +0100
committerSebastian Thiel <byronimo@gmail.com>2015-01-14 12:46:51 +0100
commit619c989915b568e4737951fafcbae14cd06d6ea6 (patch)
tree3dde760f0018eb418a8c2bb9f0587dc42f74e680 /git/config.py
parentbe074c655ad53927541fc6443eed8b0c2550e415 (diff)
downloadgitpython-619c989915b568e4737951fafcbae14cd06d6ea6.tar.gz
GitConfigParser now respects and merges 'include' sections
We implement it as described in this article: http://stackoverflow.com/questions/1557183/is-it-possible-to-include-a-file-in-your-gitconfig Thus we handle * cycles * relative and absolute include paths * write-backs in case of writable GitConfigParser instances Fixes #201
Diffstat (limited to 'git/config.py')
-rw-r--r--git/config.py83
1 files changed, 70 insertions, 13 deletions
diff --git a/git/config.py b/git/config.py
index 5828a1c1..4c4cb491 100644
--- a/git/config.py
+++ b/git/config.py
@@ -15,6 +15,7 @@ except ImportError:
import inspect
import logging
import abc
+import os
from git.odict import OrderedDict
from git.util import LockFile
@@ -164,7 +165,7 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje
# list of RawConfigParser methods able to change the instance
_mutating_methods_ = ("add_section", "remove_section", "remove_option", "set")
- def __init__(self, file_or_files, read_only=True):
+ def __init__(self, file_or_files, read_only=True, merge_includes=True):
"""Initialize a configuration reader to read the given file_or_files and to
possibly allow changes to it by setting read_only False
@@ -173,7 +174,13 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje
:param read_only:
If True, the ConfigParser may only read the data , but not change it.
- If False, only a single file path or file object may be given."""
+ If False, only a single file path or file object may be given. We will write back the changes
+ when they happen, or when the ConfigParser is released. This will not happen if other
+ configuration files have been included
+ :param merge_includes: if True, we will read files mentioned in [include] sections and merge their
+ contents into ours. This makes it impossible to write back an individual configuration file.
+ Thus, if you want to modify a single conifguration file, turn this off to leave the original
+ dataset unaltered when reading it."""
cp.RawConfigParser.__init__(self, dict_type=OrderedDict)
# Used in python 3, needs to stay in sync with sections for underlying implementation to work
@@ -183,6 +190,7 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje
self._file_or_files = file_or_files
self._read_only = read_only
self._is_initialized = False
+ self._merge_includes = merge_includes
self._lock = None
if not read_only:
@@ -313,7 +321,6 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje
if not e:
e = cp.ParsingError(fpname)
e.append(lineno, repr(line))
- print(lineno, line)
continue
else:
line = line.rstrip()
@@ -329,6 +336,9 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje
if e:
raise e
+ def _has_includes(self):
+ return self._merge_includes and self.has_section('include')
+
def read(self):
"""Reads the data stored in the files we have been initialized with. It will
ignore files that cannot be read, possibly leaving an empty configuration
@@ -337,18 +347,25 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje
:raise IOError: if a file cannot be handled"""
if self._is_initialized:
return
+ self._is_initialized = True
- files_to_read = self._file_or_files
- if not isinstance(files_to_read, (tuple, list)):
- files_to_read = [files_to_read]
-
- for file_object in files_to_read:
- fp = file_object
+ if not isinstance(self._file_or_files, (tuple, list)):
+ files_to_read = [self._file_or_files]
+ else:
+ files_to_read = list(self._file_or_files)
+ # end assure we have a copy of the paths to handle
+
+ seen = set(files_to_read)
+ num_read_include_files = 0
+ while files_to_read:
+ file_path = files_to_read.pop(0)
+ fp = file_path
close_fp = False
+
# assume a path if it is not a file-object
- if not hasattr(file_object, "seek"):
+ if not hasattr(fp, "seek"):
try:
- fp = open(file_object, 'rb')
+ fp = open(file_path, 'rb')
close_fp = True
except IOError:
continue
@@ -360,8 +377,33 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje
if close_fp:
fp.close()
# END read-handling
- # END for each file object to read
- self._is_initialized = True
+
+ # Read includes and append those that we didn't handle yet
+ # We expect all paths to be normalized and absolute (and will assure that is the case)
+ if self._has_includes():
+ for _, include_path in self.items('include'):
+ if not os.path.isabs(include_path):
+ if not close_fp:
+ continue
+ # end ignore relative paths if we don't know the configuration file path
+ assert os.path.isabs(file_path), "Need absolute paths to be sure our cycle checks will work"
+ include_path = os.path.join(os.path.dirname(file_path), include_path)
+ # end make include path absolute
+ include_path = os.path.normpath(include_path)
+ if include_path in seen or not os.access(include_path, os.R_OK):
+ continue
+ seen.add(include_path)
+ files_to_read.append(include_path)
+ num_read_include_files += 1
+ # each include path in configuration file
+ # end handle includes
+ # END for each file object to read
+
+ # If there was no file included, we can safely write back (potentially) the configuration file
+ # without altering it's meaning
+ if num_read_include_files == 0:
+ self._merge_includes = False
+ # end
def _write(self, fp):
"""Write an .ini-format representation of the configuration state in
@@ -379,6 +421,10 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje
for name, value in self._sections.items():
write_section(name, value)
+ def items(self, section_name):
+ """:return: list((option, value), ...) pairs of all items in the given section"""
+ return [(k, v) for k, v in super(GitConfigParser, self).items(section_name) if k != '__name__']
+
@needs_values
def write(self):
"""Write changes to our file, if there are changes at all
@@ -387,6 +433,17 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje
a file lock"""
self._assure_writable("write")
+ if isinstance(self._file_or_files, (list, tuple)):
+ raise AssertionError("Cannot write back if there is not exactly a single file to write to, have %i files"
+ % len(self._file_or_files))
+ # end assert multiple files
+
+ if self._has_includes():
+ log.debug("Skipping write-back of confiuration file as include files were merged in." +
+ "Set merge_includes=False to prevent this.")
+ return
+ # end
+
fp = self._file_or_files
close_fp = False