use std::path::{Path, PathBuf};
use std::sync::Mutex;
use memstead_base::{
FileWorkspaceStore, Mount, MountCapability, MountLifecycle, MountStorage, Workspace,
WorkspaceStoreAdapter,
};
fn seed_new_layout(workspace_root: &Path, gitdir: &Path, mems: &[(&str, &str, &str)]) {
let memstead = workspace_root.join(".memstead");
std::fs::create_dir_all(&memstead).unwrap();
let toml_path = memstead.join("workspace.toml");
if !toml_path.is_file() {
let mut head = String::from("format = \"memstead-git-branch-2\"\n");
head.push_str("\n[persistence_adapter]\nname = \"file-two-layer\"\n");
std::fs::write(&toml_path, head).unwrap();
}
let mounts: Vec<Mount> = mems
.iter()
.map(|(mem, branch, schema)| {
let pin: memstead_schema::SchemaRef = schema
.parse()
.unwrap_or_else(|e| panic!("test seed: invalid schema pin {schema:?}: {e}"));
Mount {
mem: mem.to_string(),
schema: Some(pin),
storage: MountStorage::GitBranch {
gitdir: gitdir.to_path_buf(),
branch: branch.to_string(),
},
capability: MountCapability::Write,
lifecycle: MountLifecycle::Eager,
cross_linkable: true,
migration_target: None,
}
})
.collect();
let workspace = Workspace {
mounts,
settings: Default::default(),
};
FileWorkspaceStore::new()
.save_state(workspace_root, &workspace)
.unwrap();
}
pub fn init_real_mem_repo(workspace_root: &Path, mems: &[(&str, &str)]) -> PathBuf {
static INIT_LOCK: Mutex<()> = Mutex::new(());
let gitdir = workspace_root.join("mem-repo").join(".git");
std::fs::create_dir_all(&gitdir).unwrap_or_else(|e| {
panic!(
"failed to create test mem-repo at {}: {e}",
gitdir.display()
)
});
let _guard = INIT_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let repo = match gix::open(&gitdir) {
Ok(r) => r,
Err(_) => gix::init_bare(&gitdir).unwrap_or_else(|e| {
panic!(
"failed to initialise bare mem-repo-git at {}: {e}",
gitdir.display()
)
}),
};
if matches!(repo.try_find_reference("refs/heads/__SYSTEM"), Ok(Some(_))) {
return workspace_root.to_path_buf();
}
let actor = gix::actor::Signature {
name: "test".into(),
email: "test@example.com".into(),
time: gix::date::Time {
seconds: 0,
offset: 0,
},
};
{
let mut buf = gix::date::parse::TimeBuf::default();
let actor_ref = actor.to_ref(&mut buf);
repo.commit_as(
actor_ref,
actor_ref,
"refs/heads/main",
"seed main",
repo.empty_tree().id().detach(),
Vec::<gix::ObjectId>::new(),
)
.unwrap();
}
let mut system_editor = repo.empty_tree().edit().unwrap();
for (name, schema) in mems {
let config = format!(r#"{{"schema": "{schema}"}}"#);
let blob = repo.write_blob(config.as_bytes()).unwrap().detach();
system_editor
.upsert(
format!("{name}/config.json"),
gix::objs::tree::EntryKind::Blob,
blob,
)
.unwrap();
}
let system_tree = system_editor.write().unwrap().detach();
let mut buf = gix::date::parse::TimeBuf::default();
let actor_ref = actor.to_ref(&mut buf);
repo.commit_as(
actor_ref,
actor_ref,
"refs/heads/__SYSTEM",
"seed __SYSTEM",
system_tree,
Vec::<gix::ObjectId>::new(),
)
.unwrap();
let empty_tree = repo.empty_tree().id().detach();
for (name, _) in mems {
let mut buf = gix::date::parse::TimeBuf::default();
let actor_ref = actor.to_ref(&mut buf);
let ref_name = format!("refs/heads/{name}");
repo.commit_as(
actor_ref,
actor_ref,
ref_name.as_str(),
format!("seed {name}"),
empty_tree,
Vec::<gix::ObjectId>::new(),
)
.unwrap();
}
crate::storage_memstead::migrate_to_memstead_ref(&gitdir).unwrap();
seed_new_layout(
workspace_root,
&gitdir,
&mems
.iter()
.map(|(n, schema)| (*n, *n, *schema))
.collect::<Vec<_>>(),
);
workspace_root.to_path_buf()
}
pub fn auto_seeded_settings(workspace_root: &Path) -> memstead_base::WorkspaceSettings {
auto_seed_with_settings(workspace_root, memstead_base::WorkspaceSettings::default())
}
pub fn auto_seed_with_settings(
workspace_root: &Path,
settings: memstead_base::WorkspaceSettings,
) -> memstead_base::WorkspaceSettings {
let mut mems: Vec<(std::path::PathBuf, String)> = Vec::new();
if let Ok(rd) = std::fs::read_dir(workspace_root) {
for entry in rd.flatten() {
let p = entry.path();
if !p.is_dir() {
continue;
}
if p.file_name().and_then(|s| s.to_str()) == Some("mem-repo") {
continue;
}
let cfg_path = p.join(".memstead").join("config.json");
if !cfg_path.is_file() {
continue;
}
if let Some(name) = p.file_name().and_then(|s| s.to_str()) {
mems.push((p.clone(), name.to_string()));
}
}
}
mems.sort_by(|a, b| a.1.cmp(&b.1));
let refs: Vec<(&Path, &str)> = mems
.iter()
.map(|(p, n)| (p.as_path(), n.as_str()))
.collect();
init_real_mem_repo_from_disk(workspace_root, &refs);
settings
}
pub fn reseed_mem_repo_from_disk(workspace_root: &Path, mems: &[(&Path, &str)]) -> PathBuf {
let gitdir = workspace_root.join("mem-repo").join(".git");
let _ = std::fs::remove_dir_all(&gitdir);
init_real_mem_repo_from_disk(workspace_root, mems)
}
pub fn init_real_mem_repo_from_disk(workspace_root: &Path, mems: &[(&Path, &str)]) -> PathBuf {
let triples: Vec<(&Path, &str, &str)> = mems
.iter()
.map(|(dir, name)| (*dir, *name, *name))
.collect();
init_real_mem_repo_from_disk_with_paths(workspace_root, &triples)
}
pub fn init_real_mem_repo_from_disk_with_paths(
workspace_root: &Path,
mems: &[(&Path, &str, &str)],
) -> PathBuf {
static INIT_LOCK: Mutex<()> = Mutex::new(());
let gitdir = workspace_root.join("mem-repo").join(".git");
std::fs::create_dir_all(&gitdir).unwrap_or_else(|e| {
panic!(
"failed to create test mem-repo at {}: {e}",
gitdir.display()
)
});
let _guard = INIT_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let repo = match gix::open(&gitdir) {
Ok(r) => r,
Err(_) => gix::init_bare(&gitdir).unwrap_or_else(|e| {
panic!(
"failed to initialise bare mem-repo-git at {}: {e}",
gitdir.display()
)
}),
};
if matches!(repo.try_find_reference("refs/heads/__SYSTEM"), Ok(Some(_))) {
return workspace_root.to_path_buf();
}
let actor = gix::actor::Signature {
name: "test".into(),
email: "test@example.com".into(),
time: gix::date::Time {
seconds: 0,
offset: 0,
},
};
{
let mut buf = gix::date::parse::TimeBuf::default();
let actor_ref = actor.to_ref(&mut buf);
repo.commit_as(
actor_ref,
actor_ref,
"refs/heads/main",
"seed main",
repo.empty_tree().id().detach(),
Vec::<gix::ObjectId>::new(),
)
.unwrap();
}
let mut system_editor = repo.empty_tree().edit().unwrap();
for (mem_dir, _leaf, full_path) in mems {
let cfg_path = mem_dir.join(".memstead").join("config.json");
let cfg_bytes = match std::fs::read(&cfg_path) {
Ok(b) => b,
Err(_) => r#"{"schema": "default@1.0.0"}"#.to_string().into_bytes(),
};
let blob = repo.write_blob(&cfg_bytes).unwrap().detach();
system_editor
.upsert(
format!("{full_path}/config.json"),
gix::objs::tree::EntryKind::Blob,
blob,
)
.unwrap();
}
let system_tree = system_editor.write().unwrap().detach();
let mut buf = gix::date::parse::TimeBuf::default();
let actor_ref = actor.to_ref(&mut buf);
repo.commit_as(
actor_ref,
actor_ref,
"refs/heads/__SYSTEM",
"seed __SYSTEM",
system_tree,
Vec::<gix::ObjectId>::new(),
)
.unwrap();
let empty_tree = repo.empty_tree().id().detach();
for (mem_dir, _leaf, full_path) in mems {
let mut entities: Vec<(String, Vec<u8>)> = Vec::new();
if mem_dir.is_dir() {
collect_md_entities(mem_dir, mem_dir, &mut entities);
}
let tree_id = if entities.is_empty() {
empty_tree
} else {
let mut editor = repo.empty_tree().edit().unwrap();
for (rel, bytes) in &entities {
let blob = repo.write_blob(bytes).unwrap().detach();
editor
.upsert(rel.clone(), gix::objs::tree::EntryKind::Blob, blob)
.unwrap();
}
editor.write().unwrap().detach()
};
let mut buf = gix::date::parse::TimeBuf::default();
let actor_ref = actor.to_ref(&mut buf);
let ref_name = format!("refs/heads/{full_path}");
repo.commit_as(
actor_ref,
actor_ref,
ref_name.as_str(),
format!("seed {full_path}"),
tree_id,
Vec::<gix::ObjectId>::new(),
)
.unwrap();
}
crate::storage_memstead::migrate_to_memstead_ref(&gitdir).unwrap();
let owned: Vec<(String, String, String)> = mems
.iter()
.map(|(mem_dir, leaf, full)| {
let pin = std::fs::read_to_string(mem_dir.join(".memstead").join("config.json"))
.ok()
.and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
.and_then(|v| {
v.get("schema")
.and_then(|s| s.as_str())
.map(|s| s.to_string())
})
.unwrap_or_else(|| "default@1.0.0".to_string());
((*leaf).to_string(), (*full).to_string(), pin)
})
.collect();
let refs: Vec<(&str, &str, &str)> = owned
.iter()
.map(|(a, b, c)| (a.as_str(), b.as_str(), c.as_str()))
.collect();
seed_new_layout(workspace_root, &gitdir, &refs);
workspace_root.to_path_buf()
}
fn collect_md_entities(root: &Path, current: &Path, out: &mut Vec<(String, Vec<u8>)>) {
let Ok(rd) = std::fs::read_dir(current) else {
return;
};
for entry in rd.flatten() {
let path = entry.path();
let name = path.file_name().and_then(|s| s.to_str()).unwrap_or("");
if name.starts_with('.') {
continue;
}
if path.is_dir() {
collect_md_entities(root, &path, out);
} else if path.extension().and_then(|s| s.to_str()) == Some("md")
&& let Ok(rel) = path.strip_prefix(root)
&& let Ok(bytes) = std::fs::read(&path)
{
out.push((rel.to_string_lossy().into_owned(), bytes));
}
}
}
pub type MemSeed<'a> = (&'a str, &'a str, &'a [(&'a str, &'a str)]);
pub fn init_real_mem_repo_with_entities(
workspace_root: &Path,
mems_with_entities: &[MemSeed<'_>],
) -> PathBuf {
static INIT_LOCK: Mutex<()> = Mutex::new(());
let gitdir = workspace_root.join("mem-repo").join(".git");
std::fs::create_dir_all(&gitdir).unwrap_or_else(|e| {
panic!(
"failed to create test mem-repo at {}: {e}",
gitdir.display()
)
});
let _guard = INIT_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let repo = match gix::open(&gitdir) {
Ok(r) => r,
Err(_) => gix::init_bare(&gitdir).unwrap_or_else(|e| {
panic!(
"failed to initialise bare mem-repo-git at {}: {e}",
gitdir.display()
)
}),
};
if matches!(repo.try_find_reference("refs/heads/__SYSTEM"), Ok(Some(_))) {
return workspace_root.to_path_buf();
}
let actor = gix::actor::Signature {
name: "test".into(),
email: "test@example.com".into(),
time: gix::date::Time {
seconds: 0,
offset: 0,
},
};
{
let mut buf = gix::date::parse::TimeBuf::default();
let actor_ref = actor.to_ref(&mut buf);
repo.commit_as(
actor_ref,
actor_ref,
"refs/heads/main",
"seed main",
repo.empty_tree().id().detach(),
Vec::<gix::ObjectId>::new(),
)
.unwrap();
}
let mut system_editor = repo.empty_tree().edit().unwrap();
for (name, schema, _) in mems_with_entities {
let config = format!(r#"{{"schema": "{schema}"}}"#);
let blob = repo.write_blob(config.as_bytes()).unwrap().detach();
system_editor
.upsert(
format!("{name}/config.json"),
gix::objs::tree::EntryKind::Blob,
blob,
)
.unwrap();
}
let system_tree = system_editor.write().unwrap().detach();
let mut buf = gix::date::parse::TimeBuf::default();
let actor_ref = actor.to_ref(&mut buf);
repo.commit_as(
actor_ref,
actor_ref,
"refs/heads/__SYSTEM",
"seed __SYSTEM",
system_tree,
Vec::<gix::ObjectId>::new(),
)
.unwrap();
let empty_tree = repo.empty_tree().id().detach();
for (name, _, entities) in mems_with_entities {
let tree_id = if entities.is_empty() {
empty_tree
} else {
let mut editor = repo.empty_tree().edit().unwrap();
for (path, content) in *entities {
let blob = repo.write_blob(content.as_bytes()).unwrap().detach();
editor
.upsert((*path).to_string(), gix::objs::tree::EntryKind::Blob, blob)
.unwrap();
}
editor.write().unwrap().detach()
};
let mut buf = gix::date::parse::TimeBuf::default();
let actor_ref = actor.to_ref(&mut buf);
let ref_name = format!("refs/heads/{name}");
repo.commit_as(
actor_ref,
actor_ref,
ref_name.as_str(),
format!("seed {name}"),
tree_id,
Vec::<gix::ObjectId>::new(),
)
.unwrap();
}
crate::storage_memstead::migrate_to_memstead_ref(&gitdir).unwrap();
seed_new_layout(
workspace_root,
&gitdir,
&mems_with_entities
.iter()
.map(|(name, schema, _)| (*name, *name, *schema))
.collect::<Vec<_>>(),
);
workspace_root.to_path_buf()
}