1use std::collections::HashMap;
8use std::path::PathBuf;
9
10use anyhow::{Context, Result};
11use serde::{Deserialize, Serialize};
12
13use crate::editor::RustEditor;
14use crate::operations::{BackupNode, Operation};
15use crate::state::{
16 FileModification, RunMetadata, RunStatus, generate_run_id, get_state_dir, hash_file,
17 save_backup_nodes, save_run_metadata,
18};
19
20#[derive(Debug, Clone, Default)]
21pub struct ExecuteOpts {
22 pub apply: bool,
24 pub output: Option<PathBuf>,
26 pub limit: Option<usize>,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct FileChange {
32 pub path: PathBuf,
33 pub old_content: String,
34 pub new_content: String,
35 pub modified_nodes: Vec<BackupNode>,
36}
37
38#[derive(Debug, Default, Serialize, Deserialize)]
39pub struct ExecuteResult {
40 pub changes: Vec<FileChange>,
41 pub total_modifications: usize,
42 pub unmatched_qualified_paths: HashMap<String, usize>,
43 pub parse_errors: Vec<(PathBuf, String)>,
44 pub last_error: Option<String>,
46 pub limit_hit: bool,
47 pub run_id: Option<String>,
49 pub files_modified: Vec<FileModification>,
51}
52
53pub fn execute(files: &[PathBuf], op: &Operation, opts: &ExecuteOpts) -> Result<ExecuteResult> {
57 let mut result = ExecuteResult::default();
58
59 for file_path in files {
60 let content = std::fs::read_to_string(file_path)
61 .with_context(|| format!("Failed to read {}", file_path.display()))?;
62
63 let mut editor = match RustEditor::new(&content) {
64 Ok(editor) => editor,
65 Err(e) => {
66 if files.len() == 1 {
67 return Err(e)
68 .with_context(|| format!("Failed to parse {}", file_path.display()));
69 }
70 result
71 .parse_errors
72 .push((file_path.clone(), format!("{}", e)));
73 continue;
74 }
75 };
76
77 match editor.apply_operation(op) {
78 Ok(op_result) => {
79 if let Some(unmatched) = op_result.unmatched_qualified_paths {
80 for (path, count) in unmatched {
81 *result.unmatched_qualified_paths.entry(path).or_insert(0) += count;
82 }
83 }
84
85 if op_result.changed {
86 result.total_modifications += op_result.modified_nodes.len();
87 let new_content = editor.to_string();
88
89 if opts.apply {
90 let write_path = opts.output.as_ref().unwrap_or(file_path);
91 std::fs::write(write_path, &new_content)
92 .with_context(|| format!("Failed to write {}", write_path.display()))?;
93 }
94
95 result.changes.push(FileChange {
96 path: file_path.clone(),
97 old_content: content,
98 new_content,
99 modified_nodes: op_result.modified_nodes,
100 });
101
102 if let Some(limit) = opts.limit
103 && result.total_modifications >= limit
104 {
105 result.limit_hit = true;
106 break;
107 }
108 }
109 }
110 Err(e) => {
111 if files.len() == 1 {
112 return Err(e);
113 }
114 result.last_error = Some(format!("{}", e));
115 }
116 }
117 }
118
119 Ok(result)
120}
121
122pub fn execute_with_state(
131 files: &[PathBuf],
132 op: &Operation,
133 opts: &ExecuteOpts,
134 local_state: bool,
135 command_line: String,
136) -> Result<ExecuteResult> {
137 if !opts.apply || opts.output.is_some() {
138 return execute(files, op, opts);
139 }
140
141 let run_id = generate_run_id();
142 let state_dir = get_state_dir(local_state)?;
143 let mut result = ExecuteResult::default();
144
145 for file_path in files {
146 let content = std::fs::read_to_string(file_path)
147 .with_context(|| format!("Failed to read {}", file_path.display()))?;
148
149 let mut editor = match RustEditor::new(&content) {
150 Ok(editor) => editor,
151 Err(e) => {
152 if files.len() == 1 {
153 return Err(e)
154 .with_context(|| format!("Failed to parse {}", file_path.display()));
155 }
156 result
157 .parse_errors
158 .push((file_path.clone(), format!("{}", e)));
159 continue;
160 }
161 };
162
163 match editor.apply_operation(op) {
164 Ok(op_result) => {
165 if let Some(unmatched) = op_result.unmatched_qualified_paths {
166 for (path, count) in unmatched {
167 *result.unmatched_qualified_paths.entry(path).or_insert(0) += count;
168 }
169 }
170
171 if op_result.changed {
172 result.total_modifications += op_result.modified_nodes.len();
173 let new_content = editor.to_string();
174
175 let hash_before = hash_file(file_path)?;
176 save_backup_nodes(file_path, &op_result.modified_nodes, &run_id, &state_dir)?;
177
178 std::fs::write(file_path, &new_content)
179 .with_context(|| format!("Failed to write {}", file_path.display()))?;
180
181 let hash_after = hash_file(file_path)?;
182
183 result.files_modified.push(FileModification {
184 path: file_path.clone(),
185 hash_before,
186 hash_after,
187 backup_nodes: op_result.modified_nodes.clone(),
188 });
189 result.changes.push(FileChange {
190 path: file_path.clone(),
191 old_content: content,
192 new_content,
193 modified_nodes: op_result.modified_nodes,
194 });
195
196 if let Some(limit) = opts.limit
197 && result.total_modifications >= limit
198 {
199 result.limit_hit = true;
200 break;
201 }
202 }
203 }
204 Err(e) => {
205 if files.len() == 1 {
206 return Err(e);
207 }
208 result.last_error = Some(format!("{}", e));
209 }
210 }
211 }
212
213 if !result.files_modified.is_empty() {
214 let metadata = RunMetadata {
215 run_id: run_id.clone(),
216 timestamp: chrono::Utc::now(),
217 command: command_line,
218 operation: op.kind_name().to_string(),
219 files_modified: result.files_modified.clone(),
220 status: RunStatus::Applied,
221 can_revert: true,
222 };
223 save_run_metadata(&metadata, &state_dir)?;
224 result.run_id = Some(run_id);
225 }
226
227 Ok(result)
228}