lgit 0.9.2

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

pub fn get_default_branch<T: Exec>(command: &T, verbose: bool) -> Result<&'static str, String> {
    for branch in ["main", "master"] {
        if search_branch(command, branch, verbose).is_ok() {
            return Ok(branch);
        }
    }

    Err("Failed to determine default branch (neither 'main' nor 'master' found)".to_string())
}

pub fn get_base<T: Exec>(command: &T, base: Option<String>, verbose: bool) -> String {
    base.unwrap_or_else(|| match get_default_branch(command, verbose) {
        Ok(branch) => branch.to_string(),
        Err(error) => panic!("{}", error),
    })
}

pub fn refresh_base<'a, T: Exec>(command: &T, base: &'a str, verbose: bool) -> Result<&'a str, ()> {
    command.exec(&["checkout", base], verbose, false)?;
    command.exec(&["pull"], verbose, false).map(|_| base)
}

fn search_branch<T: Exec>(command: &T, branch: &str, verbose: bool) -> Result<(), Option<String>> {
    let result = command
        .exec(&["branch", "-l", branch], verbose, false)
        .map_err(|()| format!("Failed to list branch '{}'", branch))?;

    if result.is_empty() {
        Err(Some(format!("Branch '{}' not found", branch)))
    } else {
        Ok(())
    }
}

pub fn stash<T: Exec>(command: &T, verbose: bool) -> Result<bool, String> {
    let result = command
        .exec(&["status", "--porcelain"], verbose, false)
        .map_err(|()| "Failed to retrieve git status (check if in git repository)".to_string())?;

    if result.is_empty() {
        return Ok(false);
    }

    command
        .exec(&["stash", "-u"], verbose, false)
        .map_err(|()| "Failed to stash uncommitted changes".to_string())?;

    Ok(true)
}

pub fn unstash<T: Exec>(command: &T, verbose: bool) -> Result<(), Option<String>> {
    command
        .exec(&["stash", "pop"], verbose, false)
        .map_err(|()| "Failed to restore stashed changes (use 'git stash pop' manually)".to_string())?;

    Ok(())
}

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

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

        command
    }

    fn cmd_branch_main_found() -> MockCmd {
        let mut command = MockCmd::new();
        command
            .expect_exec()
            .withf(|args, verbose, inherit_stderr| {
                args == ["branch", "-l", "main"] && !(*verbose) && !(*inherit_stderr)
            })
            .times(1)
            .returning(|_, _, _| Ok("* main".to_string()));

        command
    }

    fn cmd_default_branch_master_found() -> MockCmd {
        let mut command = cmd_branch_not_found();
        command
            .expect_exec()
            .withf(|args, verbose, inherit_stderr| {
                args == ["branch", "-l", "master"] && !(*verbose) && !(*inherit_stderr)
            })
            .times(1)
            .returning(|_, _, _| Ok("* master".to_string()));

        command
    }

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

        command
    }

    #[test]
    fn test_get_default_branch_main() {
        let command = cmd_branch_main_found();
        let branch = super::get_default_branch(&command, false);

        assert_eq!(branch, Ok("main"));
    }

    #[test]
    fn test_get_default_branch_master() {
        let command = cmd_default_branch_master_found();
        let branch = super::get_default_branch(&command, false);

        assert_eq!(branch, Ok("master"));
    }

    #[test]
    fn test_get_base_default_to_main() {
        let command = cmd_branch_main_found();
        let base = super::get_base(&command, None, false);

        assert_eq!(base, "main");
    }

    #[test]
    fn test_get_base_default_to_master() {
        let command = cmd_default_branch_master_found();
        let base = super::get_base(&command, None, false);

        assert_eq!(base, "master");
    }

    #[test]
    fn test_get_base_supplied_base() {
        let command = MockCmd::new();
        let base = super::get_base(&command, Some("test".to_string()), false);

        assert_eq!(base, "test");
    }

    #[test]
    fn test_refresh_base_success() {
        let mut command = cmd_checkout_main();
        command
            .expect_exec()
            .withf(|args, verbose, inherit_stderr| {
                args == ["pull"] && !(*verbose) && !(*inherit_stderr)
            })
            .times(1)
            .returning(|_, _, _| Ok(String::new()));

        let result = super::refresh_base(&command, "main", false);

        assert!(result.is_ok());
    }

    #[test]
    fn test_refresh_base_checkout_failure() {
        let mut command = MockCmd::new();
        command
            .expect_exec()
            .withf(|args, verbose, inherit_stderr| {
                args == ["checkout", "main"] && !(*verbose) && !(*inherit_stderr)
            })
            .times(1)
            .returning(|_, _, _| Err(()));

        let result = super::refresh_base(&command, "main", false);

        assert!(result.is_err());
    }

    #[test]
    fn test_refresh_base_pull_failure() {
        let mut command = cmd_checkout_main();
        command
            .expect_exec()
            .withf(|args, verbose, inherit_stderr| {
                args == ["pull"] && !(*verbose) && !(*inherit_stderr)
            })
            .times(1)
            .returning(|_, _, _| Err(()));

        let result = super::refresh_base(&command, "main", false);

        assert!(result.is_err());
    }

    #[test]
    fn test_search_branch_found() {
        let command = cmd_branch_main_found();
        let result = super::search_branch(&command, "main", false);

        assert!(result.is_ok());
    }

    #[test]
    fn test_search_branch_not_found() {
        let command = cmd_branch_not_found();
        let result = super::search_branch(&command, "main", false);

        assert!(result.is_err());
    }

    #[test]
    fn test_stash_no_changes() {
        let mut command = MockCmd::new();
        command
            .expect_exec()
            .withf(|args, verbose, inherit_stderr| {
                args == ["status", "--porcelain"] && !(*verbose) && !(*inherit_stderr)
            })
            .times(1)
            .returning(|_, _, _| Ok(String::new()));

        let result = super::stash(&command, false);

        assert_eq!(result, Ok(false));
    }

    #[test]
    fn test_stash_with_changes() {
        let mut command = MockCmd::new();
        command
            .expect_exec()
            .withf(|args, verbose, inherit_stderr| {
                args == ["status", "--porcelain"] && !(*verbose) && !(*inherit_stderr)
            })
            .times(1)
            .returning(|_, _, _| Ok("M file.txt".to_string()));

        command
            .expect_exec()
            .withf(|args, verbose, inherit_stderr| {
                args == ["stash", "-u"] && !(*verbose) && !(*inherit_stderr)
            })
            .times(1)
            .returning(|_, _, _| Ok("Saved working directory".to_string()));

        let result = super::stash(&command, false);

        assert_eq!(result, Ok(true));
    }

    #[test]
    fn test_stash_status_failure() {
        let mut command = MockCmd::new();
        command
            .expect_exec()
            .withf(|args, verbose, inherit_stderr| {
                args == ["status", "--porcelain"] && !(*verbose) && !(*inherit_stderr)
            })
            .times(1)
            .returning(|_, _, _| Err(()));

        let result = super::stash(&command, false);

        assert!(result.is_err());
        assert_eq!(result.unwrap_err(), "Failed to retrieve git status (check if in git repository)");
    }

    #[test]
    fn test_stash_stash_failure() {
        let mut command = MockCmd::new();
        command
            .expect_exec()
            .withf(|args, verbose, inherit_stderr| {
                args == ["status", "--porcelain"] && !(*verbose) && !(*inherit_stderr)
            })
            .times(1)
            .returning(|_, _, _| Ok("M file.txt".to_string()));

        command
            .expect_exec()
            .withf(|args, verbose, inherit_stderr| {
                args == ["stash", "-u"] && !(*verbose) && !(*inherit_stderr)
            })
            .times(1)
            .returning(|_, _, _| Err(()));

        let result = super::stash(&command, false);

        assert!(result.is_err());
        assert_eq!(result.unwrap_err(), "Failed to stash uncommitted changes");
    }

    #[test]
    fn test_unstash_success() {
        let mut command = MockCmd::new();
        command
            .expect_exec()
            .withf(|args, verbose, inherit_stderr| {
                args == ["stash", "pop"] && !(*verbose) && !(*inherit_stderr)
            })
            .times(1)
            .returning(|_, _, _| Ok("Applied stash".to_string()));

        let result = super::unstash(&command, false);

        assert!(result.is_ok());
    }

    #[test]
    fn test_unstash_failure() {
        let mut command = MockCmd::new();
        command
            .expect_exec()
            .withf(|args, verbose, inherit_stderr| {
                args == ["stash", "pop"] && !(*verbose) && !(*inherit_stderr)
            })
            .times(1)
            .returning(|_, _, _| Err(()));

        let result = super::unstash(&command, false);

        assert!(result.is_err());
    }
}