66 lines
2.3 KiB
Python
66 lines
2.3 KiB
Python
"""Deterministic path-level three-way merge for plugin supporting files."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from typing import Any
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class SupportingFileDecision:
|
|
path: str
|
|
source: str
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
return {"path": self.path, "source": self.source}
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class SupportingFileConflict:
|
|
path: str
|
|
reason: str
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
return {"path": self.path, "reason": self.reason}
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class SupportingFileMergePlan:
|
|
files: dict[str, SupportingFileDecision] = field(default_factory=dict)
|
|
conflicts: list[SupportingFileConflict] = field(default_factory=list)
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
return {
|
|
"files": {path: decision.to_dict() for path, decision in sorted(self.files.items())},
|
|
"conflicts": [conflict.to_dict() for conflict in self.conflicts],
|
|
}
|
|
|
|
|
|
def merge_supporting_file_trees(
|
|
*,
|
|
base: dict[str, Any],
|
|
local: dict[str, Any],
|
|
upstream: dict[str, Any],
|
|
) -> SupportingFileMergePlan:
|
|
decisions: dict[str, SupportingFileDecision] = {}
|
|
conflicts: list[SupportingFileConflict] = []
|
|
for path in sorted({*base.keys(), *local.keys(), *upstream.keys()} - {"SKILL.md"}):
|
|
b = base.get(path)
|
|
l = local.get(path)
|
|
u = upstream.get(path)
|
|
if l == u and l is not None:
|
|
decisions[path] = SupportingFileDecision(path=path, source="local")
|
|
elif l == b and u is not None:
|
|
decisions[path] = SupportingFileDecision(path=path, source="upstream")
|
|
elif u == b and l is not None:
|
|
decisions[path] = SupportingFileDecision(path=path, source="local")
|
|
elif b is None and l is None and u is not None:
|
|
decisions[path] = SupportingFileDecision(path=path, source="upstream")
|
|
elif b is None and u is None and l is not None:
|
|
decisions[path] = SupportingFileDecision(path=path, source="local")
|
|
elif b is not None and l is None and u is None:
|
|
continue
|
|
else:
|
|
conflicts.append(SupportingFileConflict(path=path, reason="divergent supporting-file change"))
|
|
return SupportingFileMergePlan(files=decisions, conflicts=conflicts)
|