ngit 2.4.1

nostr plugin for git
Documentation
use anyhow::{Context, Result};
use nostr_sdk::hashes::sha1::Hash as Sha1Hash;

use super::{Repo, RepoActions};

/**
 * returns `(from_branch,to_branch,ahead,behind)`
 */
pub fn identify_ahead_behind(
    git_repo: &Repo,
    from_branch: &Option<String>,
    to_branch: &Option<String>,
) -> Result<(String, String, Vec<Sha1Hash>, Vec<Sha1Hash>)> {
    let (from_branch, from_tip) = match from_branch {
        Some(name) => (
            name.to_string(),
            git_repo
                .get_tip_of_branch(name)
                .context(format!("failed to find from_branch '{name}'"))?,
        ),
        None => (
            if let Ok(name) = git_repo.get_checked_out_branch_name() {
                name
            } else {
                "head".to_string()
            },
            git_repo
                .get_head_commit()
                .context("failed to get head commit")
                .context(
                    "checkout a commit or specify a from_branch. head does not reveal a commit",
                )?,
        ),
    };

    let (to_branch, to_tip) = match to_branch {
        Some(name) => (
            name.to_string(),
            git_repo
                .get_tip_of_branch(name)
                .context(format!("failed to find to_branch '{name}'"))?,
        ),
        None => {
            let (name, commit) = git_repo
                .get_main_or_master_branch()
                .context("the default branches (main or master) do not exist")?;
            (name.to_string(), commit)
        }
    };

    match git_repo.get_commits_ahead_behind(&to_tip, &from_tip) {
        Err(e) => {
            if e.to_string().contains("is not an ancestor of") {
                return Err(e).context(format!(
                    "'{from_branch}' is not branched from '{to_branch}'"
                ));
            }
            Err(e).context(format!(
                "failed to get commits ahead and behind from '{from_branch}' to '{to_branch}'"
            ))
        }
        Ok((ahead, behind)) => Ok((from_branch, to_branch, ahead, behind)),
    }
}

#[cfg(test)]
mod tests {

    use test_utils::git::GitTestRepo;

    use super::*;
    use crate::git::oid_to_sha1;

    #[test]
    fn when_from_branch_doesnt_exist_return_error() -> Result<()> {
        let test_repo = GitTestRepo::default();
        let git_repo = Repo::from_path(&test_repo.dir)?;

        test_repo.populate()?;
        let branch_name = "doesnt_exist";
        assert_eq!(
            identify_ahead_behind(&git_repo, &Some(branch_name.to_string()), &None)
                .unwrap_err()
                .to_string(),
            format!("failed to find from_branch '{}'", &branch_name),
        );
        Ok(())
    }

    #[test]
    fn when_to_branch_doesnt_exist_return_error() -> Result<()> {
        let test_repo = GitTestRepo::default();
        let git_repo = Repo::from_path(&test_repo.dir)?;

        test_repo.populate()?;
        let branch_name = "doesnt_exist";
        assert_eq!(
            identify_ahead_behind(&git_repo, &None, &Some(branch_name.to_string()))
                .unwrap_err()
                .to_string(),
            format!("failed to find to_branch '{}'", &branch_name),
        );
        Ok(())
    }

    #[test]
    fn when_to_branch_is_none_and_no_main_or_master_branch_return_error() -> Result<()> {
        let test_repo = GitTestRepo::new("notmain")?;
        let git_repo = Repo::from_path(&test_repo.dir)?;

        test_repo.populate()?;

        assert_eq!(
            identify_ahead_behind(&git_repo, &None, &None)
                .unwrap_err()
                .to_string(),
            "the default branches (main or master) do not exist",
        );
        Ok(())
    }

    #[test]
    fn when_from_branch_is_not_head_return_as_from_branch() -> Result<()> {
        let test_repo = GitTestRepo::default();
        let git_repo = Repo::from_path(&test_repo.dir)?;

        test_repo.populate()?;
        // create feature branch with 1 commit ahead
        test_repo.create_branch("feature")?;
        test_repo.checkout("feature")?;
        std::fs::write(test_repo.dir.join("t3.md"), "some content")?;
        let head_oid = test_repo.stage_and_commit("add t3.md")?;

        // make feature branch 1 commit behind
        test_repo.checkout("main")?;
        std::fs::write(test_repo.dir.join("t4.md"), "some content")?;
        let main_oid = test_repo.stage_and_commit("add t4.md")?;

        let (from_branch, to_branch, ahead, behind) =
            identify_ahead_behind(&git_repo, &Some("feature".to_string()), &None)?;

        assert_eq!(from_branch, "feature");
        assert_eq!(ahead, vec![oid_to_sha1(&head_oid)]);
        assert_eq!(to_branch, "main");
        assert_eq!(behind, vec![oid_to_sha1(&main_oid)]);
        Ok(())
    }

    #[test]
    fn when_to_branch_is_not_main_return_as_to_branch() -> Result<()> {
        let test_repo = GitTestRepo::default();
        let git_repo = Repo::from_path(&test_repo.dir)?;

        test_repo.populate()?;
        // create dev branch with 1 commit ahead
        test_repo.create_branch("dev")?;
        test_repo.checkout("dev")?;
        std::fs::write(test_repo.dir.join("t3.md"), "some content")?;
        let dev_oid_first = test_repo.stage_and_commit("add t3.md")?;

        // create feature branch with 1 commit ahead of dev
        test_repo.create_branch("feature")?;
        test_repo.checkout("feature")?;
        std::fs::write(test_repo.dir.join("t4.md"), "some content")?;
        let feature_oid = test_repo.stage_and_commit("add t4.md")?;

        // make feature branch 1 behind
        test_repo.checkout("dev")?;
        std::fs::write(test_repo.dir.join("t3.md"), "some content")?;
        let dev_oid = test_repo.stage_and_commit("add t3.md")?;

        let (from_branch, to_branch, ahead, behind) = identify_ahead_behind(
            &git_repo,
            &Some("feature".to_string()),
            &Some("dev".to_string()),
        )?;

        assert_eq!(from_branch, "feature");
        assert_eq!(ahead, vec![oid_to_sha1(&feature_oid)]);
        assert_eq!(to_branch, "dev");
        assert_eq!(behind, vec![oid_to_sha1(&dev_oid)]);

        let (from_branch, to_branch, ahead, behind) =
            identify_ahead_behind(&git_repo, &Some("feature".to_string()), &None)?;

        assert_eq!(from_branch, "feature");
        assert_eq!(
            ahead,
            vec![oid_to_sha1(&feature_oid), oid_to_sha1(&dev_oid_first)]
        );
        assert_eq!(to_branch, "main");
        assert_eq!(behind, vec![]);

        Ok(())
    }
}