1use super::effect::{AppEffect, AppEffectHandler, AppEffectResult, CommitResult};
8use std::path::{Path, PathBuf};
9
10pub struct RealAppEffectHandler {
30 workspace_root: Option<PathBuf>,
35}
36
37impl RealAppEffectHandler {
38 #[must_use]
42 pub const fn new() -> Self {
43 Self {
44 workspace_root: None,
45 }
46 }
47
48 #[must_use]
56 pub const fn with_workspace_root(root: PathBuf) -> Self {
57 Self {
58 workspace_root: Some(root),
59 }
60 }
61
62 fn resolve_path(&self, path: &Path) -> PathBuf {
70 if path.is_absolute() {
71 path.to_path_buf()
72 } else if let Some(ref root) = self.workspace_root {
73 root.join(path)
74 } else {
75 path.to_path_buf()
76 }
77 }
78
79 fn execute_set_current_dir(&self, path: &Path) -> AppEffectResult {
80 let resolved = self.resolve_path(path);
81 match std::env::set_current_dir(&resolved) {
82 Ok(()) => AppEffectResult::Ok,
83 Err(error) => AppEffectResult::Error(format!(
84 "Failed to set current directory to '{}': {}",
85 resolved.display(),
86 error
87 )),
88 }
89 }
90
91 fn execute_write_file(&self, path: &Path, content: String) -> AppEffectResult {
92 let resolved = self.resolve_path(path);
93 if let Some(parent) = resolved.parent() {
94 if let Err(error) = std::fs::create_dir_all(parent) {
95 return AppEffectResult::Error(format!(
96 "Failed to create parent directories for '{}': {}",
97 resolved.display(),
98 error
99 ));
100 }
101 }
102
103 match std::fs::write(&resolved, content) {
104 Ok(()) => AppEffectResult::Ok,
105 Err(error) => AppEffectResult::Error(format!(
106 "Failed to write file '{}': {}",
107 resolved.display(),
108 error
109 )),
110 }
111 }
112
113 fn execute_read_file(&self, path: &Path) -> AppEffectResult {
114 let resolved = self.resolve_path(path);
115 match std::fs::read_to_string(&resolved) {
116 Ok(content) => AppEffectResult::String(content),
117 Err(error) => AppEffectResult::Error(format!(
118 "Failed to read file '{}': {}",
119 resolved.display(),
120 error
121 )),
122 }
123 }
124
125 fn execute_delete_file(&self, path: &Path) -> AppEffectResult {
126 let resolved = self.resolve_path(path);
127 match std::fs::remove_file(&resolved) {
128 Ok(()) => AppEffectResult::Ok,
129 Err(error) => AppEffectResult::Error(format!(
130 "Failed to delete file '{}': {}",
131 resolved.display(),
132 error
133 )),
134 }
135 }
136
137 fn execute_create_dir(&self, path: &Path) -> AppEffectResult {
138 let resolved = self.resolve_path(path);
139 match std::fs::create_dir_all(&resolved) {
140 Ok(()) => AppEffectResult::Ok,
141 Err(error) => AppEffectResult::Error(format!(
142 "Failed to create directory '{}': {}",
143 resolved.display(),
144 error
145 )),
146 }
147 }
148
149 fn execute_path_exists(&self, path: &Path) -> AppEffectResult {
150 let resolved = self.resolve_path(path);
151 AppEffectResult::Bool(resolved.exists())
152 }
153
154 fn execute_set_read_only(&self, path: &Path, readonly: bool) -> AppEffectResult {
155 let resolved = self.resolve_path(path);
156 match std::fs::metadata(&resolved) {
157 Ok(metadata) => {
158 let mut permissions = metadata.permissions();
159 permissions.set_readonly(readonly);
160 match std::fs::set_permissions(&resolved, permissions) {
161 Ok(()) => AppEffectResult::Ok,
162 Err(error) => AppEffectResult::Error(format!(
163 "Failed to set permissions on '{}': {}",
164 resolved.display(),
165 error
166 )),
167 }
168 }
169 Err(error) => AppEffectResult::Error(format!(
170 "Failed to get metadata for '{}': {}",
171 resolved.display(),
172 error
173 )),
174 }
175 }
176
177 fn execute_git_require_repo() -> AppEffectResult {
178 match crate::git_helpers::require_git_repo() {
179 Ok(()) => AppEffectResult::Ok,
180 Err(error) => AppEffectResult::Error(format!("Not in a git repository: {error}")),
181 }
182 }
183
184 fn execute_git_get_repo_root() -> AppEffectResult {
185 match crate::git_helpers::get_repo_root() {
186 Ok(root) => AppEffectResult::Path(root),
187 Err(error) => AppEffectResult::Error(format!("Failed to get repository root: {error}")),
188 }
189 }
190
191 fn execute_git_get_head_oid() -> AppEffectResult {
192 match crate::git_helpers::get_current_head_oid() {
193 Ok(oid) => AppEffectResult::String(oid),
194 Err(error) => AppEffectResult::Error(format!("Failed to get HEAD OID: {error}")),
195 }
196 }
197
198 fn execute_git_diff() -> AppEffectResult {
199 match crate::git_helpers::git_diff() {
200 Ok(diff) => AppEffectResult::String(diff),
201 Err(error) => AppEffectResult::Error(format!("Failed to get git diff: {error}")),
202 }
203 }
204
205 fn execute_git_diff_from(start_oid: &str) -> AppEffectResult {
206 match crate::git_helpers::git_diff_from(start_oid) {
207 Ok(diff) => AppEffectResult::String(diff),
208 Err(error) => AppEffectResult::Error(format!(
209 "Failed to get git diff from '{start_oid}': {error}"
210 )),
211 }
212 }
213
214 fn execute_git_diff_from_start() -> AppEffectResult {
215 match crate::git_helpers::get_git_diff_from_start() {
216 Ok(diff) => AppEffectResult::String(diff),
217 Err(error) => {
218 AppEffectResult::Error(format!("Failed to get diff from start commit: {error}"))
219 }
220 }
221 }
222
223 fn execute_git_snapshot() -> AppEffectResult {
224 match crate::git_helpers::git_snapshot() {
225 Ok(snapshot) => AppEffectResult::String(snapshot),
226 Err(error) => AppEffectResult::Error(format!("Failed to create git snapshot: {error}")),
227 }
228 }
229
230 fn execute_git_add_all() -> AppEffectResult {
231 match crate::git_helpers::git_add_all() {
232 Ok(staged) => AppEffectResult::Bool(staged),
233 Err(error) => AppEffectResult::Error(format!("Failed to stage all changes: {error}")),
234 }
235 }
236
237 fn execute_git_commit(
238 message: &str,
239 user_name: Option<&str>,
240 user_email: Option<&str>,
241 ) -> AppEffectResult {
242 match crate::git_helpers::git_commit(message, user_name, user_email, None) {
243 Ok(Some(oid)) => AppEffectResult::Commit(CommitResult::Success(oid.to_string())),
244 Ok(None) => AppEffectResult::Commit(CommitResult::NoChanges),
245 Err(error) => AppEffectResult::Error(format!("Failed to create commit: {error}")),
246 }
247 }
248
249 fn execute_git_save_start_commit() -> AppEffectResult {
250 match crate::git_helpers::save_start_commit() {
251 Ok(()) => AppEffectResult::Ok,
252 Err(error) => AppEffectResult::Error(format!("Failed to save start commit: {error}")),
253 }
254 }
255
256 fn execute_git_reset_start_commit() -> AppEffectResult {
257 match crate::git_helpers::reset_start_commit() {
258 Ok(result) => AppEffectResult::String(result.oid),
259 Err(error) => AppEffectResult::Error(format!("Failed to reset start commit: {error}")),
260 }
261 }
262
263 fn execute_git_rebase_onto(_upstream_branch: String) -> AppEffectResult {
264 AppEffectResult::Error(
265 "GitRebaseOnto requires executor injection - use pipeline runner".to_string(),
266 )
267 }
268
269 fn execute_git_get_conflicted_files() -> AppEffectResult {
270 match crate::git_helpers::get_conflicted_files() {
271 Ok(files) => AppEffectResult::StringList(files),
272 Err(error) => {
273 AppEffectResult::Error(format!("Failed to get conflicted files: {error}"))
274 }
275 }
276 }
277
278 fn execute_git_continue_rebase() -> AppEffectResult {
279 AppEffectResult::Error(
280 "GitContinueRebase requires executor injection - use pipeline runner".to_string(),
281 )
282 }
283
284 fn execute_git_abort_rebase() -> AppEffectResult {
285 AppEffectResult::Error(
286 "GitAbortRebase requires executor injection - use pipeline runner".to_string(),
287 )
288 }
289
290 fn execute_git_get_default_branch() -> AppEffectResult {
291 match crate::git_helpers::get_default_branch() {
292 Ok(branch) => AppEffectResult::String(branch),
293 Err(error) => AppEffectResult::Error(format!("Failed to get default branch: {error}")),
294 }
295 }
296
297 fn execute_git_is_main_branch() -> AppEffectResult {
298 match crate::git_helpers::is_main_or_master_branch() {
299 Ok(is_main) => AppEffectResult::Bool(is_main),
300 Err(error) => AppEffectResult::Error(format!("Failed to check branch: {error}")),
301 }
302 }
303
304 fn execute_get_env_var(name: &str) -> AppEffectResult {
305 match std::env::var(name) {
306 Ok(value) => AppEffectResult::String(value),
307 Err(std::env::VarError::NotPresent) => {
308 AppEffectResult::Error(format!("Environment variable '{name}' not set"))
309 }
310 Err(std::env::VarError::NotUnicode(_)) => AppEffectResult::Error(format!(
311 "Environment variable '{name}' contains invalid Unicode"
312 )),
313 }
314 }
315
316 fn execute_set_env_var(name: &str, value: &str) -> AppEffectResult {
317 std::env::set_var(name, value);
318 AppEffectResult::Ok
319 }
320}
321
322impl Default for RealAppEffectHandler {
323 fn default() -> Self {
324 Self::new()
325 }
326}
327
328impl AppEffectHandler for RealAppEffectHandler {
329 fn execute(&mut self, effect: AppEffect) -> AppEffectResult {
330 match effect {
331 AppEffect::SetCurrentDir { path } => self.execute_set_current_dir(&path),
332 AppEffect::WriteFile { path, content } => self.execute_write_file(&path, content),
333 AppEffect::ReadFile { path } => self.execute_read_file(&path),
334 AppEffect::DeleteFile { path } => self.execute_delete_file(&path),
335 AppEffect::CreateDir { path } => self.execute_create_dir(&path),
336 AppEffect::PathExists { path } => self.execute_path_exists(&path),
337 AppEffect::SetReadOnly { path, readonly } => {
338 self.execute_set_read_only(&path, readonly)
339 }
340 AppEffect::GitRequireRepo => Self::execute_git_require_repo(),
341 AppEffect::GitGetRepoRoot => Self::execute_git_get_repo_root(),
342 AppEffect::GitGetHeadOid => Self::execute_git_get_head_oid(),
343 AppEffect::GitDiff => Self::execute_git_diff(),
344 AppEffect::GitDiffFrom { start_oid } => Self::execute_git_diff_from(&start_oid),
345 AppEffect::GitDiffFromStart => Self::execute_git_diff_from_start(),
346 AppEffect::GitSnapshot => Self::execute_git_snapshot(),
347 AppEffect::GitAddAll => Self::execute_git_add_all(),
348 AppEffect::GitCommit {
349 message,
350 user_name,
351 user_email,
352 } => Self::execute_git_commit(&message, user_name.as_deref(), user_email.as_deref()),
353 AppEffect::GitSaveStartCommit => Self::execute_git_save_start_commit(),
354 AppEffect::GitResetStartCommit => Self::execute_git_reset_start_commit(),
355 AppEffect::GitRebaseOnto { upstream_branch } => {
356 Self::execute_git_rebase_onto(upstream_branch)
357 }
358 AppEffect::GitGetConflictedFiles => Self::execute_git_get_conflicted_files(),
359 AppEffect::GitContinueRebase => Self::execute_git_continue_rebase(),
360 AppEffect::GitAbortRebase => Self::execute_git_abort_rebase(),
361 AppEffect::GitGetDefaultBranch => Self::execute_git_get_default_branch(),
362 AppEffect::GitIsMainBranch => Self::execute_git_is_main_branch(),
363 AppEffect::GetEnvVar { name } => Self::execute_get_env_var(&name),
364 AppEffect::SetEnvVar { name, value } => Self::execute_set_env_var(&name, &value),
365 AppEffect::LogInfo { message: _ }
366 | AppEffect::LogSuccess { message: _ }
367 | AppEffect::LogWarn { message: _ }
368 | AppEffect::LogError { message: _ } => AppEffectResult::Ok,
369 }
370 }
371}
372
373#[cfg(test)]
374mod tests;