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;

use super::super::*;

use std::process::Command;

#[derive(Debug, Default, Clone, Copy)]
/// A check which checks for valid identities.
///
/// This check uses the `host` external binary to check the validity of domain names used in email
/// addresses.
pub struct ValidName;

fn check_name(name: &str) -> bool {
    name.find(' ').is_some()
}

fn check_email(email: &str) -> bool {
    let domain_part = email.splitn(2, '@')
        .skip(1)
        .next();

    if let Some(domain) = domain_part {
        let dig = Command::new("host")
            .arg("-t").arg("MX")
            .arg(format!("{}.", domain)) // Search for the absolute domain.
            .output();
        let dig_output = match dig {
            Ok(dig_output) => dig_output,
            Err(err) => {
                error!(target: "git-checks",
                       "failed to construct host command: {:?}",
                       err);

                return false;
            },
        };

        if !dig_output.status.success() {
            warn!(target: "git-checks",
                  "failed to look up MX record for domain {}: {}",
                  domain,
                  // The `host` tool always outputs to stdout
                  String::from_utf8_lossy(&dig_output.stdout));

            false
        } else {
            true
        }
    } else {
        false
    }
}

fn check_identity(what: &str, who: &str, identity: &Identity) -> CheckResult {
    let mut result = CheckResult::new();

    if !check_name(&identity.name) {
        result.add_error(format!("The {} name (`{}`) for {} has no space in it. \
                                  A full name is required for contribution. Please set the \
                                  `user.name` Git configuration value.",
                                 who,
                                 identity.name,
                                 what));
    }

    if !check_email(&identity.email) {
        result.add_error(format!("The {} email (`{}`) for {} has an unknown domain. Please set \
                                  the `user.email` Git configuration value.",
                                 who,
                                 identity.email,
                                 what));
    }

    result
}

impl ValidName {
    /// Create a new check to check identities.
    pub fn new() -> Self {
        ValidName {}
    }
}

impl Check for ValidName {
    fn name(&self) -> &str {
        "valid-name"
    }

    fn check(&self, _: &CheckGitContext, commit: &Commit) -> Result<CheckResult> {
        let what = format!("commit {}", commit.sha1_short);
        let author_res = check_identity(&what, "author", &commit.author);
        let commiter_res = check_identity(&what, "committer", &commit.committer);

        Ok(author_res.combine(commiter_res))
    }
}

impl BranchCheck for ValidName {
    fn name(&self) -> &str {
        "valid-name"
    }

    fn check(&self, ctx: &CheckGitContext, _: &CommitId) -> Result<CheckResult> {
        Ok(check_identity("the topic", "owner", ctx.topic_owner()))
    }
}

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

    static BAD_TOPIC: &'static str = "dcd8895d299031d607481b4936478f8de4cc28ae";

    #[test]
    fn test_valid_name() {
        let check = ValidName::new();
        let mut conf = GitCheckConfiguration::new();

        conf.add_check(&check);

        let result = test_check("test_valid_name", BAD_TOPIC, &conf);

        assert_eq!(result.warnings().len(), 0);
        assert_eq!(result.alerts().len(), 0);
        assert_eq!(result.errors().len(), 6);
        assert_eq!(result.errors()[0],
                   "The committer email (`bademail@baddomain.invalid`) for commit dcd8895 has an \
                   unknown domain. Please set the `user.email` Git configuration value.");
        assert_eq!(result.errors()[1],
                   "The author email (`bademail@baddomain.invalid`) for commit 9002239 has an \
                   unknown domain. Please set the `user.email` Git configuration value.");
        assert_eq!(result.errors()[2],
                   "The committer email (`bademail`) for commit da71ae0 has an unknown domain. \
                    Please set the `user.email` Git configuration value.");
        assert_eq!(result.errors()[3],
                   "The committer name (`Mononym`) for commit 1debf17 has no space in it. A full \
                    name is required for contribution. Please set the `user.name` Git \
                    configuration value.");
        assert_eq!(result.errors()[4],
                   "The author email (`bademail`) for commit 9de4928 has an unknown domain. \
                    Please set the `user.email` Git configuration value.");
        assert_eq!(result.errors()[5],
                   "The author name (`Mononym`) for commit edac4e5 has no space in it. A full \
                    name is required for contribution. Please set the `user.name` Git \
                    configuration value.");
        assert_eq!(result.allowed(), false);
        assert_eq!(result.pass(), false);
    }
}