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.

use super::super::*;

use std::ascii::AsciiExt;

#[derive(Debug, Default)]
/// A check which denies commits which adds files containing special characters in their paths.
pub struct InvalidPaths {
    invalid_characters: String,
}

impl InvalidPaths {
    /// Create a check which rejects paths which contain invalid characters.
    ///
    /// In addition to whitespace and non-ASCII characters, the characters given here are also
    /// disallowed.
    pub fn new<S: ToString>(chars: S) -> Self {
        InvalidPaths {
            invalid_characters: chars.to_string(),
        }
    }

    fn has_invalid_chars(&self, chars: &str) -> bool {
        chars.chars()
            .any(|ref c| {
                !c.is_ascii() || c.is_whitespace() || c.is_control() ||
                self.invalid_characters.chars().any(|ref ic| ic == c)
            })
    }

    fn has_invalid_bytes(&self, bytes: &[u8]) -> bool {
        bytes.iter()
            .any(|&c| !c.is_ascii() || c < 32)
    }
}

impl Check for InvalidPaths {
    fn name(&self) -> &str {
        "invalid-paths"
    }

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

        for diff in &commit.diffs {
            if let StatusChange::Added = diff.status {
                let have_bad_char = self.has_invalid_chars(diff.name.as_str());
                let have_bad_bytes = self.has_invalid_bytes(diff.name.as_bytes());

                if have_bad_char || have_bad_bytes {
                    result.add_error(format!("commit {} adds the `{}` path which contains at \
                                              least one forbidden character: \
                                              `<non-ASCII><whitespace><control>{}`.",
                                             commit.sha1_short,
                                             diff.name,
                                             self.invalid_characters));
                }
            }
        }

        Ok(result)
    }
}

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

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

    #[test]
    fn test_invalid_paths() {
        let check = InvalidPaths::new("$");
        let mut conf = GitCheckConfiguration::new();

        conf.add_check(&check);

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

        assert_eq!(result.warnings().len(), 0);
        assert_eq!(result.alerts().len(), 0);
        assert_eq!(result.errors().len(), 5);
        assert_eq!(result.errors()[0],
                   "commit f536f44 adds the `\"control-character-\\003\"` path which contains \
                    at least one forbidden character: `<non-ASCII><whitespace><control>$`.");
        assert_eq!(result.errors()[1],
                   "commit f536f44 adds the `\"invalid-utf8-\\200\"` path which contains at \
                    least one forbidden character: `<non-ASCII><whitespace><control>$`.");
        assert_eq!(result.errors()[2],
                   "commit f536f44 adds the `\"non-ascii-\\303\\251\"` path which contains at \
                    least one forbidden character: `<non-ASCII><whitespace><control>$`.");
        assert_eq!(result.errors()[3],
                   "commit f536f44 adds the `with whitespace` path which contains at least one \
                    forbidden character: `<non-ASCII><whitespace><control>$`.");
        assert_eq!(result.errors()[4],
                   "commit f536f44 adds the `with-dollar-$` path which contains at least one \
                    forbidden character: `<non-ASCII><whitespace><control>$`.");
        assert_eq!(result.allowed(), false);
        assert_eq!(result.pass(), false);
    }

    #[test]
    fn test_invalid_paths_default() {
        let check = InvalidPaths::default();
        let mut conf = GitCheckConfiguration::new();

        conf.add_check(&check);

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

        assert_eq!(result.warnings().len(), 0);
        assert_eq!(result.alerts().len(), 0);
        assert_eq!(result.errors().len(), 4);
        assert_eq!(result.errors()[0],
                   "commit f536f44 adds the `\"control-character-\\003\"` path which contains \
                    at least one forbidden character: `<non-ASCII><whitespace><control>`.");
        assert_eq!(result.errors()[1],
                   "commit f536f44 adds the `\"invalid-utf8-\\200\"` path which contains at \
                    least one forbidden character: `<non-ASCII><whitespace><control>`.");
        assert_eq!(result.errors()[2],
                   "commit f536f44 adds the `\"non-ascii-\\303\\251\"` path which contains at \
                    least one forbidden character: `<non-ASCII><whitespace><control>`.");
        assert_eq!(result.errors()[3],
                   "commit f536f44 adds the `with whitespace` path which contains at least one \
                    forbidden character: `<non-ASCII><whitespace><control>`.");
        assert_eq!(result.allowed(), false);
        assert_eq!(result.pass(), false);
    }
}