from typing import List, Dict, Optional, Tuple from dataclasses import dataclass import shutil import os from pathlib import Path @dataclass class DiffHunk: header: str lines: List[str] old_start: int old_count: int new_start: int new_count: int @dataclass class DiffFile: old_path: str new_path: str hunks: List[DiffHunk] def parse_diff_header(line: str) -> tuple[Optional[str], Optional[str], Optional[tuple[int, int, int, int]]]: if not line.startswith(("--- ", "+++ ")): return None, None, None if line.startswith("--- "): path = line[4:] if path.startswith("a/"): path = path[2:] return path, None, None elif line.startswith("+++ "): path = line[4:] if path.startswith("b/"): path = path[2:] return None, path, None return None, None, None def parse_hunk_header(line: str) -> Optional[tuple[int, int, int, int]]: if not line.startswith("@@"): return None parts = line.split() if len(parts) < 2: return None old_part = parts[1][1:] new_part = parts[2][1:] old_parts = old_part.split(",") new_parts = new_part.split(",") old_start = int(old_parts[0]) old_count = int(old_parts[1]) if len(old_parts) > 1 else 1 new_start = int(new_parts[0]) new_count = int(new_parts[1]) if len(new_parts) > 1 else 1 return (old_start, old_count, new_start, new_count) def parse_diff(diff_text: str) -> List[DiffFile]: if not diff_text or not diff_text.strip(): return [] files: List[DiffFile] = [] current_file: Optional[DiffFile] = None current_hunk: Optional[DiffHunk] = None for line in diff_text.split("\n"): if line.startswith("--- "): if current_file: if current_hunk: current_file.hunks.append(current_hunk) current_hunk = None files.append(current_file) path = line[4:] if path.startswith("a/"): path = path[2:] current_file = DiffFile(old_path=path, new_path="", hunks=[]) elif line.startswith("+++ ") and current_file: path = line[4:] if path.startswith("b/"): path = path[2:] current_file.new_path = path elif line.startswith("@@") and current_file: if current_hunk: current_file.hunks.append(current_hunk) hunk_info = parse_hunk_header(line) if hunk_info: old_start, old_count, new_start, new_count = hunk_info current_hunk = DiffHunk( header=line, lines=[], old_start=old_start, old_count=old_count, new_start=new_start, new_count=new_count ) else: current_hunk = DiffHunk( header=line, lines=[], old_start=0, old_count=0, new_start=0, new_count=0 ) elif current_hunk is not None: current_hunk.lines.append(line) elif line and not line.startswith("diff ") and not line.startswith("index "): pass if current_file: if current_hunk: current_file.hunks.append(current_hunk) files.append(current_file) return files def format_diff_for_display(diff_files: List[DiffFile]) -> str: output = [] for df in diff_files: output.append(f"File: {df.old_path}") for hunk in df.hunks: output.append(f" {hunk.header}") for line in hunk.lines: output.append(f" {line}") return "\n".join(output) def get_line_color(line: str) -> Optional[str]: if line.startswith("+"): return "green" elif line.startswith("-"): return "red" elif line.startswith("@@"): return "cyan" return None def render_diff_text_immediate(diff_files: List[DiffFile]) -> List[tuple[str, Optional[str]]]: output: List[tuple[str, Optional[str]]] = [] for df in diff_files: output.append((f"File: {df.old_path}", "white")) for hunk in df.hunks: output.append((hunk.header, "cyan")) for line in hunk.lines: color = get_line_color(line) output.append((line, color)) return output def create_backup(file_path: str) -> Optional[str]: path = Path(file_path) if not path.exists(): return None backup_path = path.with_suffix(path.suffix + ".backup") shutil.copy2(path, backup_path) return str(backup_path) def apply_patch_to_file(patch_text: str, base_dir: str = ".") -> Tuple[bool, str]: import difflib diff_files = parse_diff(patch_text) if not diff_files: return False, "No valid diff found" results = [] for df in diff_files: file_path = Path(base_dir) / df.old_path if not file_path.exists(): results.append(f"File not found: {file_path}") continue try: with open(file_path, "r", encoding="utf-8") as f: original_lines = f.read().splitlines(keepends=True) new_lines = original_lines.copy() offset = 0 for hunk in df.hunks: hunk_old_start = hunk.old_start - 1 hunk_old_count = hunk.old_count replace_start = hunk_old_start + offset replace_count = hunk_old_count hunk_new_content: List[str] = [] for line in hunk.lines: if line.startswith("+") and not line.startswith("+++"): hunk_new_content.append(line[1:] + "\n") elif line.startswith(" ") or (line and not line.startswith(("-", "+", "@@"))): hunk_new_content.append(line + "\n") new_lines = new_lines[:replace_start] + hunk_new_content + new_lines[replace_start + replace_count:] offset += len(hunk_new_content) - replace_count with open(file_path, "w", encoding="utf-8", newline="") as f: f.writelines(new_lines) results.append(f"Patched: {file_path}") except Exception as e: return False, f"Error patching {file_path}: {e}" return True, "\n".join(results) def restore_from_backup(file_path: str) -> bool: backup_path = Path(str(file_path) + ".backup") if not backup_path.exists(): return False shutil.copy2(backup_path, file_path) return True def cleanup_backup(file_path: str) -> None: backup_path = Path(str(file_path) + ".backup") if backup_path.exists(): backup_path.unlink()