"""Version history for Second Brain wiki pages.
Stores a copy of each page before it's overwritten. Versions live in
wiki/_versions/<slug>-<TIMESTAMP>.md. Capped at MAX_VERSIONS per page
to prevent disk sprawl.
"""
import logging
from pathlib import Path
from datetime import datetime
logger = logging.getLogger(__name__)
WIKI_DIR = Path.home() / "second-brain" / "wiki"
VERSIONS_DIR = WIKI_DIR / "_versions"
MAX_VERSIONS = 20 # per page
def _version_path(slug, timestamp):
"""Return the path for a version file."""
ts = datetime.fromtimestamp(timestamp).strftime("%Y%m%d-%H%M%S")
return VERSIONS_DIR / f"{slug}-{ts}.md"
def save_version(slug, content):
"""Save a version of a page before it's overwritten."""
if not content or not content.strip():
return None
VERSIONS_DIR.mkdir(parents=True, exist_ok=True)
ts = datetime.now().timestamp()
vpath = _version_path(slug, ts)
vpath.write_text(content)
logger.info("Saved version: %s", vpath.name)
# Prune old versions
_prune_versions(slug)
return vpath
def _prune_versions(slug):
"""Keep only the last MAX_VERSIONS versions for a slug."""
versions = list_versions(slug)
if len(versions) > MAX_VERSIONS:
for old in versions[:-MAX_VERSIONS]:
try:
old["path"].unlink()
logger.info("Pruned old version: %s", old["path"].name)
except OSError:
logger.exception("Failed to prune: %s", old["path"])
def list_versions(slug, limit=50):
"""List versions for a slug, newest first."""
if not VERSIONS_DIR.exists():
return []
versions = []
prefix = f"{slug}-"
for f in VERSIONS_DIR.glob(f"{prefix}*.md"):
try:
stat = f.stat()
# Extract timestamp from filename: slug-YYYYMMDD-HHMMSS.md
name = f.stem[len(prefix):] # YYYYMMDD-HHMMSS
try:
ts = datetime.strptime(name, "%Y%m%d-%H%M%S").timestamp()
except ValueError:
ts = stat.st_mtime
versions.append({
"path": f,
"name": f.name,
"timestamp": ts,
"date": datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M"),
"size": stat.st_size,
})
except OSError:
continue
versions.sort(key=lambda v: v["timestamp"], reverse=True)
return versions[:limit]
def get_version(slug, timestamp):
"""Get the content of a specific version."""
ts = datetime.fromtimestamp(timestamp).strftime("%Y%m%d-%H%M%S")
vpath = VERSIONS_DIR / f"{slug}-{ts}.md"
if vpath.exists():
return vpath.read_text()
return None
def restore_version(slug, timestamp):
"""Restore a version to the current page."""
content = get_version(slug, timestamp)
if content is None:
return False
page = WIKI_DIR / f"{slug}.md"
# Save current content as a version first
if page.exists():
save_version(slug, page.read_text())
page.write_text(content)
logger.info("Restored version for: %s (from %s)", slug, timestamp)
return True