""" Domain models for history tracking. Defines data structures for commits, branches, diffs, and history entries. """ import uuid from dataclasses import dataclass, field from datetime import datetime from typing import Dict, Any, List, Optional from enum import Enum class DiffType(Enum): """Type of diff line.""" CONTEXT = "context" # Unchanged line ADDITION = "addition" # Added line DELETION = "deletion" # Removed line @dataclass class DiffLine: """ A single line in a diff. Attributes: line_type: Type of line (context, addition, deletion) content: Line content old_line_no: Line number in old version (None for additions) new_line_no: Line number in new version (None for deletions) """ line_type: DiffType content: str old_line_no: Optional[int] = None new_line_no: Optional[int] = None @dataclass class DiffResult: """ Result of comparing two versions. Attributes: path: File path being compared old_version: Old version identifier new_version: New version identifier lines: List of diff lines additions: Number of lines added deletions: Number of lines deleted is_binary: Whether the file is binary """ path: str old_version: Optional[str] = None new_version: Optional[str] = None lines: List[DiffLine] = field(default_factory=list) additions: int = 0 deletions: int = 0 is_binary: bool = False def to_unified_diff(self) -> str: """Generate unified diff format string.""" result = [] result.append(f"--- a/{self.path}") result.append(f"+++ b/{self.path}") for line in self.lines: if line.line_type == DiffType.ADDITION: result.append(f"+{line.content}") elif line.line_type == DiffType.DELETION: result.append(f"-{line.content}") else: result.append(f" {line.content}") return "\n".join(result) @dataclass class Commit: """ Represents a commit in the history. Attributes: id: Commit identifier (hash) message: Commit message author: Author name/email timestamp: Commit timestamp parent_ids: Parent commit IDs space_id: Associated space ID files_changed: List of changed file paths metadata: Additional commit metadata """ id: str message: str author: str = "markitect" timestamp: datetime = field(default_factory=datetime.now) parent_ids: List[str] = field(default_factory=list) space_id: Optional[str] = None files_changed: List[str] = field(default_factory=list) metadata: Dict[str, Any] = field(default_factory=dict) def to_dict(self) -> Dict[str, Any]: """Convert to dictionary.""" return { "id": self.id, "message": self.message, "author": self.author, "timestamp": self.timestamp.isoformat(), "parent_ids": self.parent_ids, "space_id": self.space_id, "files_changed": self.files_changed, "metadata": self.metadata, } @classmethod def from_dict(cls, data: Dict[str, Any]) -> "Commit": """Create from dictionary.""" timestamp = data.get("timestamp") if isinstance(timestamp, str): timestamp = datetime.fromisoformat(timestamp) elif timestamp is None: timestamp = datetime.now() return cls( id=data["id"], message=data.get("message", ""), author=data.get("author", "markitect"), timestamp=timestamp, parent_ids=data.get("parent_ids", []), space_id=data.get("space_id"), files_changed=data.get("files_changed", []), metadata=data.get("metadata", {}), ) @property def short_id(self) -> str: """Get short commit ID (first 7 chars).""" return self.id[:7] if len(self.id) >= 7 else self.id @dataclass class Branch: """ Represents a branch in the history. Attributes: name: Branch name head_commit_id: Current commit ID is_current: Whether this is the current branch tracking: Remote tracking branch (if any) created_at: When the branch was created """ name: str head_commit_id: str is_current: bool = False tracking: Optional[str] = None created_at: Optional[datetime] = None def to_dict(self) -> Dict[str, Any]: """Convert to dictionary.""" return { "name": self.name, "head_commit_id": self.head_commit_id, "is_current": self.is_current, "tracking": self.tracking, "created_at": self.created_at.isoformat() if self.created_at else None, } @dataclass class HistoryEntry: """ A single entry in the history log. Combines commit information with change summary. Attributes: commit: The commit summary: Short summary of changes files_added: Number of files added files_modified: Number of files modified files_deleted: Number of files deleted """ commit: Commit summary: str = "" files_added: int = 0 files_modified: int = 0 files_deleted: int = 0 def to_dict(self) -> Dict[str, Any]: """Convert to dictionary.""" return { "commit": self.commit.to_dict(), "summary": self.summary, "files_added": self.files_added, "files_modified": self.files_modified, "files_deleted": self.files_deleted, } @dataclass class HistoryConfig: """ Configuration for history tracking. Attributes: enabled: Whether history tracking is enabled backend: Backend type ("git" or "sqlite") auto_commit: Whether to auto-commit on changes commit_on_events: List of event types that trigger commits author_name: Default author name author_email: Default author email directory: Canonical directory path (for git) options: Additional backend-specific options """ enabled: bool = False backend: str = "git" auto_commit: bool = True commit_on_events: List[str] = field(default_factory=lambda: [ "DOCUMENT_ADDED", "DOCUMENT_REMOVED", "DOCUMENT_CONTENT_CHANGED", ]) author_name: str = "markitect" author_email: str = "markitect@local" directory: Optional[str] = None options: Dict[str, Any] = field(default_factory=dict) def to_dict(self) -> Dict[str, Any]: """Convert to dictionary.""" return { "enabled": self.enabled, "backend": self.backend, "auto_commit": self.auto_commit, "commit_on_events": self.commit_on_events, "author_name": self.author_name, "author_email": self.author_email, "directory": self.directory, "options": self.options, } @classmethod def from_dict(cls, data: Dict[str, Any]) -> "HistoryConfig": """Create from dictionary.""" return cls( enabled=data.get("enabled", False), backend=data.get("backend", "git"), auto_commit=data.get("auto_commit", True), commit_on_events=data.get("commit_on_events", [ "DOCUMENT_ADDED", "DOCUMENT_REMOVED", "DOCUMENT_CONTENT_CHANGED", ]), author_name=data.get("author_name", "markitect"), author_email=data.get("author_email", "markitect@local"), directory=data.get("directory"), options=data.get("options", {}), )