use crate::error::RepoLensError;
use tracing::{debug, info};
use crate::config::Config;
use super::plan::{Action, ActionOperation, ActionPlan};
use super::{branch_protection, github_settings, gitignore, templates};
#[derive(Debug)]
pub struct ActionResult {
pub action_name: String,
pub success: bool,
pub error: Option<String>,
}
pub struct ActionExecutor {
_config: Config,
}
impl ActionExecutor {
pub fn new(config: Config) -> Self {
Self { _config: config }
}
pub async fn execute(&self, plan: &ActionPlan) -> Result<Vec<ActionResult>, RepoLensError> {
let mut results = Vec::new();
for action in plan.actions() {
info!("Executing action: {}", action.id());
let result = self.execute_action(action).await;
results.push(ActionResult {
action_name: action.description().to_string(),
success: result.is_ok(),
error: result.err().map(|e| e.to_string()),
});
}
Ok(results)
}
async fn execute_action(&self, action: &Action) -> Result<(), RepoLensError> {
match action.operation() {
ActionOperation::UpdateGitignore { entries } => {
debug!("Updating .gitignore with {} entries", entries.len());
let current_dir = std::env::current_dir().map_err(|e| {
RepoLensError::Action(crate::error::ActionError::ExecutionFailed {
message: format!("Failed to get current directory: {}", e),
})
})?;
gitignore::update_gitignore_at(¤t_dir, entries)?;
}
ActionOperation::CreateFile {
path,
template,
variables,
} => {
debug!("Creating file {} from template {}", path, template);
templates::create_file_from_template(path, template, variables)?;
}
ActionOperation::ConfigureBranchProtection { branch, settings } => {
debug!("Configuring branch protection for {}", branch);
branch_protection::configure(branch, settings).await?;
}
ActionOperation::UpdateGitHubSettings { settings } => {
debug!("Updating GitHub repository settings");
github_settings::update(settings).await?;
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::actions::plan::{Action, ActionOperation, ActionPlan};
use serial_test::serial;
use std::collections::HashMap;
use tempfile::TempDir;
#[tokio::test]
#[serial]
#[cfg_attr(tarpaulin, ignore)]
async fn test_execute_action_update_gitignore() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let root_abs = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
let original_dir =
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("/tmp"));
if std::env::current_dir().is_err() {
let _ = std::env::set_current_dir("/tmp");
}
std::env::set_current_dir(&root_abs).expect("Failed to change to temp directory");
let config = Config::default();
let executor = ActionExecutor::new(config);
let action = Action::new(
"test-gitignore",
"gitignore",
"Test gitignore update",
ActionOperation::UpdateGitignore {
entries: vec![".env".to_string(), "*.key".to_string()],
},
);
let result = executor.execute_action(&action).await;
let _ = std::env::set_current_dir(&original_dir);
assert!(
result.is_ok(),
"Action execution failed: {:?}",
result.err()
);
let gitignore_path = root_abs.join(".gitignore");
assert!(
gitignore_path.exists(),
".gitignore not found at {:?}. Root was: {:?}. Current dir: {:?}",
gitignore_path,
root_abs,
std::env::current_dir()
);
}
#[tokio::test]
#[serial]
async fn test_execute_action_create_file() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let original_dir =
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("/tmp"));
if std::env::current_dir().is_err() {
let _ = std::env::set_current_dir("/tmp");
}
std::env::set_current_dir(root).expect("Failed to change to temp directory");
let config = Config::default();
let executor = ActionExecutor::new(config);
let action = Action::new(
"test-create",
"file",
"Test file creation",
ActionOperation::CreateFile {
path: "TEST.md".to_string(),
template: "CONTRIBUTING.md".to_string(),
variables: HashMap::new(),
},
);
let result = executor.execute_action(&action).await;
let _ = result;
let _ = std::env::set_current_dir(&original_dir);
}
#[tokio::test]
#[serial]
async fn test_execute_all_actions() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let original_dir =
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("/tmp"));
if std::env::current_dir().is_err() {
let _ = std::env::set_current_dir("/tmp");
}
std::env::set_current_dir(root).expect("Failed to change to temp directory");
let config = Config::default();
let executor = ActionExecutor::new(config);
let mut plan = ActionPlan::new();
plan.add(Action::new(
"test-1",
"gitignore",
"Test 1",
ActionOperation::UpdateGitignore {
entries: vec![".env".to_string()],
},
));
let results = executor.execute(&plan).await.unwrap();
assert_eq!(results.len(), 1);
assert!(results[0].success);
let _ = std::env::set_current_dir(&original_dir);
}
#[tokio::test]
#[serial]
async fn test_execute_handles_errors_gracefully() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let original_dir =
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("/tmp"));
if std::env::current_dir().is_err() {
let _ = std::env::set_current_dir("/tmp");
}
std::env::set_current_dir(root).expect("Failed to change to temp directory");
let config = Config::default();
let executor = ActionExecutor::new(config);
let mut plan = ActionPlan::new();
plan.add(Action::new(
"test-fail",
"file",
"Test failure",
ActionOperation::CreateFile {
path: "INVALID.md".to_string(),
template: "NONEXISTENT_TEMPLATE.md".to_string(),
variables: HashMap::new(),
},
));
let results = executor.execute(&plan).await.unwrap();
assert_eq!(results.len(), 1);
assert!(!results[0].success);
assert!(results[0].error.is_some());
let _ = std::env::set_current_dir(&original_dir);
}
}