gmsg 0.1.3

AI-powered commit message generator with a TUI editor
Documentation
use std::path::Path;

use anyhow::{self, Context};
use git2::{DiffFormat, DiffOptions, Repository, Status, StatusEntry, Tree};

pub fn get_diff(repository: &Repository) -> anyhow::Result<Option<String>> {
    if get_staged_files(repository)
        .context("Could not get staged files")?
        .is_some()
    {
        let tree: Option<Tree> = match repository.head() {
            Ok(head) => head.peel_to_tree().ok(),
            Err(_) => None,
        };
        let index = repository.index().ok();
        let mut options = DiffOptions::new();

        options
            .ignore_whitespace_eol(true)
            .ignore_blank_lines(true)
            .context_lines(10);
        let diff = repository
            .diff_tree_to_index(tree.as_ref(), index.as_ref(), Some(&mut options))
            .context("Could not get diff")?;
        let mut output = String::new();
        let mut error: Option<anyhow::Error> = None;
        diff.print(DiffFormat::Patch, |_, _, line| {
            match str::from_utf8(line.content()) {
                Ok(s) => {
                    output.push_str(s);
                    true
                }
                Err(e) => {
                    error = Some(anyhow::anyhow!(e));
                    false
                }
            }
        })
        .context("Failed to print diff")?;
        if let Some(e) = error {
            return Err(e);
        }
        Ok(Some(output))
    } else {
        Ok(None)
    }
}

pub fn commit(repository: &Repository, message: &str) -> anyhow::Result<()> {
    let signature = repository
        .signature()
        .context("Could not read repository Signature")?;
    let parent = match repository.head() {
        Ok(head) => head.peel_to_commit().ok(),
        Err(_) => None,
    };
    let mut index = repository.index().context("Could not get index")?;
    let tree_id = index.write_tree().context("Could not get tree of head")?;
    let tree = repository
        .find_tree(tree_id)
        .context("Could not find tree")?;

    let parents_slice: Vec<&git2::Commit> = parent.iter().collect();
    repository
        .commit(
            Some("HEAD"),
            &signature,
            &signature,
            message,
            &tree,
            &parents_slice,
        )
        .context("Could not make commit")?;

    Ok(())
}

pub fn get_staged_files(repository: &Repository) -> anyhow::Result<Option<Vec<String>>> {
    let filter_staged = |status: &StatusEntry| {
        status.status().intersects(
            Status::INDEX_DELETED
                | Status::INDEX_MODIFIED
                | Status::INDEX_NEW
                | Status::INDEX_RENAMED
                | Status::INDEX_TYPECHANGE,
        )
    };

    let statuses = repository
        .statuses(None)
        .context("Could not get status of current repo")?;

    let files: Result<Vec<String>, anyhow::Error> = statuses
        .iter()
        .filter(|s| filter_staged(s))
        .map(|s| {
            s.path()
                .ok_or_else(|| anyhow::anyhow!("Path Contains Invalid UTF-8"))
                .map(|p| p.to_owned())
        })
        .collect();
    let files = files?;
    if files.is_empty() {
        Ok(None)
    } else {
        Ok(Some(files))
    }
}

pub fn stage_files(paths: &[String], repository: &Repository) -> anyhow::Result<()> {
    if paths.is_empty() {
        return Err(anyhow::anyhow!("No path"));
    }
    let mut index = repository.index()?;
    for path in paths {
        let path = Path::new(&path);
        index.add_path(path)?;
    }
    index.write()?;
    Ok(())
}
#[cfg(test)]
mod test {
    use crate::git::get_staged_files;
    use crate::git::{commit, get_diff, stage_files};
    use anyhow::{Context, Result};

    use crate::test_utils::setup;

    #[test]
    fn test_stage_files_works() -> Result<()> {
        let (repository, _dir) = setup()?;
        let result = stage_files(&["test.txt".to_string()], &repository);
        assert!(result.is_ok());
        let diff = get_diff(&repository).context("Could not get diff")?;

        assert!(diff.is_some());

        Ok(())
    }

    #[test]
    fn test_commit_on_empty_repo_works() -> Result<()> {
        let (repository, dir) = setup()?;
        stage_files(&["test.txt".to_string()], &repository)?;
        let diff = get_diff(&repository).context("Could not get diff")?;

        assert!(diff.is_some());

        let result = commit(&repository, "First commit");
        let diff = get_diff(&repository).context("Could not get diff")?;

        assert!(diff.is_none());

        assert!(result.is_ok());
        let _ = dir.path();

        Ok(())
    }

    #[test]
    fn test_get_staged_files_with_staged_files() -> Result<()> {
        let (repository, _directory) = setup()?;
        let file = "test.txt".to_string();
        stage_files(std::slice::from_ref(&file), &repository)?;
        let result = get_staged_files(&repository)?;
        assert!(result.is_some());
        let files = result.unwrap();
        assert_eq!(file, files[0]);

        Ok(())
    }
    #[test]
    fn test_get_staged_files_with_no_staged_files() -> Result<()> {
        let (repository, _directory) = setup()?;

        let files = get_staged_files(&repository)?;
        assert!(files.is_none());

        Ok(())
    }
}