gitcoco 0.1.11

GitCoco: A Rust-based CLI for Conventional Commits, making commit standardization effortless and consistent for seamless project versioning and collaboration.
use std::io::{stdin, Read};

use conventional::Config;
use git2::Repository;

use crate::{
    cli::CheckCommand,
    cmd::Command,
    conventional,
    git::{filter_merge_commits, filter_revert_commits},
    strip::Strip,
    Error,
};

fn print_fail(msg: &str, short_id: &str, e: Error) -> bool {
    let first_line = msg.lines().next().unwrap_or("");
    let short_msg: String = first_line.chars().take(40).collect();
    if first_line.len() > 40 {
        println!("FAIL  {}  {}  {}...", short_id, e, short_msg)
    } else {
        println!("FAIL  {}  {}  {}", short_id, e, short_msg)
    }
    false
}

fn print_wrong_type(msg: &str, short_id: &str, commit_type: String) -> bool {
    print_fail(
        msg,
        short_id,
        Error::Type {
            wrong_type: commit_type.to_string(),
        },
    )
}

fn print_check(
    msg: &str,
    short_id: &str,
    parser: &conventional::CommitParser,
    types: &[String],
) -> bool {
    let msg_parsed = parser.parse(msg);

    match msg_parsed {
        Err(e) => print_fail(msg, short_id, e.into()),
        Ok(commit) if !types.contains(&commit.r#type) => {
            print_wrong_type(msg, short_id, commit.r#type)
        }
        _ => true,
    }
}

impl Command for CheckCommand {
    fn exec(&self, mut config: Config) -> anyhow::Result<()> {
        if self.merges {
            config.merges = true;
        }
        if self.first_parent {
            config.first_parent = true;
        }

        let mut total = 0;
        let mut fail = 0;

        let parser = conventional::CommitParser::builder()
            .scope_regex(config.scope_regex)
            .strip_regex(config.strip_regex)
            .build();
        let types: Vec<String> = config
            .types
            .iter()
            .map(|ty| ty.r#type.as_str())
            .map(String::from)
            .collect();

        let Config { merges, .. } = config;

        if self.from_stdin {
            let mut stdin = stdin().lock();
            let mut commit_msg = String::new();
            stdin.read_to_string(&mut commit_msg)?;
            if self.strip {
                commit_msg = commit_msg.strip();
            }
            let is_conventional = print_check(commit_msg.as_str(), "-", &parser, &types);
            match is_conventional {
                true => return Ok(()),
                false => return Err(Error::Check)?,
            }
        }

        let repo = Repository::open_from_env()?;
        let mut revwalk = repo.revwalk()?;
        if config.first_parent {
            revwalk.simplify_first_parent()?;
        }
        let rev = match self.rev.as_ref() {
            Some(rev) if !rev.is_empty() => rev.as_str(),
            _ => "HEAD",
        };

        if rev.contains("..") {
            revwalk.push_range(rev)?;
        } else {
            let oid = repo.revparse_single(rev)?.id();
            revwalk.push(oid)?;
        }

        for commit in revwalk
            .flatten()
            .flat_map(|oid| repo.find_commit(oid).ok())
            .filter(|commit| filter_merge_commits(commit, merges))
            .filter(|commit| filter_revert_commits(commit, self.ignore_reverts))
            .take(self.number.unwrap_or(std::usize::MAX))
        {
            total += 1;
            let msg = std::str::from_utf8(commit.message_bytes()).expect("valid utf-8 message");
            let short_id = commit.as_object().short_id().unwrap();
            let short_id = short_id.as_str().expect("short id");
            fail += u32::from(!print_check(msg, short_id, &parser, &types));
        }
        if fail == 0 {
            match total {
                0 => println!("no commits checked"),
                1 => println!("no errors in {} commit", total),
                _ => println!("no errors in {} commits", total),
            }
            Ok(())
        } else {
            println!("\n{}/{} failed", fail, total);
            Err(Error::Check)?
        }
    }
}