""" Event handlers for history tracking. Subscribes to space events and triggers commits automatically. """ from pathlib import Path from typing import Dict, Any, Optional, Callable import logging from ..events import SpaceEvent, SpaceEventType, EventBus from .interfaces import IHistoryBackend from .models import HistoryConfig, Commit logger = logging.getLogger(__name__) class GitHistoryEventHandler: """ Event handler that commits changes to git on document events. Subscribes to document events and automatically creates commits when documents are added, removed, or modified. """ def __init__( self, backend: IHistoryBackend, config: HistoryConfig, directory_resolver: Callable[[str], Optional[Path]], ): """ Initialize the event handler. Args: backend: History backend to use config: History configuration directory_resolver: Function that resolves space_id to directory path """ self._backend = backend self._config = config self._directory_resolver = directory_resolver self._event_bus: Optional[EventBus] = None self._handler_ids: Dict[SpaceEventType, str] = {} def register(self, event_bus: EventBus) -> None: """ Register handlers with an event bus. Args: event_bus: Event bus to register with """ self._event_bus = event_bus # Map event type names to SpaceEventType event_type_map = { "DOCUMENT_ADDED": SpaceEventType.DOCUMENT_ADDED, "DOCUMENT_REMOVED": SpaceEventType.DOCUMENT_REMOVED, "DOCUMENT_CONTENT_CHANGED": SpaceEventType.DOCUMENT_CONTENT_CHANGED, "DOCUMENT_MOVED": SpaceEventType.DOCUMENT_MOVED, } for event_name in self._config.commit_on_events: event_type = event_type_map.get(event_name) if event_type: handler_id = event_bus.subscribe(event_type, self._handle_event) self._handler_ids[event_type] = handler_id def unregister(self) -> None: """Unregister handlers from the event bus.""" if self._event_bus: for event_type, handler_id in self._handler_ids.items(): self._event_bus.unsubscribe(event_type, handler_id) self._handler_ids.clear() self._event_bus = None def _handle_event(self, event: SpaceEvent) -> None: """ Handle a space event. Args: event: The event to handle """ if not self._config.enabled or not self._config.auto_commit: return space_id = event.space_id if not space_id: return directory = self._directory_resolver(space_id) if not directory: logger.warning(f"No directory found for space {space_id}") return try: self._commit_for_event(event, directory) except Exception as e: logger.error(f"Failed to commit for event {event.event_type}: {e}") def _commit_for_event(self, event: SpaceEvent, directory: Path) -> Optional[Commit]: """ Create a commit for an event. Args: event: The triggering event directory: Space directory Returns: The created commit, or None if no commit was made """ # Ensure history is initialized if not self._backend.is_initialized(directory): self._backend.initialize(directory, self._config) # Check for changes if not self._backend.has_changes(directory): return None # Build commit message message = self._build_commit_message(event) # Build metadata metadata = { "space_id": event.space_id, "event_type": event.event_type.value, "event_id": event.event_id, } # Get specific files to commit if available files = None if event.payload: doc_path = event.payload.get("space_path") or event.payload.get("path") if doc_path: # Normalize path if doc_path.startswith("/"): doc_path = doc_path[1:] files = [doc_path] # Create commit author = f"{self._config.author_name} <{self._config.author_email}>" return self._backend.commit( directory=directory, message=message, files=files, author=author, metadata=metadata, ) def _build_commit_message(self, event: SpaceEvent) -> str: """ Build a commit message for an event. Args: event: The event Returns: Commit message string """ payload = event.payload or {} if event.event_type == SpaceEventType.DOCUMENT_ADDED: path = payload.get("space_path", "document") return f"Add document: {path}" elif event.event_type == SpaceEventType.DOCUMENT_REMOVED: path = payload.get("space_path", "document") return f"Remove document: {path}" elif event.event_type == SpaceEventType.DOCUMENT_CONTENT_CHANGED: doc_id = payload.get("document_id", "document") return f"Update document: {doc_id}" elif event.event_type == SpaceEventType.DOCUMENT_MOVED: old_path = payload.get("old_path", "old") new_path = payload.get("new_path", "new") return f"Move document: {old_path} -> {new_path}" else: return f"Auto-commit: {event.event_type.value}" def commit_changes( self, space_id: str, message: str, files: Optional[list] = None, ) -> Optional[Commit]: """ Manually trigger a commit. Args: space_id: Space identifier message: Commit message files: Specific files to commit Returns: The created commit, or None if failed """ directory = self._directory_resolver(space_id) if not directory: return None if not self._backend.is_initialized(directory): self._backend.initialize(directory, self._config) if not self._backend.has_changes(directory): return None author = f"{self._config.author_name} <{self._config.author_email}>" return self._backend.commit( directory=directory, message=message, files=files, author=author, metadata={"space_id": space_id, "manual": True}, ) class HistoryEventCoordinator: """ Coordinates history tracking across multiple spaces. Manages event handlers for spaces with history enabled. """ def __init__( self, backend: IHistoryBackend, event_bus: EventBus, directory_resolver: Callable[[str], Optional[Path]], ): """ Initialize the coordinator. Args: backend: History backend event_bus: Event bus for subscriptions directory_resolver: Function to resolve space_id to directory """ self._backend = backend self._event_bus = event_bus self._directory_resolver = directory_resolver self._handlers: Dict[str, GitHistoryEventHandler] = {} def enable_history(self, space_id: str, config: HistoryConfig) -> None: """ Enable history tracking for a space. Args: space_id: Space identifier config: History configuration """ if space_id in self._handlers: return handler = GitHistoryEventHandler( backend=self._backend, config=config, directory_resolver=self._directory_resolver, ) handler.register(self._event_bus) self._handlers[space_id] = handler # Initialize git in the space directory directory = self._directory_resolver(space_id) if directory: self._backend.initialize(directory, config) def disable_history(self, space_id: str) -> None: """ Disable history tracking for a space. Args: space_id: Space identifier """ handler = self._handlers.pop(space_id, None) if handler: handler.unregister() def is_enabled(self, space_id: str) -> bool: """Check if history is enabled for a space.""" return space_id in self._handlers def get_handler(self, space_id: str) -> Optional[GitHistoryEventHandler]: """Get the event handler for a space.""" return self._handlers.get(space_id)