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 itertools;
use self::itertools::Itertools;

use super::super::*;

const CR_LF_ENDING: &'static str = "\r\n";
const CARRIAGE_RETURN_SYMBOL: &'static str = "\u{23ce}";

#[derive(Debug, Default, Clone, Copy)]
/// Checks for bad whitespace using Git's built-in checks. This is attribute-driven, so any
/// `gitattributes(5)` files may be used to suppress spirious errors from this check.
pub struct CheckWhitespace;

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

impl Check for CheckWhitespace {
    fn name(&self) -> &str {
        "check-whitespace"
    }

    fn check(&self, ctx: &CheckGitContext, commit: &Commit) -> Result<CheckResult> {
        let mut result = CheckResult::new();

        let diff_tree = try!(ctx.git()
            .arg("diff-tree")
            .arg("--no-commit-id")
            .arg("--root")
            .arg("-c")
            .arg("--check")
            .arg(commit.sha1.as_str())
            .output()
            .chain_err(|| "failed to construct diff-tree command"));
        if !diff_tree.status.success() {
            // Check for CR/LF line endings. This is done because most editors will mask their
            // existence making the "trailing whitespace" hard to find.
            let output = String::from_utf8_lossy(&diff_tree.stdout);
            let crlf_msg = if output.contains(CR_LF_ENDING) {
                " including CR/LF line endings"
            } else {
                ""
            };
            let formatted_output = output.split('\n')
                // Git seems to add a trailing newline to its output, so drop the last line.
                .dropping_back(1)
                .map(|line| format!("        {}\n", line))
                .join("")
                .replace('\r', CARRIAGE_RETURN_SYMBOL);

            result.add_error(format!("commit {} adds bad whitespace{}:\n\n{}",
                                     commit.sha1_short,
                                     crlf_msg,
                                     formatted_output));
        }

        Ok(result)
    }
}

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

    static DEFAULT_TOPIC: &'static str = "829cdf8cb069b8f8a634a034d3f85089271601cf";
    static ALL_IGNORED_TOPIC: &'static str = "3a87e0f3f7430bbb81ebbd8ae8764b7f26384f1c";
    static ALL_IGNORED_BLANKET_TOPIC: &'static str = "92cac7579a26f7d8449512476bd64b3000688fd5";

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

        conf.add_check(&check);

        let result = test_check("test_check_whitespace_defaults", DEFAULT_TOPIC, &conf);

        assert_eq!(result.warnings().len(), 0);
        assert_eq!(result.alerts().len(), 0);
        assert_eq!(result.errors().len(), 1);
        assert_eq!(&result.errors()[0],
                   "commit 829cdf8 adds bad whitespace including CR/LF line endings:\n\
                    \n        \
                    crlf-file:1: trailing whitespace.\n        \
                    +This file contains CRLF lines.\u{23ce}\n        \
                    crlf-file:2: trailing whitespace.\n        \
                    +\u{23ce}\n        \
                    crlf-file:3: trailing whitespace.\n        \
                    +line1\u{23ce}\n        \
                    crlf-file:4: trailing whitespace.\n        \
                    +line2\u{23ce}\n        \
                    crlf-mixed-file:3: trailing whitespace.\n        \
                    +crlf\u{23ce}\n        \
                    extra-newlines:2: new blank line at EOF.\n        \
                    mixed-tabs-spaces:3: space before tab in indent.\n        \
                    +   \tmixed indent\n        \
                    trailing-spaces:3: trailing whitespace.\n        \
                    +trailing \n        \
                    trailing-tab:3: trailing whitespace.\n        \
                    +trailing\t\n");
        assert_eq!(result.allowed(), false);
        assert_eq!(result.pass(), false);
    }

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

        conf.add_check(&check);

        let result = test_check("test_check_whitespace_all_ignored",
                                ALL_IGNORED_TOPIC,
                                &conf);

        assert_eq!(result.warnings().len(), 0);
        assert_eq!(result.alerts().len(), 0);
        assert_eq!(result.errors().len(), 0);
        assert_eq!(result.allowed(), false);
        assert_eq!(result.pass(), true);
    }

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

        conf.add_check(&check);

        let result = test_check("test_check_whitespace_all_ignored_blanket",
                                ALL_IGNORED_BLANKET_TOPIC,
                                &conf);

        assert_eq!(result.warnings().len(), 0);
        assert_eq!(result.alerts().len(), 0);
        assert_eq!(result.errors().len(), 0);
        assert_eq!(result.allowed(), false);
        assert_eq!(result.pass(), true);
    }
}