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};
#[derive(Debug)]
pub struct RepairArgs {
pub mode: String,
pub target_branch: Option<String>,
pub dry_run: bool,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RepairOutput {
pub mode: String,
pub dry_run: bool,
pub commits_preserved: usize,
pub commits_discarded: usize,
pub new_head: String,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub branches_updated: Vec<BranchUpdateEntry>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub warnings: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub cid_mapping: Vec<CidMappingEntry>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BranchUpdateEntry {
pub name: String,
pub old_cid: String,
pub new_cid: String,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CidMappingEntry {
pub old_cid: String,
pub new_cid: String,
}
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
))),
}
}
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(),
})
}
})
}