git-checks-core 1.2.0

Checks to run against a topic in git to enforce coding standards.
Documentation
// Copyright Kitware, Inc.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.

use std::error::Error;
use std::fmt::Debug;

use git_workarea::CommitId;

use crate::commit::{Commit, Content, Topic};
use crate::context::CheckGitContext;

/// The results of a check.
#[derive(Debug, Default)]
pub struct CheckResult {
    /// The warnings from running checks.
    warnings: Vec<String>,
    /// The alerts from running checks.
    ///
    /// These are meant to be directed towards administrators of the repository.
    alerts: Vec<String>,
    /// The errors from running checks.
    ///
    /// Errors cause the checks to be in a "failed" state.
    errors: Vec<String>,

    /// Whether any messages may be temporary.
    temporary: bool,
    /// Whether errors should be ignored or not.
    allow: bool,
    /// Whether the checks succeeded or not.
    pass: bool,
}

/// The severity of a message.
pub enum Severity {
    /// The message is a warning.
    Warning,
    /// The message is an error.
    Error,
    /// The message should be brought to the attention of project maintainers.
    Alert {
        /// Whether the checks should fail due to the message or not.
        blocking: bool,
    },
}

impl CheckResult {
    /// Create a new results structure.
    pub fn new() -> Self {
        Self {
            warnings: Vec::new(),
            alerts: Vec::new(),
            errors: Vec::new(),

            temporary: false,
            allow: false,
            pass: true,
        }
    }

    /// Add a message to the result.
    pub fn add_message<S>(&mut self, severity: Severity, message: S) -> &mut Self
    where
        S: Into<String>,
    {
        match severity {
            Severity::Warning => &mut self.warnings,
            Severity::Error => {
                self.pass = false;
                &mut self.errors
            },
            Severity::Alert {
                blocking,
            } => {
                if blocking {
                    self.pass = false;
                }
                &mut self.alerts
            },
        }
        .push(message.into());

        self
    }

    /// Adds a warning message to the results.
    pub fn add_warning<S: Into<String>>(&mut self, warning: S) -> &mut Self {
        self.add_message(Severity::Warning, warning.into())
    }

    /// Adds an alert to the results.
    ///
    /// These messages should be brought to the attention of those maintaining the deployment of
    /// the checks.
    pub fn add_alert<S: Into<String>>(&mut self, alert: S, should_block: bool) -> &mut Self {
        self.add_message(
            Severity::Alert {
                blocking: should_block,
            },
            alert.into(),
        )
    }

    /// Adds a error message to the results.
    ///
    /// Also marks the checks as having failed.
    pub fn add_error<S: Into<String>>(&mut self, error: S) -> &mut Self {
        self.add_message(Severity::Error, error.into())
    }

    /// Indicates that there are messages which may be temporary.
    pub fn make_temporary(&mut self) -> &mut Self {
        self.temporary = true;

        self
    }

    /// Allows the checks to pass no matter what.
    pub fn whitelist(&mut self) -> &mut Self {
        self.allow = true;

        self
    }

    /// The warnings from the checks.
    pub fn warnings(&self) -> &Vec<String> {
        &self.warnings
    }

    /// The alerts from the checks.
    pub fn alerts(&self) -> &Vec<String> {
        &self.alerts
    }

    /// The errors from the checks.
    pub fn errors(&self) -> &Vec<String> {
        &self.errors
    }

    /// Whether there are temporary messages or not.
    pub fn temporary(&self) -> bool {
        self.temporary
    }

    /// Whether the checks will allow the commit no matter what.
    pub fn allowed(&self) -> bool {
        self.allow
    }

    /// Whether the checks passed or failed.
    pub fn pass(&self) -> bool {
        self.pass
    }

    /// Combine two results together.
    pub fn combine(self, other: Self) -> Self {
        Self {
            warnings: self
                .warnings
                .into_iter()
                .chain(other.warnings.into_iter())
                .collect(),
            alerts: self
                .alerts
                .into_iter()
                .chain(other.alerts.into_iter())
                .collect(),
            errors: self
                .errors
                .into_iter()
                .chain(other.errors.into_iter())
                .collect(),

            temporary: self.temporary || other.temporary,
            allow: self.allow || other.allow,
            pass: self.pass && other.pass,
        }
    }
}

/// Interface for checks which run for each commit.
pub trait Check: Debug + Send + Sync {
    /// The name of the check.
    fn name(&self) -> &str;

    /// Run the check.
    fn check(&self, ctx: &CheckGitContext, commit: &Commit) -> Result<CheckResult, Box<dyn Error>>;
}

/// Interface for checks which runs once for the entire branch.
///
/// This is intended for checks which do not need to check the content, but instead, look at
/// metadata such as the author or the topology of the branch.
pub trait BranchCheck: Debug + Send + Sync {
    /// The name of the check.
    fn name(&self) -> &str;

    /// Run the check.
    fn check(
        &self,
        ctx: &CheckGitContext,
        commit: &CommitId,
    ) -> Result<CheckResult, Box<dyn Error>>;
}

/// Interface for checks which runs once for the entire branch, but with access to the content.
///
/// These checks are given the content of the entire topic against a base branch to check.
pub trait TopicCheck: Debug + Send + Sync {
    /// The name of the check.
    fn name(&self) -> &str;

    /// Run the check.
    fn check(&self, ctx: &CheckGitContext, topic: &Topic) -> Result<CheckResult, Box<dyn Error>>;
}

/// Interface for checks which check the content of files.
///
/// These checks are not given any metadata, but only information about the content of a commit or
/// topic.
pub trait ContentCheck: Debug + Send + Sync {
    /// The name of the check.
    fn name(&self) -> &str;

    /// Run the check.
    fn check(
        &self,
        ctx: &CheckGitContext,
        content: &dyn Content,
    ) -> Result<CheckResult, Box<dyn Error>>;
}

impl<T> Check for T
where
    T: ContentCheck,
{
    fn name(&self) -> &str {
        self.name()
    }

    fn check(&self, ctx: &CheckGitContext, commit: &Commit) -> Result<CheckResult, Box<dyn Error>> {
        self.check(ctx, commit)
    }
}

impl<T> TopicCheck for T
where
    T: ContentCheck,
{
    fn name(&self) -> &str {
        self.name()
    }

    fn check(&self, ctx: &CheckGitContext, topic: &Topic) -> Result<CheckResult, Box<dyn Error>> {
        self.check(ctx, topic)
    }
}