use tracing::warn;
use crate::ansi_colors::Colorize;
use crate::stack;
fn is_strict_env() -> bool {
std::env::var("BATUTA_STRICT")
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
.unwrap_or(false)
}
fn format_self_drift_warning(batuta_version: &str, drifts: &[stack::DriftReport]) -> String {
let mut output = String::new();
output.push_str(&format!(
"{}\n\n",
format!("⚠️ batuta {} has outdated dependencies", batuta_version).bright_yellow().bold()
));
for drift in drifts {
output.push_str(&format!(
" {} {} → {}\n",
drift.dependency, drift.uses_version, drift.latest_version
));
}
output.push_str(&format!("\n{}", "Update: cargo install batuta\n".dimmed()));
output
}
fn format_self_drift_error(batuta_version: &str, drifts: &[stack::DriftReport]) -> String {
let mut output = String::new();
output.push_str(&format!("🔴 batuta {} has outdated dependencies\n\n", batuta_version));
for drift in drifts {
output.push_str(&format!(
" {} {} → {}\n",
drift.dependency, drift.uses_version, drift.latest_version
));
}
output.push_str("\nUpdate: cargo install batuta\n");
output.push_str("Or use --allow-drift to bypass.\n");
output
}
fn check_self_drift() -> anyhow::Result<Option<(String, Vec<stack::DriftReport>)>> {
let rt = match tokio::runtime::Runtime::new() {
Ok(rt) => rt,
Err(_) => return Ok(None),
};
rt.block_on(async {
let mut client = stack::CratesIoClient::new().with_persistent_cache();
let mut checker = stack::DriftChecker::new();
match checker.detect_self_drift(&mut client).await {
Ok(drifts) => {
let version = checker
.latest_versions()
.get("batuta")
.map(|v| v.to_string())
.unwrap_or_else(|| env!("CARGO_PKG_VERSION").to_string());
Ok(Some((version, drifts)))
}
Err(_) => Ok(None),
}
})
}
fn drift_marker_path() -> std::path::PathBuf {
let workspace_id = std::env::current_dir()
.ok()
.and_then(|p| p.to_str().map(|s| s.bytes().map(|b| b as u64).sum::<u64>() % 100000))
.unwrap_or(0);
std::env::temp_dir().join(format!("batuta-drift-shown-{}", workspace_id))
}
fn drift_already_shown() -> bool {
let marker = drift_marker_path();
if marker.exists() {
if let Ok(meta) = std::fs::metadata(&marker) {
if let Ok(modified) = meta.modified() {
if let Ok(elapsed) = modified.elapsed() {
return elapsed.as_secs() < 3600; }
}
}
}
false
}
fn mark_drift_shown() {
let _ = std::fs::write(drift_marker_path(), "shown");
}
pub(crate) fn enforce_drift_check(strict: bool) -> anyhow::Result<()> {
let strict_mode = strict || is_strict_env();
if !strict_mode && drift_already_shown() {
return Ok(());
}
let Some((version, drifts)) = check_self_drift()? else {
return Ok(());
};
if drifts.is_empty() {
return Ok(());
}
if strict_mode {
eprintln!("{}", format_self_drift_error(&version, &drifts));
std::process::exit(1);
} else if !drift_already_shown() {
warn!("batuta has outdated dependencies (non-blocking)");
eprintln!("{}", format_self_drift_warning(&version, &drifts));
mark_drift_shown();
}
Ok(())
}