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_hunk_header(line: str) -> Optional[tuple[int, int, int, int]]: """ [C: tests/test_diff_viewer.py:test_parse_hunk_header] """ 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]: """ [C: src/gui_2.py:App.request_patch_from_tier4, tests/test_diff_viewer.py:test_diff_line_classification, tests/test_diff_viewer.py:test_parse_diff_empty, tests/test_diff_viewer.py:test_parse_diff_none, tests/test_diff_viewer.py:test_parse_diff_with_context, tests/test_diff_viewer.py:test_parse_multiple_files, tests/test_diff_viewer.py:test_parse_simple_diff, tests/test_diff_viewer.py:test_render_diff_text_immediate] """ 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 get_line_color(line: str) -> Optional[str]: """ [C: tests/test_diff_viewer.py:test_get_line_color] """ if line.startswith("+"): return "green" elif line.startswith("-"): return "red" elif line.startswith("@@"): return "cyan" return None def apply_patch_to_file(patch_text: str, base_dir: str = ".") -> Tuple[bool, str]: """ [C: src/gui_2.py:App._apply_pending_patch, tests/test_diff_viewer.py:test_apply_patch_simple, tests/test_diff_viewer.py:test_apply_patch_with_context] """ 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)