Skip to content

Commit c4707a9

Browse files
committed
src/cli(fix[add]): preserve duplicate workspace sections when --no-merge
why: should respect duplicate workspace roots instead of rewriting configs when users opt out of merging. what: - track ordered top-level items from the loader and aggregate them for duplicate-aware operations - introduce duplicate-preserving YAML writer and invoke it for --no-merge updates
1 parent 306562a commit c4707a9

File tree

2 files changed

+79
-5
lines changed

2 files changed

+79
-5
lines changed

src/vcspull/cli/add.py

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

33
from __future__ import annotations
44

5+
import copy
56
import logging
67
import pathlib
78
import subprocess
@@ -18,6 +19,7 @@
1819
merge_duplicate_workspace_roots,
1920
normalize_workspace_roots,
2021
save_config_yaml,
22+
save_config_yaml_with_items,
2123
workspace_root_label,
2224
)
2325
from vcspull.util import contract_user_home
@@ -335,14 +337,15 @@ def add_repo(
335337
# Load existing config
336338
raw_config: dict[str, t.Any]
337339
duplicate_root_occurrences: dict[str, list[t.Any]]
340+
top_level_items: list[tuple[str, t.Any]]
338341
display_config_path = contract_user_home(config_file_path)
339342

340343
if config_file_path.exists() and config_file_path.is_file():
341344
try:
342345
(
343346
raw_config,
344347
duplicate_root_occurrences,
345-
_top_level_items,
348+
top_level_items,
346349
) = DuplicateAwareConfigReader.load_with_duplicates(config_file_path)
347350
except TypeError:
348351
log.exception(
@@ -358,11 +361,32 @@ def add_repo(
358361
else:
359362
raw_config = {}
360363
duplicate_root_occurrences = {}
364+
top_level_items = []
361365
log.info(
362366
"Config file %s not found. A new one will be created.",
363367
display_config_path,
364368
)
365369

370+
config_items: list[tuple[str, t.Any]] = (
371+
[(label, copy.deepcopy(section)) for label, section in top_level_items]
372+
if top_level_items
373+
else [(label, copy.deepcopy(section)) for label, section in raw_config.items()]
374+
)
375+
376+
def _aggregate_items(items: list[tuple[str, t.Any]]) -> dict[str, t.Any]:
377+
aggregated: dict[str, t.Any] = {}
378+
for label, section in items:
379+
if isinstance(section, dict):
380+
workspace_section = aggregated.setdefault(label, {})
381+
for repo_name, repo_config in section.items():
382+
workspace_section[repo_name] = copy.deepcopy(repo_config)
383+
else:
384+
aggregated[label] = copy.deepcopy(section)
385+
return aggregated
386+
387+
if not merge_duplicates:
388+
raw_config = _aggregate_items(config_items)
389+
366390
duplicate_merge_conflicts: list[str] = []
367391
duplicate_merge_changes = 0
368392
duplicate_merge_details: list[tuple[str, int]] = []
@@ -417,14 +441,18 @@ def add_repo(
417441
cwd = pathlib.Path.cwd()
418442
home = pathlib.Path.home()
419443

444+
aggregated_config = (
445+
raw_config if merge_duplicates else _aggregate_items(config_items)
446+
)
447+
420448
if merge_duplicates:
421449
(
422450
raw_config,
423451
workspace_map,
424452
merge_conflicts,
425453
merge_changes,
426454
) = normalize_workspace_roots(
427-
raw_config,
455+
aggregated_config,
428456
cwd=cwd,
429457
home=home,
430458
)
@@ -436,7 +464,7 @@ def add_repo(
436464
merge_conflicts,
437465
_merge_changes,
438466
) = normalize_workspace_roots(
439-
raw_config,
467+
aggregated_config,
440468
cwd=cwd,
441469
home=home,
442470
)
@@ -459,15 +487,24 @@ def add_repo(
459487
)
460488
workspace_map[workspace_path] = workspace_label
461489
raw_config.setdefault(workspace_label, {})
490+
if not merge_duplicates:
491+
config_items.append((workspace_label, {}))
462492

463493
if workspace_label not in raw_config:
464494
raw_config[workspace_label] = {}
495+
if not merge_duplicates:
496+
config_items.append((workspace_label, {}))
465497
elif not isinstance(raw_config[workspace_label], dict):
466498
log.error(
467499
"Workspace root '%s' in configuration is not a dictionary. Aborting.",
468500
workspace_label,
469501
)
470502
return
503+
workspace_sections: list[tuple[int, dict[str, t.Any]]] = [
504+
(idx, section)
505+
for idx, (label, section) in enumerate(config_items)
506+
if label == workspace_label and isinstance(section, dict)
507+
]
471508

472509
# Check if repo already exists
473510
if name in raw_config[workspace_label]:
@@ -518,7 +555,18 @@ def add_repo(
518555
return
519556

520557
# Add the repository in verbose format
521-
raw_config[workspace_label][name] = {"repo": url}
558+
new_repo_entry = {"repo": url}
559+
if merge_duplicates:
560+
raw_config[workspace_label][name] = new_repo_entry
561+
else:
562+
target_section: dict[str, t.Any]
563+
if workspace_sections:
564+
_, target_section = workspace_sections[-1]
565+
else:
566+
target_section = {}
567+
config_items.append((workspace_label, target_section))
568+
target_section[name] = copy.deepcopy(new_repo_entry)
569+
raw_config[workspace_label][name] = copy.deepcopy(new_repo_entry)
522570

523571
# Save or preview config
524572
if dry_run:
@@ -541,7 +589,10 @@ def add_repo(
541589
)
542590
else:
543591
try:
544-
save_config_yaml(config_file_path, raw_config)
592+
if merge_duplicates:
593+
save_config_yaml(config_file_path, raw_config)
594+
else:
595+
save_config_yaml_with_items(config_file_path, config_items)
545596
log.info(
546597
"%s✓%s Successfully added %s'%s'%s (%s%s%s) to %s%s%s under '%s%s%s'.",
547598
Fore.GREEN,

src/vcspull/config.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,29 @@ def save_config_yaml(config_file_path: pathlib.Path, data: dict[t.Any, t.Any]) -
481481
config_file_path.write_text(yaml_content, encoding="utf-8")
482482

483483

484+
def save_config_yaml_with_items(
485+
config_file_path: pathlib.Path,
486+
items: list[tuple[str, t.Any]],
487+
) -> None:
488+
"""Persist configuration data while preserving duplicate top-level sections."""
489+
documents: list[str] = []
490+
491+
for label, section in items:
492+
dumped = ConfigReader._dump(
493+
fmt="yaml",
494+
content={label: section},
495+
indent=2,
496+
).rstrip()
497+
if dumped:
498+
documents.append(dumped)
499+
500+
yaml_content = "\n".join(documents)
501+
if yaml_content:
502+
yaml_content += "\n"
503+
504+
config_file_path.write_text(yaml_content, encoding="utf-8")
505+
506+
484507
def merge_duplicate_workspace_root_entries(
485508
label: str,
486509
occurrences: list[t.Any],

0 commit comments

Comments
 (0)