use crate::cli::args::BackupCommands;
use crate::cli::UI;
use crate::ops::backup;
use crate::ops::utils::short_oid;
use anyhow::Result;
use std::path::PathBuf;
pub async fn execute(action: BackupCommands, ui: &UI) -> Result<()> {
match action {
BackupCommands::Add {
name,
destination,
backend_type,
auto,
} => add(&name, &destination, backend_type.as_deref(), auto, ui),
BackupCommands::Remove { name } => remove(&name, ui),
BackupCommands::List => list(ui),
BackupCommands::Push { name, all_branches } => push(name.as_deref(), all_branches, ui),
BackupCommands::Status => status(ui),
BackupCommands::Restore { source, output } => restore(&source, output.as_deref(), ui),
BackupCommands::Verify { file } => verify(&file, ui),
}
}
fn add(name: &str, destination: &str, type_hint: Option<&str>, auto: bool, ui: &UI) -> Result<()> {
let path = PathBuf::from(".");
let backend = type_hint
.map(|s| s.to_string())
.unwrap_or_else(|| backup::detect_backend(destination).to_string());
backup::add_destination(&path, name, destination, type_hint, auto)?;
ui.header("Backup");
ui.blank();
ui.success(format!("Added backup destination '{}' ({})", name, backend));
ui.blank();
ui.field("Destination", destination);
ui.field("Auto-backup", if auto { "enabled" } else { "disabled" });
ui.blank();
if auto {
ui.info("Backups will run automatically after every push");
}
Ok(())
}
fn remove(name: &str, ui: &UI) -> Result<()> {
let path = PathBuf::from(".");
backup::remove_destination(&path, name)?;
ui.success(format!("Removed backup destination '{}'", name));
Ok(())
}
fn list(ui: &UI) -> Result<()> {
let path = PathBuf::from(".");
let config = backup::load_config(&path)?;
ui.header("Backup Destinations");
ui.blank();
if config.destinations.is_empty() {
ui.info("No backup destinations configured");
ui.blank();
ui.info("Add one with: securegit backup add <name> <destination>");
return Ok(());
}
for dest in &config.destinations {
let auto_tag = if dest.auto_backup { " [auto]" } else { "" };
ui.list_item(format!(
"{:<12} {:<8} {}{}",
dest.name, dest.backend, dest.destination, auto_tag
));
}
if ui.json {
ui.json_out(&serde_json::to_value(&config.destinations).unwrap_or_default());
}
ui.blank();
Ok(())
}
fn push(name: Option<&str>, all_branches: bool, ui: &UI) -> Result<()> {
let path = PathBuf::from(".");
let mut config = backup::load_config(&path)?;
if config.destinations.is_empty() {
anyhow::bail!("No backup destinations configured. Add one with: securegit backup add <name> <destination>");
}
let targets: Vec<usize> = if let Some(n) = name {
let idx = config
.destinations
.iter()
.position(|d| d.name == n)
.ok_or_else(|| anyhow::anyhow!("No backup destination named '{}'", n))?;
vec![idx]
} else {
(0..config.destinations.len()).collect()
};
let repo = git2::Repository::discover(&path)?;
let head = repo.head()?;
let commit = head.peel_to_commit()?;
let short_sha = short_oid(&commit.id());
let branch = head.shorthand().unwrap_or("HEAD");
let repo_name = repo
.workdir()
.and_then(|p| p.file_name())
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| "repo".to_string());
ui.header("Backup");
ui.blank();
ui.field("Repository", &repo_name);
ui.field("Branch", branch);
ui.field("Commit", short_sha);
ui.blank();
let spinner = ui.spinner("Creating bundle...");
let bundle = backup::create_bundle(&path, all_branches)?;
ui.finish_progress(
&spinner,
&format!(
"Bundle created: {} ({})",
bundle
.path
.file_name()
.unwrap_or_default()
.to_string_lossy(),
backup::format_size(bundle.size_bytes)
),
);
let mut success_count = 0;
let now = chrono::Utc::now().to_rfc3339();
let sha = commit.id().to_string();
for &idx in &targets {
let dest = &config.destinations[idx];
let spinner = ui.spinner(&format!("Pushing to {}...", dest.name));
match backup::push_to_destination(dest, &bundle.path) {
Ok(()) => {
ui.finish_progress(
&spinner,
&format!("{}: uploaded ({})", dest.name, dest.backend),
);
success_count += 1;
config.destinations[idx].last_backup = Some(now.clone());
config.destinations[idx].last_sha = Some(sha.clone());
}
Err(e) => {
ui.finish_progress(&spinner, "");
ui.warning(format!("{}: failed ({})", dest.name, e));
}
}
}
let _ = backup::save_config(&path, &config);
let _ = std::fs::remove_file(&bundle.path);
if let Some(parent) = bundle.path.parent() {
let _ = std::fs::remove_dir(parent);
}
ui.blank();
if success_count == targets.len() {
ui.success(format!(
"Backup complete — {} destination{} updated",
success_count,
if success_count == 1 { "" } else { "s" }
));
} else {
ui.warning(format!(
"{}/{} destinations updated",
success_count,
targets.len()
));
}
ui.blank();
Ok(())
}
fn status(ui: &UI) -> Result<()> {
let path = PathBuf::from(".");
let config = backup::load_config(&path)?;
let repo = git2::Repository::discover(&path)?;
let head = repo.head()?;
let commit = head.peel_to_commit()?;
let short_sha = short_oid(&commit.id());
let branch = head.shorthand().unwrap_or("HEAD");
let repo_name = repo
.workdir()
.and_then(|p| p.file_name())
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| "repo".to_string());
ui.header("Backup Status");
ui.blank();
ui.field(
"Repository",
format!("{} ({} @ {})", repo_name, branch, short_sha),
);
ui.blank();
if config.destinations.is_empty() {
ui.info("No backup destinations configured");
ui.blank();
ui.info("Add one with: securegit backup add <name> <destination>");
return Ok(());
}
ui.section("Destinations");
for dest in &config.destinations {
let auto_tag = if dest.auto_backup { " auto" } else { "" };
let last = dest
.last_backup
.as_deref()
.map(format_relative_time)
.unwrap_or_else(|| "never".to_string());
let status_icon = dest.last_backup.is_some();
ui.status_item(
status_icon,
format!(
"{:<12} {:<8} {:<16}{}",
dest.name, dest.backend, last, auto_tag
),
);
}
if ui.json {
ui.json_out(&serde_json::to_value(&config).unwrap_or_default());
}
ui.blank();
Ok(())
}
fn restore(source: &str, output: Option<&std::path::Path>, ui: &UI) -> Result<()> {
ui.header("Restore");
ui.blank();
ui.field("Source", source);
ui.blank();
let source_path = std::path::Path::new(source);
if source_path.exists() {
let spinner = ui.spinner("Verifying bundle...");
let verify_output = backup::verify_bundle(source_path)?;
ui.finish_progress(&spinner, "Bundle verified");
if !verify_output.is_empty() {
for line in verify_output.lines().take(5) {
let trimmed = line.trim();
if !trimmed.is_empty() {
ui.info(trimmed);
}
}
}
}
let spinner = ui.spinner("Restoring from bundle...");
let out_dir = backup::restore_from_bundle(source, output)?;
ui.finish_progress(&spinner, "");
ui.blank();
ui.success("Restored successfully");
ui.field("Location", out_dir.display());
ui.blank();
Ok(())
}
fn verify(file: &std::path::Path, ui: &UI) -> Result<()> {
ui.header("Bundle Verify");
ui.blank();
ui.field("File", file.display());
let meta = std::fs::metadata(file)?;
ui.field("Size", backup::format_size(meta.len()));
ui.blank();
let output = backup::verify_bundle(file)?;
ui.success("Bundle is valid");
ui.blank();
for line in output.lines() {
let trimmed = line.trim();
if !trimmed.is_empty() {
ui.info(trimmed);
}
}
ui.blank();
Ok(())
}
fn format_relative_time(timestamp: &str) -> String {
let Ok(dt) = chrono::DateTime::parse_from_rfc3339(timestamp) else {
return timestamp.to_string();
};
let now = chrono::Utc::now();
let duration = now.signed_duration_since(dt);
if duration.num_days() > 0 {
format!(
"{} day{} ago",
duration.num_days(),
if duration.num_days() == 1 { "" } else { "s" }
)
} else if duration.num_hours() > 0 {
format!(
"{} hour{} ago",
duration.num_hours(),
if duration.num_hours() == 1 { "" } else { "s" }
)
} else if duration.num_minutes() > 0 {
format!("{} min ago", duration.num_minutes())
} else {
"just now".to_string()
}
}