Skip to content

Commit 664d499

Browse files
committed
Ensure that ddev does not treat worktrees as extra packages
1 parent 04f0081 commit 664d499

File tree

17 files changed

+279
-142
lines changed

17 files changed

+279
-142
lines changed

ddev/changelog.d/20444.added

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add support for worktrees. Ddev now understand and differentiate worktrees from other packages

ddev/src/ddev/repo/core.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ def git(self) -> GitRepository:
4343
def integrations(self) -> IntegrationRegistry:
4444
return self.__integrations
4545

46+
@cached_property
47+
def worktrees(self) -> list[str]:
48+
return self.git.worktrees
49+
4650
@cached_property
4751
def config(self) -> RepositoryConfig:
4852
from ddev.repo.config import RepositoryConfig
@@ -80,7 +84,7 @@ def get(self, name: str) -> Integration:
8084
return self.__cache[name]
8185

8286
path = self.repo.path / name
83-
if not path.is_dir():
87+
if not (path.is_dir() and path.name not in self.repo.worktrees):
8488
raise OSError(f'Integration does not exist: {Path(self.repo.path.name, name)}')
8589

8690
integration = Integration(path, self.repo.path, self.repo.config)
@@ -176,6 +180,10 @@ def __iter_filtered(self, selection: Iterable[str] = ()) -> Iterable[Integration
176180
return
177181

178182
for path in sorted(self.repo.path.iterdir()):
183+
# Ignore any directory that is a worktree
184+
if path.name in self.repo.worktrees:
185+
continue
186+
179187
integration = self.__get_from_path(path)
180188
if selected and integration.name not in selected:
181189
continue

ddev/src/ddev/utils/git.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,21 @@ def __init__(self, repo_root: Path):
3333
def repo_root(self) -> Path:
3434
return self.__repo_root
3535

36+
@property
37+
def worktrees(self) -> list[str]:
38+
from ddev.utils.fs import Path
39+
40+
worktree_output = self.capture('worktree', 'list', '--porcelain')
41+
worktrees = [
42+
Path(line.split()[1]).relative_to(self.repo_root.resolve()).name
43+
for line in worktree_output.splitlines()
44+
if line.startswith('worktree')
45+
]
46+
47+
# Avoid returning empty strings. The root of the repo is always a worktree and
48+
# the name of "." (the root of the repo relative to itself) is ""
49+
return [worktree for worktree in worktrees if worktree]
50+
3651
def current_branch(self) -> str:
3752
return self.capture('rev-parse', '--abbrev-ref', 'HEAD').strip()
3853

@@ -87,7 +102,12 @@ def changed_files(self) -> list[str]:
87102
changed_files.add(line)
88103

89104
# Untracked
90-
changed_files.update(self.capture('ls-files', '--others', '--exclude-standard').splitlines())
105+
# Remove worktrees as they can be untracked and should not be taken into account
106+
changed_files.update(
107+
untracked_file
108+
for untracked_file in self.capture('ls-files', '--others', '--exclude-standard').splitlines()
109+
if not any(untracked_file.startswith(worktree) for worktree in self.worktrees)
110+
)
91111

92112
return sorted(changed_files, key=lambda relative_path: (-relative_path.count('/'), relative_path))
93113

ddev/tests/cli/env/conftest.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,12 @@ def _write(*args, **kwargs):
3636
return _write
3737

3838
return _write_result_file
39+
40+
41+
@pytest.fixture(autouse=True)
42+
def mock_repo_worktrees(mocker):
43+
from ddev.repo.core import Repository
44+
45+
# Patch the cached_property 'worktrees' on the Repository class
46+
# For all instances, it will now return an empty list, bypassing the git call.
47+
mocker.patch.object(Repository, "worktrees", new_callable=mocker.PropertyMock, return_value=[])

ddev/tests/cli/meta/scripts/conftest.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@
1212

1313

1414
@pytest.fixture
15-
def fake_repo(tmp_path_factory, config_file, ddev):
15+
def fake_repo(tmp_path_factory, config_file, ddev, mocker):
1616
repo_path = tmp_path_factory.mktemp('integrations-core')
1717
repo = Repository('integrations-core', str(repo_path))
1818

19-
config_file.model.repos['core'] = str(repo.path)
19+
mocker.patch.object(Repository, 'worktrees', new_callable=mocker.PropertyMock, return_value=[])
20+
21+
config_file.model.repos["core"] = str(repo.path)
2022
config_file.save()
2123

2224
write_file(

ddev/tests/cli/release/test_changelog.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@ def fragments_dir(self, repo_with_towncrier, network_replay, mocker):
261261
'M ddev/pyproject.toml',
262262
'',
263263
'',
264+
'',
264265
'M ddev/pyproject.toml',
265266
'',
266267
'',

ddev/tests/cli/test/test_test.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@
1111
from ddev.utils.structures import EnvVars
1212

1313

14+
@pytest.fixture(autouse=True)
15+
def mock_worktrees(mocker):
16+
# Mock the access to worktrees because these tests mock the global subprocess run
17+
# Should be refactored to avoid such a broad mock
18+
mocker.patch('ddev.utils.git.GitRepository.worktrees', return_value=[])
19+
20+
1421
class TestInputValidation:
1522
@pytest.mark.parametrize('flag', ('--lint', '--fmt', '--bench', '--latest'))
1623
def test_specific_environment_and_functionality(self, ddev, helpers, flag):

ddev/tests/cli/test_dep.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,14 +291,17 @@ def add_mock(url, response_value):
291291

292292

293293
@pytest.fixture
294-
def fake_repo(tmp_path, config_file):
294+
def fake_repo(tmp_path, config_file, mocker):
295295
data_folder = tmp_path / 'datadog_checks_base' / 'datadog_checks' / 'base' / 'data'
296296
data_folder.mkdir(parents=True)
297297

298298
# Set this as core repo in the config
299299
config_file.model.repos['core'] = str(tmp_path)
300300
config_file.save()
301301

302+
# Mock the access to worktrees because this is a fake repo
303+
mocker.patch('ddev.utils.git.GitRepository.worktrees', return_value=[])
304+
302305
yield tmp_path
303306

304307

ddev/tests/cli/validate/conftest.py

Lines changed: 66 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -5,43 +5,32 @@
55
from datadog_checks.dev.tooling.constants import set_root
66

77
from ddev.repo.core import Repository
8+
from tests.helpers.api import write_file
89

9-
10-
def _fake_repo(tmp_path_factory, config_file, name):
11-
set_root('') # for dcd compatibility running the tests
12-
repo_path = tmp_path_factory.mktemp(name)
13-
repo = Repository(name, str(repo_path))
14-
15-
config_file.model.repos[name] = str(repo.path)
16-
config_file.model.repo = name
17-
config_file.save()
18-
19-
write_file(
20-
repo.path / '.github',
10+
FILES_IN_FAKE_REPO = [
11+
# Codeowners file
12+
(
13+
'.github',
2114
'CODEOWNERS',
2215
"""
2316
/dummy/ @DataDog/agent-integrations
2417
/dummy2/ @DataDog/agent-integrations
2518
""",
26-
)
27-
28-
write_file(
29-
repo_path / ".ddev",
19+
),
20+
# Ddev config file
21+
(
22+
'.ddev',
3023
'config.toml',
3124
"""[overrides.validate.labeler]
3225
include = ["datadog_checks_tests_helper"]
3326
""",
34-
)
35-
36-
for integration in ('dummy', 'dummy2'):
37-
write_file(
38-
repo_path / integration,
39-
'manifest.json',
40-
"""We don't need the content for this test, we just need the file""",
41-
)
42-
43-
write_file(
44-
repo_path / '.github' / 'workflows' / 'config',
27+
),
28+
# Dummy manifest files
29+
('dummy', 'manifest.json', """We don't need the content for this test, we just need the file"""),
30+
('dummy2', 'manifest.json', """We don't need the content for this test, we just need the file"""),
31+
# Labeler config file
32+
(
33+
'.github/workflows/config',
4534
'labeler.yml',
4635
"""changelog/no-changelog:
4736
- any:
@@ -60,30 +49,68 @@ def _fake_repo(tmp_path_factory, config_file, name):
6049
release:
6150
- '*/__about__.py'
6251
""",
63-
)
52+
),
53+
]
54+
55+
56+
def _fake_repo(tmp_path_factory, config_file, name, files_to_write):
57+
set_root('') # for dcd compatibility running the tests
58+
repo_path = tmp_path_factory.mktemp(name)
59+
repo = Repository(name, str(repo_path))
60+
61+
config_file.model.repos[name] = str(repo.path)
62+
config_file.model.repo = name
63+
config_file.save()
64+
for file_path, file_name, content in files_to_write:
65+
write_file(repo_path / file_path, file_name, content)
6466

6567
return repo
6668

6769

6870
@pytest.fixture
6971
def fake_repo(
72+
request,
7073
tmp_path_factory,
7174
config_file,
75+
mocker,
7276
):
73-
yield _fake_repo(tmp_path_factory, config_file, 'core')
74-
77+
mocker.patch('ddev.utils.git.GitRepository.worktrees', return_value=[])
7578

76-
@pytest.fixture
77-
def fake_extras_repo(tmp_path_factory, config_file):
78-
yield _fake_repo(tmp_path_factory, config_file, 'extras')
79+
yield _fake_repo(
80+
tmp_path_factory,
81+
config_file,
82+
'core',
83+
request.param if hasattr(request, 'param') else FILES_IN_FAKE_REPO,
84+
)
7985

8086

8187
@pytest.fixture
82-
def fake_marketplace_repo(tmp_path_factory, config_file):
83-
yield _fake_repo(tmp_path_factory, config_file, 'marketplace')
88+
def fake_extras_repo(
89+
request,
90+
tmp_path_factory,
91+
config_file,
92+
mocker,
93+
):
94+
mocker.patch('ddev.utils.git.GitRepository.worktrees', return_value=[])
95+
yield _fake_repo(
96+
tmp_path_factory,
97+
config_file,
98+
'extras',
99+
request.param if hasattr(request, 'param') else FILES_IN_FAKE_REPO,
100+
)
84101

85102

86-
def write_file(folder, file, content):
87-
folder.mkdir(exist_ok=True, parents=True)
88-
file_path = folder / file
89-
file_path.write_text(content)
103+
@pytest.fixture
104+
def fake_marketplace_repo(
105+
request,
106+
tmp_path_factory,
107+
config_file,
108+
mocker,
109+
):
110+
mocker.patch('ddev.utils.git.GitRepository.worktrees', return_value=[])
111+
yield _fake_repo(
112+
tmp_path_factory,
113+
config_file,
114+
'marketplace',
115+
request.param if hasattr(request, 'param') else FILES_IN_FAKE_REPO,
116+
)

ddev/tests/cli/validate/test_codeowners.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# (C) Datadog, Inc. 2024-present
22
# All rights reserved
33
# Licensed under a 3-clause BSD style license (see LICENSE)
4-
from .conftest import write_file
4+
from tests.helpers.api import write_file
55

66

77
def test_codeowners_integrations_core(fake_repo, ddev):

0 commit comments

Comments
 (0)