KeiSeiKit-1.0/_primitives/_rust/kei-store/src/filesystem.rs
Parfii-bot 19ee220e0a feat(primitives): 4 Rust crates for deep-sleep — conflict-scan, refactor-engine, graph-check, store
- kei-conflict-scan: rules/hooks/blocks/orphans/CP detection (6 tests)
- kei-refactor-engine: plan-mode + advisory patch format, zero-conflict guarantee (5 tests)
- kei-graph-check: wikilinks/handoffs/block-refs validator (4 tests)
- kei-store: trait + 5 backends (filesystem/github/forgejo/gitea prod, s3 stub) (8 tests)

1916 LOC Rust total; all files <200 LOC; 23/23 tests pass.
2026-04-22 08:28:22 +08:00

105 lines
3.1 KiB
Rust

//! FilesystemStore — local `.git` repo, no remotes.
//!
//! Reuses git2 for branch/commit so behavior parity with remote stores is
//! maintained. `push`/`pull` are intentional no-ops.
use crate::store_trait::MemoryStore;
use anyhow::{Context, Result};
use std::fs;
use std::path::{Path, PathBuf};
pub struct FilesystemStore {
pub root: PathBuf,
}
impl FilesystemStore {
pub fn new(root: impl Into<PathBuf>) -> Result<Self> {
let root = root.into();
fs::create_dir_all(&root).with_context(|| format!("mkdir {}", root.display()))?;
ensure_repo(&root)?;
Ok(Self { root })
}
fn full(&self, rel: &str) -> PathBuf {
self.root.join(rel)
}
}
fn ensure_repo(root: &Path) -> Result<()> {
if root.join(".git").exists() {
return Ok(());
}
git2::Repository::init(root).context("git init")?;
Ok(())
}
impl MemoryStore for FilesystemStore {
fn read(&self, path: &str) -> Result<Vec<u8>> {
fs::read(self.full(path)).with_context(|| format!("read {}", path))
}
fn write(&self, path: &str, bytes: &[u8]) -> Result<()> {
let full = self.full(path);
if let Some(parent) = full.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&full, bytes).with_context(|| format!("write {}", path))
}
fn list(&self, dir: &str) -> Result<Vec<String>> {
let full = self.full(dir);
if !full.exists() {
return Ok(Vec::new());
}
let mut out = Vec::new();
for e in fs::read_dir(&full)? {
let e = e?;
if e.file_type()?.is_file() {
if let Some(name) = e.file_name().to_str() {
out.push(name.to_string());
}
}
}
out.sort();
Ok(out)
}
fn branch(&self, name: &str) -> Result<()> {
let repo = git2::Repository::open(&self.root)?;
if repo.find_branch(name, git2::BranchType::Local).is_ok() {
return Ok(());
}
if let Ok(head) = repo.head().and_then(|h| h.peel_to_commit()) {
repo.branch(name, &head, false)?;
}
// If there is no HEAD yet (empty repo), silently no-op; first commit
// will be on default branch.
Ok(())
}
fn commit(&self, message: &str) -> Result<String> {
let repo = git2::Repository::open(&self.root)?;
let mut index = repo.index()?;
index.add_all(["*"].iter(), git2::IndexAddOption::DEFAULT, None)?;
index.write()?;
let tree_oid = index.write_tree()?;
let tree = repo.find_tree(tree_oid)?;
let sig = git2::Signature::now("kei-store", "kei-store@local")?;
let parent = repo.head().ok().and_then(|h| h.peel_to_commit().ok());
let parents: Vec<&git2::Commit> = parent.iter().collect();
let oid = repo.commit(Some("HEAD"), &sig, &sig, message, &tree, &parents)?;
Ok(oid.to_string())
}
fn push(&self, _branch: &str) -> Result<()> {
Ok(())
}
fn pull(&self, _branch: &str) -> Result<()> {
Ok(())
}
fn backend_name(&self) -> &'static str {
"filesystem"
}
}