void-cli 0.0.3

CLI for void — anonymous encrypted source control
//! Repository repair command.
//!
//! Repairs a corrupted repository by truncating to the recovery boundary
//! or rewriting history to skip broken commits.

use std::path::Path;

use serde::Serialize;
use void_core::ops::repair::{preview_repair, repair, RepairMode, RepairOptions};

use crate::context::{open_repo, void_err_to_cli};
use crate::output::{run_command, CliError, CliOptions};

/// Command-line arguments for repair.
#[derive(Debug)]
pub struct RepairArgs {
    /// Repair mode: "truncate" (default), "best-effort", "rewrite-history".
    pub mode: String,
    /// Target branch for rewrite-history mode.
    pub target_branch: Option<String>,
    /// If true, show what would happen without making changes.
    pub dry_run: bool,
}

/// JSON output for the repair command.
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RepairOutput {
    /// Repair mode used.
    pub mode: String,
    /// Whether this was a dry run.
    pub dry_run: bool,
    /// Number of commits preserved after repair.
    pub commits_preserved: usize,
    /// Number of commits discarded/skipped.
    pub commits_discarded: usize,
    /// CID of the new HEAD after repair.
    pub new_head: String,
    /// Branches that were updated.
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub branches_updated: Vec<BranchUpdateEntry>,
    /// Warnings generated during repair.
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub warnings: Vec<String>,
    /// CID mapping from old to new (only for rewrite-history mode).
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub cid_mapping: Vec<CidMappingEntry>,
}

/// A branch update entry for JSON output.
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BranchUpdateEntry {
    /// Branch name.
    pub name: String,
    /// Old CID before repair.
    pub old_cid: String,
    /// New CID after repair.
    pub new_cid: String,
}

/// A CID mapping entry for JSON output (rewrite-history mode).
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CidMappingEntry {
    /// Original CID.
    pub old_cid: String,
    /// New CID after rewrite.
    pub new_cid: String,
}

/// Parse mode string into RepairMode.
fn parse_mode(mode: &str, target_branch: Option<String>) -> Result<RepairMode, CliError> {
    match mode {
        "truncate" | "" => Ok(RepairMode::Truncate),
        "best-effort" | "best_effort" | "truncate-best-effort" => {
            Ok(RepairMode::TruncateBestEffort)
        }
        "rewrite-history" | "rewrite_history" => Ok(RepairMode::RewriteHistory { target_branch }),
        other => Err(CliError::invalid_args(format!(
            "unknown repair mode '{}': expected truncate, best-effort, or rewrite-history",
            other
        ))),
    }
}

/// Run the repair command.
///
/// # Arguments
///
/// * `cwd` - Current working directory
/// * `args` - Repair arguments
/// * `opts` - CLI options
pub fn run(cwd: &Path, args: RepairArgs, opts: &CliOptions) -> Result<(), CliError> {
    run_command("repair", opts, |ctx| {
        let repo = open_repo(cwd)?;
        let repair_mode = parse_mode(&args.mode, args.target_branch)?;

        let repair_opts = RepairOptions {
            ctx: repo.context().clone(),
            mode: repair_mode,
            dry_run: args.dry_run,
            checkout: false,
        };

        if args.dry_run {
            ctx.progress("Previewing repair...");

            let preview = preview_repair(&repair_opts).map_err(void_err_to_cli)?;

            if !ctx.use_json() {
                ctx.info(format!("Mode: {} (dry run)", preview.mode));
                ctx.info(format!("Commits preserved: {}", preview.commits_preserved));
                ctx.info(format!("Commits discarded: {}", preview.commits_discarded));
                ctx.info(format!("New HEAD: {}", preview.new_head));

                for bu in &preview.branch_updates {
                    ctx.info(format!(
                        "Branch '{}': {} -> {}",
                        bu.name, bu.old_cid, bu.new_cid
                    ));
                }

                for w in &preview.warnings {
                    ctx.warn(w);
                }
            }

            Ok(RepairOutput {
                mode: preview.mode,
                dry_run: true,
                commits_preserved: preview.commits_preserved,
                commits_discarded: preview.commits_discarded,
                new_head: preview.new_head,
                branches_updated: preview
                    .branch_updates
                    .into_iter()
                    .map(|bu| BranchUpdateEntry {
                        name: bu.name,
                        old_cid: bu.old_cid,
                        new_cid: bu.new_cid,
                    })
                    .collect(),
                warnings: preview.warnings,
                cid_mapping: Vec::new(),
            })
        } else {
            ctx.progress("Repairing repository...");

            let result = repair(repair_opts).map_err(void_err_to_cli)?;

            if !ctx.use_json() {
                ctx.info(format!("Mode: {}", result.mode));
                ctx.info(format!("Commits preserved: {}", result.commits_preserved));
                ctx.info(format!("Commits discarded: {}", result.commits_discarded));
                ctx.info(format!("New HEAD: {}", result.new_head));

                for bu in &result.branches_updated {
                    ctx.info(format!(
                        "Branch '{}': {} -> {}",
                        bu.name, bu.old_cid, bu.new_cid
                    ));
                }

                for w in &result.warnings {
                    ctx.warn(w);
                }

                if !result.cid_mapping.is_empty() {
                    ctx.info(format!(
                        "CID mappings: {} rewritten",
                        result.cid_mapping.len()
                    ));
                }
            }

            Ok(RepairOutput {
                mode: result.mode,
                dry_run: false,
                commits_preserved: result.commits_preserved,
                commits_discarded: result.commits_discarded,
                new_head: result.new_head,
                branches_updated: result
                    .branches_updated
                    .into_iter()
                    .map(|bu| BranchUpdateEntry {
                        name: bu.name,
                        old_cid: bu.old_cid,
                        new_cid: bu.new_cid,
                    })
                    .collect(),
                warnings: result.warnings,
                cid_mapping: result
                    .cid_mapping
                    .into_iter()
                    .map(|(old, new)| CidMappingEntry {
                        old_cid: old,
                        new_cid: new,
                    })
                    .collect(),
            })
        }
    })
}