246 lines
8.3 KiB
Python
Raw Normal View History

2025-11-04 05:17:27 +01:00
import difflib
2025-11-04 08:09:12 +01:00
from typing import Dict, List, Optional, Tuple
2025-11-04 05:17:27 +01:00
from .colors import Colors
class DiffStats:
def __init__(self):
self.insertions = 0
self.deletions = 0
self.modifications = 0
self.files_changed = 0
@property
def total_changes(self):
return self.insertions + self.deletions
def __str__(self):
return f"{self.files_changed} file(s) changed, {self.insertions} insertions(+), {self.deletions} deletions(-)"
class DiffLine:
2025-11-04 08:09:12 +01:00
def __init__(
self,
line_type: str,
content: str,
old_line_num: Optional[int] = None,
new_line_num: Optional[int] = None,
):
2025-11-04 05:17:27 +01:00
self.line_type = line_type
self.content = content
self.old_line_num = old_line_num
self.new_line_num = new_line_num
def format(self, show_line_nums: bool = True) -> str:
color = {
2025-11-04 08:09:12 +01:00
"add": Colors.GREEN,
"delete": Colors.RED,
"context": Colors.GRAY,
"header": Colors.CYAN,
"stats": Colors.BLUE,
2025-11-04 05:17:27 +01:00
}.get(self.line_type, Colors.RESET)
prefix = {
2025-11-04 08:09:12 +01:00
"add": "+ ",
"delete": "- ",
"context": " ",
"header": "",
"stats": "",
}.get(self.line_type, " ")
if show_line_nums and self.line_type in ("add", "delete", "context"):
old_num = str(self.old_line_num) if self.old_line_num else " "
new_num = str(self.new_line_num) if self.new_line_num else " "
2025-11-04 05:17:27 +01:00
line_num_str = f"{Colors.YELLOW}{old_num:>4} {new_num:>4}{Colors.RESET} "
else:
2025-11-04 08:09:12 +01:00
line_num_str = ""
2025-11-04 05:17:27 +01:00
return f"{line_num_str}{color}{prefix}{self.content}{Colors.RESET}"
class DiffDisplay:
def __init__(self, context_lines: int = 3):
self.context_lines = context_lines
2025-11-04 08:09:12 +01:00
def create_diff(
self, old_content: str, new_content: str, filename: str = "file"
) -> Tuple[List[DiffLine], DiffStats]:
2025-11-04 05:17:27 +01:00
old_lines = old_content.splitlines(keepends=True)
new_lines = new_content.splitlines(keepends=True)
diff_lines = []
stats = DiffStats()
stats.files_changed = 1
diff = difflib.unified_diff(
2025-11-04 08:09:12 +01:00
old_lines,
new_lines,
2025-11-04 05:17:27 +01:00
fromfile=f"a/{filename}",
tofile=f"b/{filename}",
2025-11-04 08:09:12 +01:00
n=self.context_lines,
2025-11-04 05:17:27 +01:00
)
old_line_num = 0
new_line_num = 0
for line in diff:
2025-11-04 08:09:12 +01:00
if line.startswith("---") or line.startswith("+++"):
diff_lines.append(DiffLine("header", line.rstrip()))
elif line.startswith("@@"):
diff_lines.append(DiffLine("header", line.rstrip()))
2025-11-04 05:17:27 +01:00
old_line_num, new_line_num = self._parse_hunk_header(line)
2025-11-04 08:09:12 +01:00
elif line.startswith("+"):
2025-11-04 05:17:27 +01:00
stats.insertions += 1
2025-11-04 08:09:12 +01:00
diff_lines.append(
DiffLine("add", line[1:].rstrip(), None, new_line_num)
)
2025-11-04 05:17:27 +01:00
new_line_num += 1
2025-11-04 08:09:12 +01:00
elif line.startswith("-"):
2025-11-04 05:17:27 +01:00
stats.deletions += 1
2025-11-04 08:09:12 +01:00
diff_lines.append(
DiffLine("delete", line[1:].rstrip(), old_line_num, None)
)
2025-11-04 05:17:27 +01:00
old_line_num += 1
2025-11-04 08:09:12 +01:00
elif line.startswith(" "):
diff_lines.append(
DiffLine("context", line[1:].rstrip(), old_line_num, new_line_num)
)
2025-11-04 05:17:27 +01:00
old_line_num += 1
new_line_num += 1
stats.modifications = min(stats.insertions, stats.deletions)
return diff_lines, stats
def _parse_hunk_header(self, header: str) -> Tuple[int, int]:
try:
2025-11-04 08:09:12 +01:00
parts = header.split("@@")[1].strip().split()
old_start = int(parts[0].split(",")[0].replace("-", ""))
new_start = int(parts[1].split(",")[0].replace("+", ""))
2025-11-04 05:17:27 +01:00
return old_start, new_start
except (IndexError, ValueError):
return 0, 0
2025-11-04 08:09:12 +01:00
def render_diff(
self,
diff_lines: List[DiffLine],
stats: DiffStats,
show_line_nums: bool = True,
show_stats: bool = True,
) -> str:
2025-11-04 05:17:27 +01:00
output = []
if show_stats:
output.append(f"\n{Colors.BOLD}{Colors.BLUE}{'=' * 60}{Colors.RESET}")
output.append(f"{Colors.BOLD}{Colors.BLUE}DIFF SUMMARY{Colors.RESET}")
output.append(f"{Colors.BOLD}{Colors.BLUE}{'=' * 60}{Colors.RESET}")
output.append(f"{Colors.BLUE}{stats}{Colors.RESET}\n")
for line in diff_lines:
output.append(line.format(show_line_nums))
if show_stats:
output.append(f"\n{Colors.BOLD}{Colors.BLUE}{'=' * 60}{Colors.RESET}\n")
2025-11-04 08:09:12 +01:00
return "\n".join(output)
2025-11-04 05:17:27 +01:00
2025-11-04 08:09:12 +01:00
def display_file_diff(
self,
old_content: str,
new_content: str,
filename: str = "file",
show_line_nums: bool = True,
) -> str:
2025-11-04 05:17:27 +01:00
diff_lines, stats = self.create_diff(old_content, new_content, filename)
if not diff_lines:
return f"{Colors.GRAY}No changes detected{Colors.RESET}"
return self.render_diff(diff_lines, stats, show_line_nums)
2025-11-04 08:09:12 +01:00
def display_side_by_side(
self,
old_content: str,
new_content: str,
filename: str = "file",
width: int = 80,
) -> str:
2025-11-04 05:17:27 +01:00
old_lines = old_content.splitlines()
new_lines = new_content.splitlines()
matcher = difflib.SequenceMatcher(None, old_lines, new_lines)
output = []
output.append(f"\n{Colors.BOLD}{Colors.BLUE}{'=' * width}{Colors.RESET}")
2025-11-04 08:09:12 +01:00
output.append(
f"{Colors.BOLD}{Colors.BLUE}SIDE-BY-SIDE COMPARISON: {filename}{Colors.RESET}"
)
2025-11-04 05:17:27 +01:00
output.append(f"{Colors.BOLD}{Colors.BLUE}{'=' * width}{Colors.RESET}\n")
half_width = (width - 5) // 2
for tag, i1, i2, j1, j2 in matcher.get_opcodes():
2025-11-04 08:09:12 +01:00
if tag == "equal":
for i, (old_line, new_line) in enumerate(
zip(old_lines[i1:i2], new_lines[j1:j2])
):
2025-11-04 05:17:27 +01:00
old_display = old_line[:half_width].ljust(half_width)
new_display = new_line[:half_width].ljust(half_width)
2025-11-04 08:09:12 +01:00
output.append(
f"{Colors.GRAY}{old_display} | {new_display}{Colors.RESET}"
)
elif tag == "replace":
2025-11-04 05:17:27 +01:00
max_lines = max(i2 - i1, j2 - j1)
for i in range(max_lines):
old_line = old_lines[i1 + i] if i1 + i < i2 else ""
new_line = new_lines[j1 + i] if j1 + i < j2 else ""
old_display = old_line[:half_width].ljust(half_width)
new_display = new_line[:half_width].ljust(half_width)
2025-11-04 08:09:12 +01:00
output.append(
f"{Colors.RED}{old_display}{Colors.RESET} | {Colors.GREEN}{new_display}{Colors.RESET}"
)
elif tag == "delete":
2025-11-04 05:17:27 +01:00
for old_line in old_lines[i1:i2]:
old_display = old_line[:half_width].ljust(half_width)
2025-11-04 08:09:12 +01:00
output.append(
f"{Colors.RED}{old_display} | {' ' * half_width}{Colors.RESET}"
)
elif tag == "insert":
2025-11-04 05:17:27 +01:00
for new_line in new_lines[j1:j2]:
new_display = new_line[:half_width].ljust(half_width)
2025-11-04 08:09:12 +01:00
output.append(
f"{' ' * half_width} | {Colors.GREEN}{new_display}{Colors.RESET}"
)
2025-11-04 05:17:27 +01:00
output.append(f"\n{Colors.BOLD}{Colors.BLUE}{'=' * width}{Colors.RESET}\n")
2025-11-04 08:09:12 +01:00
return "\n".join(output)
2025-11-04 05:17:27 +01:00
2025-11-04 08:09:12 +01:00
def display_diff(
old_content: str,
new_content: str,
filename: str = "file",
format_type: str = "unified",
context_lines: int = 3,
) -> str:
2025-11-04 05:17:27 +01:00
displayer = DiffDisplay(context_lines)
if format_type == "side-by-side":
return displayer.display_side_by_side(old_content, new_content, filename)
else:
return displayer.display_file_diff(old_content, new_content, filename)
def get_diff_stats(old_content: str, new_content: str) -> Dict[str, int]:
displayer = DiffDisplay()
_, stats = displayer.create_diff(old_content, new_content)
return {
2025-11-04 08:09:12 +01:00
"insertions": stats.insertions,
"deletions": stats.deletions,
"modifications": stats.modifications,
"total_changes": stats.total_changes,
"files_changed": stats.files_changed,
2025-11-04 05:17:27 +01:00
}