diff --git a/src/diff_viewer.py b/src/diff_viewer.py index c3bec4a..ef1e55f 100644 --- a/src/diff_viewer.py +++ b/src/diff_viewer.py @@ -1,5 +1,8 @@ -from typing import List, Dict, Optional +from typing import List, Dict, Optional, Tuple from dataclasses import dataclass +import shutil +import os +from pathlib import Path @dataclass class DiffHunk: @@ -146,4 +149,72 @@ def render_diff_text_immediate(diff_files: List[DiffFile]) -> List[tuple[str, Op for line in hunk.lines: color = get_line_color(line) output.append((line, color)) - return output \ No newline at end of file + 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() \ No newline at end of file diff --git a/tests/test_diff_viewer.py b/tests/test_diff_viewer.py index bacf65f..33bed81 100644 --- a/tests/test_diff_viewer.py +++ b/tests/test_diff_viewer.py @@ -1,5 +1,12 @@ import pytest -from src.diff_viewer import parse_diff, DiffFile, DiffHunk, parse_hunk_header, get_line_color, render_diff_text_immediate +import tempfile +import os +from pathlib import Path +from src.diff_viewer import ( + parse_diff, DiffFile, DiffHunk, parse_hunk_header, + get_line_color, render_diff_text_immediate, + create_backup, apply_patch_to_file, restore_from_backup, cleanup_backup +) def test_parse_diff_empty() -> None: result = parse_diff("") @@ -101,4 +108,74 @@ def test_render_diff_text_immediate() -> None: assert ("File: test.py", "white") in output assert ("@@ -1 +1 @@", "cyan") in output assert ("-old", "red") in output - assert ("+new", "green") in output \ No newline at end of file + assert ("+new", "green") in output + +def test_create_backup() -> None: + with tempfile.TemporaryDirectory() as tmpdir: + test_file = Path(tmpdir) / "test.py" + test_file.write_text("original content\n") + + backup_path = create_backup(str(test_file)) + assert backup_path is not None + assert Path(backup_path).exists() + assert Path(backup_path).read_text() == "original content\n" + +def test_create_backup_nonexistent() -> None: + result = create_backup("/nonexistent/file.py") + assert result is None + +def test_apply_patch_simple() -> None: + with tempfile.TemporaryDirectory() as tmpdir: + test_file = Path(tmpdir) / "test.py" + test_file.write_text("old\n") + + patch = f"""--- a/{test_file.name} ++++ b/{test_file.name} +@@ -1 +1 @@ +-old ++new""" + + success, msg = apply_patch_to_file(patch, tmpdir) + assert success + assert test_file.read_text() == "new\n" + +def test_apply_patch_with_context() -> None: + with tempfile.TemporaryDirectory() as tmpdir: + test_file = Path(tmpdir) / "example.py" + test_file.write_text("line 1\nline 2\nline 3\n") + + patch = f"""--- a/{test_file.name} ++++ b/{test_file.name} +@@ -1,3 +1,3 @@ +-line 1 +-line 2 ++line one ++line two + line 3""" + + success, msg = apply_patch_to_file(patch, tmpdir) + assert success + content = test_file.read_text() + assert "line one" in content + assert "line two" in content + +def test_restore_from_backup() -> None: + with tempfile.TemporaryDirectory() as tmpdir: + test_file = Path(tmpdir) / "test.py" + test_file.write_text("modified\n") + backup_file = test_file.with_suffix(".py.backup") + backup_file.write_text("original\n") + + success = restore_from_backup(str(test_file)) + assert success + assert test_file.read_text() == "original\n" + +def test_cleanup_backup() -> None: + with tempfile.TemporaryDirectory() as tmpdir: + test_file = Path(tmpdir) / "test.py" + test_file.write_text("content\n") + backup_file = test_file.with_suffix(".py.backup") + backup_file.write_text("backup\n") + + cleanup_backup(str(test_file)) + assert not backup_file.exists() \ No newline at end of file