//! Cleanup-worktrees command: list or remove orphaned worktrees from chat. //! //! `{bot_name} cleanup_worktrees` lists orphaned worktrees (dry run). //! `{bot_name} cleanup_worktrees --confirm` removes them. //! //! Both paths share the [`crate::worktree::run_cleanup`] handler. use crate::chat::util::strip_bot_mention; use crate::config::ProjectConfig; use std::path::Path; /// A parsed cleanup_worktrees command from a Matrix message body. #[derive(Debug, PartialEq)] pub enum CleanupWorktreesCommand { /// List orphaned worktrees without removing (dry run). DryRun, /// Remove all orphaned worktrees. Confirm, } /// Parse a cleanup_worktrees command from a raw Matrix message body. /// /// Strips the bot mention prefix and checks whether the first word is /// `cleanup_worktrees`. Returns `None` when the message is not this command. pub fn extract_cleanup_worktrees_command( message: &str, bot_name: &str, bot_user_id: &str, ) -> Option { let stripped = strip_bot_mention(message, bot_name, bot_user_id); let trimmed = stripped .trim() .trim_start_matches(|c: char| !c.is_alphanumeric()); let (cmd, args) = match trimmed.split_once(char::is_whitespace) { Some((c, a)) => (c, a.trim()), None => (trimmed, ""), }; if !cmd.eq_ignore_ascii_case("cleanup_worktrees") { return None; } if args == "--confirm" { Some(CleanupWorktreesCommand::Confirm) } else { Some(CleanupWorktreesCommand::DryRun) } } /// Handle a cleanup_worktrees command asynchronously. /// /// Delegates to [`crate::worktree::run_cleanup`] and formats the result as /// a Markdown string suitable for posting to the chat room. pub async fn handle_cleanup_worktrees(project_root: &Path, confirm: bool) -> String { let config = match ProjectConfig::load(project_root) { Ok(c) => c, Err(e) => return format!("Failed to load project config: {e}"), }; let report = crate::worktree::run_cleanup(project_root, &config, confirm).await; crate::worktree::format_report(&report, confirm) } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- #[cfg(test)] mod tests { use super::*; // -- extract_cleanup_worktrees_command ----------------------------------- #[test] fn extract_dry_run_with_full_user_id() { let cmd = extract_cleanup_worktrees_command( "@timmy:home.local cleanup_worktrees", "Timmy", "@timmy:home.local", ); assert_eq!(cmd, Some(CleanupWorktreesCommand::DryRun)); } #[test] fn extract_confirm_with_display_name() { let cmd = extract_cleanup_worktrees_command( "Timmy cleanup_worktrees --confirm", "Timmy", "@timmy:home.local", ); assert_eq!(cmd, Some(CleanupWorktreesCommand::Confirm)); } #[test] fn extract_case_insensitive() { let cmd = extract_cleanup_worktrees_command( "Timmy CLEANUP_WORKTREES", "Timmy", "@timmy:home.local", ); assert_eq!(cmd, Some(CleanupWorktreesCommand::DryRun)); } #[test] fn extract_with_localpart() { let cmd = extract_cleanup_worktrees_command( "@timmy cleanup_worktrees --confirm", "Timmy", "@timmy:home.local", ); assert_eq!(cmd, Some(CleanupWorktreesCommand::Confirm)); } #[test] fn extract_other_command_returns_none() { let cmd = extract_cleanup_worktrees_command("Timmy rmtree 42", "Timmy", "@timmy:home.local"); assert_eq!(cmd, None); } #[test] fn extract_unrelated_text_returns_none() { let cmd = extract_cleanup_worktrees_command("Timmy help", "Timmy", "@timmy:home.local"); assert_eq!(cmd, None); } // -- handle_cleanup_worktrees integration -------------------------------- #[tokio::test] async fn handle_dry_run_no_worktrees() { let tmp = tempfile::tempdir().unwrap(); std::fs::create_dir_all(tmp.path().join(".huskies").join("worktrees")).unwrap(); let response = handle_cleanup_worktrees(tmp.path(), false).await; assert!( response.contains("No orphaned"), "unexpected response: {response}" ); } #[tokio::test] async fn handle_confirm_no_worktrees() { let tmp = tempfile::tempdir().unwrap(); std::fs::create_dir_all(tmp.path().join(".huskies").join("worktrees")).unwrap(); let response = handle_cleanup_worktrees(tmp.path(), true).await; assert!( response.contains("No orphaned"), "unexpected response: {response}" ); } }