use std::fs;
use std::path::PathBuf;
use anyhow::{Context, anyhow};
use crate::domain::corpus::index_md::{INDEX_END_MARKER, INDEX_START_MARKER};
use crate::domain::repository::{RepoCorpusKind, WIKI_RELATIVE_PATH, repo_corpus_name};
#[derive(Debug)]
pub struct InitRepoArgs {
pub name: String,
pub path: Option<PathBuf>,
pub force: bool,
}
pub fn cmd_init_repo(args: InitRepoArgs) -> anyhow::Result<()> {
repo_corpus_name(&args.name, RepoCorpusKind::Wiki)
.map_err(|e| anyhow!("invalid repository name {:?}: {e}", args.name))?;
let root = match args.path {
Some(p) => p,
None => std::env::current_dir().context("resolve current directory")?,
};
let config_path = root.join(".hallouminate").join("config.toml");
if config_path.exists() && !args.force {
return Err(anyhow!(
"repo config already exists at {}; pass --force to overwrite",
config_path.display()
));
}
let wiki_dir = root.join(WIKI_RELATIVE_PATH);
fs::create_dir_all(&wiki_dir).with_context(|| format!("create {}", wiki_dir.display()))?;
let config_body = format!(
"[[repository]]\nname = {}\npath = \".\"\n",
toml::Value::String(args.name.clone())
);
fs::write(&config_path, config_body)
.with_context(|| format!("write {}", config_path.display()))?;
let index_path = wiki_dir.join("index.md");
if !index_path.exists() {
let index_body = format!(
"# {} wiki\n\n{INDEX_START_MARKER}\n{INDEX_END_MARKER}\n",
args.name
);
fs::write(&index_path, index_body)
.with_context(|| format!("write {}", index_path.display()))?;
}
println!("wrote {}", config_path.display());
println!(
"wiki skeleton at {} (corpus: repo:{}:wiki)",
wiki_dir.display(),
args.name
);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::app::config::{Config, resolve_for_cwd};
fn init(name: &str, root: &std::path::Path, force: bool) -> anyhow::Result<()> {
cmd_init_repo(InitRepoArgs {
name: name.to_string(),
path: Some(root.to_path_buf()),
force,
})
}
#[test]
fn seeds_config_and_wiki_skeleton_that_the_config_loader_accepts() {
let dir = tempfile::tempdir().expect("tempdir");
init("demo", dir.path(), false).expect("init-repo");
let (effective, layers) =
resolve_for_cwd(&Config::default(), dir.path(), None).expect("resolve seeded repo");
let repo_path = layers.repo_path.expect("repo config path");
assert_eq!(
repo_path.canonicalize().expect("canonicalize repo path"),
dir.path()
.canonicalize()
.expect("canonicalize")
.join(".hallouminate/config.toml")
.canonicalize()
.expect("canonicalize expected config path")
);
let names: Vec<String> = effective
.effective_corpora()
.expect("effective corpora")
.into_iter()
.map(|c| c.name)
.collect();
assert!(
names.contains(&"repo:demo:wiki".to_string()),
"seeded repo must derive the wiki corpus, got {names:?}"
);
let index = fs::read_to_string(dir.path().join(".hallouminate/wiki/index.md"))
.expect("read index.md");
assert!(index.starts_with("# demo wiki\n"), "H1-first convention");
assert!(index.contains(INDEX_START_MARKER) && index.contains(INDEX_END_MARKER));
}
#[test]
fn refuses_to_overwrite_existing_config_without_force() {
let dir = tempfile::tempdir().expect("tempdir");
init("demo", dir.path(), false).expect("first init");
let err = init("other", dir.path(), false).expect_err("second init must fail");
assert!(err.to_string().contains("--force"), "err: {err}");
let config =
fs::read_to_string(dir.path().join(".hallouminate/config.toml")).expect("read config");
assert!(config.contains("name = \"demo\""), "original config kept");
}
#[test]
fn force_overwrites_config_but_preserves_existing_wiki_index() {
let dir = tempfile::tempdir().expect("tempdir");
init("demo", dir.path(), false).expect("first init");
let index_path = dir.path().join(".hallouminate/wiki/index.md");
fs::write(&index_path, "# hand-written index\n").expect("write index");
init("renamed", dir.path(), true).expect("forced init");
let config =
fs::read_to_string(dir.path().join(".hallouminate/config.toml")).expect("read config");
assert!(config.contains("name = \"renamed\""), "config: {config}");
let index = fs::read_to_string(&index_path).expect("read index");
assert_eq!(index, "# hand-written index\n", "wiki must survive --force");
}
#[test]
fn rejects_names_the_config_loader_would_reject() {
let dir = tempfile::tempdir().expect("tempdir");
for bad in ["", "with:colon"] {
let err = init(bad, dir.path(), false).expect_err("bad name must fail");
assert!(
err.to_string().contains("invalid repository name"),
"err for {bad:?}: {err}"
);
}
assert!(
!dir.path().join(".hallouminate").exists(),
"nothing written on validation failure"
);
}
#[test]
fn quotes_names_needing_toml_escaping() {
let dir = tempfile::tempdir().expect("tempdir");
init("has \"quotes\"", dir.path(), false).expect("init-repo");
let (effective, _) =
resolve_for_cwd(&Config::default(), dir.path(), None).expect("resolve seeded repo");
assert_eq!(
effective.repositories[0].name, "has \"quotes\"",
"name must round-trip through TOML escaping"
);
}
#[test]
fn seeds_a_target_directory_that_does_not_exist_yet() {
let dir = tempfile::tempdir().expect("tempdir");
let target = dir.path().join("brand/new/repo");
init("demo", &target, false).expect("init-repo into missing dir");
let (effective, _) =
resolve_for_cwd(&Config::default(), &target, None).expect("resolve seeded repo");
assert_eq!(effective.repositories[0].name, "demo");
assert!(target.join(".hallouminate/wiki/index.md").is_file());
}
}