Skip to content

Commit 94bbe51

Browse files
authored
Merge pull request #1054 from buddly27/read-conditional-include
Read conditional include
2 parents fb7fd31 + 5b88532 commit 94bbe51

File tree

3 files changed

+189
-6
lines changed

3 files changed

+189
-6
lines changed

git/config.py

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import logging
1414
import os
1515
import re
16+
import fnmatch
1617
from collections import OrderedDict
1718

1819
from git.compat import (
@@ -38,6 +39,10 @@
3839
# represents the configuration level of a configuration file
3940
CONFIG_LEVELS = ("system", "user", "global", "repository")
4041

42+
# Section pattern to detect conditional includes.
43+
# https://git-scm.com/docs/git-config#_conditional_includes
44+
CONDITIONAL_INCLUDE_REGEXP = re.compile(r"(?<=includeIf )\"(gitdir|gitdir/i|onbranch):(.+)\"")
45+
4146

4247
class MetaParserBuilder(abc.ABCMeta):
4348

@@ -247,7 +252,7 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje
247252
# list of RawConfigParser methods able to change the instance
248253
_mutating_methods_ = ("add_section", "remove_section", "remove_option", "set")
249254

250-
def __init__(self, file_or_files=None, read_only=True, merge_includes=True, config_level=None):
255+
def __init__(self, file_or_files=None, read_only=True, merge_includes=True, config_level=None, repo=None):
251256
"""Initialize a configuration reader to read the given file_or_files and to
252257
possibly allow changes to it by setting read_only False
253258
@@ -262,7 +267,10 @@ def __init__(self, file_or_files=None, read_only=True, merge_includes=True, conf
262267
:param merge_includes: if True, we will read files mentioned in [include] sections and merge their
263268
contents into ours. This makes it impossible to write back an individual configuration file.
264269
Thus, if you want to modify a single configuration file, turn this off to leave the original
265-
dataset unaltered when reading it."""
270+
dataset unaltered when reading it.
271+
:param repo: Reference to repository to use if [includeIf] sections are found in configuration files.
272+
273+
"""
266274
cp.RawConfigParser.__init__(self, dict_type=_OMD)
267275

268276
# Used in python 3, needs to stay in sync with sections for underlying implementation to work
@@ -284,6 +292,7 @@ def __init__(self, file_or_files=None, read_only=True, merge_includes=True, conf
284292
self._dirty = False
285293
self._is_initialized = False
286294
self._merge_includes = merge_includes
295+
self._repo = repo
287296
self._lock = None
288297
self._acquire_lock()
289298

@@ -443,7 +452,57 @@ def string_decode(v):
443452
raise e
444453

445454
def _has_includes(self):
446-
return self._merge_includes and self.has_section('include')
455+
return self._merge_includes and len(self._included_paths())
456+
457+
def _included_paths(self):
458+
"""Return all paths that must be included to configuration.
459+
"""
460+
paths = []
461+
462+
for section in self.sections():
463+
if section == "include":
464+
paths += self.items(section)
465+
466+
match = CONDITIONAL_INCLUDE_REGEXP.search(section)
467+
if match is None or self._repo is None:
468+
continue
469+
470+
keyword = match.group(1)
471+
value = match.group(2).strip()
472+
473+
if keyword in ["gitdir", "gitdir/i"]:
474+
value = osp.expanduser(value)
475+
476+
if not any(value.startswith(s) for s in ["./", "/"]):
477+
value = "**/" + value
478+
if value.endswith("/"):
479+
value += "**"
480+
481+
# Ensure that glob is always case insensitive if required.
482+
if keyword.endswith("/i"):
483+
value = re.sub(
484+
r"[a-zA-Z]",
485+
lambda m: "[{}{}]".format(
486+
m.group().lower(),
487+
m.group().upper()
488+
),
489+
value
490+
)
491+
492+
if fnmatch.fnmatchcase(self._repo.git_dir, value):
493+
paths += self.items(section)
494+
495+
elif keyword == "onbranch":
496+
try:
497+
branch_name = self._repo.active_branch.name
498+
except TypeError:
499+
# Ignore section if active branch cannot be retrieved.
500+
continue
501+
502+
if fnmatch.fnmatchcase(branch_name, value):
503+
paths += self.items(section)
504+
505+
return paths
447506

448507
def read(self):
449508
"""Reads the data stored in the files we have been initialized with. It will
@@ -482,7 +541,7 @@ def read(self):
482541
# Read includes and append those that we didn't handle yet
483542
# We expect all paths to be normalized and absolute (and will assure that is the case)
484543
if self._has_includes():
485-
for _, include_path in self.items('include'):
544+
for _, include_path in self._included_paths():
486545
if include_path.startswith('~'):
487546
include_path = osp.expanduser(include_path)
488547
if not osp.isabs(include_path):

git/repo/base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -452,7 +452,7 @@ def config_reader(self, config_level=None):
452452
files = [self._get_config_path(f) for f in self.config_level]
453453
else:
454454
files = [self._get_config_path(config_level)]
455-
return GitConfigParser(files, read_only=True)
455+
return GitConfigParser(files, read_only=True, repo=self)
456456

457457
def config_writer(self, config_level="repository"):
458458
"""
@@ -467,7 +467,7 @@ def config_writer(self, config_level="repository"):
467467
system = system wide configuration file
468468
global = user level configuration file
469469
repository = configuration file for this repostory only"""
470-
return GitConfigParser(self._get_config_path(config_level), read_only=False)
470+
return GitConfigParser(self._get_config_path(config_level), read_only=False, repo=self)
471471

472472
def commit(self, rev=None):
473473
"""The Commit object for the specified revision

test/test_config.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
import glob
88
import io
9+
import os
10+
from unittest import mock
911

1012
from git import (
1113
GitConfigParser
@@ -238,6 +240,128 @@ def check_test_value(cr, value):
238240
with GitConfigParser(fpa, read_only=True) as cr:
239241
check_test_value(cr, tv)
240242

243+
@with_rw_directory
244+
def test_conditional_includes_from_git_dir(self, rw_dir):
245+
# Initiate repository path
246+
git_dir = osp.join(rw_dir, "target1", "repo1")
247+
os.makedirs(git_dir)
248+
249+
# Initiate mocked repository
250+
repo = mock.Mock(git_dir=git_dir)
251+
252+
# Initiate config files.
253+
path1 = osp.join(rw_dir, "config1")
254+
path2 = osp.join(rw_dir, "config2")
255+
template = "[includeIf \"{}:{}\"]\n path={}\n"
256+
257+
with open(path1, "w") as stream:
258+
stream.write(template.format("gitdir", git_dir, path2))
259+
260+
# Ensure that config is ignored if no repo is set.
261+
with GitConfigParser(path1) as config:
262+
assert not config._has_includes()
263+
assert config._included_paths() == []
264+
265+
# Ensure that config is included if path is matching git_dir.
266+
with GitConfigParser(path1, repo=repo) as config:
267+
assert config._has_includes()
268+
assert config._included_paths() == [("path", path2)]
269+
270+
# Ensure that config is ignored if case is incorrect.
271+
with open(path1, "w") as stream:
272+
stream.write(template.format("gitdir", git_dir.upper(), path2))
273+
274+
with GitConfigParser(path1, repo=repo) as config:
275+
assert not config._has_includes()
276+
assert config._included_paths() == []
277+
278+
# Ensure that config is included if case is ignored.
279+
with open(path1, "w") as stream:
280+
stream.write(template.format("gitdir/i", git_dir.upper(), path2))
281+
282+
with GitConfigParser(path1, repo=repo) as config:
283+
assert config._has_includes()
284+
assert config._included_paths() == [("path", path2)]
285+
286+
# Ensure that config is included with path using glob pattern.
287+
with open(path1, "w") as stream:
288+
stream.write(template.format("gitdir", "**/repo1", path2))
289+
290+
with GitConfigParser(path1, repo=repo) as config:
291+
assert config._has_includes()
292+
assert config._included_paths() == [("path", path2)]
293+
294+
# Ensure that config is ignored if path is not matching git_dir.
295+
with open(path1, "w") as stream:
296+
stream.write(template.format("gitdir", "incorrect", path2))
297+
298+
with GitConfigParser(path1, repo=repo) as config:
299+
assert not config._has_includes()
300+
assert config._included_paths() == []
301+
302+
# Ensure that config is included if path in hierarchy.
303+
with open(path1, "w") as stream:
304+
stream.write(template.format("gitdir", "target1/", path2))
305+
306+
with GitConfigParser(path1, repo=repo) as config:
307+
assert config._has_includes()
308+
assert config._included_paths() == [("path", path2)]
309+
310+
@with_rw_directory
311+
def test_conditional_includes_from_branch_name(self, rw_dir):
312+
# Initiate mocked branch
313+
branch = mock.Mock()
314+
type(branch).name = mock.PropertyMock(return_value="/foo/branch")
315+
316+
# Initiate mocked repository
317+
repo = mock.Mock(active_branch=branch)
318+
319+
# Initiate config files.
320+
path1 = osp.join(rw_dir, "config1")
321+
path2 = osp.join(rw_dir, "config2")
322+
template = "[includeIf \"onbranch:{}\"]\n path={}\n"
323+
324+
# Ensure that config is included is branch is correct.
325+
with open(path1, "w") as stream:
326+
stream.write(template.format("/foo/branch", path2))
327+
328+
with GitConfigParser(path1, repo=repo) as config:
329+
assert config._has_includes()
330+
assert config._included_paths() == [("path", path2)]
331+
332+
# Ensure that config is included is branch is incorrect.
333+
with open(path1, "w") as stream:
334+
stream.write(template.format("incorrect", path2))
335+
336+
with GitConfigParser(path1, repo=repo) as config:
337+
assert not config._has_includes()
338+
assert config._included_paths() == []
339+
340+
# Ensure that config is included with branch using glob pattern.
341+
with open(path1, "w") as stream:
342+
stream.write(template.format("/foo/**", path2))
343+
344+
with GitConfigParser(path1, repo=repo) as config:
345+
assert config._has_includes()
346+
assert config._included_paths() == [("path", path2)]
347+
348+
@with_rw_directory
349+
def test_conditional_includes_from_branch_name_error(self, rw_dir):
350+
# Initiate mocked repository to raise an error if HEAD is detached.
351+
repo = mock.Mock()
352+
type(repo).active_branch = mock.PropertyMock(side_effect=TypeError)
353+
354+
# Initiate config file.
355+
path1 = osp.join(rw_dir, "config1")
356+
357+
# Ensure that config is ignored when active branch cannot be found.
358+
with open(path1, "w") as stream:
359+
stream.write("[includeIf \"onbranch:foo\"]\n path=/path\n")
360+
361+
with GitConfigParser(path1, repo=repo) as config:
362+
assert not config._has_includes()
363+
assert config._included_paths() == []
364+
241365
def test_rename(self):
242366
file_obj = self._to_memcache(fixture_path('git_config'))
243367
with GitConfigParser(file_obj, read_only=False, merge_includes=False) as cw:

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy