Initial upload of auto-changelog-generator
This commit is contained in:
126
src/changeloggen/git_client.py
Normal file
126
src/changeloggen/git_client.py
Normal 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
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user