git-checks 3.2.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.

use impl_prelude::*;

use std::process::Command;

#[derive(Debug, Clone, Copy)]
/// Configuration value for `ValidName` policy for use of full names in identities.
pub enum ValidNameFullNamePolicy {
    /// A full name is required, error when missing.
    Required,
    /// A full name is preferred, warning when missing.
    Preferred,
    /// A full name is optional, no diagnostic when missing.
    Optional,
}

impl Default for ValidNameFullNamePolicy {
    fn default() -> Self {
        ValidNameFullNamePolicy::Required
    }
}

impl ValidNameFullNamePolicy {
    /// Apply the policy to a check result.
    fn apply<F>(&self, result: &mut CheckResult, msg: F)
        where F: Fn(&str) -> String,
    {
        match *self {
            ValidNameFullNamePolicy::Required => {
                result.add_error(msg("required"));
            },
            ValidNameFullNamePolicy::Preferred => {
                result.add_warning(msg("preferred"));
            },
            ValidNameFullNamePolicy::Optional => {},
        }
    }
}

#[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.
///
/// The check can be configured with a policy on how to enforce use of full names.
pub struct ValidName {
    /// The policy for names in commits.
    full_name_policy: ValidNameFullNamePolicy,
}

/// Check that a name is valid.
fn check_name(name: &str) -> bool {
    name.find(' ').is_some()
}

/// Check that an email address is valid.
fn check_email(email: &str) -> bool {
    let domain_part = email.splitn(2, '@')
        .nth(1);

    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/valid_name",
                       "failed to construct host command: {:?}",
                       err);

                return false;
            },
        };

        if dig_output.status.success() {
            true
        } else {
            warn!(target: "git-checks/valid_name",
                  "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 {
        false
    }
}

/// Check an identity for its validity.
fn check_identity(what: &str, who: &str, identity: &Identity,
                  full_name_policy: ValidNameFullNamePolicy)
                  -> CheckResult {
    let mut result = CheckResult::new();

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

    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 {
        Default::default()
    }

    /// Set policy for enforcement of use of full names.
    pub fn set_full_name_policy(&mut self, policy: ValidNameFullNamePolicy) -> &mut Self {
        self.full_name_policy = policy;
        self
    }
}

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

    fn check(&self, _: &CheckGitContext, commit: &Commit) -> Result<CheckResult> {
        let what = format!("commit {}", commit.sha1);

        Ok(if commit.author == commit.committer {
            check_identity(&what, "given", &commit.author, self.full_name_policy)
        } else {
            let author_res = check_identity(&what, "author", &commit.author, self.full_name_policy);
            let commiter_res =
                check_identity(&what, "committer", &commit.committer, self.full_name_policy);

            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(),
                          self.full_name_policy))
    }
}

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

    static BAD_TOPIC: &'static str = "91d9fceb226bfc0faeb8a4e54b4f0b5a1ffd39e8";
    static BAD_AUTHOR_NAME: &'static str = "edac4e5b3a00eac60280a78ee84b5ef8d4cce97a";

    #[test]
    fn test_valid_name_required() {
        let check = ValidName::new();
        let result = run_check("test_valid_name_required", BAD_TOPIC, check);
        test_result_errors(result, &[
            "The given name (`Mononym`) for commit 91d9fceb226bfc0faeb8a4e54b4f0b5a1ffd39e8 has \
             no space in it. A full name is required for contribution. Please set the `user.name` \
             Git configuration value.",
            "The given email (`bademail`) for commit 91d9fceb226bfc0faeb8a4e54b4f0b5a1ffd39e8 has \
             an unknown domain. Please set the `user.email` Git configuration value.",
            "The committer email (`bademail@baddomain.invalid`) for commit \
             dcd8895d299031d607481b4936478f8de4cc28ae has an unknown domain. Please set the \
            `user.email` Git configuration value.",
            "The author email (`bademail@baddomain.invalid`) for commit \
             9002239437a06e81a58fed07150b215a917028d6 has an unknown domain. Please set the \
            `user.email` Git configuration value.",
            "The committer email (`bademail`) for commit da71ae048e5a387d6809558d59ad073d0e4fb089 \
             has an unknown domain. Please set the `user.email` Git configuration value.",
            "The committer name (`Mononym`) for commit 1debf1735a6e28880ef08f13baeea4b71a08a846 \
             has no space in it. A full name is required for contribution. Please set the \
             `user.name` Git configuration value.",
            "The author email (`bademail`) for commit 9de4928f5ec425eef414ee7620d0692fda56ebb0 \
             has an unknown domain. Please set the `user.email` Git configuration value.",
            "The author name (`Mononym`) for commit edac4e5b3a00eac60280a78ee84b5ef8d4cce97a has \
             no space in it. A full name is required for contribution. Please set the `user.name` \
             Git configuration value.",
        ]);
    }

    #[test]
    fn test_valid_name_preferred() {
        let mut check = ValidName::new();
        check.set_full_name_policy(ValidNameFullNamePolicy::Preferred);
        let result = run_check("test_valid_name_preferred", BAD_AUTHOR_NAME, check);
        test_result_warnings(result, &[
            "The author name (`Mononym`) for commit edac4e5b3a00eac60280a78ee84b5ef8d4cce97a has \
             no space in it. A full name is preferred for contribution. Please set the \
             `user.name` Git configuration value.",
        ]);
    }

    #[test]
    fn test_valid_name_optional() {
        let mut check = ValidName::new();
        check.set_full_name_policy(ValidNameFullNamePolicy::Optional);
        run_check_ok("test_valid_name_optional", BAD_AUTHOR_NAME, check);
    }
}