rft-cli 0.5.3

Zero-config Docker Compose isolation for git worktrees
use owo_colors::OwoColorize;
use tokio::task::JoinSet;

use crate::context::build_context;
use crate::error::{Result, RftError};
use crate::sanitize::compose_project_name;

pub async fn run() -> Result<()> {
    let context = build_context().await?;

    let non_main: Vec<_> = context.worktrees.iter().filter(|wt| !wt.is_main).collect();

    if non_main.is_empty() {
        println!("{}", "Nothing to clean.".dimmed());
        return Ok(());
    }

    println!("{}", "Stopping all worktree stacks...".bold());

    let mut stop_set = JoinSet::new();
    for worktree in &non_main {
        let project_name =
            compose_project_name(&context.repo_name, worktree.index, &worktree.branch);
        let branch = worktree.branch.clone();
        let index = worktree.index;
        let worktree_path = worktree.path.clone();

        stop_set.spawn(async move {
            super::stop::stop_single_project(&project_name, &worktree_path, index, &branch).await
        });
    }

    while let Some(result) = stop_set.join_next().await {
        if let Ok(Err(error)) = result {
            eprintln!("{}", format!("warning: stop failed: {error}").yellow());
        }
    }

    println!("{}", "Removing worktrees...".bold());

    for worktree in &non_main {
        println!(
            "{} {}",
            "Removing".red().bold(),
            format!("[{}] {}", worktree.index, worktree.branch).bold()
        );

        let output = tokio::process::Command::new("git")
            .args(["worktree", "remove", "--force"])
            .arg(&worktree.path)
            .current_dir(&context.repo_root)
            .output()
            .await?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr).to_string();
            eprintln!(
                "{}",
                format!(
                    "warning: failed to remove worktree {}: {stderr}",
                    worktree.branch
                )
                .yellow()
            );
        }
    }

    let prune_output = tokio::process::Command::new("git")
        .args(["worktree", "prune"])
        .current_dir(&context.repo_root)
        .output()
        .await?;

    if !prune_output.status.success() {
        let stderr = String::from_utf8_lossy(&prune_output.stderr).to_string();
        eprintln!(
            "{}",
            format!("warning: git worktree prune failed: {stderr}").yellow()
        );
    }

    println!("{}", "Cleaning Docker resources...".bold());

    let pattern = format!("{}-rft-*", crate::sanitize::sanitize(&context.repo_name));
    remove_docker_resources_by_pattern(&pattern).await?;

    println!("{}", "Clean complete.".green().bold());

    Ok(())
}

async fn remove_docker_resources_by_pattern(pattern: &str) -> Result<()> {
    let containers_output = tokio::process::Command::new("docker")
        .args(["ps", "-a", "--filter", &format!("name={pattern}"), "-q"])
        .output()
        .await?;

    let container_ids = String::from_utf8_lossy(&containers_output.stdout)
        .trim()
        .to_string();

    if !container_ids.is_empty() {
        let ids: Vec<&str> = container_ids.lines().collect();
        let mut rm_args = vec!["rm", "-f"];
        rm_args.extend(ids);

        let output = tokio::process::Command::new("docker")
            .args(rm_args)
            .output()
            .await?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr).to_string();
            return Err(RftError::CommandFailed {
                cmd: "docker rm -f".to_string(),
                stderr,
            });
        }
    }

    let networks_output = tokio::process::Command::new("docker")
        .args([
            "network",
            "ls",
            "--filter",
            &format!("name={pattern}"),
            "-q",
        ])
        .output()
        .await?;

    let network_ids = String::from_utf8_lossy(&networks_output.stdout)
        .trim()
        .to_string();

    if !network_ids.is_empty() {
        for network_id in network_ids.lines() {
            let _ = tokio::process::Command::new("docker")
                .args(["network", "rm", network_id])
                .output()
                .await;
        }
    }

    let volumes_output = tokio::process::Command::new("docker")
        .args(["volume", "ls", "--filter", &format!("name={pattern}"), "-q"])
        .output()
        .await?;

    let volume_ids = String::from_utf8_lossy(&volumes_output.stdout)
        .trim()
        .to_string();

    if !volume_ids.is_empty() {
        for volume_id in volume_ids.lines() {
            let _ = tokio::process::Command::new("docker")
                .args(["volume", "rm", "-f", volume_id])
                .output()
                .await;
        }
    }

    Ok(())
}