void-cli 0.0.2

CLI for void — anonymous encrypted source control
//! Reset (unstage) command implementation.
//!
//! Unstages files from the index, reverting them to their HEAD state.

use std::path::Path;
use std::sync::Arc;

use serde::Serialize;
use void_core::workspace::stage::{reset_paths, ResetOptions};

use crate::context::{build_void_context, void_err_to_cli};
use crate::observer::ProgressObserver;
use crate::output::{run_command, CliError, CliOptions};

/// JSON output structure for the reset command.
#[derive(Debug, Clone, Serialize)]
pub struct ResetOutput {
    /// List of paths that were reset (unstaged).
    pub reset: Vec<String>,
    /// Total count of files reset.
    pub count: usize,
}

/// Run the reset command.
///
/// Unstages the specified files from the index, reverting them to their HEAD state.
///
/// # Arguments
///
/// * `cwd` - Working directory to operate in.
/// * `paths` - Paths/patterns to reset (empty defaults to ".").
/// * `opts` - CLI options.
pub fn run(cwd: &Path, paths: Vec<String>, opts: &CliOptions) -> Result<(), CliError> {
    // Filter empty/whitespace paths, default to "." if empty
    let paths: Vec<String> = paths.into_iter().filter(|p| !p.trim().is_empty()).collect();
    let patterns = if paths.is_empty() {
        vec![".".to_string()]
    } else {
        paths
    };

    run_command("reset", opts, |ctx| {
        let void_ctx = build_void_context(cwd)?;

        // Create observer for progress reporting
        let observer: Arc<ProgressObserver> = if ctx.use_json() {
            Arc::new(ProgressObserver::new_hidden())
        } else {
            Arc::new(ProgressObserver::new("Resetting 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)?;

        // Finish progress bar
        observer.finish();

        // Print human-readable summary if not JSON mode
        if !ctx.use_json() {
            if result.reset.is_empty() {
                ctx.info("Nothing to reset");
            } else {
                for path in &result.reset {
                    ctx.info(format!("Unstaged: {}", path));
                }
                ctx.info(format!("\n{} file(s) unstaged", result.reset.len()));
            }
        }

        let count = result.reset.len();
        Ok(ResetOutput {
            reset: result.reset,
            count,
        })
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_reset_output_serialization() {
        let output = ResetOutput {
            reset: vec!["src/main.rs".to_string(), "src/lib.rs".to_string()],
            count: 2,
        };

        let json = serde_json::to_string(&output).unwrap();
        assert!(json.contains("\"reset\""));
        assert!(json.contains("\"count\":2"));
        assert!(json.contains("src/main.rs"));
        assert!(json.contains("src/lib.rs"));
    }

    #[test]
    fn test_reset_output_empty() {
        let output = ResetOutput {
            reset: vec![],
            count: 0,
        };

        let json = serde_json::to_string(&output).unwrap();
        assert!(json.contains("\"reset\":[]"));
        assert!(json.contains("\"count\":0"));
    }
}