git-checks 3.5.1

Checks to run against a topic in git to enforce coding standards.
Documentation
// Copyright 2016 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 crates::git_workarea::{CommitId, GitContext, Identity};
use crates::rayon::prelude::*;

use check::{BranchCheck, Check, CheckResult, TopicCheck};
use commit::{Commit, Topic};
use context::CheckGitContext;
use error::*;

use std::fmt::{self, Debug};
use std::iter;
use std::slice;

#[derive(Default)]
/// Configuration for checks to run against a repository.
pub struct GitCheckConfiguration<'a> {
    /// Checks to run on each commit.
    checks: Vec<&'a Check>,
    /// Checks to run on the branch as a whole.
    checks_branch: Vec<&'a BranchCheck>,
    /// Checks to run on the topic.
    checks_topic: Vec<&'a TopicCheck>,
}

#[derive(Debug)]
/// Results from checking a topic.
pub struct TopicCheckResult {
    /// The results for each commit in the topic.
    commit_results: Vec<(CommitId, CheckResult)>,
    /// The results for the branch as a whole.
    topic_result: CheckResult,
}

impl TopicCheckResult {
    /// The results for each commit checked.
    pub fn commit_results(&self) -> slice::Iter<(CommitId, CheckResult)> {
        self.commit_results.iter()
    }

    /// The results for the topic as a whole.
    pub fn topic_result(&self) -> &CheckResult {
        &self.topic_result
    }
}

impl From<TopicCheckResult> for CheckResult {
    fn from(res: TopicCheckResult) -> Self {
        res.commit_results
            .into_iter()
            .map(|(_, result)| result)
            .chain(iter::once(res.topic_result))
            .fold(Self::new(), Self::combine)
    }
}

impl<'a> GitCheckConfiguration<'a> {
    /// Create a new check configuration.
    pub fn new() -> Self {
        GitCheckConfiguration {
            checks: vec![],
            checks_branch: vec![],
            checks_topic: vec![],
        }
    }

    /// Add a check to be run on every commit.
    pub fn add_check(&mut self, check: &'a Check) -> &mut Self {
        self.checks.push(check);

        self
    }

    /// Add a check to be once for the entire branch.
    pub fn add_branch_check(&mut self, check: &'a BranchCheck) -> &mut Self {
        self.checks_branch.push(check);

        self
    }

    /// Add a check to be once for the entire topic.
    pub fn add_topic_check(&mut self, check: &'a TopicCheck) -> &mut Self {
        self.checks_topic.push(check);

        self
    }

    /// Find refs that should be checked given a target branch and the topic names.
    fn list(&self, ctx: &GitContext, reason: &str, base_branch: &CommitId, topic: &CommitId)
            -> Result<Vec<CommitId>> {
        let (new_ref, base_ref) = ctx.reserve_refs(&format!("check/{}", reason), topic)?;
        let update_ref = ctx.git()
            .arg("update-ref")
            .arg("-m").arg(reason)
            .arg(&base_ref)
            .arg(base_branch.as_str())
            .output()
            .chain_err(|| "failed to construct update-ref command")?;
        if !update_ref.status.success() {
            bail!(ErrorKind::Git(format!("failed to update the {} ref: {}",
                                         base_ref,
                                         String::from_utf8_lossy(&update_ref.stderr))));
        }

        let rev_list = ctx.git()
            .arg("rev-list")
            .arg(&new_ref)
            .arg(&format!("^{}", base_ref))
            .output()
            .chain_err(|| "failed to construct rev-list command")?;
        if !rev_list.status.success() {
            bail!(ErrorKind::Git(format!("failed to list all branch refs: {}",
                                         String::from_utf8_lossy(&rev_list.stderr))));
        }
        let refs = String::from_utf8_lossy(&rev_list.stdout);

        Ok(refs.lines().map(CommitId::new).collect())
    }

    /// Run a commit check over a commit.
    fn run_check(ctx: &CheckGitContext, check: &Check, commit: &Commit) -> CheckResult {
        debug!(target: "git-checks",
               "running check {} on commit {}",
               check.name(),
               commit.sha1);

        check.check(ctx, commit)
            .unwrap_or_else(|err| {
                error!(target: "git-checks",
                       "check {} failed on commit {}: {:?}",
                       check.name(),
                       commit.sha1,
                       err);

                let mut res = CheckResult::new();
                res.add_alert(format!("failed to run the {} check on commit {}",
                                      check.name(),
                                      commit.sha1),
                              true);
                res
            })
    }

    /// Run a branch check over a commit.
    fn run_branch_check(ctx: &CheckGitContext, check: &BranchCheck, commit: &CommitId)
                        -> CheckResult {
        debug!(target: "git-checks", "running check {}", check.name());

        check.check(ctx, commit)
            .unwrap_or_else(|err| {
                error!(target: "git-checks",
                       "branch check {}: {:?}",
                       check.name(),
                       err);

                let mut res = CheckResult::new();
                res.add_alert(format!("failed to run the {} branch check", check.name()),
                              true);
                res
            })
    }

    /// Run a topic check over a topic.
    fn run_topic_check(ctx: &CheckGitContext, check: &TopicCheck, topic: &Topic)
                       -> CheckResult {
        debug!(target: "git-checks", "running check {}", check.name());

        check.check(ctx, topic)
            .unwrap_or_else(|err| {
                error!(target: "git-checks",
                       "topic check {}: {:?}",
                       check.name(),
                       err);

                let mut res = CheckResult::new();
                res.add_alert(format!("failed to run the {} topic check", check.name()),
                              true);
                res
            })
    }

    /// Run checks over a given topic and collect results from the checks.
    fn run_topic_impl(&self, ctx: &GitContext, base: &CommitId, refs: Vec<CommitId>,
                      owner: &Identity)
                      -> Result<TopicCheckResult> {
        let topic_result = refs.first()
            .map_or_else(|| Ok(CheckResult::new()) as Result<_>, |head_commit| {
                let workarea = ctx.prepare(head_commit)?;
                let check_ctx = CheckGitContext::new(workarea, owner.clone());
                let topic = Topic::new(ctx, base, head_commit)?;

                Ok(self.checks_branch
                    .par_iter()
                    .map(|&check| Self::run_branch_check(&check_ctx, check, head_commit))
                    .chain(self.checks_topic
                        .par_iter()
                        .map(|&check| Self::run_topic_check(&check_ctx, check, &topic)))
                    .reduce(CheckResult::new, CheckResult::combine))
            })?;
        let commit_results = refs.into_par_iter()
            .map(|sha1| {
                self.run_commit(ctx, &sha1, owner)
                    .map(|result| (sha1, result))
            })
            .collect::<Vec<Result<_>>>()
            .into_iter()
            .collect::<Result<Vec<_>>>()?;

        Ok(TopicCheckResult {
            commit_results: commit_results,
            topic_result: topic_result,
        })
    }

    /// Run checks over a given topic.
    pub fn run_commit(&self, ctx: &GitContext, commit: &CommitId, owner: &Identity)
                      -> Result<CheckResult> {
        let workarea = ctx.prepare(commit)?;
        let check_ctx = CheckGitContext::new(workarea, owner.clone());

        let commit = Commit::new(ctx, commit)?;

        Ok(self.checks
            .par_iter()
            .map(|&check| Self::run_check(&check_ctx, check, &commit))
            .reduce(CheckResult::new, CheckResult::combine))
    }

    /// Run checks over a given topic and collect results from the checks.
    pub fn run_topic<R>(&self, ctx: &GitContext, reason: R, base_branch: &CommitId,
                        topic: &CommitId, owner: &Identity)
                        -> Result<TopicCheckResult>
        where R: AsRef<str>,
    {
        let refs = self.list(ctx, reason.as_ref(), base_branch, topic)?;
        self.run_topic_impl(ctx, base_branch, refs, owner)
    }
}

impl<'a> Debug for GitCheckConfiguration<'a> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f,
               "GitCheckConfiguration {{ {} commit checks, {} branch checks }}",
               self.checks.len(),
               self.checks_branch.len())
    }
}