diff --git a/app/src/confgen/diff.py b/app/src/confgen/diff.py new file mode 100644 index 0000000..287df43 --- /dev/null +++ b/app/src/confgen/diff.py @@ -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), + }