splice 2.8.0

Span-safe refactoring kernel for 7 languages with Magellan code graph integration
Documentation
//! Snapshot management command handlers.

use super::helpers::confirm_action;

pub(crate) fn execute_snapshots(
    cmd: splice::cli::SnapshotsCommands,
    json_output: bool,
) -> Result<splice::cli::CliSuccessPayload, splice::SpliceError> {
    match cmd {
        splice::cli::SnapshotsCommands::List {
            operation,
            limit,
            disk_usage,
            output,
        } => execute_snapshots_list(operation, limit, disk_usage, output, json_output),
        splice::cli::SnapshotsCommands::Delete { id, force } => {
            execute_snapshots_delete(&id, force, json_output)
        }
        splice::cli::SnapshotsCommands::Cleanup { keep, dry_run, yes } => {
            execute_snapshots_cleanup(keep, dry_run, yes, json_output)
        }
    }
}

pub(crate) fn execute_snapshots_list(
    operation_filter: Option<String>,
    limit: Option<usize>,
    disk_usage: bool,
    output_format: splice::cli::OutputFormat,
    json_output: bool,
) -> Result<splice::cli::CliSuccessPayload, splice::SpliceError> {
    use serde_json::json;
    use splice::proof::storage::SnapshotStorage;

    let storage = SnapshotStorage::new()?;
    let snapshots = storage.list_snapshots_filtered(operation_filter.as_deref(), limit)?;

    let total_size = if disk_usage {
        Some(storage.get_total_size()?)
    } else {
        None
    };

    if output_format.is_json() || json_output {
        let snapshot_data: Vec<serde_json::Value> = snapshots
            .iter()
            .map(|meta| {
                json!({
                    "operation": meta.operation,
                    "timestamp": meta.timestamp,
                    "snapshot_path": meta.snapshot_path,
                    "symbols_count": meta.symbols_count,
                    "edges_count": meta.edges_count,
                })
            })
            .collect();

        let mut result = json!({
            "snapshots": snapshot_data,
            "count": snapshots.len(),
        });

        if let Some(size) = total_size {
            result["total_size_bytes"] = json!(size);
        }

        let json_output = output_format
            .format_json(&result)
            .map_err(|e| splice::SpliceError::Other(format!("JSON serialization error: {}", e)))?;
        println!("{}", json_output);

        Ok(splice::cli::CliSuccessPayload::with_data(
            format!("Listed {} snapshots", snapshots.len()),
            result,
        )
        .already_emitted())
    } else {
        if snapshots.is_empty() {
            println!("No snapshots found.");
            return Ok(splice::cli::CliSuccessPayload::message_only(
                "No snapshots found".to_string(),
            ));
        }

        println!("Snapshots ({} total)", snapshots.len());
        println!();

        for meta in &snapshots {
            let timestamp_str = chrono::DateTime::from_timestamp(meta.timestamp, 0)
                .map(|dt| dt.format("%Y-%m-%d %H:%M:%S").to_string())
                .unwrap_or_else(|| "Unknown".to_string());

            println!(
                "  {} | {} | {} symbols, {} edges",
                timestamp_str, meta.operation, meta.symbols_count, meta.edges_count,
            );
            println!("    Path: {}", meta.snapshot_path.display());
        }

        if let Some(size) = total_size {
            println!();
            println!("Total disk usage: {} bytes ({} MB)", size, size / 1_048_576,);
        }

        Ok(splice::cli::CliSuccessPayload::message_only(format!(
            "Listed {} snapshots",
            snapshots.len()
        )))
    }
}

pub(crate) fn execute_snapshots_delete(
    snapshot_id: &str,
    force: bool,
    json_output: bool,
) -> Result<splice::cli::CliSuccessPayload, splice::SpliceError> {
    use splice::proof::storage::SnapshotStorage;

    let storage = SnapshotStorage::new()?;

    let snapshot_info = storage.get_by_id(snapshot_id)?;

    if snapshot_info.is_none() {
        return Err(splice::SpliceError::Other(format!(
            "Snapshot '{}' not found",
            snapshot_id
        )));
    }

    let (path, meta) = snapshot_info.expect("invariant: None case returned Err above");

    if !force
        && !confirm_action(&format!(
            "Delete snapshot '{}' from {}? (y/N): ",
            meta.operation,
            chrono::DateTime::from_timestamp(meta.timestamp, 0)
                .map(|dt| dt.format("%Y-%m-%d %H:%M:%S").to_string())
                .unwrap_or_else(|| "Unknown".to_string())
        ))?
    {
        println!("Deletion cancelled.");
        return Ok(splice::cli::CliSuccessPayload::message_only(
            "Deletion cancelled".to_string(),
        ));
    }

    let deleted = storage.delete_by_id(snapshot_id)?;

    if !deleted {
        return Err(splice::SpliceError::Other(format!(
            "Failed to delete snapshot '{}'",
            snapshot_id
        )));
    }

    if json_output {
        let result = serde_json::json!({
            "deleted": true,
            "snapshot_id": snapshot_id,
            "snapshot_path": path,
        });
        println!("{}", result);
        Ok(splice::cli::CliSuccessPayload::with_data(
            format!("Deleted snapshot '{}'", snapshot_id),
            result,
        )
        .already_emitted())
    } else {
        println!("Deleted snapshot: {}", path.display());
        Ok(splice::cli::CliSuccessPayload::message_only(format!(
            "Deleted snapshot '{}'",
            snapshot_id
        )))
    }
}

pub(crate) fn execute_snapshots_cleanup(
    keep: usize,
    dry_run: bool,
    yes: bool,
    json_output: bool,
) -> Result<splice::cli::CliSuccessPayload, splice::SpliceError> {
    use splice::proof::storage::SnapshotStorage;

    const BULK_DELETE_THRESHOLD: usize = 50;

    let storage = SnapshotStorage::new()?;
    let snapshots = storage.list_snapshots()?;

    if snapshots.len() <= keep {
        let message = format!(
            "Only {} snapshots exist (keeping {}), nothing to clean up",
            snapshots.len(),
            keep
        );

        if json_output {
            let result = serde_json::json!({
                "deleted_count": 0,
                "kept_count": snapshots.len(),
                "keep": keep,
                "dry_run": dry_run,
            });
            println!("{}", result);
            return Ok(splice::cli::CliSuccessPayload::with_data(message, result).already_emitted());
        } else {
            println!("{}", message);
            return Ok(splice::cli::CliSuccessPayload::message_only(message));
        }
    }

    let to_delete_count = snapshots.len() - keep;
    let to_delete = &snapshots[keep..];

    if dry_run {
        let message = format!(
            "Would delete {} snapshots (keeping {} most recent)",
            to_delete_count, keep
        );

        if json_output {
            let snapshot_paths: Vec<String> = to_delete
                .iter()
                .map(|s| s.snapshot_path.to_string_lossy().to_string())
                .collect();

            let result = serde_json::json!({
                "dry_run": true,
                "would_delete_count": to_delete_count,
                "kept_count": keep,
                "to_delete": snapshot_paths,
            });
            println!("{}", result);
            return Ok(splice::cli::CliSuccessPayload::with_data(message, result).already_emitted());
        } else {
            println!("{}", message);
            println!();
            println!("Snapshots to delete:");
            for meta in to_delete {
                println!("  - {} ({})", meta.snapshot_path.display(), meta.operation);
            }
            return Ok(splice::cli::CliSuccessPayload::message_only(message));
        }
    }

    if !yes && to_delete_count > BULK_DELETE_THRESHOLD {
        return Err(splice::SpliceError::Other(format!(
            "Refusing to delete {} snapshots (threshold {}). Re-run with --yes to confirm \
             the bulk delete, or use --dry-run to preview what would be removed.",
            to_delete_count, BULK_DELETE_THRESHOLD
        )));
    }
    let deleted_paths = storage.cleanup_old_snapshots(keep)?;

    if json_output {
        let deleted_paths_str: Vec<String> = deleted_paths
            .iter()
            .map(|p| p.to_string_lossy().to_string())
            .collect();

        let result = serde_json::json!({
            "deleted_count": deleted_paths.len(),
            "kept_count": keep,
            "deleted_paths": deleted_paths_str,
        });
        println!("{}", result);
        Ok(splice::cli::CliSuccessPayload::with_data(
            format!("Deleted {} snapshots", deleted_paths.len()),
            result,
        )
        .already_emitted())
    } else {
        println!(
            "Deleted {} snapshots (kept {} most recent)",
            deleted_paths.len(),
            keep
        );
        for path in &deleted_paths {
            println!("  - {}", path.display());
        }
        Ok(splice::cli::CliSuccessPayload::message_only(format!(
            "Deleted {} snapshots",
            deleted_paths.len()
        )))
    }
}