use rand::RngCore;
use serde::Serialize;
use std::collections::HashMap;
#[cfg(test)]
use std::fs;
use std::path::Path;
use void_core::collab::manifest::RepoKey;
use crate::context::load_public_identity;
use crate::output::{run_command, CliError, CliOptions, ErrorCode};
use crate::repo_init::{self, NewRepoOpts};
#[derive(Debug, Serialize)]
pub struct InitOutput {
pub path: String,
pub created: bool,
pub repo_id: String,
pub repo_name: String,
}
pub fn run(cwd: &Path, opts: &CliOptions) -> Result<(), CliError> {
run_command("init", opts, |ctx| {
let void_dir = cwd.join(".void");
if void_dir.exists() {
let mut details = HashMap::new();
details.insert(
"path".to_string(),
serde_json::Value::String(void_dir.display().to_string()),
);
return Err(CliError::with_details(
ErrorCode::Conflict,
"repository already initialized",
details,
));
}
ctx.progress("Checking identity...");
load_public_identity().map_err(|_| {
CliError::new(
ErrorCode::InvalidArgs,
"no identity found — run 'void identity init' first",
)
})?;
ctx.progress("Creating .void directory...");
let repo_id = uuid::Uuid::new_v4().to_string();
let abs_cwd = cwd.canonicalize().unwrap_or_else(|_| cwd.to_path_buf());
let repo_name = abs_cwd
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unnamed")
.to_string();
repo_init::create_void_dir_structure(&void_dir)?;
ctx.verbose("Created .void directory structure");
let mut key = [0u8; 32];
rand::thread_rng().fill_bytes(&mut key);
let repo_key = RepoKey::from_bytes(key);
ctx.verbose("Generated encryption key");
let result = repo_init::setup_owner_manifest(&void_dir, &repo_key, NewRepoOpts {
repo_name: Some(repo_name.clone()),
repo_id: Some(repo_id.clone()),
repo_secret: Some(repo_init::generate_repo_secret()),
})?;
ctx.verbose("Created contributor manifest and config");
match crate::registry::register_repo(&repo_id, &repo_name, cwd, "self", None) {
Ok(_record) => {}
Err(e) => {
ctx.warn(format!("Failed to register repo in registry: {}", e));
}
}
let void_dir_str = void_dir.display().to_string();
if !ctx.use_json() {
ctx.info(format!(
"Initialized void repository in {} (collaboration mode, owner: {})",
void_dir_str, result.username
));
}
Ok(InitOutput {
path: void_dir_str,
created: true,
repo_id,
repo_name,
})
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::output::CliOptions;
use tempfile::tempdir;
use void_core::collab::manifest::{load_manifest, SigningPubKey, RecipientPubKey};
use void_core::collab::Identity;
fn default_opts() -> CliOptions {
CliOptions {
human: true,
..Default::default()
}
}
fn setup_test_identity(
home_dir: &Path,
username: &str,
) -> (SigningPubKey, RecipientPubKey, crate::context::VoidHomeGuard) {
let identity = Identity::generate();
let signing_pub = identity.signing_pubkey();
let recipient_pub = identity.recipient_pubkey();
let identity_dir = home_dir.join(".void").join("identity");
fs::create_dir_all(&identity_dir).unwrap();
fs::write(
identity_dir.join("signing.pub"),
signing_pub.to_hex(),
)
.unwrap();
fs::write(
identity_dir.join("recipient.pub"),
recipient_pub.to_hex(),
)
.unwrap();
fs::write(
identity_dir.join("profile.json"),
format!(r#"{{"username":"{}"}}"#, username),
)
.unwrap();
let guard = crate::context::VoidHomeGuard::new(home_dir);
(signing_pub, recipient_pub, guard)
}
#[test]
fn test_init_creates_void_dir() {
let home = tempdir().unwrap();
let _guard = setup_test_identity(home.path(), "alice");
let dir = tempdir().unwrap();
let result = run(dir.path(), &default_opts());
assert!(result.is_ok());
let void_dir = dir.path().join(".void");
assert!(void_dir.exists());
assert!(
!void_dir.join("key.legacy").exists(),
"should NOT have key.legacy (plaintext key storage eliminated)"
);
assert!(!void_dir.join("key").exists(), "should NOT have plaintext key");
assert!(void_dir.join("contributors.json").exists());
assert!(void_dir.join("config.json").exists());
assert!(void_dir.join("objects").is_dir());
assert!(void_dir.join("refs/heads").is_dir());
assert!(void_dir.join("staged").is_dir());
assert!(void_dir.join("HEAD").exists());
}
#[test]
fn test_init_head_file_content() {
let home = tempdir().unwrap();
let _guard = setup_test_identity(home.path(), "alice");
let dir = tempdir().unwrap();
run(dir.path(), &default_opts()).unwrap();
let head_content = fs::read_to_string(dir.path().join(".void/HEAD")).unwrap();
assert_eq!(head_content, "ref: refs/heads/trunk\n");
}
#[test]
fn test_init_key_wrapped_in_manifest() {
let home = tempdir().unwrap();
let (signing_pub, _recipient_pub, _guard) = setup_test_identity(home.path(), "alice");
let dir = tempdir().unwrap();
run(dir.path(), &default_opts()).unwrap();
let void_dir = dir.path().join(".void");
assert!(!void_dir.join("key.legacy").exists());
let manifest = load_manifest(&void_dir).unwrap().expect("manifest should exist");
assert!(
manifest.read_keys.wrapped.contains_key(&signing_pub),
"manifest should contain a wrapped key for the owner"
);
}
#[test]
fn test_init_repo_secret_in_config() {
let home = tempdir().unwrap();
let _guard = setup_test_identity(home.path(), "alice");
let dir = tempdir().unwrap();
run(dir.path(), &default_opts()).unwrap();
let config = void_core::config::load(&dir.path().join(".void")).unwrap();
assert!(config.repo_secret.is_some());
let repo_secret = config.repo_secret.unwrap();
assert_eq!(repo_secret.len(), 64);
assert!(hex::decode(&repo_secret).is_ok());
}
#[test]
fn test_init_already_initialized() {
let home = tempdir().unwrap();
let _guard = setup_test_identity(home.path(), "alice");
let dir = tempdir().unwrap();
run(dir.path(), &default_opts()).unwrap();
let result = run(dir.path(), &default_opts());
assert!(result.is_err());
}
#[test]
fn test_init_creates_manifest() {
let home = tempdir().unwrap();
let _guard = setup_test_identity(home.path(), "alice");
let dir = tempdir().unwrap();
run(dir.path(), &default_opts()).unwrap();
let void_dir = dir.path().join(".void");
let manifest = load_manifest(&void_dir).unwrap().expect("manifest should exist");
let owner = &manifest.owner;
assert_eq!(manifest.contributors.len(), 1);
assert!(manifest.is_owner(&manifest.contributors[0].identity.signing));
assert_eq!(&manifest.contributors[0].added_by, owner);
assert!(manifest.read_keys.wrapped.contains_key(owner));
assert!(manifest.repo_id.is_some());
assert!(manifest.repo_name.is_some());
}
#[test]
fn test_init_no_identity_fails() {
let home = tempdir().unwrap();
let _guard = crate::context::VoidHomeGuard::new(home.path());
let dir = tempdir().unwrap();
let result = run(dir.path(), &default_opts());
let err = result.unwrap_err();
assert!(
err.message.contains("void identity init"),
"error should mention 'void identity init', got: {}",
err.message
);
assert!(!dir.path().join(".void").exists());
}
#[test]
fn test_init_config_has_username() {
let home = tempdir().unwrap();
let _guard = setup_test_identity(home.path(), "bob");
let dir = tempdir().unwrap();
run(dir.path(), &default_opts()).unwrap();
let void_dir = dir.path().join(".void");
let config = void_core::config::load(&void_dir).unwrap();
assert!(config.user.name.is_some());
let manifest = load_manifest(&void_dir).unwrap().expect("manifest should exist");
assert_eq!(config.user.name, manifest.contributors[0].name);
}
#[test]
fn test_init_repo_name_not_unnamed() {
let home = tempdir().unwrap();
let _guard = setup_test_identity(home.path(), "alice");
let dir = tempdir().unwrap();
let project_dir = dir.path().join("my-project");
fs::create_dir(&project_dir).unwrap();
run(&project_dir, &default_opts()).unwrap();
let config = void_core::config::load(&project_dir.join(".void")).unwrap();
assert_eq!(
config.repo_name,
Some("my-project".to_string()),
"repo_name should be derived from directory name"
);
}
#[test]
fn test_init_repo_name_with_relative_path() {
let home = tempdir().unwrap();
let _guard = setup_test_identity(home.path(), "alice");
let dir = tempdir().unwrap();
let project_dir = dir.path().join("my-repo");
fs::create_dir(&project_dir).unwrap();
let old_cwd = std::env::current_dir().unwrap();
std::env::set_current_dir(&project_dir).unwrap();
let result = run(Path::new("."), &default_opts());
std::env::set_current_dir(&old_cwd).unwrap();
result.unwrap();
let config = void_core::config::load(&project_dir.join(".void")).unwrap();
assert_ne!(
config.repo_name,
Some("unnamed".to_string()),
"repo_name should NOT be 'unnamed' when using relative path"
);
assert_eq!(
config.repo_name,
Some("my-repo".to_string()),
"repo_name should be 'my-repo' even when cwd is '.'"
);
}
}