use std::fs;
use std::path::{Path, PathBuf};
use chrono::Utc;
use colored::Colorize;
use crate::strategies::XbpConfig;
use super::types::DeployRunRecord;
fn history_dir(project_root: &Path, config: &XbpConfig) -> PathBuf {
let rel = config
.deploy
.as_ref()
.and_then(|d| d.history_dir.as_deref())
.filter(|s| !s.trim().is_empty())
.unwrap_or(".xbp/deployments");
project_root.join(rel)
}
fn lock_path(project_root: &Path, config: &XbpConfig) -> PathBuf {
let rel = config
.deploy
.as_ref()
.and_then(|d| d.lock_file.as_deref())
.filter(|s| !s.trim().is_empty())
.unwrap_or(".xbp/deploy-lock.json");
project_root.join(rel)
}
pub fn write_history_record(
project_root: &Path,
config: &XbpConfig,
record: &DeployRunRecord,
) -> Result<(), String> {
let dir = history_dir(project_root, config);
fs::create_dir_all(&dir).map_err(|e| format!("create history dir: {e}"))?;
let stamp = record.timestamp.format("%Y%m%dT%H%M%SZ");
let safe_target = record
.target
.chars()
.map(|c| if c.is_ascii_alphanumeric() || c == '-' || c == '_' { c } else { '_' })
.collect::<String>();
let path = dir.join(format!("{stamp}-{safe_target}-{}.json", record.env));
let body = serde_json::to_string_pretty(record).map_err(|e| e.to_string())?;
fs::write(&path, body).map_err(|e| format!("write history {}: {e}", path.display()))?;
let lock = serde_json::json!({
"updated_at": Utc::now().to_rfc3339(),
"target": record.target,
"env": record.env,
"ok": record.ok,
"history_file": path.file_name().and_then(|s| s.to_str()),
});
let lock_file = lock_path(project_root, config);
if let Some(parent) = lock_file.parent() {
let _ = fs::create_dir_all(parent);
}
fs::write(
&lock_file,
serde_json::to_string_pretty(&lock).map_err(|e| e.to_string())?,
)
.map_err(|e| format!("write lock {}: {e}", lock_file.display()))?;
println!(
"{} {}",
"History".bright_cyan().bold(),
path.display().to_string().bright_white()
);
Ok(())
}
pub fn list_history(
project_root: &Path,
config: &XbpConfig,
target: &str,
env: &str,
limit: usize,
) -> Result<(), String> {
let dir = history_dir(project_root, config);
if !dir.exists() {
println!(
"{}",
format!("No deploy history under {}", dir.display()).dimmed()
);
return Ok(());
}
let mut entries: Vec<PathBuf> = fs::read_dir(&dir)
.map_err(|e| e.to_string())?
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| p.extension().and_then(|e| e.to_str()) == Some("json"))
.collect();
entries.sort();
entries.reverse();
let mut shown = 0usize;
for path in entries {
if shown >= limit {
break;
}
let raw = fs::read_to_string(&path).map_err(|e| e.to_string())?;
let record: DeployRunRecord =
serde_json::from_str(&raw).map_err(|e| format!("{}: {e}", path.display()))?;
if target != "all" && !target.is_empty() && record.target != target && !record.services.iter().any(|s| s == target)
{
continue;
}
if !env.is_empty() && record.env != env {
}
let status = if record.ok {
"ok".bright_green().to_string()
} else {
"fail".bright_red().to_string()
};
println!(
"{} {} {} {} {}",
record.timestamp.format("%Y-%m-%d %H:%M:%S UTC"),
status,
record.target.bright_white(),
record.env.bright_yellow(),
record.services.join(",")
);
shown += 1;
}
if shown == 0 {
println!("{}", "No matching deploy history entries.".dimmed());
}
Ok(())
}