feat: config profile multi-repo mounts
- Add mappings array support to git_mount entries - Clone repository once per git_mount entry, mount multiple subdirectories - Normalize legacy source_path+target_path to mappings on read - Update _merge_git_mounts to dedup by (remote_url, branch) and concatenate mappings - Add _normalize_git_mount, _clone_git_repo, _resolve_git_mount_mappings helpers - Update GitMountItem Pydantic model with GitMountMapping and model_validator - Update frontend GitMountEditor component with mappings UI - Auto-convert legacy git mount entries to mappings format on load - Add 15 backend unit tests for normalization, resolution, and glob expansion - Update existing config profile resolver tests for new merge behavior Quality gates: pytest 167 passed, frontend typecheck clean Addresses: config-profile-multi-repo-mounts
This commit is contained in:
@@ -176,21 +176,59 @@ def _merge_git_mounts(
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Merge git mounts from included profiles.
|
||||
|
||||
Later mounts override earlier ones with the same remote_url + target_path combo.
|
||||
Entries with the same remote_url + branch have their mappings concatenated.
|
||||
Different repos are kept as separate entries.
|
||||
All entries are normalized to the mappings format.
|
||||
"""
|
||||
result = list(base)
|
||||
# Build lookup by (remote_url, target_path)
|
||||
seen = {(m["remote_url"], m["target_path"]): i for i, m in enumerate(result)}
|
||||
# Normalize existing entries to mappings format
|
||||
for i, m in enumerate(result):
|
||||
result[i] = _normalize_git_mount_entry(dict(m))
|
||||
|
||||
# Build lookup by (remote_url, branch)
|
||||
seen = {}
|
||||
for i, m in enumerate(result):
|
||||
key = (m["remote_url"], m.get("branch"))
|
||||
seen[key] = i
|
||||
|
||||
for mount in overlay:
|
||||
key = (mount["remote_url"], mount["target_path"])
|
||||
mount = _normalize_git_mount_entry(dict(mount))
|
||||
key = (mount["remote_url"], mount.get("branch"))
|
||||
if key in seen:
|
||||
result[seen[key]] = dict(mount)
|
||||
# Same repo+branch: concatenate mappings, dedup by (source_path, target_path)
|
||||
existing = result[seen[key]]
|
||||
existing_sources = {
|
||||
(m["source_path"], m["target_path"])
|
||||
for m in existing.get("mappings", [])
|
||||
}
|
||||
for mapping in mount.get("mappings", []):
|
||||
map_key = (mapping["source_path"], mapping["target_path"])
|
||||
if map_key not in existing_sources:
|
||||
existing["mappings"].append(dict(mapping))
|
||||
existing_sources.add(map_key)
|
||||
else:
|
||||
seen[key] = len(result)
|
||||
result.append(dict(mount))
|
||||
result.append(mount)
|
||||
return result
|
||||
|
||||
|
||||
def _normalize_git_mount_entry(entry: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Normalize a git mount entry to the unified mappings format.
|
||||
|
||||
Converts legacy source_path + target_path into a single-entry mappings array.
|
||||
"""
|
||||
entry = dict(entry)
|
||||
if "mappings" not in entry or not entry.get("mappings"):
|
||||
source = entry.get("source_path", ".")
|
||||
target = entry.get("target_path")
|
||||
if target is not None:
|
||||
entry["mappings"] = [{"source_path": source, "target_path": target}]
|
||||
# Remove legacy fields once normalized
|
||||
entry.pop("source_path", None)
|
||||
entry.pop("target_path", None)
|
||||
return entry
|
||||
|
||||
|
||||
async def _resolve_profile_recursive(
|
||||
session: AsyncSession,
|
||||
profile_id: uuid.UUID,
|
||||
|
||||
Reference in New Issue
Block a user