git-checks 1.0.0

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.

extern crate git_workarea;
use self::git_workarea::{CommitId, GitContext, Identity};

extern crate rayon;
use self::rayon::prelude::*;

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

#[derive(Default)]
/// Configuration for checks to run against a repository.
pub struct GitCheckConfiguration<'a> {
    checks: Vec<&'a Check>,
    checks_branch: Vec<&'a BranchCheck>,
}

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

    /// Find refs that should be checked given a target branch and the topic names.
    pub fn list(&self, ctx: &GitContext, reason: &str, base_branch: &CommitId, topic: &CommitId)
                -> Result<Vec<CommitId>> {
        let (new_ref, base_ref) = try!(ctx.reserve_refs(&format!("check/{}", reason), &topic));
        let update_ref = try!(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 = try!(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 checks over all refs and collect results from the checks.
    fn _run(&self, ctx: &GitContext, refs: &[CommitId], topic_owner: &Identity)
            -> Result<CheckResult> {
        refs.par_iter()
            .map(|sha1| {
                let workarea = try!(ctx.prepare(&sha1));
                let check_ctx = CheckGitContext::new(workarea, topic_owner.clone());

                let commit = try!(Commit::new(ctx, sha1));

                Ok(self.checks
                    .par_iter()
                    .map(|check| {
                        debug!(target: "git-checks",
                               "running check {} on commit {}",
                               check.name(),
                               commit.sha1);

                        match check.check(&check_ctx, &commit) {
                            Ok(check_res) => check_res,
                            Err(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_short),
                                              true);
                                res
                            },
                        }
                    })
                    .reduce(CheckResult::new, CheckResult::combine))
            })
            .chain({
                refs.first()
                    .map(|head_commit| {
                        let workarea = try!(ctx.prepare(head_commit));
                        let check_ctx = CheckGitContext::new(workarea, topic_owner.clone());

                        Ok(self.checks_branch
                            .par_iter()
                            .map(|check| {
                                debug!(target: "git-checks", "running check {}", check.name());

                                match check.check(&check_ctx, head_commit) {
                                    Ok(check_res) => check_res,
                                    Err(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
                                    },
                                }
                            })
                            .reduce(CheckResult::new, CheckResult::combine))
                    })
            })
            .collect::<Vec<Result<_>>>()
            .into_iter()
            .collect::<Result<Vec<_>>>()
            .map(|check_results| {
                check_results.into_iter()
                    .fold(CheckResult::new(), CheckResult::combine)
            })
    }

    /// Run checks over all refs and collect results from the checks.
    pub fn run(&self, ctx: &GitContext, refs: &[CommitId], topic_owner: &Identity)
               -> Result<CheckResult> {
        self._run(ctx, refs, topic_owner)
    }

    /// 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<CheckResult>
        where R: AsRef<str>,
    {
        let refs = try!(self.list(ctx, reason.as_ref(), base_branch, topic));
        self._run(ctx, &refs, owner)
    }
}