SurfSense/plans/git-native-kb/01-git-storage-core.md

3.5 KiB

Phase 1 — Git storage core

Build first; every later phase uses this. Umbrella: 00-umbrella-plan.md. No agent wiring here — this phase is a standalone, testable git service + per-workspace lock.

Objective

A GitRepoService that owns one Git repository per workspace on disk and exposes the small set of primitives the rest of the pivot needs, with single-writer safety. Engine = dulwich (pure Python; no system git dependency in the container; real wire protocol so future "bring your own remote" is free).

Locked model

  • One repo per workspace, persistent working tree, at {FILE_STORAGE_LOCAL_PATH parent}/.kb_git/{workspace_id} (see 00c-shared-contract.md C1 for the exact layout + filename rules — reuse path_resolver's safe_filename/safe_folder_segment, keep .xml).
  • Markdown/text only in git; binaries stay in the blob store (Phase-agnostic; see umbrella).
  • Single-writer per repo via a Redis lock keyed kb_git:lock:{workspace_id} — mandatory from v1, not an in-process asyncio.Lock. The backend runs as multiple OS processes (uvicorn workers + Celery workers), so an in-process lock gives false safety; Redis is already deployed. ponytail: v1 ceiling = one Redis lock held per commit; upgrade path = per-workspace write queue. Full rationale: 00c-shared-contract.md C3.
  • dulwich for the hot path, shell out to git gc/repack only for periodic maintenance (not in v1).

Work items

  1. Add dulwich to surfsense_backend deps (pyproject/requirements) — latest release.
  2. New package app/kb_git/:
    • paths.pyrepo_path(workspace_id); ensure-dir helper.
    • service.pyGitRepoService(workspace_id) with async-friendly wrappers (run dulwich in a threadpool via asyncio.to_thread) over:
      • ensure_repo() — init if absent.
      • read_file(path), write_file(path, content), list_tree(prefix), move(src, dst), remove(path).
      • commit(message, author) -> sha, log(path=None), read_at(sha, path), revert(sha).
      • blob_sha(path) (content-addressed id, consumed by Phase 4).
    • lock.pyworkspace_lock(workspace_id) async context manager over the Redis lock (C3).
  3. Config flag KB_GIT_ENABLED (off by default) + repo root setting.

Tests

  • ensure_repo() is idempotent; second call no-ops.
  • write → commitlog shows the commit; read_file returns content; read_at(prev) returns the pre-commit content.
  • move/remove reflected in list_tree; revert(sha) restores prior state.
  • blob_sha is stable for identical content and differs after an edit.
  • Two concurrent commits on the same workspace serialize under the Redis lock (no corruption); different workspaces do not block each other. Cover cross-process contention, not just async tasks.

Out of scope

  • Agent/backend wiring → Phase 2. Commit-on-turn → Phase 3. Indexing → Phase 4.
  • Remotes (push/pull), Git-LFS, gc/repack scheduling — deferred (umbrella).

Resolved (see 00c-shared-contract.md)

  • Lock: Redis lock, from v1 (C3) — deploy topology is multi-process, so in-process locks are out.
  • Repo model: persistent working tree per workspace (C1).
  • Repo root: {blob store parent}/.kb_git/{workspace_id} (C1); backup/retention folds into existing blob-store backup.

Open questions

  1. gc/repack scheduling threshold (deferred to a later ops pass, not v1).