Skip to content

Commit 113ce52

Browse files
committed
config/load(feat[duplicates]): preserve workspace roots when reading
why: Commands using load_configs were still dropping earlier workspace-root entries, so duplicate paths silently lost repositories outside fmt/add/discover. what: - load configs through DuplicateAwareConfigReader and merge duplicates by default while logging conflicts or skipped merges - keep an opt-out via merge_duplicates=False for callers that want legacy behavior - add regression tests ensuring both paths keep expected repos and emit logs
1 parent b59175b commit 113ce52

File tree

2 files changed

+104
-4
lines changed

2 files changed

+104
-4
lines changed

src/vcspull/config.py

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from vcspull.validator import is_valid_config
1515

1616
from . import exc
17-
from ._internal.config_reader import ConfigReader
17+
from ._internal.config_reader import ConfigReader, DuplicateAwareConfigReader
1818
from .util import get_config_dir, update_dict
1919

2020
log = logging.getLogger(__name__)
@@ -241,6 +241,8 @@ def find_config_files(
241241
def load_configs(
242242
files: list[pathlib.Path],
243243
cwd: pathlib.Path | Callable[[], pathlib.Path] = pathlib.Path.cwd,
244+
*,
245+
merge_duplicates: bool = True,
244246
) -> list[ConfigDict]:
245247
"""Return repos from a list of files.
246248
@@ -268,9 +270,44 @@ def load_configs(
268270
if isinstance(file, str):
269271
file = pathlib.Path(file)
270272
assert isinstance(file, pathlib.Path)
271-
conf = ConfigReader._from_file(file)
272-
assert is_valid_config(conf)
273-
newrepos = extract_repos(conf, cwd=cwd)
273+
274+
config_content, duplicate_roots = (
275+
DuplicateAwareConfigReader.load_with_duplicates(file)
276+
)
277+
278+
if merge_duplicates:
279+
(
280+
config_content,
281+
merge_conflicts,
282+
_merge_change_count,
283+
merge_details,
284+
) = merge_duplicate_workspace_roots(config_content, duplicate_roots)
285+
286+
for conflict in merge_conflicts:
287+
log.warning("%s: %s", file, conflict)
288+
289+
for root_label, occurrence_count in merge_details:
290+
duplicate_count = max(occurrence_count - 1, 0)
291+
if duplicate_count == 0:
292+
continue
293+
plural = "entry" if duplicate_count == 1 else "entries"
294+
log.info(
295+
"%s: merged %d duplicate %s for workspace root '%s'",
296+
file,
297+
duplicate_count,
298+
plural,
299+
root_label,
300+
)
301+
elif duplicate_roots:
302+
duplicate_list = ", ".join(sorted(duplicate_roots.keys()))
303+
log.warning(
304+
"%s: duplicate workspace roots detected (%s); keeping last occurrences",
305+
file,
306+
duplicate_list,
307+
)
308+
309+
assert is_valid_config(config_content)
310+
newrepos = extract_repos(config_content, cwd=cwd)
274311

275312
if not repos:
276313
repos.extend(newrepos)

tests/test_config.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import logging
56
import typing as t
67

78
import pytest
@@ -143,3 +144,65 @@ def test_extract_repos_injects_workspace_root(
143144
assert repo["workspace_root"] == expected_root
144145
expected_path = config.expand_dir(pl.Path(expected_root), cwd=tmp_path) / name
145146
assert repo["path"] == expected_path
147+
148+
149+
def _write_duplicate_config(tmp_path: pathlib.Path) -> pathlib.Path:
150+
config_path = tmp_path / "config.yaml"
151+
config_path.write_text(
152+
(
153+
"~/workspace/:\n"
154+
" alpha:\n"
155+
" repo: git+https://example.com/alpha.git\n"
156+
"~/workspace/:\n"
157+
" beta:\n"
158+
" repo: git+https://example.com/beta.git\n"
159+
),
160+
encoding="utf-8",
161+
)
162+
return config_path
163+
164+
165+
def test_load_configs_merges_duplicate_workspace_roots(
166+
tmp_path: pathlib.Path,
167+
caplog: pytest.LogCaptureFixture,
168+
monkeypatch: pytest.MonkeyPatch,
169+
) -> None:
170+
"""Duplicate workspace roots are merged to keep every repository."""
171+
monkeypatch.setenv("HOME", str(tmp_path))
172+
caplog.set_level(logging.INFO, logger="vcspull.config")
173+
174+
config_path = _write_duplicate_config(tmp_path)
175+
176+
repos = config.load_configs([config_path], cwd=tmp_path)
177+
178+
repo_names = {repo["name"] for repo in repos}
179+
assert repo_names == {"alpha", "beta"}
180+
181+
merged_messages = [message for message in caplog.messages if "merged" in message]
182+
assert merged_messages, "Expected a merge log entry for duplicate roots"
183+
184+
185+
def test_load_configs_can_skip_merging_duplicates(
186+
tmp_path: pathlib.Path,
187+
caplog: pytest.LogCaptureFixture,
188+
monkeypatch: pytest.MonkeyPatch,
189+
) -> None:
190+
"""The merge step can be skipped while still warning about duplicates."""
191+
monkeypatch.setenv("HOME", str(tmp_path))
192+
caplog.set_level(logging.WARNING, logger="vcspull.config")
193+
194+
config_path = _write_duplicate_config(tmp_path)
195+
196+
repos = config.load_configs(
197+
[config_path],
198+
cwd=tmp_path,
199+
merge_duplicates=False,
200+
)
201+
202+
repo_names = {repo["name"] for repo in repos}
203+
assert repo_names == {"beta"}
204+
205+
warning_messages = [
206+
message for message in caplog.messages if "duplicate" in message
207+
]
208+
assert warning_messages, "Expected a warning about duplicate workspace roots"

0 commit comments

Comments
 (0)