lgit 0.9.2

CLI tool for managing git repositories
use crate::commands::Exec;

pub fn run<T: Exec>(command: &T, dry_run: bool, verbose: bool) -> Result<(), Option<String>> {
    match delete_branches(command, dry_run, verbose) {
        Ok(output) => {
            println!("{output}");
            Ok(())
        }
        Err(err) => Err(Some(err)),
    }
}

fn delete_branches<T: Exec>(command: &T, dry_run: bool, verbose: bool) -> Result<String, String> {
    command
        .exec(&["fetch", "--prune"], verbose, false)
        .map_err(|()| "Failed to fetch and prune from remote (check network connection)".to_string())?;

    let branches = command
        .exec(&["branch", "-vv"], verbose, false)
        .map_err(|()| "Failed to get branch information with tracking details".to_string())?;

    let mut result = Vec::new();

    for line in branches.lines() {
        if line.starts_with('*') || !line.contains(": gone]") {
            continue;
        }

        let branch_name = line
            .split_whitespace()
            .next()
            .ok_or_else(|| format!("Failed to parse branch name from line: '{}'", line.trim()))?;

        if !dry_run {
            command
                .exec(&["branch", "-D", branch_name], verbose, false)
                .map_err(|()| format!("Failed to delete branch '{}'", branch_name))?;
        }

        result.push(format!("Deleted branch {branch_name}"));
    }

    Ok(if result.is_empty() {
        "No branches to delete".to_string()
    } else {
        result.join("\n")
    })
}

#[cfg(test)]
mod tests {
    use crate::commands::delete_branches::delete_branches;
    use crate::commands::MockCmd;

    fn cmd_fetch_prune() -> MockCmd {
        let mut command = MockCmd::new();
        command
            .expect_exec()
            .withf(|args, verbose, inherit_stderr| {
                args == ["fetch", "--prune"] && !(*verbose) && !(*inherit_stderr)
            })
            .times(1)
            .returning(|_, _, _| Ok(String::new()));

        command
    }

    fn cmd_fetch_prune_branch() -> MockCmd {
        let mut command = cmd_fetch_prune();
        command
            .expect_exec()
            .withf(|args, verbose, inherit_stderr| args == ["branch", "-vv"] && !(*verbose) && !(*inherit_stderr))
            .times(1)
            .returning(|_, _, _| Ok("  branch1 [origin/branch1: gone]\n  branch2 [origin/branch2: gone]\n* branch3 [origin/branch3]".to_string()));

        command
    }

    #[test]
    fn delete_branches_does_not_delete_when_dry_run() {
        let command = cmd_fetch_prune_branch();

        let result = delete_branches(&command, true, false);

        assert!(result.is_ok());
        assert_eq!(
            result.unwrap(),
            "Deleted branch branch1\nDeleted branch branch2"
        );
    }

    #[test]
    fn delete_branches_does_not_delete_current_branch() {
        let mut command = cmd_fetch_prune_branch();
        command
            .expect_exec()
            .withf(|args, verbose, inherit_stderr| {
                args == ["branch", "-D", "branch1"] && !(*verbose) && !(*inherit_stderr)
            })
            .times(1)
            .returning(|_, _, _| Ok(String::new()));
        command
            .expect_exec()
            .withf(|args, verbose, inherit_stderr| {
                args == ["branch", "-D", "branch2"] && !(*verbose) && !(*inherit_stderr)
            })
            .times(1)
            .returning(|_, _, _| Ok(String::new()));

        let result = delete_branches(&command, false, false);

        assert!(result.is_ok());
        assert_eq!(
            result.unwrap(),
            "Deleted branch branch1\nDeleted branch branch2"
        );
    }

    #[test]
    fn delete_branches_returns_error_when_delete_fails() {
        let mut command = cmd_fetch_prune_branch();
        command
            .expect_exec()
            .withf(|args, verbose, inherit_stderr| {
                args == ["branch", "-D", "branch1"] && !(*verbose) && !(*inherit_stderr)
            })
            .times(1)
            .returning(|_, _, _| Err(()));

        let result = delete_branches(&command, false, false);

        assert!(result.is_err());
    }

    #[test]
    fn delete_branches_no_branches_to_delete() {
        let mut command = cmd_fetch_prune();
        command
            .expect_exec()
            .withf(|args, verbose, inherit_stderr| {
                args == ["branch", "-vv"] && !(*verbose) && !(*inherit_stderr)
            })
            .times(1)
            .returning(|_, _, _| Ok("* branch3 [origin/branch3]".to_string()));

        let result = delete_branches(&command, false, false);

        assert!(result.is_ok());
        assert_eq!(result.unwrap(), "No branches to delete");
    }
}