markitect-main/markitect/spaces/history/queries.py

259 lines
8.3 KiB
Python
Raw Normal View History

"""
History query service implementation.
Provides high-level queries on top of the history backend.
"""
from pathlib import Path
from typing import List, Optional, Dict, Callable
from .interfaces import IHistoryBackend, IHistoryQuery
from .models import HistoryEntry, DiffResult, Commit
class HistoryQueryService(IHistoryQuery):
"""
Service for querying history information.
Provides convenience methods on top of the raw history backend.
"""
def __init__(
self,
backend: IHistoryBackend,
directory_resolver: Callable[[str], Optional[Path]],
):
"""
Initialize the query service.
Args:
backend: History backend to query
directory_resolver: Function that resolves space_id to directory path
"""
self._backend = backend
self._directory_resolver = directory_resolver
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."""
directory = self._directory_resolver(space_id)
if not directory or not self._backend.is_initialized(directory):
return []
commits = self._backend.get_log(directory, limit, offset, path)
return [self._commit_to_entry(commit, directory) for commit in commits]
def _commit_to_entry(self, commit: Commit, directory: Path) -> HistoryEntry:
"""Convert a commit to a history entry with summary."""
# Count file changes by type
files_added = 0
files_modified = 0
files_deleted = 0
# Get diff to determine change types
if commit.parent_ids:
parent = commit.parent_ids[0]
diffs = self._backend.get_diff(directory, parent, commit.id)
for diff in diffs:
if diff.additions > 0 and diff.deletions == 0:
files_added += 1
elif diff.additions == 0 and diff.deletions > 0:
files_deleted += 1
else:
files_modified += 1
else:
# First commit - all files are additions
files_added = len(commit.files_changed)
# Build summary
parts = []
if files_added:
parts.append(f"+{files_added}")
if files_modified:
parts.append(f"~{files_modified}")
if files_deleted:
parts.append(f"-{files_deleted}")
summary = ", ".join(parts) if parts else "No changes"
return HistoryEntry(
commit=commit,
summary=summary,
files_added=files_added,
files_modified=files_modified,
files_deleted=files_deleted,
)
def get_version_content(
self,
space_id: str,
document_path: str,
version: str,
) -> Optional[str]:
"""Get document content at a specific version."""
directory = self._directory_resolver(space_id)
if not directory or not self._backend.is_initialized(directory):
return None
# Normalize path (remove leading /)
if document_path.startswith("/"):
document_path = document_path[1:]
return self._backend.get_file_at_version(directory, document_path, version)
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."""
directory = self._directory_resolver(space_id)
if not directory or not self._backend.is_initialized(directory):
return None
# Normalize path
if document_path.startswith("/"):
document_path = document_path[1:]
diffs = self._backend.get_diff(directory, old_version, new_version, document_path)
return diffs[0] if diffs else None
def search_history(
self,
space_id: str,
query: str,
limit: int = 20,
) -> List[HistoryEntry]:
"""Search commit messages."""
directory = self._directory_resolver(space_id)
if not directory or not self._backend.is_initialized(directory):
return []
# Get all recent commits
commits = self._backend.get_log(directory, limit=limit * 2)
# Filter by query in message
matching_commits = [
c for c in commits
if query.lower() in c.message.lower()
][:limit]
return [self._commit_to_entry(commit, directory) for commit in matching_commits]
def restore_version(
self,
space_id: str,
document_path: str,
version: str,
) -> bool:
"""Restore a document to a specific version."""
directory = self._directory_resolver(space_id)
if not directory or not self._backend.is_initialized(directory):
return False
# Normalize path
if document_path.startswith("/"):
document_path = document_path[1:]
# Get content at version
content = self._backend.get_file_at_version(directory, document_path, version)
if content is None:
return False
# Write to working directory
file_path = directory / document_path
file_path.parent.mkdir(parents=True, exist_ok=True)
file_path.write_text(content)
return True
def get_latest_commit(self, space_id: str) -> Optional[Commit]:
"""Get the latest commit for a space."""
directory = self._directory_resolver(space_id)
if not directory or not self._backend.is_initialized(directory):
return None
commits = self._backend.get_log(directory, limit=1)
return commits[0] if commits else None
def get_commit_by_id(self, space_id: str, commit_id: str) -> Optional[Commit]:
"""Get a specific commit by ID."""
directory = self._directory_resolver(space_id)
if not directory:
return None
return self._backend.get_commit(directory, commit_id)
def get_file_history(
self,
space_id: str,
document_path: str,
limit: int = 20,
) -> List[HistoryEntry]:
"""Get history for a specific file."""
# Normalize path
if document_path.startswith("/"):
document_path = document_path[1:]
return self.get_history(space_id, limit=limit, path=document_path)
def has_history(self, space_id: str) -> bool:
"""Check if a space has history tracking enabled."""
directory = self._directory_resolver(space_id)
if not directory:
return False
return self._backend.is_initialized(directory)
def get_branches(self, space_id: str) -> List[str]:
"""Get list of branch names for a space."""
directory = self._directory_resolver(space_id)
if not directory or not self._backend.is_initialized(directory):
return []
branches = self._backend.list_branches(directory)
return [b.name for b in branches]
def get_current_branch(self, space_id: str) -> Optional[str]:
"""Get the current branch name."""
directory = self._directory_resolver(space_id)
if not directory:
return None
return self._backend.get_current_branch(directory)
def create_branch(
self,
space_id: str,
name: str,
start_point: Optional[str] = None,
) -> bool:
"""Create a new branch."""
directory = self._directory_resolver(space_id)
if not directory or not self._backend.is_initialized(directory):
return False
try:
self._backend.create_branch(directory, name, start_point)
return True
except Exception:
return False
def switch_branch(self, space_id: str, name: str) -> bool:
"""Switch to a different branch."""
directory = self._directory_resolver(space_id)
if not directory:
return False
return self._backend.switch_branch(directory, name)
def has_uncommitted_changes(self, space_id: str) -> bool:
"""Check if space has uncommitted changes."""
directory = self._directory_resolver(space_id)
if not directory:
return False
return self._backend.has_changes(directory)