use crate::types::IndexMeta;
use anyhow::{Context as _, Result};
use rusqlite::Connection;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
pub const LAYOUT_VERSION: u32 = 13;
pub const DEFAULT_BRANCH_KEY: &str = "default";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProjectMeta {
pub layout_version: u32,
pub project_id: String,
pub default_branch: String,
#[serde(flatten)]
pub active_index_meta: IndexMeta,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct BranchMeta {
pub branch: String,
pub branch_key: String,
#[serde(default)]
pub head_commit: Option<String>,
}
pub fn container_dir(project_root: &Path) -> PathBuf {
project_root.join(".semantex")
}
pub fn indexes_root(project_root: &Path) -> PathBuf {
container_dir(project_root).join("indexes")
}
pub fn branch_index_dir(project_root: &Path, branch_key: &str) -> PathBuf {
indexes_root(project_root).join(branch_key)
}
pub fn history_db_path(project_root: &Path) -> PathBuf {
container_dir(project_root).join("history.db")
}
pub fn memory_db_path(project_root: &Path) -> PathBuf {
container_dir(project_root).join("memory.db")
}
pub fn project_meta_path(project_root: &Path) -> PathBuf {
container_dir(project_root).join("meta.json")
}
pub fn root_branch_meta_path(project_root: &Path) -> PathBuf {
container_dir(project_root).join("branch.json")
}
pub fn read_root_branch_meta(project_root: &Path) -> Option<BranchMeta> {
let content = std::fs::read_to_string(root_branch_meta_path(project_root)).ok()?;
serde_json::from_str(&content).ok()
}
pub fn read_branch_dir_meta(project_root: &Path, branch_key: &str) -> Option<BranchMeta> {
let path = branch_index_dir(project_root, branch_key).join("branch.json");
let content = std::fs::read_to_string(path).ok()?;
serde_json::from_str(&content).ok()
}
const BRANCH_KEY_NAME_MAX: usize = 64;
fn sanitize_branch_name(name: &str) -> String {
name.chars()
.take(BRANCH_KEY_NAME_MAX)
.map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
.collect()
}
fn ref_name_hash8(ref_name: &str) -> String {
use sha2::{Digest, Sha256};
use std::fmt::Write as _;
let mut hasher = Sha256::new();
hasher.update(ref_name.as_bytes());
let digest = hasher.finalize();
digest[..4].iter().fold(String::new(), |mut out, b| {
let _ = write!(out, "{b:02x}");
out
})
}
pub fn branch_key_for_branch(branch_name: &str) -> String {
format!(
"{}-{}",
sanitize_branch_name(branch_name),
ref_name_hash8(branch_name)
)
}
fn resolve_git_dir(project_root: &Path) -> Option<PathBuf> {
let dot_git = project_root.join(".git");
if dot_git.is_dir() {
return Some(dot_git);
}
if dot_git.is_file() {
let content = std::fs::read_to_string(&dot_git).ok()?;
let pointer = content
.lines()
.find_map(|l| l.trim().strip_prefix("gitdir:"))?;
let p = PathBuf::from(pointer.trim());
return Some(if p.is_absolute() {
p
} else {
project_root.join(p)
});
}
None
}
pub fn git_head_file(project_root: &Path) -> Option<PathBuf> {
resolve_git_dir(project_root).map(|d| d.join("HEAD"))
}
pub fn resolve_git_head_branch(project_root: &Path) -> Option<String> {
let git_dir = resolve_git_dir(project_root)?;
let head = std::fs::read_to_string(git_dir.join("HEAD")).ok()?;
head.trim()
.strip_prefix("ref: refs/heads/")
.map(str::to_string)
}
fn resolve_ref_in(git_dir: &Path, ref_name: &str) -> Option<String> {
if let Ok(hash) = std::fs::read_to_string(git_dir.join(ref_name)) {
return Some(hash.trim().to_string());
}
let packed = std::fs::read_to_string(git_dir.join("packed-refs")).ok()?;
packed.lines().find_map(|line| {
let line = line.trim();
if line.starts_with('#') {
return None;
}
let mut parts = line.split_whitespace();
let hash = parts.next()?;
let name = parts.next()?;
(name == ref_name).then(|| hash.to_string())
})
}
pub fn resolve_git_head_commit(project_root: &Path) -> Option<String> {
let git_dir = resolve_git_dir(project_root)?;
let head = std::fs::read_to_string(git_dir.join("HEAD")).ok()?;
let head = head.trim();
if let Some(rest) = head.strip_prefix("ref: ") {
if let Some(hash) = resolve_ref_in(&git_dir, rest) {
return Some(hash);
}
if let Ok(common) = std::fs::read_to_string(git_dir.join("commondir")) {
let common = PathBuf::from(common.trim());
let common_dir = if common.is_absolute() {
common
} else {
git_dir.join(common)
};
return resolve_ref_in(&common_dir, rest);
}
return None;
}
Some(head.to_string())
}
pub fn current_branch_key(project_root: &Path) -> String {
match resolve_git_head_branch(project_root) {
Some(branch) => branch_key_for_branch(&branch),
None => DEFAULT_BRANCH_KEY.to_string(),
}
}
pub fn current_branch_name(project_root: &Path) -> String {
resolve_git_head_branch(project_root).unwrap_or_else(|| DEFAULT_BRANCH_KEY.to_string())
}
pub fn open_history_db(path: &Path) -> Result<Connection> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let conn = Connection::open(path)?;
conn.execute_batch(
"
CREATE TABLE IF NOT EXISTS commits (
hash TEXT PRIMARY KEY,
author TEXT NOT NULL,
ts INTEGER NOT NULL,
message TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS file_commits (
path TEXT NOT NULL,
hash TEXT NOT NULL,
PRIMARY KEY (path, hash)
);
CREATE TABLE IF NOT EXISTS chunk_blame (
chunk_id INTEGER NOT NULL,
hash TEXT NOT NULL,
PRIMARY KEY (chunk_id, hash)
);
",
)?;
Ok(conn)
}
pub fn open_memory_db(path: &Path) -> Result<Connection> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let conn = Connection::open(path)?;
conn.execute_batch(
"
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_ts INTEGER NOT NULL,
updated_ts INTEGER NOT NULL,
scope TEXT NOT NULL,
key TEXT NOT NULL,
content TEXT NOT NULL,
source TEXT NOT NULL,
UNIQUE(scope, key)
);
",
)?;
Ok(conn)
}
fn hardlink_tree(src: &Path, dst: &Path) -> Result<()> {
if src.is_dir() {
std::fs::create_dir_all(dst)?;
for entry in std::fs::read_dir(src)? {
let entry = entry?;
let name = entry.file_name();
hardlink_tree(&src.join(&name), &dst.join(&name))?;
}
} else if src.is_file() {
if let Some(parent) = dst.parent() {
std::fs::create_dir_all(parent)?;
}
if std::fs::hard_link(src, dst).is_err() {
std::fs::copy(src, dst)?;
}
}
Ok(())
}
fn copy_tree(src: &Path, dst: &Path) -> Result<()> {
if src.is_dir() {
std::fs::create_dir_all(dst)?;
for entry in std::fs::read_dir(src)? {
let entry = entry?;
let name = entry.file_name();
copy_tree(&src.join(&name), &dst.join(&name))?;
}
} else if src.is_file() {
if let Some(parent) = dst.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::copy(src, dst)?;
}
Ok(())
}
fn snapshot_sqlite_db(src: &Path, dst: &Path) -> Result<()> {
if let Some(parent) = dst.parent() {
std::fs::create_dir_all(parent)?;
}
let conn = Connection::open(src)?;
if dst.exists() {
std::fs::remove_file(dst)?;
}
let dst_str = dst.to_string_lossy().into_owned();
match conn.execute("VACUUM INTO ?1", [&dst_str]) {
Ok(_) => Ok(()),
Err(e) => {
tracing::debug!("VACUUM INTO failed ({e}); falling back to checkpoint + copy");
let _ = conn.query_row("PRAGMA wal_checkpoint(TRUNCATE)", [], |_| Ok(()));
std::fs::copy(src, dst)?;
Ok(())
}
}
}
pub fn mirror_into_branch_dir(project_root: &Path, branch_key: &str) -> Result<()> {
let branch_meta = BranchMeta {
branch: current_branch_name(project_root),
branch_key: branch_key.to_string(),
head_commit: resolve_git_head_commit(project_root),
};
mirror_root_as(project_root, &branch_meta)
}
pub fn mirror_root_as(project_root: &Path, branch_meta: &BranchMeta) -> Result<()> {
let container = container_dir(project_root);
if !container.join("chunks.db").exists() {
return Ok(());
}
let branch_key = branch_meta.branch_key.as_str();
let branch_dir = branch_index_dir(project_root, branch_key);
let tmp_dir =
indexes_root(project_root).join(format!("{branch_key}.tmp-mirror.{}", std::process::id()));
if tmp_dir.exists() {
std::fs::remove_dir_all(&tmp_dir)?;
}
std::fs::create_dir_all(&tmp_dir)?;
snapshot_sqlite_db(&container.join("chunks.db"), &tmp_dir.join("chunks.db"))?;
for name in ["meta.json", "models.toml"] {
let src = container.join(name);
if src.is_file() {
copy_tree(&src, &tmp_dir.join(name))?;
}
}
let dense_src = container.join("dense");
if dense_src.is_dir() {
copy_tree(&dense_src, &tmp_dir.join("dense"))?;
}
let sparse_src = container.join("sparse");
if sparse_src.is_dir() {
hardlink_tree(&sparse_src, &tmp_dir.join("sparse"))?;
}
let json = serde_json::to_string_pretty(branch_meta)?;
std::fs::write(tmp_dir.join("branch.json"), &json)?;
if branch_dir.exists() {
std::fs::remove_dir_all(&branch_dir)?;
}
std::fs::rename(&tmp_dir, &branch_dir)?;
std::fs::write(container.join("branch.json"), &json)?;
Ok(())
}
pub fn restore_branch_dir_into_root(project_root: &Path, branch_key: &str) -> Result<()> {
let branch_dir = branch_index_dir(project_root, branch_key);
if !branch_dir.join("chunks.db").exists() {
return Ok(());
}
let container = container_dir(project_root);
std::fs::create_dir_all(&container)?;
let pid = std::process::id();
let tmp_chunks =
indexes_root(project_root).join(format!("{branch_key}.chunks.tmp-restore.{pid}"));
if tmp_chunks.exists() {
std::fs::remove_file(&tmp_chunks)?;
}
snapshot_sqlite_db(&branch_dir.join("chunks.db"), &tmp_chunks)?;
std::fs::rename(&tmp_chunks, container.join("chunks.db"))?;
for suffix in ["-wal", "-shm", "-journal"] {
let sidecar = container.join(format!("chunks.db{suffix}"));
match std::fs::remove_file(&sidecar) {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
Err(e) => {
return Err(e).with_context(|| {
format!(
"restore: failed to remove stale {} beside the restored chunks.db",
sidecar.display()
)
});
}
}
}
for name in ["meta.json", "models.toml"] {
let src = branch_dir.join(name);
let dst = container.join(name);
if !src.is_file() {
match std::fs::remove_file(&dst) {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
Err(e) => {
return Err(e).with_context(|| {
format!("restore: failed to remove leftover {}", dst.display())
});
}
}
continue;
}
let tmp = indexes_root(project_root).join(format!("{branch_key}.{name}.tmp-restore.{pid}"));
std::fs::copy(&src, &tmp)?;
std::fs::rename(&tmp, dst)?;
}
let dense_dst = container.join("dense");
let dense_src = branch_dir.join("dense");
if dense_src.is_dir() {
let tmp_dense =
indexes_root(project_root).join(format!("{branch_key}.dense.tmp-restore.{pid}"));
if tmp_dense.exists() {
std::fs::remove_dir_all(&tmp_dense)?;
}
copy_tree(&dense_src, &tmp_dense)?;
if dense_dst.exists() {
std::fs::remove_dir_all(&dense_dst)?;
}
std::fs::rename(&tmp_dense, &dense_dst)?;
} else if dense_dst.exists() {
std::fs::remove_dir_all(&dense_dst)?;
}
let sparse_dst = container.join("sparse");
let sparse_src = branch_dir.join("sparse");
if sparse_src.is_dir() {
let tmp_sparse =
indexes_root(project_root).join(format!("{branch_key}.sparse.tmp-restore.{pid}"));
if tmp_sparse.exists() {
std::fs::remove_dir_all(&tmp_sparse)?;
}
hardlink_tree(&sparse_src, &tmp_sparse)?;
if sparse_dst.exists() {
std::fs::remove_dir_all(&sparse_dst)?;
}
std::fs::rename(&tmp_sparse, &sparse_dst)?;
} else if sparse_dst.exists() {
std::fs::remove_dir_all(&sparse_dst)?;
}
if let Ok(branch_meta_json) = std::fs::read_to_string(branch_dir.join("branch.json")) {
std::fs::write(container.join("branch.json"), branch_meta_json)?;
}
Ok(())
}
pub fn cleanup_stale_tmp_artifacts(project_root: &Path) {
let indexes = indexes_root(project_root);
let Ok(entries) = std::fs::read_dir(&indexes) else {
return;
};
for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().into_owned();
if !name.contains(".tmp-") {
continue;
}
let path = entry.path();
let removed = if path.is_dir() {
std::fs::remove_dir_all(&path)
} else {
std::fs::remove_file(&path)
};
match removed {
Ok(()) => tracing::info!(path = %path.display(), "Removed orphaned tmp artifact"),
Err(e) => tracing::debug!(path = %path.display(), "Could not remove tmp artifact: {e}"),
}
}
}
pub fn sync_v13_layout(project_root: &Path, project_id: &str) -> Result<PathBuf> {
let container = container_dir(project_root);
std::fs::create_dir_all(&container)?;
let branch_key = current_branch_key(project_root);
if let Err(e) = mirror_into_branch_dir(project_root, &branch_key) {
tracing::warn!("v13 layout mirror failed (root index still valid): {e}");
}
let _ = open_history_db(&history_db_path(project_root))?;
let _ = open_memory_db(&memory_db_path(project_root))?;
let meta_path = project_meta_path(project_root);
if let Ok(existing) = std::fs::read_to_string(&meta_path)
&& let Ok(active_index_meta) = serde_json::from_str::<IndexMeta>(&existing)
{
let project_meta = ProjectMeta {
layout_version: LAYOUT_VERSION,
project_id: project_id.to_string(),
default_branch: current_branch_name(project_root),
active_index_meta,
};
let json = serde_json::to_string_pretty(&project_meta)?;
std::fs::write(&meta_path, json)?;
}
Ok(branch_index_dir(project_root, &branch_key))
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn sanitize_replaces_non_alnum() {
assert_eq!(sanitize_branch_name("feature/foo-bar"), "feature-foo-bar");
assert_eq!(sanitize_branch_name("main"), "main");
assert_eq!(sanitize_branch_name("a b.c"), "a-b-c");
}
#[test]
fn branch_key_is_deterministic_and_unique_per_name() {
let k1 = branch_key_for_branch("main");
let k2 = branch_key_for_branch("main");
let k3 = branch_key_for_branch("develop");
assert_eq!(k1, k2, "same branch name must yield the same key");
assert_ne!(k1, k3, "different branch names must yield different keys");
assert!(k1.starts_with("main-"));
let hash_part = k1.rsplit('-').next().unwrap();
assert_eq!(hash_part.len(), 8);
assert!(hash_part.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn non_git_dir_resolves_to_default_branch_key() {
let tmp = TempDir::new().unwrap();
assert_eq!(current_branch_key(tmp.path()), DEFAULT_BRANCH_KEY);
assert_eq!(current_branch_name(tmp.path()), DEFAULT_BRANCH_KEY);
}
#[test]
fn resolves_current_branch_from_real_git_dir() {
let tmp = TempDir::new().unwrap();
let git_dir = tmp.path().join(".git");
std::fs::create_dir_all(git_dir.join("refs").join("heads")).unwrap();
std::fs::write(git_dir.join("HEAD"), "ref: refs/heads/feature/x\n").unwrap();
std::fs::write(git_dir.join("refs").join("heads").join("x"), "deadbeef\n").unwrap();
assert_eq!(
resolve_git_head_branch(tmp.path()),
Some("feature/x".to_string())
);
assert_eq!(
current_branch_key(tmp.path()),
branch_key_for_branch("feature/x")
);
}
#[test]
fn git_head_file_resolves_in_main_repo_and_worktree() {
let tmp = TempDir::new().unwrap();
std::fs::create_dir_all(tmp.path().join(".git")).unwrap();
assert_eq!(
git_head_file(tmp.path()),
Some(tmp.path().join(".git").join("HEAD"))
);
let non_git = TempDir::new().unwrap();
assert_eq!(git_head_file(non_git.path()), None);
let real_git = tmp.path().join("real.git").join("worktrees").join("wt1");
std::fs::create_dir_all(&real_git).unwrap();
let worktree = tmp.path().join("worktree-dir");
std::fs::create_dir_all(&worktree).unwrap();
std::fs::write(
worktree.join(".git"),
format!("gitdir: {}\n", real_git.display()),
)
.unwrap();
assert_eq!(git_head_file(&worktree), Some(real_git.join("HEAD")));
}
#[test]
fn detached_head_resolves_to_default() {
let tmp = TempDir::new().unwrap();
let git_dir = tmp.path().join(".git");
std::fs::create_dir_all(&git_dir).unwrap();
std::fs::write(git_dir.join("HEAD"), "deadbeefdeadbeefdeadbeef\n").unwrap();
assert_eq!(resolve_git_head_branch(tmp.path()), None);
assert_eq!(current_branch_key(tmp.path()), DEFAULT_BRANCH_KEY);
}
#[test]
fn worktree_gitdir_pointer_is_followed() {
let tmp = TempDir::new().unwrap();
let real_git = tmp.path().join("real.git").join("worktrees").join("wt1");
std::fs::create_dir_all(&real_git).unwrap();
std::fs::write(real_git.join("HEAD"), "ref: refs/heads/main\n").unwrap();
let worktree = tmp.path().join("worktree-dir");
std::fs::create_dir_all(&worktree).unwrap();
std::fs::write(
worktree.join(".git"),
format!("gitdir: {}\n", real_git.display()),
)
.unwrap();
assert_eq!(resolve_git_head_branch(&worktree), Some("main".to_string()));
}
fn sample_index_meta() -> IndexMeta {
IndexMeta {
schema_version: IndexMeta::CURRENT_SCHEMA_VERSION,
project_path: PathBuf::from("/x"),
created_at: "0".to_string(),
updated_at: "0".to_string(),
file_count: 3,
chunk_count: 9,
embedding_model: "CodeRankEmbed".to_string(),
embedding_dim: 768,
use_bm25_stemmer: true,
dense_backend: "coderank-hnsw".to_string(),
embedder_fingerprint: "fp".to_string(),
}
}
#[test]
fn project_meta_is_superset_compatible_with_index_meta() {
let project_meta = ProjectMeta {
layout_version: LAYOUT_VERSION,
project_id: "proj-abc".to_string(),
default_branch: "main".to_string(),
active_index_meta: sample_index_meta(),
};
let json = serde_json::to_string(&project_meta).unwrap();
let back: IndexMeta = serde_json::from_str(&json).unwrap();
assert_eq!(back.schema_version, IndexMeta::CURRENT_SCHEMA_VERSION);
assert_eq!(back.embedder_fingerprint, "fp");
let back2: ProjectMeta = serde_json::from_str(&json).unwrap();
assert_eq!(back2.layout_version, LAYOUT_VERSION);
assert_eq!(back2.project_id, "proj-abc");
assert_eq!(back2.default_branch, "main");
assert_eq!(back2.active_index_meta.chunk_count, 9);
}
#[test]
fn history_and_memory_db_schemas_are_created() {
let tmp = TempDir::new().unwrap();
let history = open_history_db(&tmp.path().join("history.db")).unwrap();
history
.execute(
"INSERT INTO commits (hash, author, ts, message) VALUES ('h1', 'a', 1, 'm')",
[],
)
.unwrap();
history
.execute(
"INSERT INTO file_commits (path, hash) VALUES ('f.rs', 'h1')",
[],
)
.unwrap();
history
.execute(
"INSERT INTO chunk_blame (chunk_id, hash) VALUES (1, 'h1')",
[],
)
.unwrap();
let memory = open_memory_db(&tmp.path().join("memory.db")).unwrap();
memory
.execute(
"INSERT INTO notes (created_ts, updated_ts, scope, key, content, source) \
VALUES (1, 1, 's', 'k', 'c', 'src')",
[],
)
.unwrap();
let dup = memory.execute(
"INSERT INTO notes (created_ts, updated_ts, scope, key, content, source) \
VALUES (2, 2, 's', 'k', 'c2', 'src2')",
[],
);
assert!(dup.is_err(), "duplicate (scope,key) must be rejected");
open_history_db(&tmp.path().join("history.db")).unwrap();
open_memory_db(&tmp.path().join("memory.db")).unwrap();
}
#[test]
fn legacy_layout_round_trips_through_v13_migration() {
use crate::index::storage::ChunkStore;
use crate::types::{Chunk, ChunkType};
let tmp = TempDir::new().unwrap();
let project = tmp.path();
let container = container_dir(project);
std::fs::create_dir_all(&container).unwrap();
let chunk_id = {
let store = ChunkStore::open(&container.join("chunks.db")).unwrap();
let chunk = Chunk {
id: 0,
file_path: PathBuf::from("src/lib.rs"),
start_line: 1,
end_line: 5,
content: "fn legacy() {}".to_string(),
chunk_type: ChunkType::TextWindow { window_index: 0 },
};
store.insert_chunk(&chunk, 0xdead, 0).unwrap()
};
std::fs::write(
container.join("meta.json"),
serde_json::to_string(&sample_index_meta()).unwrap(),
)
.unwrap();
std::fs::create_dir_all(container.join("sparse")).unwrap();
std::fs::write(container.join("sparse").join("seg.dat"), b"tantivy-ish").unwrap();
let branch_dir = sync_v13_layout(project, "proj-123").unwrap();
assert!(branch_dir.join("chunks.db").exists());
assert!(branch_dir.join("meta.json").exists());
assert!(branch_dir.join("branch.json").exists());
assert!(branch_dir.join("sparse").join("seg.dat").exists());
let migrated_store = ChunkStore::open(&branch_dir.join("chunks.db")).unwrap();
let chunk = migrated_store.get_chunk(chunk_id).unwrap();
assert_eq!(chunk.content, "fn legacy() {}");
let branch_meta_json = std::fs::read_to_string(branch_dir.join("meta.json")).unwrap();
let branch_meta: IndexMeta = serde_json::from_str(&branch_meta_json).unwrap();
assert_eq!(branch_meta.chunk_count, 9);
let root_store = ChunkStore::open(&container.join("chunks.db")).unwrap();
let root_chunk = root_store.get_chunk(chunk_id).unwrap();
assert_eq!(root_chunk.content, "fn legacy() {}");
let top_json = std::fs::read_to_string(container.join("meta.json")).unwrap();
let top_as_project: ProjectMeta = serde_json::from_str(&top_json).unwrap();
assert_eq!(top_as_project.layout_version, LAYOUT_VERSION);
assert_eq!(top_as_project.project_id, "proj-123");
let top_as_legacy: IndexMeta = serde_json::from_str(&top_json).unwrap();
assert_eq!(top_as_legacy.chunk_count, 9);
let branch_dir_2 = sync_v13_layout(project, "proj-123").unwrap();
assert_eq!(branch_dir, branch_dir_2);
assert!(branch_dir_2.join("chunks.db").exists());
assert!(history_db_path(project).exists());
assert!(memory_db_path(project).exists());
}
#[test]
fn sync_on_empty_project_is_a_safe_no_op_for_mirror() {
let tmp = TempDir::new().unwrap();
let branch_dir = sync_v13_layout(tmp.path(), "proj-empty").unwrap();
assert!(!branch_dir.join("chunks.db").exists());
assert!(history_db_path(tmp.path()).exists());
assert!(memory_db_path(tmp.path()).exists());
}
#[test]
fn branch_key_name_prefix_is_capped() {
let long_name = "x".repeat(500);
let key = branch_key_for_branch(&long_name);
assert_eq!(key.len(), BRANCH_KEY_NAME_MAX + 1 + 8, "key was: {key}");
let key2 = branch_key_for_branch(&format!("{}{}", "x".repeat(64), "different-tail"));
assert_ne!(key, key2);
assert_eq!(key, branch_key_for_branch(&long_name));
}
#[test]
fn worktree_commondir_fallback_resolves_head_commit() {
let tmp = TempDir::new().unwrap();
let main_git = tmp.path().join("main.git");
std::fs::create_dir_all(main_git.join("refs").join("heads")).unwrap();
std::fs::write(
main_git.join("refs").join("heads").join("main"),
"cafebabe\n",
)
.unwrap();
let wt_git = main_git.join("worktrees").join("wt1");
std::fs::create_dir_all(&wt_git).unwrap();
std::fs::write(wt_git.join("HEAD"), "ref: refs/heads/main\n").unwrap();
std::fs::write(wt_git.join("commondir"), "../..\n").unwrap();
let worktree = tmp.path().join("wt");
std::fs::create_dir_all(&worktree).unwrap();
std::fs::write(
worktree.join(".git"),
format!("gitdir: {}\n", wt_git.display()),
)
.unwrap();
assert_eq!(
resolve_git_head_commit(&worktree),
Some("cafebabe".to_string())
);
std::fs::remove_file(main_git.join("refs").join("heads").join("main")).unwrap();
std::fs::write(main_git.join("packed-refs"), "deadf00d refs/heads/main\n").unwrap();
assert_eq!(
resolve_git_head_commit(&worktree),
Some("deadf00d".to_string())
);
}
#[test]
fn mirror_is_a_snapshot_not_an_alias_of_the_root() {
use crate::index::storage::ChunkStore;
use crate::types::{Chunk, ChunkType};
let tmp = TempDir::new().unwrap();
let project = tmp.path();
let container = container_dir(project);
std::fs::create_dir_all(container.join("dense").join("backend")).unwrap();
let chunk = |id: u64, content: &str| Chunk {
id,
file_path: PathBuf::from("src/lib.rs"),
start_line: 1,
end_line: 2,
content: content.to_string(),
chunk_type: ChunkType::TextWindow { window_index: 0 },
};
let store = ChunkStore::open(&container.join("chunks.db")).unwrap();
let old_id = store
.insert_chunk(&chunk(0, "fn old_branch() {}"), 1, 0)
.unwrap();
std::fs::write(
container.join("meta.json"),
serde_json::to_string(&sample_index_meta()).unwrap(),
)
.unwrap();
std::fs::write(
container.join("dense").join("backend").join("vectors.bin"),
b"old-branch-vectors",
)
.unwrap();
mirror_into_branch_dir(project, "oldbranch-aaaa1111").unwrap();
let mirror = branch_index_dir(project, "oldbranch-aaaa1111");
let new_id = store
.insert_chunk(&chunk(0, "fn new_branch() {}"), 2, 0)
.unwrap();
drop(store);
std::fs::write(
container.join("dense").join("backend").join("vectors.bin"),
b"NEW-branch-vectors-completely-different",
)
.unwrap();
let mut new_meta = sample_index_meta();
new_meta.chunk_count = 999;
std::fs::write(
container.join("meta.json"),
serde_json::to_string(&new_meta).unwrap(),
)
.unwrap();
let mirror_store = ChunkStore::open(&mirror.join("chunks.db")).unwrap();
assert_eq!(
mirror_store.get_chunk(old_id).unwrap().content,
"fn old_branch() {}"
);
assert!(
mirror_store.get_chunk(new_id).is_err(),
"post-mirror root writes must NOT leak into the mirror"
);
assert_eq!(
std::fs::read(mirror.join("dense").join("backend").join("vectors.bin")).unwrap(),
b"old-branch-vectors"
);
let mirror_meta: IndexMeta =
serde_json::from_str(&std::fs::read_to_string(mirror.join("meta.json")).unwrap())
.unwrap();
assert_eq!(
mirror_meta.chunk_count, 9,
"meta snapshot must not follow the root"
);
}
#[test]
fn mirror_captures_unchekpointed_wal_content() {
use crate::index::storage::ChunkStore;
use crate::types::{Chunk, ChunkType};
let tmp = TempDir::new().unwrap();
let project = tmp.path();
let container = container_dir(project);
std::fs::create_dir_all(&container).unwrap();
let store = ChunkStore::open(&container.join("chunks.db")).unwrap();
let id = store
.insert_chunk(
&Chunk {
id: 0,
file_path: PathBuf::from("src/wal.rs"),
start_line: 1,
end_line: 1,
content: "fn only_in_wal() {}".to_string(),
chunk_type: ChunkType::TextWindow { window_index: 0 },
},
7,
0,
)
.unwrap();
mirror_into_branch_dir(project, "walbranch-bbbb2222").unwrap();
let mirror = branch_index_dir(project, "walbranch-bbbb2222");
let mirror_store = ChunkStore::open(&mirror.join("chunks.db")).unwrap();
assert_eq!(
mirror_store.get_chunk(id).unwrap().content,
"fn only_in_wal() {}",
"rows still in the root's WAL must be present in the snapshot"
);
drop(store);
}
#[test]
fn sync_after_branch_switch_creates_new_branch_dir_and_keeps_old() {
use crate::index::storage::ChunkStore;
use crate::types::{Chunk, ChunkType};
let tmp = TempDir::new().unwrap();
let project = tmp.path();
let container = container_dir(project);
std::fs::create_dir_all(&container).unwrap();
let git = project.join(".git");
std::fs::create_dir_all(git.join("refs").join("heads")).unwrap();
std::fs::write(git.join("HEAD"), "ref: refs/heads/main\n").unwrap();
std::fs::write(git.join("refs").join("heads").join("main"), "abc123\n").unwrap();
{
let store = ChunkStore::open(&container.join("chunks.db")).unwrap();
store
.insert_chunk(
&Chunk {
id: 0,
file_path: PathBuf::from("src/a.rs"),
start_line: 1,
end_line: 1,
content: "fn a() {}".to_string(),
chunk_type: ChunkType::TextWindow { window_index: 0 },
},
1,
0,
)
.unwrap();
}
std::fs::write(
container.join("meta.json"),
serde_json::to_string(&sample_index_meta()).unwrap(),
)
.unwrap();
sync_v13_layout(project, "proj-sw").unwrap();
let main_dir = branch_index_dir(project, &branch_key_for_branch("main"));
assert!(main_dir.join("chunks.db").exists());
std::fs::write(git.join("HEAD"), "ref: refs/heads/feature/new\n").unwrap();
std::fs::write(git.join("refs").join("heads").join("feature"), "").unwrap(); sync_v13_layout(project, "proj-sw").unwrap();
let feature_dir = branch_index_dir(project, &branch_key_for_branch("feature/new"));
assert!(
feature_dir.join("chunks.db").exists(),
"new branch dir must be created by a no-change sync"
);
assert!(
main_dir.join("chunks.db").exists(),
"old branch mirror must survive the switch"
);
let feature_meta: BranchMeta = serde_json::from_str(
&std::fs::read_to_string(feature_dir.join("branch.json")).unwrap(),
)
.unwrap();
assert_eq!(feature_meta.branch, "feature/new");
let main_meta: BranchMeta =
serde_json::from_str(&std::fs::read_to_string(main_dir.join("branch.json")).unwrap())
.unwrap();
assert_eq!(main_meta.branch, "main");
}
#[test]
fn mirror_stamps_root_branch_sidecar() {
use crate::index::storage::ChunkStore;
use crate::types::{Chunk, ChunkType};
let tmp = TempDir::new().unwrap();
let project = tmp.path();
let container = container_dir(project);
std::fs::create_dir_all(&container).unwrap();
assert!(
read_root_branch_meta(project).is_none(),
"no root sidecar before anything is built"
);
let store = ChunkStore::open(&container.join("chunks.db")).unwrap();
let _ = store
.insert_chunk(
&Chunk {
id: 0,
file_path: PathBuf::from("src/a.rs"),
start_line: 1,
end_line: 1,
content: "fn a() {}".to_string(),
chunk_type: ChunkType::TextWindow { window_index: 0 },
},
1,
0,
)
.unwrap();
drop(store);
std::fs::write(
container.join("meta.json"),
serde_json::to_string(&sample_index_meta()).unwrap(),
)
.unwrap();
mirror_into_branch_dir(project, "somebranch-cafe1234").unwrap();
let root_meta = read_root_branch_meta(project).expect("root sidecar written");
assert_eq!(root_meta.branch_key, "somebranch-cafe1234");
}
#[test]
fn restore_branch_dir_into_root_replaces_live_content() {
use crate::index::storage::ChunkStore;
use crate::types::{Chunk, ChunkType};
let tmp = TempDir::new().unwrap();
let project = tmp.path();
let container = container_dir(project);
std::fs::create_dir_all(container.join("dense").join("backend")).unwrap();
{
let store = ChunkStore::open(&container.join("chunks.db")).unwrap();
store
.insert_chunk(
&Chunk {
id: 0,
file_path: PathBuf::from("src/old.rs"),
start_line: 1,
end_line: 1,
content: "fn old_branch_fn() {}".to_string(),
chunk_type: ChunkType::TextWindow { window_index: 0 },
},
1,
0,
)
.unwrap();
}
std::fs::write(
container.join("meta.json"),
serde_json::to_string(&sample_index_meta()).unwrap(),
)
.unwrap();
std::fs::write(
container.join("dense").join("backend").join("vectors.bin"),
b"old-vectors",
)
.unwrap();
mirror_into_branch_dir(project, "old-key11112222").unwrap();
std::fs::remove_file(container.join("chunks.db")).unwrap();
let store = ChunkStore::open(&container.join("chunks.db")).unwrap();
store
.insert_chunk(
&Chunk {
id: 0,
file_path: PathBuf::from("src/new.rs"),
start_line: 1,
end_line: 1,
content: "fn new_branch_fn() {}".to_string(),
chunk_type: ChunkType::TextWindow { window_index: 0 },
},
2,
0,
)
.unwrap();
drop(store);
std::fs::write(
container.join("dense").join("backend").join("vectors.bin"),
b"new-vectors-totally-different",
)
.unwrap();
restore_branch_dir_into_root(project, "old-key11112222").unwrap();
let restored_store = ChunkStore::open(&container.join("chunks.db")).unwrap();
let ids = restored_store.get_all_chunk_ids().unwrap();
assert_eq!(ids.len(), 1, "restore must replace, not merge, chunks.db");
let restored = restored_store.get_chunk(ids[0]).unwrap();
assert_eq!(restored.content, "fn old_branch_fn() {}");
assert_eq!(
std::fs::read(container.join("dense").join("backend").join("vectors.bin")).unwrap(),
b"old-vectors"
);
let root_meta = read_root_branch_meta(project).unwrap();
assert_eq!(root_meta.branch_key, "old-key11112222");
}
#[test]
fn restore_of_nonexistent_branch_dir_is_a_no_op() {
let tmp = TempDir::new().unwrap();
let project = tmp.path();
restore_branch_dir_into_root(project, "never-existed-00000000").unwrap();
assert!(!branch_index_dir(project, "never-existed-00000000").exists());
}
#[test]
fn history_db_is_repo_global_excluded_from_mirror_and_restore() {
use crate::index::storage::ChunkStore;
use crate::types::{Chunk, ChunkType};
let tmp = TempDir::new().unwrap();
let project = tmp.path();
let container = container_dir(project);
std::fs::create_dir_all(&container).unwrap();
{
let store = ChunkStore::open(&container.join("chunks.db")).unwrap();
store
.insert_chunk(
&Chunk {
id: 0,
file_path: PathBuf::from("src/a.rs"),
start_line: 1,
end_line: 1,
content: "fn a() {}".to_string(),
chunk_type: ChunkType::TextWindow { window_index: 0 },
},
1,
0,
)
.unwrap();
}
{
let history = open_history_db(&container.join("history.db")).unwrap();
history
.execute(
"INSERT INTO commits (hash, author, ts, message) VALUES ('h1', 'a', 1, 'm')",
[],
)
.unwrap();
}
mirror_into_branch_dir(project, "histbranch-dddd3333").unwrap();
let mirror = branch_index_dir(project, "histbranch-dddd3333");
assert!(mirror.join("chunks.db").exists());
assert!(
!mirror.join("history.db").exists(),
"history.db is repo-global and must be excluded from branch mirrors"
);
{
let root_history = open_history_db(&container.join("history.db")).unwrap();
root_history
.execute(
"INSERT INTO commits (hash, author, ts, message) VALUES ('h2', 'b', 2, 'm2')",
[],
)
.unwrap();
}
restore_branch_dir_into_root(project, "histbranch-dddd3333").unwrap();
let root_history = open_history_db(&container.join("history.db")).unwrap();
let count: i64 = root_history
.query_row("SELECT COUNT(*) FROM commits", [], |r| r.get(0))
.unwrap();
assert_eq!(
count, 2,
"branch switches must leave the repo-global history.db untouched"
);
}
}