splice 2.8.0

Span-safe refactoring kernel for 7 languages with Magellan code graph integration
Documentation
//! Apply-files command handler.

use std::env;

use serde_json::{json, Value};

use super::helpers::log_execution_error;

#[allow(
    clippy::too_many_arguments,
    reason = "CLI handler aggregates clap-parsed flags"
)]
#[allow(unused_variables, reason = "stub args reserved for future expansion")]
pub(crate) fn execute_apply_files(
    glob_pattern: &str,
    find_pattern: &str,
    replace_pattern: &str,
    language: Option<splice::cli::Language>,
    context_before: usize,
    context_after: usize,
    context_both: usize,
    validate: bool,
    create_backup: bool,
    operation_id: Option<String>,
    metadata: Option<String>,
    dry_run: bool,
    _json_output: bool,
) -> Result<splice::cli::CliSuccessPayload, splice::SpliceError> {
    use splice::execution::log;
    use splice::patch::{
        apply_pattern_replace, find_pattern_in_files, BackupWriter, PatternReplaceConfig,
    };

    let start = std::time::Instant::now();
    let command_line = std::env::args().collect::<Vec<_>>().join(" ");

    let workspace_root = env::current_dir().map_err(|err| {
        splice::SpliceError::Other(format!("Failed to resolve current directory: {}", err))
    })?;

    let symbol_language = language.map(|l| l.to_symbol_language());

    if dry_run {
        let find_config = PatternReplaceConfig {
            glob_pattern: glob_pattern.to_string(),
            find_pattern: find_pattern.to_string(),
            replace_pattern: replace_pattern.to_string(),
            language: symbol_language,
            validate: false,
        };
        let matches = find_pattern_in_files(&find_config)?;

        let mut files_to_patch: std::collections::BTreeSet<std::path::PathBuf> =
            std::collections::BTreeSet::new();
        for m in &matches {
            files_to_patch.insert(m.file.clone());
        }

        let mut response_data = serde_json::Map::new();
        response_data.insert("dry_run".to_string(), serde_json::Value::Bool(true));
        response_data.insert(
            "files_would_patch".to_string(),
            json!(files_to_patch.iter().collect::<Vec<_>>()),
        );
        response_data.insert("matches_count".to_string(), json!(matches.len()));

        let message = format!(
            "[dry-run] Would replace {} occurrence(s) of {:?} across {} file(s). No changes written.",
            matches.len(),
            find_pattern,
            files_to_patch.len()
        );

        return Ok(splice::cli::CliSuccessPayload::with_data(
            message,
            serde_json::Value::Object(response_data),
        ));
    }

    let backup_manifest_path = if create_backup {
        let mut backup_writer = BackupWriter::new(&workspace_root, operation_id.clone())?;

        let find_config = PatternReplaceConfig {
            glob_pattern: glob_pattern.to_string(),
            find_pattern: find_pattern.to_string(),
            replace_pattern: replace_pattern.to_string(),
            language: symbol_language,
            validate: false,
        };
        let matches = find_pattern_in_files(&find_config)?;

        for m in &matches {
            backup_writer.backup_file(&m.file)?;
        }

        Some(backup_writer.finalize()?)
    } else {
        None
    };

    let config = PatternReplaceConfig {
        glob_pattern: glob_pattern.to_string(),
        find_pattern: find_pattern.to_string(),
        replace_pattern: replace_pattern.to_string(),
        language: symbol_language,
        validate,
    };

    let result = apply_pattern_replace(&config, &workspace_root)?;

    let mut response_data = serde_json::Map::new();
    response_data.insert("files_patched".to_string(), json!(result.files_patched));
    response_data.insert(
        "replacements_count".to_string(),
        json!(result.replacements_count),
    );
    if let Some(manifest_path) = backup_manifest_path {
        response_data.insert(
            "backup_manifest".to_string(),
            json!(manifest_path.to_string_lossy()),
        );
    }
    if let Some(ref op_id) = operation_id {
        response_data.insert("operation_id".to_string(), json!(op_id));
    }
    if let Some(meta) = metadata {
        if let Ok(parsed) = serde_json::from_str::<Value>(&meta) {
            response_data.insert("metadata".to_string(), parsed);
        } else {
            response_data.insert("metadata".to_string(), json!(meta));
        }
    }

    let message = format!(
        "Applied replacements to {} file(s) ({} replacements).",
        result.files_patched.len(),
        result.replacements_count
    );

    let duration_ms = start.elapsed().as_millis() as i64;
    let file_count = result.files_patched.len();
    let parameters = serde_json::json!({
        "glob": glob_pattern,
        "find": find_pattern,
        "replace": replace_pattern,
        "language": language.map(|l| l.as_str().to_string()),
        "file_count": file_count,
    });
    if let Err(e) = log::record_execution_with_params(
        &splice::output::OperationResult::with_execution_id(
            "apply-files".to_string(),
            operation_id.clone(),
        )
        .success(message.clone()),
        duration_ms,
        Some(command_line.clone()),
        parameters,
    ) {
        log_execution_error("apply-files", &e);
    }

    Ok(splice::cli::CliSuccessPayload::with_data(
        message,
        serde_json::Value::Object(response_data),
    ))
}