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 generate_run_id, get_state_dir, hash_file, save_backup_nodes, save_run_metadata,
17 FileModification, RunMetadata, RunStatus,
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
82 .unmatched_qualified_paths
83 .entry(path)
84 .or_insert(0) += count;
85 }
86 }
87
88 if op_result.changed {
89 result.total_modifications += op_result.modified_nodes.len();
90 let new_content = editor.to_string();
91
92 if opts.apply {
93 let write_path = opts.output.as_ref().unwrap_or(file_path);
94 std::fs::write(write_path, &new_content).with_context(|| {
95 format!("Failed to write {}", write_path.display())
96 })?;
97 }
98
99 result.changes.push(FileChange {
100 path: file_path.clone(),
101 old_content: content,
102 new_content,
103 modified_nodes: op_result.modified_nodes,
104 });
105
106 if let Some(limit) = opts.limit {
107 if result.total_modifications >= limit {
108 result.limit_hit = true;
109 break;
110 }
111 }
112 }
113 }
114 Err(e) => {
115 if files.len() == 1 {
116 return Err(e);
117 }
118 result.last_error = Some(format!("{}", e));
119 }
120 }
121 }
122
123 Ok(result)
124}
125
126pub fn execute_with_state(
134 files: &[PathBuf],
135 op: &Operation,
136 opts: &ExecuteOpts,
137 local_state: bool,
138 command_line: String,
139) -> Result<ExecuteResult> {
140 if !opts.apply || opts.output.is_some() {
141 return execute(files, op, opts);
142 }
143
144 let run_id = generate_run_id();
145 let state_dir = get_state_dir(local_state)?;
146 let mut result = ExecuteResult::default();
147
148 for file_path in files {
149 let content = std::fs::read_to_string(file_path)
150 .with_context(|| format!("Failed to read {}", file_path.display()))?;
151
152 let mut editor = match RustEditor::new(&content) {
153 Ok(editor) => editor,
154 Err(e) => {
155 if files.len() == 1 {
156 return Err(e)
157 .with_context(|| format!("Failed to parse {}", file_path.display()));
158 }
159 result
160 .parse_errors
161 .push((file_path.clone(), format!("{}", e)));
162 continue;
163 }
164 };
165
166 match editor.apply_operation(op) {
167 Ok(op_result) => {
168 if let Some(unmatched) = op_result.unmatched_qualified_paths {
169 for (path, count) in unmatched {
170 *result
171 .unmatched_qualified_paths
172 .entry(path)
173 .or_insert(0) += count;
174 }
175 }
176
177 if op_result.changed {
178 result.total_modifications += op_result.modified_nodes.len();
179 let new_content = editor.to_string();
180
181 let hash_before = hash_file(file_path)?;
182 save_backup_nodes(file_path, &op_result.modified_nodes, &run_id, &state_dir)?;
183
184 std::fs::write(file_path, &new_content)
185 .with_context(|| format!("Failed to write {}", file_path.display()))?;
186
187 let hash_after = hash_file(file_path)?;
188
189 result.files_modified.push(FileModification {
190 path: file_path.clone(),
191 hash_before,
192 hash_after,
193 backup_nodes: op_result.modified_nodes.clone(),
194 });
195 result.changes.push(FileChange {
196 path: file_path.clone(),
197 old_content: content,
198 new_content,
199 modified_nodes: op_result.modified_nodes,
200 });
201
202 if let Some(limit) = opts.limit {
203 if result.total_modifications >= limit {
204 result.limit_hit = true;
205 break;
206 }
207 }
208 }
209 }
210 Err(e) => {
211 if files.len() == 1 {
212 return Err(e);
213 }
214 result.last_error = Some(format!("{}", e));
215 }
216 }
217 }
218
219 if !result.files_modified.is_empty() {
220 let metadata = RunMetadata {
221 run_id: run_id.clone(),
222 timestamp: chrono::Utc::now(),
223 command: command_line,
224 operation: op.kind_name().to_string(),
225 files_modified: result.files_modified.clone(),
226 status: RunStatus::Applied,
227 can_revert: true,
228 };
229 save_run_metadata(&metadata, &state_dir)?;
230 result.run_id = Some(run_id);
231 }
232
233 Ok(result)
234}