void-cli 0.0.3

CLI for void — anonymous encrypted source control
//! The `void add` command implementation.
//!
//! Stages files for the next commit by adding them to the index.

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

use serde::Serialize;
use crate::context::{open_repo, void_err_to_cli};
use crate::observer::ProgressObserver;
use crate::output::{run_command, CliError, CliOptions};

/// JSON output structure for the add command.
#[derive(Debug, Clone, Serialize)]
pub struct AddOutput {
    /// Files that were staged (new or modified).
    pub staged: Vec<String>,
    /// Files that were marked as deleted (existed in index but not on disk).
    pub deleted: Vec<String>,
    /// Total count of files affected (staged + deleted).
    pub count: usize,
}

/// Run the add command.
///
/// # Arguments
///
/// * `cwd` - Current working directory.
/// * `paths` - Paths/patterns to stage.
/// * `opts` - CLI options.
///
/// # Returns
///
/// Returns `Ok(())` on success, or a `CliError` on failure.
pub fn run(cwd: &Path, paths: Vec<String>, opts: &CliOptions) -> Result<(), CliError> {
    // Filter empty/whitespace paths
    let paths: Vec<String> = paths.into_iter().filter(|p| !p.trim().is_empty()).collect();

    run_command("add", opts, |ctx| {
        // Validate paths provided (inside run_command for proper JSON error output)
        if paths.is_empty() {
            return Err(CliError::invalid_args(
                "No paths specified. Use `.` to add all files.",
            ));
        }
        let repo = open_repo(cwd)?;

        // Create progress observer (hidden in JSON mode)
        let progress = if ctx.use_json() {
            ProgressObserver::new_hidden()
        } else {
            ProgressObserver::new("Staging files...")
        };
        let observer = Arc::new(progress);

        ctx.progress("Staging files...");

        let path_strs: Vec<&str> = paths.iter().map(|s| s.as_str()).collect();
        let result = repo
            .add_with_options(&path_strs, Some(observer.clone()))
            .map_err(void_err_to_cli)?;

        // Finish progress bar
        observer.finish();

        // Print human-readable summary if not in JSON mode
        if !ctx.use_json() {
            let total_staged = result.staged.len();
            let total_deleted = result.deleted.len();

            if total_staged == 0 && total_deleted == 0 {
                ctx.info("Nothing to stage");
            } else {
                if total_staged > 0 {
                    ctx.info(format!("Staged {} file(s)", total_staged));
                    if ctx.is_verbose() {
                        for path in &result.staged {
                            ctx.info(format!("  {}", path));
                        }
                    }
                }
                if total_deleted > 0 {
                    ctx.info(format!("Deleted {} file(s)", total_deleted));
                    if ctx.is_verbose() {
                        for path in &result.deleted {
                            ctx.info(format!("  {}", path));
                        }
                    }
                }
            }
        }

        let count = result.staged.len() + result.deleted.len();
        Ok(AddOutput {
            staged: result.staged,
            deleted: result.deleted,
            count,
        })
    })
}

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

    #[test]
    fn test_add_output_serialization() {
        let output = AddOutput {
            staged: vec!["src/main.rs".to_string(), "src/lib.rs".to_string()],
            deleted: vec!["old_file.txt".to_string()],
            count: 3,
        };

        let json = serde_json::to_string(&output).unwrap();
        assert!(json.contains("\"staged\""));
        assert!(json.contains("\"deleted\""));
        assert!(json.contains("\"count\":3"));
        assert!(json.contains("src/main.rs"));
        assert!(json.contains("old_file.txt"));
    }

    #[test]
    fn test_add_output_empty() {
        let output = AddOutput {
            staged: vec![],
            deleted: vec![],
            count: 0,
        };

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