diff --git a/docs/use-cases/default-groups.md b/docs/use-cases/default-groups.md index a42bdb7c..fcda587a 100644 --- a/docs/use-cases/default-groups.md +++ b/docs/use-cases/default-groups.md @@ -13,6 +13,20 @@ To ignore the `default_group` and install all dependencies, use the `all` keywor $ gitman install all ``` +## Empty Default Groups + +To prevent any dependencies from being cloned by default, define a group with no members and set it as the `default_group`: + +```yaml +groups: + - name: empty + members: null + +default_group: empty +``` + +When `gitman install` is run, no dependencies will be installed unless a specific group or dependency is requested, or the default group is overridden with `all` or `--no-defaults`. + ## Nested Projects When nested Gitman projects are used default groups are installed if they diff --git a/gitman/models/config.py b/gitman/models/config.py index 243aaf50..7b903732 100644 --- a/gitman/models/config.py +++ b/gitman/models/config.py @@ -420,8 +420,9 @@ def _get_sources_filter( [source.name for source in sources if source.name in names_list] ) - # Fall back to all sources if allowed - if not sources_filter: + # Fall back to all sources if allowed, unless a group matched (an + # empty group is intentionally empty, not a request for everything) + if not sources_filter and not groups_filter: if names and names_list != ["all"]: log.warn(f"No dependencies match: {' '.join(names)}") else: diff --git a/gitman/tests/test_models_config.py b/gitman/tests/test_models_config.py index 9c1c2ba4..d5c59318 100644 --- a/gitman/tests/test_models_config.py +++ b/gitman/tests/test_models_config.py @@ -5,7 +5,7 @@ import pytest from expecter import expect -from gitman.models import Config, load_config +from gitman.models import Config, Group, Source, load_config from .conftest import FILES @@ -135,6 +135,62 @@ def it_can_get_dependency_path(config): "m/root/m/location/foobar" ) + def describe_get_sources_filter(): + def it_returns_empty_when_default_group_has_no_members(config): + config.sources = [Source(repo="http://example.com/repo1", name="repo1")] + config.groups = [Group(name="empty", members=[])] + config.default_group = "empty" + + expect( + config._get_sources_filter( + sources=config.sources, skip_default_group=False + ) + ) == [] + + def it_returns_empty_when_empty_group_specified_by_name(config): + config.sources = [Source(repo="http://example.com/repo1", name="repo1")] + config.groups = [Group(name="empty", members=[])] + config.default_group = "" + + expect( + config._get_sources_filter( + "empty", sources=config.sources, skip_default_group=False + ) + ) == [] + + def it_returns_all_when_skip_default_group_is_true(config): + config.sources = [Source(repo="http://example.com/repo1", name="repo1")] + config.groups = [Group(name="empty", members=[])] + config.default_group = "empty" + + expect( + config._get_sources_filter( + sources=config.sources, skip_default_group=True + ) + ) == ["repo1"] + + def it_returns_all_when_name_all_is_specified(config): + config.sources = [Source(repo="http://example.com/repo1", name="repo1")] + config.groups = [Group(name="empty", members=[])] + config.default_group = "empty" + + expect( + config._get_sources_filter( + "all", sources=config.sources, skip_default_group=False + ) + ) == ["repo1"] + + def it_returns_all_when_default_group_is_blank(config): + config.sources = [Source(repo="http://example.com/repo1", name="repo1")] + config.groups = [Group(name="empty", members=[])] + config.default_group = "" + + expect( + config._get_sources_filter( + sources=config.sources, skip_default_group=False + ) + ) == ["repo1"] + class TestLoad: def test_load_from_directory_with_config_file(self): diff --git a/tests/test_api.py b/tests/test_api.py index 38ff8b87..e9e50888 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -2,7 +2,6 @@ import inspect import os -import shutil from contextlib import suppress import log @@ -74,8 +73,11 @@ def config(): log.info("Temporary directory: %s", TMP) - with suppress(FileNotFoundError, PermissionError): - shutil.rmtree(TMP) + # Leave TMP first: Windows can't delete the current working directory, + # and other tests (e.g. `Config.install_dependencies`) may leave us inside it + os.chdir(ROOT) + # 'shell.rm' handles read-only files (e.g. `.git` objects) on Windows + shell.rm(TMP) with suppress(FileExistsError): os.makedirs(TMP) os.chdir(TMP) @@ -623,6 +625,79 @@ def it_installs_all_sources_when_all_specified(config_with_default_group): ) ) == True + @pytest.fixture + def config_with_empty_default_group(config): + config.datafile.text = strip(""" + location: deps + sources: + - repo: https://github.com/jacebrowning/gitman-demo + name: gitman_1 + type: git + params: + sparse_paths: + - + rev: example-branch + links: + - + scripts: + - + patches: + - + - repo: https://github.com/jacebrowning/gitman-demo + name: gitman_2 + type: git + params: + sparse_paths: + - + rev: example-tag + links: + - + scripts: + - + patches: + - + groups: + - name: empty + members: null + - name: secondary + members: + - gitman_2 + default_group: 'empty' + """) + config.datafile.load() + + return config + + def it_installs_nothing_when_default_group_empty( + config_with_empty_default_group, + ): + gitman.install(depth=1, force=True) + expect( + os.path.exists( + os.path.join(config_with_empty_default_group.location, "gitman_1") + ) + ) == False + expect( + os.path.exists( + os.path.join(config_with_empty_default_group.location, "gitman_2") + ) + ) == False + + def it_installs_all_sources_when_all_specified_and_empty_default_group( + config_with_empty_default_group, + ): + expect(gitman.install(depth=1, force=True, skip_default_group=True)) == True + expect( + os.path.exists( + os.path.join(config_with_empty_default_group.location, "gitman_1") + ) + ) == True + expect( + os.path.exists( + os.path.join(config_with_empty_default_group.location, "gitman_2") + ) + ) == True + def describe_uninstall(): def it_deletes_dependencies_when_they_exist(config):