Initial upload with full project structure
This commit is contained in:
157
app/src/confgen/diff.py
Normal file
157
app/src/confgen/diff.py
Normal file
@@ -0,0 +1,157 @@
|
||||
"""Diff display for configuration changes using Rich."""
|
||||
|
||||
|
||||
class ConfigDiff:
|
||||
"""Diff viewer for configuration changes."""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def show_diff(
|
||||
self,
|
||||
old_content: str,
|
||||
new_content: str,
|
||||
old_label: str = "Current",
|
||||
new_label: str = "Generated",
|
||||
) -> None:
|
||||
"""Show a unified diff between two configurations."""
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
|
||||
console = Console()
|
||||
|
||||
if old_content == new_content:
|
||||
console.print(Panel("[yellow]No changes detected.[/yellow]"))
|
||||
return
|
||||
|
||||
diff_lines = self._generate_diff(old_content, new_content)
|
||||
|
||||
diff_text = Text()
|
||||
for line in diff_lines:
|
||||
if line.startswith("+") and not line.startswith("+++"):
|
||||
diff_text.append(line + "\n", style="green")
|
||||
elif line.startswith("-") and not line.startswith("---"):
|
||||
diff_text.append(line + "\n", style="red")
|
||||
elif line.startswith("@@"):
|
||||
diff_text.append(line + "\n", style="dim")
|
||||
else:
|
||||
diff_text.append(line + "\n", style="white")
|
||||
|
||||
console.print(
|
||||
Panel(
|
||||
diff_text,
|
||||
title=f"[bold]Diff: {old_label} → {new_label}[/bold]",
|
||||
expand=False,
|
||||
)
|
||||
)
|
||||
|
||||
def _generate_diff(
|
||||
self,
|
||||
old_content: str,
|
||||
new_content: str,
|
||||
context: int = 3,
|
||||
old_label: str = "Current",
|
||||
new_label: str = "Generated",
|
||||
) -> list[str]:
|
||||
"""Generate unified diff lines."""
|
||||
old_lines = old_content.splitlines(keepends=True)
|
||||
new_lines = new_content.splitlines(keepends=True)
|
||||
|
||||
diff = []
|
||||
diff.append(f"--- {old_label}")
|
||||
diff.append(f"+++ {new_label}")
|
||||
|
||||
i, j = 0, 0
|
||||
while i < len(old_lines) or j < len(new_lines):
|
||||
if (
|
||||
i < len(old_lines)
|
||||
and j < len(new_lines)
|
||||
and old_lines[i] == new_lines[j]
|
||||
):
|
||||
diff.append(" " + old_lines[i].rstrip())
|
||||
i += 1
|
||||
j += 1
|
||||
else:
|
||||
start_i, start_j = i, j
|
||||
|
||||
while i < len(old_lines) and (
|
||||
j >= len(new_lines) or old_lines[i] != new_lines[j]
|
||||
):
|
||||
i += 1
|
||||
|
||||
while j < len(new_lines) and (
|
||||
i >= len(old_lines) or old_lines[i - 1] != new_lines[j]
|
||||
):
|
||||
j += 1
|
||||
|
||||
before = max(0, start_i - context)
|
||||
after = min(len(old_lines), i + context)
|
||||
|
||||
if before < start_i:
|
||||
diff.append("...")
|
||||
|
||||
for k in range(before, start_i):
|
||||
diff.append(" " + old_lines[k].rstrip())
|
||||
|
||||
for k in range(start_i, i):
|
||||
diff.append("-" + old_lines[k].rstrip())
|
||||
|
||||
for k in range(start_j, j):
|
||||
diff.append("+" + new_lines[k].rstrip())
|
||||
|
||||
if after > i:
|
||||
diff.append("...")
|
||||
|
||||
return diff
|
||||
|
||||
def show_json_diff(
|
||||
self,
|
||||
old_data: dict,
|
||||
new_data: dict,
|
||||
old_label: str = "Current",
|
||||
new_label: str = "Generated",
|
||||
) -> None:
|
||||
"""Show diff between two JSON configurations."""
|
||||
import json
|
||||
|
||||
old_content = json.dumps(old_data, indent=2, sort_keys=True)
|
||||
new_content = json.dumps(new_data, indent=2, sort_keys=True)
|
||||
|
||||
self.show_diff(old_content, new_content, old_label, new_label)
|
||||
|
||||
def show_yaml_diff(
|
||||
self,
|
||||
old_data: dict,
|
||||
new_data: dict,
|
||||
old_label: str = "Current",
|
||||
new_label: str = "Generated",
|
||||
) -> None:
|
||||
"""Show diff between two YAML configurations."""
|
||||
import yaml
|
||||
|
||||
old_content = yaml.dump(old_data, default_flow_style=False, sort_keys=True)
|
||||
new_content = yaml.dump(new_data, default_flow_style=False, sort_keys=True)
|
||||
|
||||
self.show_diff(old_content, new_content, old_label, new_label)
|
||||
|
||||
def format_change_summary(
|
||||
self,
|
||||
old_content: str,
|
||||
new_content: str,
|
||||
) -> dict[str, int]:
|
||||
"""Get a summary of changes."""
|
||||
old_lines = set(old_content.splitlines())
|
||||
new_lines = set(new_content.splitlines())
|
||||
|
||||
added = len(new_lines - old_lines)
|
||||
removed = len(old_lines - new_lines)
|
||||
unchanged = len(old_lines & new_lines)
|
||||
|
||||
return {
|
||||
"added": added,
|
||||
"removed": removed,
|
||||
"unchanged": unchanged,
|
||||
"total_old": len(old_lines),
|
||||
"total_new": len(new_lines),
|
||||
}
|
||||
Reference in New Issue
Block a user