use std::path::Path;
use std::sync::Arc;
use camino::Utf8PathBuf;
use serde::Serialize;
use void_core::store::FsStore;
use void_core::workspace::checkout::{checkout_tree, CheckoutOptions, CheckoutStats};
use void_core::workspace::stage::{reset_paths, ResetOptions};
use void_core::{cid, VoidError};
use crate::context::{build_void_context, find_void_dir, resolve_ref, void_err_to_cli};
use crate::observer::ProgressObserver;
use crate::output::{run_command, CliError, CliOptions};
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RestoreOutput {
pub files_restored: usize,
pub bytes_written: u64,
pub files_skipped: usize,
}
impl From<CheckoutStats> for RestoreOutput {
fn from(stats: CheckoutStats) -> Self {
Self {
files_restored: stats.files_restored,
bytes_written: stats.bytes_written,
files_skipped: stats.files_skipped,
}
}
}
pub struct RestoreArgs {
pub paths: Vec<String>,
pub source: Option<String>,
pub staged: bool,
pub force: bool,
}
pub fn run(cwd: &Path, args: RestoreArgs, opts: &CliOptions) -> Result<(), CliError> {
run_command("restore", opts, |ctx| {
if args.staged {
run_staged_restore(cwd, &args, ctx)
} else {
run_working_tree_restore(cwd, &args, ctx)
}
})
}
fn run_staged_restore(
cwd: &Path,
args: &RestoreArgs,
ctx: &mut crate::output::CommandContext,
) -> Result<RestoreOutput, CliError> {
let void_ctx = build_void_context(cwd)?;
let patterns: Vec<String> = args
.paths
.iter()
.filter(|p| !p.trim().is_empty())
.cloned()
.collect();
let patterns = if patterns.is_empty() {
vec![".".to_string()]
} else {
patterns
};
let observer: Arc<ProgressObserver> = if ctx.use_json() {
Arc::new(ProgressObserver::new_hidden())
} else {
Arc::new(ProgressObserver::new("Restoring staged files..."))
};
let reset_opts = ResetOptions {
ctx: void_ctx,
patterns,
observer: Some(observer.clone()),
};
let result = reset_paths(reset_opts).map_err(void_err_to_cli)?;
observer.finish();
if !ctx.use_json() {
if result.reset.is_empty() {
ctx.info("Nothing to restore");
} else {
for path in &result.reset {
ctx.info(format!("Unstaged: {}", path));
}
ctx.info(format!("\n{} file(s) restored", result.reset.len()));
}
}
Ok(RestoreOutput {
files_restored: result.reset.len(),
bytes_written: 0,
files_skipped: 0,
})
}
fn run_working_tree_restore(
cwd: &Path,
args: &RestoreArgs,
ctx: &mut crate::output::CommandContext,
) -> Result<RestoreOutput, CliError> {
let void_dir = find_void_dir(cwd)?;
let void_ctx = build_void_context(cwd)?;
let vault = void_ctx.crypto.vault.clone();
let source_ref = args.source.as_deref().unwrap_or("HEAD");
let commit_cid_typed = resolve_ref(&void_dir, source_ref)?;
let commit_cid =
cid::from_bytes(commit_cid_typed.as_bytes()).map_err(|e| CliError::internal(e.to_string()))?;
let workspace = void_dir
.parent()
.ok_or_else(|| CliError::internal("void_dir has no parent"))?;
let objects_dir = Utf8PathBuf::try_from(void_dir.join("objects"))
.map_err(|e| CliError::internal(format!("invalid objects path: {}", e)))?;
let store = FsStore::new(objects_dir).map_err(|e: VoidError| void_err_to_cli(e))?;
let paths: Vec<String> = args
.paths
.iter()
.filter(|p| !p.trim().is_empty())
.cloned()
.collect();
let observer: Arc<ProgressObserver> = if ctx.use_json() {
Arc::new(ProgressObserver::new_hidden())
} else {
Arc::new(ProgressObserver::new("Restoring files..."))
};
let checkout_opts = CheckoutOptions {
paths: if paths.is_empty() { None } else { Some(paths) },
force: args.force,
observer: Some(observer.clone()),
workspace_dir: None,
};
let stats = checkout_tree(&store, &*vault, &commit_cid, workspace, &checkout_opts)
.map_err(void_err_to_cli)?;
observer.finish();
if !ctx.use_json() {
if stats.files_restored == 0 && stats.files_skipped == 0 {
ctx.info("Nothing to restore");
} else {
ctx.info(format!(
"Restored {} file(s), {} bytes written",
stats.files_restored, stats.bytes_written
));
if stats.files_skipped > 0 {
ctx.info(format!("{} file(s) skipped", stats.files_skipped));
}
}
}
Ok(stats.into())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_restore_output_serialization() {
let output = RestoreOutput {
files_restored: 5,
bytes_written: 12345,
files_skipped: 2,
};
let json = serde_json::to_string(&output).unwrap();
assert!(json.contains("\"filesRestored\":5"));
assert!(json.contains("\"bytesWritten\":12345"));
assert!(json.contains("\"filesSkipped\":2"));
}
#[test]
fn test_restore_output_from_checkout_stats() {
use void_core::workspace::checkout::CheckoutStats;
let stats = CheckoutStats {
files_restored: 10,
bytes_written: 54321,
files_skipped: 1,
shards_read: 3,
};
let output: RestoreOutput = stats.into();
assert_eq!(output.files_restored, 10);
assert_eq!(output.bytes_written, 54321);
assert_eq!(output.files_skipped, 1);
}
}