Implements optional git-based version control for information spaces: - HistoryConfig model for configuring history tracking - Commit, Branch, HistoryEntry, DiffResult models - IHistoryBackend and IHistoryQuery interfaces - GitHistoryBackend using git CLI for version control - GitHistoryEventHandler for event-driven auto-commits - HistoryEventCoordinator for managing space history - HistoryQueryService for high-level history queries - Automatic commits on DOCUMENT_ADDED/REMOVED/CONTENT_CHANGED events - Support for: * Commit log with pagination and filtering * Diff between versions * File content at specific versions * Branch creation and switching * Version restoration * Uncommitted changes detection - 43 comprehensive unit tests with git availability checks Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
351 lines
7.7 KiB
Python
351 lines
7.7 KiB
Python
"""
|
|
Abstract interfaces for history tracking.
|
|
|
|
Defines the contract for history backends and query services.
|
|
"""
|
|
|
|
from abc import ABC, abstractmethod
|
|
from pathlib import Path
|
|
from typing import List, Optional, Dict, Any
|
|
|
|
from .models import Commit, Branch, DiffResult, HistoryEntry, HistoryConfig
|
|
|
|
|
|
class IHistoryBackend(ABC):
|
|
"""
|
|
Abstract interface for history storage backends.
|
|
|
|
Implementations can use git, SQLite, or other storage mechanisms.
|
|
"""
|
|
|
|
@abstractmethod
|
|
def initialize(self, directory: Path, config: HistoryConfig) -> None:
|
|
"""
|
|
Initialize the history backend.
|
|
|
|
Args:
|
|
directory: Root directory for the space
|
|
config: History configuration
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def is_initialized(self, directory: Path) -> bool:
|
|
"""
|
|
Check if history is initialized for a directory.
|
|
|
|
Args:
|
|
directory: Directory to check
|
|
|
|
Returns:
|
|
True if history tracking is initialized
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def commit(
|
|
self,
|
|
directory: Path,
|
|
message: str,
|
|
files: Optional[List[str]] = None,
|
|
author: Optional[str] = None,
|
|
metadata: Optional[Dict[str, Any]] = None,
|
|
) -> Commit:
|
|
"""
|
|
Create a new commit.
|
|
|
|
Args:
|
|
directory: Root directory
|
|
message: Commit message
|
|
files: Specific files to commit (None for all changes)
|
|
author: Author string (name <email>)
|
|
metadata: Additional metadata to store
|
|
|
|
Returns:
|
|
The created commit
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def get_commit(self, directory: Path, commit_id: str) -> Optional[Commit]:
|
|
"""
|
|
Get a commit by ID.
|
|
|
|
Args:
|
|
directory: Root directory
|
|
commit_id: Commit identifier
|
|
|
|
Returns:
|
|
The commit if found, None otherwise
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def get_log(
|
|
self,
|
|
directory: Path,
|
|
limit: int = 50,
|
|
offset: int = 0,
|
|
path: Optional[str] = None,
|
|
) -> List[Commit]:
|
|
"""
|
|
Get commit history.
|
|
|
|
Args:
|
|
directory: Root directory
|
|
limit: Maximum number of commits
|
|
offset: Number of commits to skip
|
|
path: Filter by file path
|
|
|
|
Returns:
|
|
List of commits, newest first
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def get_diff(
|
|
self,
|
|
directory: Path,
|
|
old_version: Optional[str] = None,
|
|
new_version: Optional[str] = None,
|
|
path: Optional[str] = None,
|
|
) -> List[DiffResult]:
|
|
"""
|
|
Get diff between versions.
|
|
|
|
Args:
|
|
directory: Root directory
|
|
old_version: Old commit ID (None for working directory)
|
|
new_version: New commit ID (None for working directory)
|
|
path: Filter by file path
|
|
|
|
Returns:
|
|
List of diff results
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def checkout(
|
|
self,
|
|
directory: Path,
|
|
version: str,
|
|
path: Optional[str] = None,
|
|
) -> bool:
|
|
"""
|
|
Checkout a specific version.
|
|
|
|
Args:
|
|
directory: Root directory
|
|
version: Commit ID or ref to checkout
|
|
path: Specific file to checkout (None for all)
|
|
|
|
Returns:
|
|
True if successful
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def get_file_at_version(
|
|
self,
|
|
directory: Path,
|
|
path: str,
|
|
version: str,
|
|
) -> Optional[str]:
|
|
"""
|
|
Get file content at a specific version.
|
|
|
|
Args:
|
|
directory: Root directory
|
|
path: File path relative to directory
|
|
version: Commit ID or ref
|
|
|
|
Returns:
|
|
File content as string, or None if not found
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def list_branches(self, directory: Path) -> List[Branch]:
|
|
"""
|
|
List all branches.
|
|
|
|
Args:
|
|
directory: Root directory
|
|
|
|
Returns:
|
|
List of branches
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def create_branch(
|
|
self,
|
|
directory: Path,
|
|
name: str,
|
|
start_point: Optional[str] = None,
|
|
) -> Branch:
|
|
"""
|
|
Create a new branch.
|
|
|
|
Args:
|
|
directory: Root directory
|
|
name: Branch name
|
|
start_point: Starting commit/ref (None for HEAD)
|
|
|
|
Returns:
|
|
The created branch
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def switch_branch(self, directory: Path, name: str) -> bool:
|
|
"""
|
|
Switch to a branch.
|
|
|
|
Args:
|
|
directory: Root directory
|
|
name: Branch name
|
|
|
|
Returns:
|
|
True if successful
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def get_current_branch(self, directory: Path) -> Optional[str]:
|
|
"""
|
|
Get the current branch name.
|
|
|
|
Args:
|
|
directory: Root directory
|
|
|
|
Returns:
|
|
Branch name or None if detached
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def has_changes(self, directory: Path) -> bool:
|
|
"""
|
|
Check if there are uncommitted changes.
|
|
|
|
Args:
|
|
directory: Root directory
|
|
|
|
Returns:
|
|
True if there are uncommitted changes
|
|
"""
|
|
pass
|
|
|
|
|
|
class IHistoryQuery(ABC):
|
|
"""
|
|
Abstract interface for history queries.
|
|
|
|
Provides higher-level query operations on top of the backend.
|
|
"""
|
|
|
|
@abstractmethod
|
|
def get_history(
|
|
self,
|
|
space_id: str,
|
|
limit: int = 50,
|
|
offset: int = 0,
|
|
path: Optional[str] = None,
|
|
) -> List[HistoryEntry]:
|
|
"""
|
|
Get history entries for a space.
|
|
|
|
Args:
|
|
space_id: Space identifier
|
|
limit: Maximum entries
|
|
offset: Skip count
|
|
path: Filter by document path
|
|
|
|
Returns:
|
|
List of history entries
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def get_version_content(
|
|
self,
|
|
space_id: str,
|
|
document_path: str,
|
|
version: str,
|
|
) -> Optional[str]:
|
|
"""
|
|
Get document content at a specific version.
|
|
|
|
Args:
|
|
space_id: Space identifier
|
|
document_path: Path to document
|
|
version: Version identifier
|
|
|
|
Returns:
|
|
Document content or None
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def compare_versions(
|
|
self,
|
|
space_id: str,
|
|
document_path: str,
|
|
old_version: str,
|
|
new_version: Optional[str] = None,
|
|
) -> Optional[DiffResult]:
|
|
"""
|
|
Compare two versions of a document.
|
|
|
|
Args:
|
|
space_id: Space identifier
|
|
document_path: Path to document
|
|
old_version: Old version ID
|
|
new_version: New version ID (None for current)
|
|
|
|
Returns:
|
|
Diff result or None
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def search_history(
|
|
self,
|
|
space_id: str,
|
|
query: str,
|
|
limit: int = 20,
|
|
) -> List[HistoryEntry]:
|
|
"""
|
|
Search commit messages.
|
|
|
|
Args:
|
|
space_id: Space identifier
|
|
query: Search query
|
|
limit: Maximum results
|
|
|
|
Returns:
|
|
Matching history entries
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def restore_version(
|
|
self,
|
|
space_id: str,
|
|
document_path: str,
|
|
version: str,
|
|
) -> bool:
|
|
"""
|
|
Restore a document to a specific version.
|
|
|
|
Args:
|
|
space_id: Space identifier
|
|
document_path: Path to document
|
|
version: Version to restore
|
|
|
|
Returns:
|
|
True if successful
|
|
"""
|
|
pass
|