use std::path::PathBuf;
use anyhow::{Result, bail};
use objects::object::Principal;
use repo::{Repository, RepositoryCapability};
use serde::Serialize;
use sley::Repository as SleyRepository;
use tracing::{debug, info};
use super::{
RecoveryAdvice,
action_line::print_next,
git_overlay_health::{RepositoryVerificationState, build_repository_verification_state},
snapshot::{is_placeholder_principal, placeholder_principal_warning},
};
use crate::{
cli::{Cli, InitArgs, should_output_json, style},
config::UserConfig,
};
#[derive(Serialize)]
struct InitOutput {
output_kind: &'static str,
status: String,
action: String,
path: PathBuf,
repository_mode: String,
git_detected: bool,
heddle_initialized: bool,
installed_heddleignore: bool,
principal_configured: bool,
principal_status: String,
principal_source: Option<String>,
principal: Option<InitPrincipalOutput>,
principal_recommended_action: Option<String>,
#[serde(skip)]
placeholder_principal_warning: Option<String>,
side_effects: Vec<String>,
message: String,
next_action: Option<String>,
recommended_action: Option<String>,
#[allow(dead_code)]
#[serde(skip_serializing)]
#[serde(rename = "verification")]
trust: RepositoryVerificationState,
}
#[derive(Serialize)]
struct InitPrincipalOutput {
name: String,
email: String,
}
pub fn cmd_init(cli: &Cli, args: InitArgs) -> Result<()> {
let path = match (args.path.clone(), cli.repo.clone()) {
(Some(positional), Some(repo_path)) => {
if absolute_path(&positional)? != absolute_path(&repo_path)? {
bail!(RecoveryAdvice::init_path_conflict(
&positional.display().to_string(),
&repo_path.display().to_string(),
));
}
positional
}
(Some(positional), None) => positional,
(None, Some(repo_path)) => repo_path,
(None, None) => std::env::current_dir()
.map_err(|e| anyhow::anyhow!("Failed to determine current directory: {}", e))?,
};
let path = path.canonicalize().unwrap_or(path.clone());
info!(path = %path.display(), "Initializing repository");
let has_git = SleyRepository::discover(&path).is_ok();
let repo = if has_git {
Repository::bootstrap_git_overlay(&path)?
} else {
Repository::init_default(&path)?
};
debug!(heddle_dir = %repo.heddle_dir().display(), "Repository initialized");
let installed_heddleignore = false;
let mut user_config = UserConfig::load_default()?;
let mut principal_configured = false;
if args.principal_name.is_some() || args.principal_email.is_some() {
let name = args.principal_name.clone().ok_or_else(|| {
anyhow::anyhow!(RecoveryAdvice::init_principal_field_required(
"--principal-name"
))
})?;
let email = args.principal_email.clone().ok_or_else(|| {
anyhow::anyhow!(RecoveryAdvice::init_principal_field_required(
"--principal-email"
))
})?;
user_config.set_principal(name.clone(), email.clone());
let config_path = user_config.save_default()?;
info!(principal_name = %name, principal_email = %email, "Principal configured");
debug!(config_path = %config_path.display(), "User config updated");
principal_configured = true;
}
super::maybe_prompt_init_install(cli, &repo, &args)?;
let repo_is_git_overlay = has_git;
let message = if repo_is_git_overlay {
format!(
"Initialized Heddle data in {} for Git-overlay workflows",
repo.heddle_dir().display()
)
} else {
format!(
"Initialized Heddle repository in {}",
repo.heddle_dir().display()
)
};
let trust = build_repository_verification_state(&repo);
let next_action = if !trust.recommended_action.is_empty() {
Some(trust.recommended_action.clone())
} else {
Some("heddle commit -m \"...\"".to_string())
};
let principal_status = init_principal_status(&repo, &user_config)?;
let placeholder_principal_warning = principal_status
.principal
.as_ref()
.map(|principal| Principal::new(&principal.name, &principal.email))
.filter(is_placeholder_principal)
.map(|principal| placeholder_principal_warning(&principal));
let output = InitOutput {
output_kind: "init",
status: "initialized".to_string(),
action: "init".to_string(),
path: repo.heddle_dir().to_path_buf(),
repository_mode: repo.capability_label().to_string(),
git_detected: repo_is_git_overlay,
heddle_initialized: true,
installed_heddleignore,
principal_configured,
principal_status: principal_status.status,
principal_source: principal_status.source,
principal: principal_status.principal,
principal_recommended_action: principal_status.recommended_action,
placeholder_principal_warning,
side_effects: init_side_effects(repo_is_git_overlay, principal_configured),
message,
next_action: next_action.clone(),
recommended_action: next_action,
trust,
};
render_init(&output, should_output_json(cli, Some(repo.config())))
}
fn absolute_path(path: &std::path::Path) -> Result<PathBuf> {
if path.is_absolute() {
Ok(path.to_path_buf())
} else {
Ok(std::env::current_dir()
.map_err(|e| anyhow::anyhow!("Failed to determine current directory: {}", e))?
.join(path))
}
}
fn render_init(output: &InitOutput, json: bool) -> Result<()> {
if json {
println!("{}", serde_json::to_string(output)?);
} else {
println!("{}", output.message);
match output.principal.as_ref() {
Some(principal) => {
let source = output
.principal_source
.as_deref()
.map(|source| format!(" from {source}"))
.unwrap_or_default();
println!(
"Principal: {} <{}>{source}",
principal.name, principal.email
);
}
None => {
println!("Principal: not configured");
if let Some(action) = output.principal_recommended_action.as_deref() {
println!(" set with: {action}");
}
}
}
if let Some(warning) = output.placeholder_principal_warning.as_deref() {
eprintln!("{}", style::warn(warning));
}
if !output.side_effects.is_empty() {
println!("Side effects:");
for effect in &output.side_effects {
println!(" - {effect}");
}
}
if let Some(next) = output.recommended_action.as_deref() {
print_next(next);
}
}
Ok(())
}
struct InitPrincipalStatus {
status: String,
source: Option<String>,
principal: Option<InitPrincipalOutput>,
recommended_action: Option<String>,
}
fn init_principal_status(
repo: &Repository,
user_config: &UserConfig,
) -> Result<InitPrincipalStatus> {
if let Some(principal) = Principal::from_env()
&& !principal_is_unconfigured(&principal)
{
return Ok(configured_principal_status("environment", principal));
}
if let Some(config) = &repo.config().principal {
let principal = Principal::new(&config.name, &config.email);
if !principal_is_unconfigured(&principal) {
return Ok(configured_principal_status("repository", principal));
}
}
if repo.capability() == RepositoryCapability::GitOverlay {
let principal = repo.get_principal()?;
if !principal_is_unconfigured(&principal) {
return Ok(configured_principal_status("git_config", principal));
}
}
if let Some(config) = &user_config.principal {
let principal = Principal::new(&config.name, &config.email);
if !principal_is_unconfigured(&principal) {
return Ok(configured_principal_status("user_config", principal));
}
}
Ok(InitPrincipalStatus {
status: "not_configured".to_string(),
source: None,
principal: None,
recommended_action: Some(set_principal_command().to_string()),
})
}
fn configured_principal_status(source: &str, principal: Principal) -> InitPrincipalStatus {
InitPrincipalStatus {
status: "configured".to_string(),
source: Some(source.to_string()),
principal: Some(InitPrincipalOutput {
name: principal.name,
email: principal.email,
}),
recommended_action: None,
}
}
fn principal_is_unconfigured(principal: &Principal) -> bool {
principal.name.trim().is_empty()
|| principal.email.trim().is_empty()
|| (principal.name.trim() == "Unknown" && principal.email.trim() == "unknown@example.com")
}
fn set_principal_command() -> &'static str {
"heddle init --principal-name <name> --principal-email <email>"
}
fn init_side_effects(has_git: bool, principal_configured: bool) -> Vec<String> {
let mut side_effects = Vec::new();
if has_git {
side_effects.push("created Heddle sidecar for the existing Git repository".to_string());
side_effects.push("updated .git/info/exclude for Heddle metadata".to_string());
side_effects.push("left Git-tracked files untouched".to_string());
} else {
side_effects.push("created Heddle repository metadata".to_string());
}
if principal_configured {
side_effects.push("updated default principal attribution".to_string());
}
side_effects
}