use std::collections::HashMap;
use std::path::PathBuf;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use crate::editor::RustEditor;
use crate::operations::{BackupNode, Operation};
use crate::state::{
generate_run_id, get_state_dir, hash_file, save_backup_nodes, save_run_metadata,
FileModification, RunMetadata, RunStatus,
};
#[derive(Debug, Clone, Default)]
pub struct ExecuteOpts {
pub apply: bool,
pub output: Option<PathBuf>,
pub limit: Option<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileChange {
pub path: PathBuf,
pub old_content: String,
pub new_content: String,
pub modified_nodes: Vec<BackupNode>,
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct ExecuteResult {
pub changes: Vec<FileChange>,
pub total_modifications: usize,
pub unmatched_qualified_paths: HashMap<String, usize>,
pub parse_errors: Vec<(PathBuf, String)>,
pub last_error: Option<String>,
pub limit_hit: bool,
pub run_id: Option<String>,
pub files_modified: Vec<FileModification>,
}
pub fn execute(files: &[PathBuf], op: &Operation, opts: &ExecuteOpts) -> Result<ExecuteResult> {
let mut result = ExecuteResult::default();
for file_path in files {
let content = std::fs::read_to_string(file_path)
.with_context(|| format!("Failed to read {}", file_path.display()))?;
let mut editor = match RustEditor::new(&content) {
Ok(editor) => editor,
Err(e) => {
if files.len() == 1 {
return Err(e)
.with_context(|| format!("Failed to parse {}", file_path.display()));
}
result
.parse_errors
.push((file_path.clone(), format!("{}", e)));
continue;
}
};
match editor.apply_operation(op) {
Ok(op_result) => {
if let Some(unmatched) = op_result.unmatched_qualified_paths {
for (path, count) in unmatched {
*result
.unmatched_qualified_paths
.entry(path)
.or_insert(0) += count;
}
}
if op_result.changed {
result.total_modifications += op_result.modified_nodes.len();
let new_content = editor.to_string();
if opts.apply {
let write_path = opts.output.as_ref().unwrap_or(file_path);
std::fs::write(write_path, &new_content).with_context(|| {
format!("Failed to write {}", write_path.display())
})?;
}
result.changes.push(FileChange {
path: file_path.clone(),
old_content: content,
new_content,
modified_nodes: op_result.modified_nodes,
});
if let Some(limit) = opts.limit {
if result.total_modifications >= limit {
result.limit_hit = true;
break;
}
}
}
}
Err(e) => {
if files.len() == 1 {
return Err(e);
}
result.last_error = Some(format!("{}", e));
}
}
}
Ok(result)
}
pub fn execute_with_state(
files: &[PathBuf],
op: &Operation,
opts: &ExecuteOpts,
local_state: bool,
command_line: String,
) -> Result<ExecuteResult> {
if !opts.apply || opts.output.is_some() {
return execute(files, op, opts);
}
let run_id = generate_run_id();
let state_dir = get_state_dir(local_state)?;
let mut result = ExecuteResult::default();
for file_path in files {
let content = std::fs::read_to_string(file_path)
.with_context(|| format!("Failed to read {}", file_path.display()))?;
let mut editor = match RustEditor::new(&content) {
Ok(editor) => editor,
Err(e) => {
if files.len() == 1 {
return Err(e)
.with_context(|| format!("Failed to parse {}", file_path.display()));
}
result
.parse_errors
.push((file_path.clone(), format!("{}", e)));
continue;
}
};
match editor.apply_operation(op) {
Ok(op_result) => {
if let Some(unmatched) = op_result.unmatched_qualified_paths {
for (path, count) in unmatched {
*result
.unmatched_qualified_paths
.entry(path)
.or_insert(0) += count;
}
}
if op_result.changed {
result.total_modifications += op_result.modified_nodes.len();
let new_content = editor.to_string();
let hash_before = hash_file(file_path)?;
save_backup_nodes(file_path, &op_result.modified_nodes, &run_id, &state_dir)?;
std::fs::write(file_path, &new_content)
.with_context(|| format!("Failed to write {}", file_path.display()))?;
let hash_after = hash_file(file_path)?;
result.files_modified.push(FileModification {
path: file_path.clone(),
hash_before,
hash_after,
backup_nodes: op_result.modified_nodes.clone(),
});
result.changes.push(FileChange {
path: file_path.clone(),
old_content: content,
new_content,
modified_nodes: op_result.modified_nodes,
});
if let Some(limit) = opts.limit {
if result.total_modifications >= limit {
result.limit_hit = true;
break;
}
}
}
}
Err(e) => {
if files.len() == 1 {
return Err(e);
}
result.last_error = Some(format!("{}", e));
}
}
}
if !result.files_modified.is_empty() {
let metadata = RunMetadata {
run_id: run_id.clone(),
timestamp: chrono::Utc::now(),
command: command_line,
operation: op.kind_name().to_string(),
files_modified: result.files_modified.clone(),
status: RunStatus::Applied,
can_revert: true,
};
save_run_metadata(&metadata, &state_dir)?;
result.run_id = Some(run_id);
}
Ok(result)
}