From 9e7fd72429fdc96dcc1d6785c2924a2fb9c4ce9e Mon Sep 17 00:00:00 2001 From: 7000pctAUTO Date: Mon, 2 Feb 2026 13:56:56 +0000 Subject: [PATCH] Add source files: models and parser --- src/gdiffer/models.py | 108 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 src/gdiffer/models.py diff --git a/src/gdiffer/models.py b/src/gdiffer/models.py new file mode 100644 index 0000000..a404b9d --- /dev/null +++ b/src/gdiffer/models.py @@ -0,0 +1,108 @@ +"""Data models for git diff parsing and analysis.""" + +from dataclasses import dataclass, field +from typing import Optional + + +@dataclass +class DiffHunk: + """Represents a single hunk (chunk) of changes in a diff.""" + old_start: int + old_lines: int + new_start: int + new_lines: int + old_lines_content: list[str] = field(default_factory=list) + new_lines_content: list[str] = field(default_factory=list) + header: str = "" + + def get_added_lines(self) -> list[tuple[int, str]]: + result = [] + for i, line in enumerate(self.new_lines_content): + if line.startswith('+') and not line.startswith('+++'): + result.append((self.new_start + i, line[1:])) + return result + + def get_removed_lines(self) -> list[tuple[int, str]]: + result = [] + for i, line in enumerate(self.old_lines_content): + if line.startswith('-') and not line.startswith('---'): + result.append((self.old_start + i, line[1:])) + return result + + def get_modified_lines(self) -> list[tuple[int, str, str]]: + result = [] + added = self.get_added_lines() + removed = self.get_removed_lines() + for i, (old_line, old_content) in enumerate(removed): + if i < len(added): + new_line, new_content = added[i] + if old_content != new_content: + result.append((old_line, old_content, new_content)) + return result + + +@dataclass +class DiffFile: + """Represents a file in the diff with its changes.""" + old_path: Optional[str] + new_path: Optional[str] + new_file_mode: Optional[str] = None + deleted_file_mode: Optional[str] = None + similarity_index: Optional[str] = None + rename_from: Optional[str] = None + rename_to: Optional[str] = None + hunks: list[DiffHunk] = field(default_factory=list) + change_type: str = "modify" + + @property + def filename(self) -> str: + if self.new_path: + return self.new_path + return self.old_path or "" + + @property + def is_new(self) -> bool: + return self.new_file_mode is not None or self.old_path in [None, "/dev/null"] + + @property + def is_deleted(self) -> bool: + return self.deleted_file_mode is not None + + @property + def is_rename(self) -> bool: + return self.rename_from is not None + + @property + def extension(self) -> str: + filename = self.filename + if '.' in filename: + return filename.rsplit('.', 1)[-1].lower() + return "" + + +@dataclass +class CodeChange: + """Represents a code change with context.""" + file: DiffFile + hunk: Optional[DiffHunk] + old_code: str + new_code: str + language: str = "unknown" + summary: str = "" + issues: list[dict] = field(default_factory=list) + suggestions: list[str] = field(default_factory=list) + + +@dataclass +class DiffAnalysis: + """Complete analysis result for a diff.""" + files: list[DiffFile] = field(default_factory=list) + total_files: int = 0 + files_added: int = 0 + files_deleted: int = 0 + files_modified: int = 0 + files_renamed: int = 0 + total_changes: int = 0 + language_breakdown: dict[str, int] = field(default_factory=dict) + all_issues: list[dict] = field(default_factory=list) + all_suggestions: list[str] = field(default_factory=list)