Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 619c989

Browse files
committed
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 gitpython-developers#201
1 parent be074c6 commit 619c989

File tree

5 files changed

+143
-17
lines changed

5 files changed

+143
-17
lines changed

.gitmodules

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
[submodule "gitdb"]
2-
path = git/ext/gitdb
32
url = https://github.com/gitpython-developers/gitdb.git
3+
path = git/ext/gitdb

git/config.py

Lines changed: 70 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import inspect
1616
import logging
1717
import abc
18+
import os
1819

1920
from git.odict import OrderedDict
2021
from git.util import LockFile
@@ -164,7 +165,7 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje
164165
# list of RawConfigParser methods able to change the instance
165166
_mutating_methods_ = ("add_section", "remove_section", "remove_option", "set")
166167

167-
def __init__(self, file_or_files, read_only=True):
168+
def __init__(self, file_or_files, read_only=True, merge_includes=True):
168169
"""Initialize a configuration reader to read the given file_or_files and to
169170
possibly allow changes to it by setting read_only False
170171
@@ -173,7 +174,13 @@ def __init__(self, file_or_files, read_only=True):
173174
174175
:param read_only:
175176
If True, the ConfigParser may only read the data , but not change it.
176-
If False, only a single file path or file object may be given."""
177+
If False, only a single file path or file object may be given. We will write back the changes
178+
when they happen, or when the ConfigParser is released. This will not happen if other
179+
configuration files have been included
180+
:param merge_includes: if True, we will read files mentioned in [include] sections and merge their
181+
contents into ours. This makes it impossible to write back an individual configuration file.
182+
Thus, if you want to modify a single conifguration file, turn this off to leave the original
183+
dataset unaltered when reading it."""
177184
cp.RawConfigParser.__init__(self, dict_type=OrderedDict)
178185

179186
# Used in python 3, needs to stay in sync with sections for underlying implementation to work
@@ -183,6 +190,7 @@ def __init__(self, file_or_files, read_only=True):
183190
self._file_or_files = file_or_files
184191
self._read_only = read_only
185192
self._is_initialized = False
193+
self._merge_includes = merge_includes
186194
self._lock = None
187195

188196
if not read_only:
@@ -313,7 +321,6 @@ def string_decode(v):
313321
if not e:
314322
e = cp.ParsingError(fpname)
315323
e.append(lineno, repr(line))
316-
print(lineno, line)
317324
continue
318325
else:
319326
line = line.rstrip()
@@ -329,6 +336,9 @@ def string_decode(v):
329336
if e:
330337
raise e
331338

339+
def _has_includes(self):
340+
return self._merge_includes and self.has_section('include')
341+
332342
def read(self):
333343
"""Reads the data stored in the files we have been initialized with. It will
334344
ignore files that cannot be read, possibly leaving an empty configuration
@@ -337,18 +347,25 @@ def read(self):
337347
:raise IOError: if a file cannot be handled"""
338348
if self._is_initialized:
339349
return
350+
self._is_initialized = True
340351

341-
files_to_read = self._file_or_files
342-
if not isinstance(files_to_read, (tuple, list)):
343-
files_to_read = [files_to_read]
344-
345-
for file_object in files_to_read:
346-
fp = file_object
352+
if not isinstance(self._file_or_files, (tuple, list)):
353+
files_to_read = [self._file_or_files]
354+
else:
355+
files_to_read = list(self._file_or_files)
356+
# end assure we have a copy of the paths to handle
357+
358+
seen = set(files_to_read)
359+
num_read_include_files = 0
360+
while files_to_read:
361+
file_path = files_to_read.pop(0)
362+
fp = file_path
347363
close_fp = False
364+
348365
# assume a path if it is not a file-object
349-
if not hasattr(file_object, "seek"):
366+
if not hasattr(fp, "seek"):
350367
try:
351-
fp = open(file_object, 'rb')
368+
fp = open(file_path, 'rb')
352369
close_fp = True
353370
except IOError:
354371
continue
@@ -360,8 +377,33 @@ def read(self):
360377
if close_fp:
361378
fp.close()
362379
# END read-handling
363-
# END for each file object to read
364-
self._is_initialized = True
380+
381+
# Read includes and append those that we didn't handle yet
382+
# We expect all paths to be normalized and absolute (and will assure that is the case)
383+
if self._has_includes():
384+
for _, include_path in self.items('include'):
385+
if not os.path.isabs(include_path):
386+
if not close_fp:
387+
continue
388+
# end ignore relative paths if we don't know the configuration file path
389+
assert os.path.isabs(file_path), "Need absolute paths to be sure our cycle checks will work"
390+
include_path = os.path.join(os.path.dirname(file_path), include_path)
391+
# end make include path absolute
392+
include_path = os.path.normpath(include_path)
393+
if include_path in seen or not os.access(include_path, os.R_OK):
394+
continue
395+
seen.add(include_path)
396+
files_to_read.append(include_path)
397+
num_read_include_files += 1
398+
# each include path in configuration file
399+
# end handle includes
400+
# END for each file object to read
401+
402+
# If there was no file included, we can safely write back (potentially) the configuration file
403+
# without altering it's meaning
404+
if num_read_include_files == 0:
405+
self._merge_includes = False
406+
# end
365407

366408
def _write(self, fp):
367409
"""Write an .ini-format representation of the configuration state in
@@ -379,6 +421,10 @@ def write_section(name, section_dict):
379421
for name, value in self._sections.items():
380422
write_section(name, value)
381423

424+
def items(self, section_name):
425+
""":return: list((option, value), ...) pairs of all items in the given section"""
426+
return [(k, v) for k, v in super(GitConfigParser, self).items(section_name) if k != '__name__']
427+
382428
@needs_values
383429
def write(self):
384430
"""Write changes to our file, if there are changes at all
@@ -387,6 +433,17 @@ def write(self):
387433
a file lock"""
388434
self._assure_writable("write")
389435

436+
if isinstance(self._file_or_files, (list, tuple)):
437+
raise AssertionError("Cannot write back if there is not exactly a single file to write to, have %i files"
438+
% len(self._file_or_files))
439+
# end assert multiple files
440+
441+
if self._has_includes():
442+
log.debug("Skipping write-back of confiuration file as include files were merged in." +
443+
"Set merge_includes=False to prevent this.")
444+
return
445+
# end
446+
390447
fp = self._file_or_files
391448
close_fp = False
392449

git/repo/base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -359,11 +359,11 @@ def _get_config_path(self, config_level):
359359
return "/etc/gitconfig"
360360
elif config_level == "user":
361361
config_home = os.environ.get("XDG_CONFIG_HOME") or os.path.join(os.environ.get("HOME", '~'), ".config")
362-
return os.path.expanduser(join(config_home, "git", "config"))
362+
return os.path.normpath(os.path.expanduser(join(config_home, "git", "config")))
363363
elif config_level == "global":
364364
return os.path.normpath(os.path.expanduser("~/.gitconfig"))
365365
elif config_level == "repository":
366-
return join(self.git_dir, "config")
366+
return os.path.normpath(join(self.git_dir, "config"))
367367

368368
raise ValueError("Invalid configuration level: %r" % config_level)
369369

git/test/fixtures/git_config

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,6 @@
2727
[branch "mainline_performance"]
2828
remote = mainline
2929
merge = refs/heads/master
30+
[include]
31+
path = doesntexist.cfg
32+
abspath = /usr/bin/foodoesntexist.bar

git/test/test_config.py

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,17 @@
77
from git.test.lib import (
88
TestCase,
99
fixture_path,
10-
assert_equal
10+
assert_equal,
1111
)
12+
from gitdb.test.lib import with_rw_directory
1213
from git import (
1314
GitConfigParser
1415
)
1516
from git.compat import (
1617
string_types,
1718
)
1819
import io
20+
import os
1921
from copy import copy
2022
from git.config import cp
2123

@@ -127,3 +129,67 @@ def test_base(self):
127129

128130
# it raises if there is no default though
129131
self.failUnlessRaises(cp.NoSectionError, r_config.get_value, "doesnt", "exist")
132+
133+
@with_rw_directory
134+
def test_config_include(self, rw_dir):
135+
def write_test_value(cw, value):
136+
cw.set_value(value, 'value', value)
137+
# end
138+
139+
def check_test_value(cr, value):
140+
assert cr.get_value(value, 'value') == value
141+
# end
142+
143+
# PREPARE CONFIG FILE A
144+
fpa = os.path.join(rw_dir, 'a')
145+
cw = GitConfigParser(fpa, read_only=False)
146+
write_test_value(cw, 'a')
147+
148+
fpb = os.path.join(rw_dir, 'b')
149+
fpc = os.path.join(rw_dir, 'c')
150+
cw.set_value('include', 'relative_path_b', 'b')
151+
cw.set_value('include', 'doesntexist', 'foobar')
152+
cw.set_value('include', 'relative_cycle_a_a', 'a')
153+
cw.set_value('include', 'absolute_cycle_a_a', fpa)
154+
cw.release()
155+
assert os.path.exists(fpa)
156+
157+
# PREPARE CONFIG FILE B
158+
cw = GitConfigParser(fpb, read_only=False)
159+
write_test_value(cw, 'b')
160+
cw.set_value('include', 'relative_cycle_b_a', 'a')
161+
cw.set_value('include', 'absolute_cycle_b_a', fpa)
162+
cw.set_value('include', 'relative_path_c', 'c')
163+
cw.set_value('include', 'absolute_path_c', fpc)
164+
cw.release()
165+
166+
# PREPARE CONFIG FILE C
167+
cw = GitConfigParser(fpc, read_only=False)
168+
write_test_value(cw, 'c')
169+
cw.release()
170+
171+
cr = GitConfigParser(fpa, read_only=True)
172+
for tv in ('a', 'b', 'c'):
173+
check_test_value(cr, tv)
174+
# end for each test to verify
175+
assert len(cr.items('include')) == 8, "Expected all include sections to be merged"
176+
cr.release()
177+
178+
# test writable config writers - assure write-back doesn't involve includes
179+
cw = GitConfigParser(fpa, read_only=False, merge_includes=True)
180+
tv = 'x'
181+
write_test_value(cw, tv)
182+
cw.release()
183+
184+
cr = GitConfigParser(fpa, read_only=True)
185+
self.failUnlessRaises(cp.NoSectionError, check_test_value, cr, tv)
186+
cr.release()
187+
188+
# But can make it skip includes alltogether, and thus allow write-backs
189+
cw = GitConfigParser(fpa, read_only=False, merge_includes=False)
190+
write_test_value(cw, tv)
191+
cw.release()
192+
193+
cr = GitConfigParser(fpa, read_only=True)
194+
check_test_value(cr, tv)
195+
cr.release()

0 commit comments

Comments
 (0)