Initial upload of auto-changelog-generator
Some checks failed
CI / test (push) Has been cancelled
CI / build (push) Has been cancelled

This commit is contained in:
2026-01-29 12:00:29 +00:00
parent 848e487b75
commit bc8d571785

View File

@@ -0,0 +1,126 @@
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class FileChange:
"""Represents a single file change with its diff content."""
file_path: str
change_type: str
diff_content: str
staged: bool = False
lines_added: int = 0
lines_removed: int = 0
def __post_init__(self):
self._calculate_line_changes()
def _calculate_line_changes(self):
lines_added = 0
lines_removed = 0
for line in self.diff_content.split('\n'):
if line.startswith('+') and not line.startswith('+++'):
lines_added += 1
elif line.startswith('-') and not line.startswith('---'):
lines_removed += 1
self.lines_added = lines_added
self.lines_removed = lines_removed
@dataclass
class ChangeSet:
"""Represents a collection of file changes from git diff."""
staged_changes: list[FileChange] = field(default_factory=list)
unstaged_changes: list[FileChange] = field(default_factory=list)
commit_hash: Optional[str] = None
commit_message: Optional[str] = None
author: Optional[str] = None
timestamp: Optional[str] = None
@property
def all_changes(self) -> list[FileChange]:
"""Return all changes (staged and unstaged)."""
return self.staged_changes + self.unstaged_changes
@property
def total_files_changed(self) -> int:
"""Return total number of files changed."""
return len(self.all_changes)
@property
def total_lines_added(self) -> int:
"""Return total lines added across all changes."""
return sum(change.lines_added for change in self.all_changes)
@property
def total_lines_removed(self) -> int:
"""Return total lines removed across all changes."""
return sum(change.lines_removed for change in self.all_changes)
def parse_change_type(status_char: str) -> str:
"""Parse git status character to change type."""
status_map = {
'A': 'added',
'M': 'modified',
'D': 'deleted',
'R': 'renamed',
'C': 'copied',
'M': 'modified',
'??': 'untracked',
}
return status_map.get(status_char, 'modified')
def get_diff_lines(diff_output: str) -> list[str]:
"""Parse diff output into individual file diffs."""
if not diff_output.strip():
return []
lines = diff_output.split('\n')
file_diffs = []
current_diff = []
for line in lines:
if line.startswith('diff --git'):
if current_diff:
file_diffs.append('\n'.join(current_diff))
current_diff = [line]
else:
current_diff.append(line)
if current_diff:
file_diffs.append('\n'.join(current_diff))
return file_diffs
def parse_file_diff(diff_text: str, staged: bool = False) -> Optional[FileChange]:
"""Parse a single file diff into a FileChange object."""
lines = diff_text.split('\n')
file_path = None
change_type = 'modified'
for line in lines:
if line.startswith('diff --git'):
parts = line.split(' ')
if len(parts) >= 4:
old_path = parts[2].lstrip('a/')
new_path = parts[3].lstrip('b/')
file_path = new_path if new_path else old_path
elif line.startswith('new file mode'):
change_type = 'added'
elif line.startswith('deleted file mode'):
change_type = 'deleted'
elif line.startswith('similarity index'):
change_type = 'renamed'
if not file_path:
return None
return FileChange(
file_path=file_path,
change_type=change_type,
diff_content=diff_text,
staged=staged
)