use crate::cook::execution::errors::{MapReduceError, MapReduceResult};
use async_trait::async_trait;
use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::{debug, error, info};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum CleanupPriority {
Critical = 0,
High = 1,
Normal = 2,
Low = 3,
}
#[async_trait]
pub trait CleanupTask: Send + Sync {
async fn cleanup(&self) -> MapReduceResult<()>;
fn priority(&self) -> CleanupPriority {
CleanupPriority::Normal
}
fn description(&self) -> String {
"Cleanup task".to_string()
}
}
pub struct CleanupRegistry {
tasks: Arc<RwLock<Vec<Box<dyn CleanupTask>>>>,
}
impl Default for CleanupRegistry {
fn default() -> Self {
Self::new()
}
}
impl CleanupRegistry {
pub fn new() -> Self {
Self {
tasks: Arc::new(RwLock::new(Vec::new())),
}
}
pub async fn register(&self, task: Box<dyn CleanupTask>) {
let mut tasks = self.tasks.write().await;
tasks.push(task);
}
pub async fn execute_all(&self) -> MapReduceResult<()> {
let mut tasks = {
let mut tasks = self.tasks.write().await;
tasks.drain(..).collect::<Vec<_>>()
};
tasks.sort_by_key(|a| a.priority());
let mut errors = Vec::new();
for task in tasks {
let description = task.description();
debug!("Executing cleanup task: {}", description);
if let Err(e) = task.cleanup().await {
error!("Cleanup task '{}' failed: {}", description, e);
errors.push(format!("{}: {}", description, e));
} else {
info!("Cleanup task '{}' completed successfully", description);
}
}
if !errors.is_empty() {
return Err(MapReduceError::General {
message: format!(
"Cleanup failed with {} errors: {}",
errors.len(),
errors.join("; ")
),
source: None,
});
}
Ok(())
}
pub async fn execute_priority(&self, priority: CleanupPriority) -> MapReduceResult<()> {
let tasks = self.tasks.read().await;
let mut errors = Vec::new();
for task in tasks.iter() {
if task.priority() == priority {
let description = task.description();
debug!(
"Executing priority {} cleanup task: {}",
priority as u8, description
);
if let Err(e) = task.cleanup().await {
error!("Cleanup task '{}' failed: {}", description, e);
errors.push(format!("{}: {}", description, e));
}
}
}
if !errors.is_empty() {
return Err(MapReduceError::General {
message: format!(
"Priority {} cleanup failed with {} errors: {}",
priority as u8,
errors.len(),
errors.join("; ")
),
source: None,
});
}
Ok(())
}
pub async fn clear(&self) {
let mut tasks = self.tasks.write().await;
tasks.clear();
}
pub async fn count(&self) -> usize {
let tasks = self.tasks.read().await;
tasks.len()
}
}
pub struct WorktreeCleanupTask {
worktree_name: String,
worktree_path: Option<std::path::PathBuf>,
}
impl WorktreeCleanupTask {
pub fn new(worktree_name: String, worktree_path: Option<std::path::PathBuf>) -> Self {
Self {
worktree_name,
worktree_path,
}
}
}
#[async_trait]
impl CleanupTask for WorktreeCleanupTask {
async fn cleanup(&self) -> MapReduceResult<()> {
let output = tokio::process::Command::new("git")
.args(["worktree", "remove", "--force", &self.worktree_name])
.output()
.await
.map_err(|e| MapReduceError::General {
message: format!("Failed to execute git worktree remove: {}", e),
source: None,
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
if !stderr.contains("not a valid path") && !stderr.contains("is not a working tree") {
return Err(MapReduceError::General {
message: format!(
"Failed to cleanup worktree {}: {}",
self.worktree_name, stderr
),
source: None,
});
}
}
if let Some(path) = &self.worktree_path {
if path.exists() {
if let Err(e) = tokio::fs::remove_dir_all(path).await {
debug!("Failed to remove worktree directory {:?}: {}", path, e);
}
}
}
Ok(())
}
fn priority(&self) -> CleanupPriority {
CleanupPriority::Normal
}
fn description(&self) -> String {
format!("Cleanup worktree: {}", self.worktree_name)
}
}
pub struct GitBranchCleanupTask {
branch_name: String,
worktree_path: std::path::PathBuf,
}
impl GitBranchCleanupTask {
pub fn new(branch_name: String, worktree_path: std::path::PathBuf) -> Self {
Self {
branch_name,
worktree_path,
}
}
}
#[async_trait]
impl CleanupTask for GitBranchCleanupTask {
async fn cleanup(&self) -> MapReduceResult<()> {
let output = tokio::process::Command::new("git")
.args(["branch", "-D", &self.branch_name])
.current_dir(&self.worktree_path)
.output()
.await
.map_err(|e| MapReduceError::General {
message: format!("Failed to delete git branch: {}", e),
source: None,
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
if !stderr.contains("not found") {
debug!("Failed to delete branch {}: {}", self.branch_name, stderr);
}
}
Ok(())
}
fn priority(&self) -> CleanupPriority {
CleanupPriority::Low
}
fn description(&self) -> String {
format!("Cleanup git branch: {}", self.branch_name)
}
}
pub struct GenericCleanupTask<F>
where
F: Fn() -> futures::future::BoxFuture<'static, MapReduceResult<()>> + Send + Sync,
{
cleanup_fn: F,
priority: CleanupPriority,
description: String,
}
impl<F> GenericCleanupTask<F>
where
F: Fn() -> futures::future::BoxFuture<'static, MapReduceResult<()>> + Send + Sync,
{
pub fn new(cleanup_fn: F, priority: CleanupPriority, description: String) -> Self {
Self {
cleanup_fn,
priority,
description,
}
}
}
#[async_trait]
impl<F> CleanupTask for GenericCleanupTask<F>
where
F: Fn() -> futures::future::BoxFuture<'static, MapReduceResult<()>> + Send + Sync,
{
async fn cleanup(&self) -> MapReduceResult<()> {
(self.cleanup_fn)().await
}
fn priority(&self) -> CleanupPriority {
self.priority
}
fn description(&self) -> String {
self.description.clone()
}
}