Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/use-cases/default-groups.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions gitman/models/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
58 changes: 57 additions & 1 deletion gitman/tests/test_models_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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):
Expand Down
81 changes: 78 additions & 3 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import inspect
import os
import shutil
from contextlib import suppress

import log
Expand Down Expand Up @@ -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)

@jacebrowning jacebrowning Oct 12, 2026 •

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to have introduced a new failure on Windows.

with suppress(FileExistsError):
os.makedirs(TMP)
os.chdir(TMP)
Expand Down Expand Up @@ -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):
Expand Down
Loading