| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent be074c6 commit 619c989
5 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,3 +1,3 @@ | |||
| 1 | 1 | [submodule "gitdb"] | |
| 2 | - path = git/ext/gitdb | ||
| 3 | 2 | url = https://github.com/gitpython-developers/gitdb.git | |
| 3 | + path = git/ext/gitdb | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -15,6 +15,7 @@ | |||
| 15 | 15 | import inspect | |
| 16 | 16 | import logging | |
| 17 | 17 | import abc | |
| 18 | + import os | ||
| 18 | 19 | ||
| 19 | 20 | from git.odict import OrderedDict | |
| 20 | 21 | from git.util import LockFile | |
@@ -164,7 +165,7 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje | |||
| 164 | 165 | # list of RawConfigParser methods able to change the instance | |
| 165 | 166 | _mutating_methods_ = ("add_section", "remove_section", "remove_option", "set") | |
| 166 | 167 | ||
| 167 | - def __init__(self, file_or_files, read_only=True): | ||
| 168 | + def __init__(self, file_or_files, read_only=True, merge_includes=True): | ||
| 168 | 169 | """Initialize a configuration reader to read the given file_or_files and to | |
| 169 | 170 | possibly allow changes to it by setting read_only False | |
| 170 | 171 | ||
@@ -173,7 +174,13 @@ def __init__(self, file_or_files, read_only=True): | |||
| 173 | 174 | ||
| 174 | 175 | :param read_only: | |
| 175 | 176 | 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.""" | ||
| 177 | 184 | cp.RawConfigParser.__init__(self, dict_type=OrderedDict) | |
| 178 | 185 | ||
| 179 | 186 | # 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): | |||
| 183 | 190 | self._file_or_files = file_or_files | |
| 184 | 191 | self._read_only = read_only | |
| 185 | 192 | self._is_initialized = False | |
| 193 | + self._merge_includes = merge_includes | ||
| 186 | 194 | self._lock = None | |
| 187 | 195 | ||
| 188 | 196 | if not read_only: | |
@@ -313,7 +321,6 @@ def string_decode(v): | |||
| 313 | 321 | if not e: | |
| 314 | 322 | e = cp.ParsingError(fpname) | |
| 315 | 323 | e.append(lineno, repr(line)) | |
| 316 | - print(lineno, line) | ||
| 317 | 324 | continue | |
| 318 | 325 | else: | |
| 319 | 326 | line = line.rstrip() | |
@@ -329,6 +336,9 @@ def string_decode(v): | |||
| 329 | 336 | if e: | |
| 330 | 337 | raise e | |
| 331 | 338 | ||
| 339 | + def _has_includes(self): | ||
| 340 | + return self._merge_includes and self.has_section('include') | ||
| 341 | + | ||
| 332 | 342 | def read(self): | |
| 333 | 343 | """Reads the data stored in the files we have been initialized with. It will | |
| 334 | 344 | ignore files that cannot be read, possibly leaving an empty configuration | |
@@ -337,18 +347,25 @@ def read(self): | |||
| 337 | 347 | :raise IOError: if a file cannot be handled""" | |
| 338 | 348 | if self._is_initialized: | |
| 339 | 349 | return | |
| 350 | + self._is_initialized = True | ||
| 340 | 351 | ||
| 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 | ||
| 347 | 363 | close_fp = False | |
| 364 | + | ||
| 348 | 365 | # assume a path if it is not a file-object | |
| 349 | - if not hasattr(file_object, "seek"): | ||
| 366 | + if not hasattr(fp, "seek"): | ||
| 350 | 367 | try: | |
| 351 | - fp = open(file_object, 'rb') | ||
| 368 | + fp = open(file_path, 'rb') | ||
| 352 | 369 | close_fp = True | |
| 353 | 370 | except IOError: | |
| 354 | 371 | continue | |
@@ -360,8 +377,33 @@ def read(self): | |||
| 360 | 377 | if close_fp: | |
| 361 | 378 | fp.close() | |
| 362 | 379 | # 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 | ||
| 365 | 407 | ||
| 366 | 408 | def _write(self, fp): | |
| 367 | 409 | """Write an .ini-format representation of the configuration state in | |
@@ -379,6 +421,10 @@ def write_section(name, section_dict): | |||
| 379 | 421 | for name, value in self._sections.items(): | |
| 380 | 422 | write_section(name, value) | |
| 381 | 423 | ||
| 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 | + | ||
| 382 | 428 | @needs_values | |
| 383 | 429 | def write(self): | |
| 384 | 430 | """Write changes to our file, if there are changes at all | |
@@ -387,6 +433,17 @@ def write(self): | |||
| 387 | 433 | a file lock""" | |
| 388 | 434 | self._assure_writable("write") | |
| 389 | 435 | ||
| 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 | + | ||
| 390 | 447 | fp = self._file_or_files | |
| 391 | 448 | close_fp = False | |
| 392 | 449 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -359,11 +359,11 @@ def _get_config_path(self, config_level): | |||
| 359 | 359 | return "/etc/gitconfig" | |
| 360 | 360 | elif config_level == "user": | |
| 361 | 361 | 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"))) | ||
| 363 | 363 | elif config_level == "global": | |
| 364 | 364 | return os.path.normpath(os.path.expanduser("~/.gitconfig")) | |
| 365 | 365 | elif config_level == "repository": | |
| 366 | - return join(self.git_dir, "config") | ||
| 366 | + return os.path.normpath(join(self.git_dir, "config")) | ||
| 367 | 367 | ||
| 368 | 368 | raise ValueError("Invalid configuration level: %r" % config_level) | |
| 369 | 369 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -27,3 +27,6 @@ | |||
| 27 | 27 | [branch "mainline_performance"] | |
| 28 | 28 | remote = mainline | |
| 29 | 29 | merge = refs/heads/master | |
| 30 | + [include] | ||
| 31 | + path = doesntexist.cfg | ||
| 32 | + abspath = /usr/bin/foodoesntexist.bar | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -7,15 +7,17 @@ | |||
| 7 | 7 | from git.test.lib import ( | |
| 8 | 8 | TestCase, | |
| 9 | 9 | fixture_path, | |
| 10 | - assert_equal | ||
| 10 | + assert_equal, | ||
| 11 | 11 | ) | |
| 12 | + from gitdb.test.lib import with_rw_directory | ||
| 12 | 13 | from git import ( | |
| 13 | 14 | GitConfigParser | |
| 14 | 15 | ) | |
| 15 | 16 | from git.compat import ( | |
| 16 | 17 | string_types, | |
| 17 | 18 | ) | |
| 18 | 19 | import io | |
| 20 | + import os | ||
| 19 | 21 | from copy import copy | |
| 20 | 22 | from git.config import cp | |
| 21 | 23 | ||
@@ -127,3 +129,67 @@ def test_base(self): | |||
| 127 | 129 | ||
| 128 | 130 | # it raises if there is no default though | |
| 129 | 131 | 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() | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments