lintje 0.9.0

Lintje is an opinionated linter for Git.
use std::fmt;

use crate::branch::Branch;
use crate::commit::Commit;
use crate::issue::Issue;
use crate::rules::*;

const BASE_URL: &str = "https://lintje.dev/docs/";

#[derive(Debug, PartialEq)]
pub enum Rule {
    MergeCommit,
    RebaseCommit,
    SubjectLength,
    SubjectMood,
    SubjectWhitespace,
    SubjectCapitalization,
    SubjectPunctuation,
    SubjectTicketNumber,
    SubjectPrefix,
    SubjectBuildTag,
    SubjectCliche,
    MessageEmptyFirstLine,
    MessagePresence,
    MessageLineLength,
    MessageSkipBuildTag,
    MessageTicketNumber,
    DiffChangeset,
    DiffPresence,
    BranchNameTicketNumber,
    BranchNameLength,
    BranchNamePunctuation,
    BranchNameCliche,
}

impl fmt::Display for Rule {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let label = match self {
            Rule::MergeCommit => "MergeCommit",
            Rule::RebaseCommit => "RebaseCommit",
            Rule::SubjectLength => "SubjectLength",
            Rule::SubjectMood => "SubjectMood",
            Rule::SubjectWhitespace => "SubjectWhitespace",
            Rule::SubjectCapitalization => "SubjectCapitalization",
            Rule::SubjectPunctuation => "SubjectPunctuation",
            Rule::SubjectTicketNumber => "SubjectTicketNumber",
            Rule::SubjectPrefix => "SubjectPrefix",
            Rule::SubjectBuildTag => "SubjectBuildTag",
            Rule::SubjectCliche => "SubjectCliche",
            Rule::MessageEmptyFirstLine => "MessageEmptyFirstLine",
            Rule::MessagePresence => "MessagePresence",
            Rule::MessageLineLength => "MessageLineLength",
            Rule::MessageSkipBuildTag => "MessageSkipBuildTag",
            Rule::MessageTicketNumber => "MessageTicketNumber",
            Rule::DiffChangeset => "DiffChangeset",
            Rule::DiffPresence => "DiffPresence",
            Rule::BranchNameTicketNumber => "BranchNameTicketNumber",
            Rule::BranchNameLength => "BranchNameLength",
            Rule::BranchNamePunctuation => "BranchNamePunctuation",
            Rule::BranchNameCliche => "BranchNameCliche",
        };
        write!(f, "{}", label)
    }
}

impl Rule {
    pub fn validate_commit(&self, commit: &Commit) -> Option<Vec<Issue>> {
        let rule_validator: Box<dyn RuleValidator<Commit>> = match self {
            Rule::MergeCommit => Box::new(MergeCommit::new()),
            Rule::RebaseCommit => Box::new(RebaseCommit::new()),
            Rule::SubjectLength => Box::new(SubjectLength::new()),
            Rule::SubjectMood => Box::new(SubjectMood::new()),
            Rule::SubjectWhitespace => Box::new(SubjectWhitespace::new()),
            Rule::SubjectCapitalization => Box::new(SubjectCapitalization::new()),
            Rule::SubjectPunctuation => Box::new(SubjectPunctuation::new()),
            Rule::SubjectTicketNumber => Box::new(SubjectTicketNumber::new()),
            Rule::SubjectPrefix => Box::new(SubjectPrefix::new()),
            Rule::SubjectBuildTag => Box::new(SubjectBuildTag::new()),
            Rule::SubjectCliche => Box::new(SubjectCliche::new()),
            Rule::MessagePresence => Box::new(MessagePresence::new()),
            Rule::MessageEmptyFirstLine => Box::new(MessageEmptyFirstLine::new()),
            Rule::MessageLineLength => Box::new(MessageLineLength::new()),
            Rule::MessageSkipBuildTag => Box::new(MessageSkipBuildTag::new()),
            Rule::MessageTicketNumber => Box::new(MessageTicketNumber::new()),
            Rule::DiffChangeset => Box::new(DiffChangeset::new()),
            Rule::DiffPresence => Box::new(DiffPresence::new()),
            Rule::BranchNameTicketNumber
            | Rule::BranchNameLength
            | Rule::BranchNamePunctuation
            | Rule::BranchNameCliche => {
                panic!("Unknown rule for commit validation: {}", self)
            }
        };
        rule_validator.validate(commit)
    }

    pub fn validate_branch(&self, branch: &Branch) -> Option<Vec<Issue>> {
        let rule_validator: Box<dyn RuleValidator<Branch>> = match self {
            Rule::MergeCommit
            | Rule::RebaseCommit
            | Rule::SubjectLength
            | Rule::SubjectMood
            | Rule::SubjectWhitespace
            | Rule::SubjectCapitalization
            | Rule::SubjectPunctuation
            | Rule::SubjectTicketNumber
            | Rule::SubjectPrefix
            | Rule::SubjectBuildTag
            | Rule::SubjectCliche
            | Rule::MessagePresence
            | Rule::MessageEmptyFirstLine
            | Rule::MessageLineLength
            | Rule::MessageSkipBuildTag
            | Rule::MessageTicketNumber
            | Rule::DiffChangeset
            | Rule::DiffPresence => panic!("Unknown rule for branch validation: {}", self),
            Rule::BranchNameLength => Box::new(BranchNameLength::new()),
            Rule::BranchNameTicketNumber => Box::new(BranchNameTicketNumber::new()),
            Rule::BranchNamePunctuation => Box::new(BranchNamePunctuation::new()),
            Rule::BranchNameCliche => Box::new(BranchNameCliche::new()),
        };
        rule_validator.validate(branch)
    }

    pub fn link(&self) -> String {
        let path = match self {
            Rule::MergeCommit => "commit-type/#mergecommit",
            Rule::RebaseCommit => "commit-type/#rebasecommit",
            Rule::SubjectLength => "commit-subject/#subjectlength",
            Rule::SubjectMood => "commit-subject/#subjectmood",
            Rule::SubjectWhitespace => "commit-subject/#subjectwhitespace",
            Rule::SubjectCapitalization => "commit-subject/#subjectcapitalization",
            Rule::SubjectPunctuation => "commit-subject/#subjectpunctuation",
            Rule::SubjectTicketNumber => "commit-subject/#subjectticketnumber",
            Rule::SubjectPrefix => "commit-subject/#subjectprefix",
            Rule::SubjectBuildTag => "commit-subject/#subjectbuildtag",
            Rule::SubjectCliche => "commit-subject/#subjectcliche",
            Rule::MessageEmptyFirstLine => "commit-message/#messageemptyfirstline",
            Rule::MessagePresence => "commit-message/#messagepresence",
            Rule::MessageLineLength => "commit-message/#messagelinelength",
            Rule::MessageSkipBuildTag => "commit-messsage/#messageskipbuildtag",
            Rule::MessageTicketNumber => "commit-message/#messageticketnumber",
            Rule::DiffChangeset => "commit-type/#diffchangeset",
            Rule::DiffPresence => "commit-type/#diffpresence",
            Rule::BranchNameTicketNumber => "branch/#branchnameticketnumber",
            Rule::BranchNameLength => "branch/#branchnamelength",
            Rule::BranchNamePunctuation => "branch/#branchnamepunctuation",
            Rule::BranchNameCliche => "branch/#branchnamecliche",
        };
        format!("{}rules/{}", BASE_URL, path)
    }
}

pub trait RuleValidator<T> {
    fn validate(&self, commit: &T) -> Option<Vec<Issue>>;
}

pub fn rule_by_name(name: &str) -> Option<Rule> {
    match name {
        "MergeCommit" => Some(Rule::MergeCommit),
        "RebaseCommit" | "NeedsRebase" => Some(Rule::RebaseCommit),
        "SubjectLength" => Some(Rule::SubjectLength),
        "SubjectMood" => Some(Rule::SubjectMood),
        "SubjectWhitespace" => Some(Rule::SubjectWhitespace),
        "SubjectCapitalization" => Some(Rule::SubjectCapitalization),
        "SubjectPunctuation" => Some(Rule::SubjectPunctuation),
        "SubjectTicketNumber" => Some(Rule::SubjectTicketNumber),
        "SubjectBuildTag" => Some(Rule::SubjectBuildTag),
        "SubjectPrefix" => Some(Rule::SubjectPrefix),
        "SubjectCliche" => Some(Rule::SubjectCliche),
        "MessageEmptyFirstLine" => Some(Rule::MessageEmptyFirstLine),
        "MessagePresence" => Some(Rule::MessagePresence),
        "MessageLineLength" => Some(Rule::MessageLineLength),
        "MessageSkipBuildTag" => Some(Rule::MessageSkipBuildTag),
        "MessageTicketNumber" => Some(Rule::MessageTicketNumber),
        "DiffChangeset" => Some(Rule::DiffChangeset),
        "DiffPresence" => Some(Rule::DiffPresence),
        _ => None,
    }
}

#[cfg(test)]
mod tests {
    use super::Rule;

    #[test]
    fn link_to_docs() {
        assert_eq!(
            Rule::SubjectLength.link(),
            "https://lintje.dev/docs/rules/commit-subject/#subjectlength"
        );
    }
}