feat(spaces): implement Phase 8 Git History Tracking
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>
This commit is contained in:
350
markitect/spaces/history/interfaces.py
Normal file
350
markitect/spaces/history/interfaces.py
Normal file
@@ -0,0 +1,350 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user