use std::path::{Path, PathBuf};
use std::sync::Mutex;
use chrono::{DateTime, SubsecRound, Utc};
use super::schema::SCHEMA_VERSION;
use super::store::{
create_project_toml, find_by_root, lock_manifests, read_manifest, read_project_toml,
reap_orphans, write_manifest, write_project_toml,
};
use super::{
LegacySection, ProjectError, ProjectManifest, ProjectState, ProjectToml, ProjectTomlPresence,
SeedSection, ServeSection, new_id,
};
pub(super) static WRITE_LOCK: Mutex<()> = Mutex::new(());
pub(super) fn now() -> DateTime<Utc> {
Utc::now().trunc_subsecs(0)
}
pub(super) fn default_name(root: &Path) -> String {
root.file_name()
.and_then(|n| n.to_str())
.unwrap_or("project")
.to_string()
}
fn canonical_root(project_root: &Path) -> Result<PathBuf, ProjectError> {
std::fs::canonicalize(project_root)
.ok()
.filter(|p| p.is_dir())
.ok_or_else(|| ProjectError::BadRoot(project_root.to_path_buf()))
}
fn new_manifest(
id: String,
name: String,
root: PathBuf,
created: DateTime<Utc>,
) -> ProjectManifest {
let stamp = now();
ProjectManifest {
schema_version: SCHEMA_VERSION,
id,
name,
legacy: Some(LegacySection {
runtime_dir: Some(root.join(".weftos").join("runtime")),
}),
root,
state: ProjectState::Active,
created,
last_seen: stamp,
project_toml: ProjectTomlPresence::Present,
seed: Some(SeedSection {
source: "project-init".into(),
legacy_name: None,
}),
serve: Some(ServeSection::default()),
chain: None,
binary: None,
extra: Default::default(),
}
}
pub fn adopt_or_init(
project_root: &Path,
manifests_dir: &Path,
name: Option<&str>,
) -> Result<ProjectManifest, ProjectError> {
let root = canonical_root(project_root)?;
let _guard = WRITE_LOCK.lock().unwrap_or_else(|p| p.into_inner());
let _flock = lock_manifests(manifests_dir)?;
reap_orphans(manifests_dir);
let seeded = find_by_root(manifests_dir, &root)?;
let pt = match read_project_toml(&root)? {
Some(pt) => pt,
None => {
let candidate = ProjectToml {
schema_version: SCHEMA_VERSION,
id: seeded.as_ref().map_or_else(new_id, |m| m.id.clone()),
name: name
.map(str::to_string)
.or_else(|| seeded.as_ref().map(|m| m.name.clone()))
.unwrap_or_else(|| default_name(&root)),
created: seeded.as_ref().map_or_else(now, |m| m.created),
parent: None,
governance: None,
weave: None,
extra: Default::default(),
};
if create_project_toml(&root, &candidate)? {
candidate
} else {
read_project_toml(&root)?.ok_or_else(|| ProjectError::BadRoot(root.clone()))?
}
}
};
register(manifests_dir, &pt, root)
}
pub fn register_existing_nested(
child_root: &Path,
manifests_dir: &Path,
master_id: &str,
) -> Result<ProjectManifest, ProjectError> {
super::validate_id(master_id)?;
let root = canonical_root(child_root)?;
let _guard = WRITE_LOCK.lock().unwrap_or_else(|p| p.into_inner());
let _flock = lock_manifests(manifests_dir)?;
let master = read_manifest(manifests_dir, master_id)?
.ok_or_else(|| ProjectError::NestedRefused("master is not registered".into()))?;
if master.state != ProjectState::Active {
return Err(ProjectError::NestedRefused("master is not active".into()));
}
let parent_root = canonical_root(&master.root)?;
if root == parent_root || !root.starts_with(&parent_root) {
return Err(ProjectError::NestedRefused("child root is outside the master".into()));
}
let parent_toml = read_project_toml(&parent_root)?
.ok_or_else(|| ProjectError::NestedRefused("master has no project.toml".into()))?;
if parent_toml.id != master_id || !parent_toml.is_weave_master() {
return Err(ProjectError::NestedRefused("master identity or weave.master changed".into()));
}
let child_toml = read_project_toml(&root)?
.ok_or_else(|| ProjectError::NestedRefused("child has no project.toml".into()))?;
if child_toml.id == master_id || child_toml.parent.as_deref() != Some(master_id) {
return Err(ProjectError::NestedRefused("child does not name this master".into()));
}
register(manifests_dir, &child_toml, root)
}
fn register(
manifests_dir: &Path,
pt: &ProjectToml,
root: PathBuf,
) -> Result<ProjectManifest, ProjectError> {
let Some(mut m) = read_manifest(manifests_dir, &pt.id)? else {
let m = new_manifest(pt.id.clone(), pt.name.clone(), root, pt.created);
write_manifest(manifests_dir, &m)?;
return Ok(m);
};
if m.root != root && m.state != ProjectState::Missing && m.root.exists() {
return Err(ProjectError::RootConflict {
id: pt.id.clone(),
root,
existing: m.root,
});
}
if m.root == root
&& m.state == ProjectState::Active
&& m.project_toml == ProjectTomlPresence::Present
{
return Ok(m);
}
m.root = root;
m.state = ProjectState::Active;
m.project_toml = ProjectTomlPresence::Present;
m.last_seen = now();
write_manifest(manifests_dir, &m)?;
Ok(m)
}
pub fn reinit_fork(
project_root: &Path,
manifests_dir: &Path,
name: Option<&str>,
force: bool,
) -> Result<ProjectManifest, ProjectError> {
let root = canonical_root(project_root)?;
let _guard = WRITE_LOCK.lock().unwrap_or_else(|p| p.into_inner());
let _flock = lock_manifests(manifests_dir)?;
reap_orphans(manifests_dir);
let old = read_project_toml(&root)?;
if let Some(o) = &old
&& let Some(mut home) = read_manifest(manifests_dir, &o.id)?
&& home.root == root
&& home.state != ProjectState::Archived
{
if !force {
return Err(ProjectError::RegisteredHome {
id: o.id.clone(),
root,
});
}
home.state = ProjectState::Archived;
write_manifest(manifests_dir, &home)?;
}
let mut pt = old.clone().unwrap_or_else(|| ProjectToml {
schema_version: SCHEMA_VERSION,
id: String::new(),
name: default_name(&root),
created: now(),
parent: None,
governance: None,
weave: None,
extra: Default::default(),
});
pt.parent = old.map(|o| o.id);
pt.id = new_id();
pt.created = now();
if let Some(n) = name {
pt.name = n.to_string();
}
write_project_toml(&root, &pt)?;
let m = new_manifest(pt.id.clone(), pt.name.clone(), root, pt.created);
write_manifest(manifests_dir, &m)?;
Ok(m)
}