git-checks 4.0.1

Checks to run against a topic in git to enforce coding standards.
Documentation
// Copyright 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 crates::git_checks_core::impl_prelude::*;

/// A check which denies commits which modify files underneath certain path.
#[derive(Builder, Debug, Clone)]
#[builder(field(private))]
pub struct RestrictedPath {
    /// The path which may not be edited.
    ///
    /// Configuration: Required
    #[builder(setter(into))]
    path: String,
    /// Whether the check is an error or a warning.
    ///
    /// Configuration: Optional
    /// Default: `true`
    #[builder(default = "true")]
    required: bool,
}

impl RestrictedPath {
    /// Create a new builder.
    pub fn builder() -> RestrictedPathBuilder {
        RestrictedPathBuilder::default()
    }
}

impl ContentCheck for RestrictedPath {
    fn name(&self) -> &str {
        "restricted-path"
    }

    fn check(
        &self,
        _: &CheckGitContext,
        content: &dyn Content,
    ) -> Result<CheckResult, Box<dyn Error>> {
        let mut result = CheckResult::new();

        let is_restricted = content
            .diffs()
            .iter()
            .map(|diff| diff.name.as_path())
            .any(|path| path.starts_with(&self.path));

        if is_restricted {
            if self.required {
                result.add_error(format!(
                    "{}the `{}` path is restricted.",
                    commit_prefix_str(content, "not allowed;"),
                    self.path,
                ));
            } else {
                result.add_warning(format!(
                    "{}the `{}` path is restricted.",
                    commit_prefix_str(content, "should be inspected;"),
                    self.path,
                ));
            };
        }

        Ok(result)
    }
}

#[cfg(feature = "config")]
pub(crate) mod config {
    use crates::git_checks_config::{CommitCheckConfig, IntoCheck, TopicCheckConfig};
    use crates::inventory;
    #[cfg(test)]
    use crates::serde_json;

    #[cfg(test)]
    use test;
    use RestrictedPath;

    /// Configuration for the `RestrictedPath` check.
    ///
    /// The `restricted_path` key is a string with the path to the content which should be watched.
    /// The `required` key is a boolean which defaults to `true` which indicates whether modifying
    /// the path is an error or a warning.
    ///
    /// This check is registered as a commit check with the name `"restricted_path"` and as a topic
    /// check with the name `"restricted_path/topic"`.
    ///
    /// # Example
    ///
    /// ```json
    /// {
    ///     "restricted_path": "path/to/restricted/content",
    ///     "required": false
    /// }
    /// ```
    #[derive(Deserialize, Debug)]
    pub struct RestrictedPathConfig {
        path: String,
        #[serde(default)]
        required: Option<bool>,
    }

    impl IntoCheck for RestrictedPathConfig {
        type Check = RestrictedPath;

        fn into_check(self) -> Self::Check {
            let mut builder = RestrictedPath::builder();

            builder.path(self.path);

            if let Some(required) = self.required {
                builder.required(required);
            }

            builder
                .build()
                .expect("configuration mismatch for `RestrictedPath`")
        }
    }

    register_checks! {
        RestrictedPathConfig {
            "restricted_path" => CommitCheckConfig,
            "restricted_path/topic" => TopicCheckConfig,
        },
    }

    #[test]
    fn test_restricted_path_config_empty() {
        let json = json!({});
        let err = serde_json::from_value::<RestrictedPathConfig>(json).unwrap_err();
        test::check_missing_json_field(err, "path");
    }

    #[test]
    fn test_restricted_path_config_minimum_fields() {
        let exp_restricted_path = "path/to/restricted/content";
        let json = json!({
            "path": exp_restricted_path,
        });
        let check: RestrictedPathConfig = serde_json::from_value(json).unwrap();

        assert_eq!(check.path, exp_restricted_path);
        assert_eq!(check.required, None);
    }

    #[test]
    fn test_restricted_path_config_all_fields() {
        let exp_restricted_path = "path/to/restricted/content";
        let json = json!({
            "path": exp_restricted_path,
            "required": false,
        });
        let check: RestrictedPathConfig = serde_json::from_value(json).unwrap();

        assert_eq!(check.path, exp_restricted_path);
        assert_eq!(check.required, Some(false));
    }
}

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

    const BAD_TOPIC: &str = "e845fa2521c17bdd31d5891c1c644fb17f0629db";
    const FIX_TOPIC: &str = "d8a2f22943cdcca373f00892a23b85f3a6ba1196";

    #[test]
    fn test_restricted_path_builder_default() {
        assert!(RestrictedPath::builder().build().is_err());
    }

    #[test]
    fn test_restricted_path_builder_minimum_fields() {
        assert!(RestrictedPath::builder().path("restricted").build().is_ok());
    }

    #[test]
    fn test_restricted_path() {
        let check = RestrictedPath::builder()
            .path("restricted")
            .build()
            .unwrap();
        let result = run_check("test_restricted_path", BAD_TOPIC, check);
        test_result_errors(result, &[
            "commit e845fa2521c17bdd31d5891c1c644fb17f0629db not allowed; the `restricted` path \
             is restricted.",
        ]);
    }

    #[test]
    fn test_restricted_path_topic() {
        let check = RestrictedPath::builder()
            .path("restricted")
            .build()
            .unwrap();
        let result = run_topic_check("test_restricted_path_topic", BAD_TOPIC, check);
        test_result_errors(result, &["the `restricted` path is restricted."]);
    }

    #[test]
    fn test_restricted_path_warning() {
        let check = RestrictedPath::builder()
            .path("restricted")
            .required(false)
            .build()
            .unwrap();
        let result = run_check("test_restricted_path_warning", BAD_TOPIC, check);
        test_result_warnings(
            result,
            &[
                "commit e845fa2521c17bdd31d5891c1c644fb17f0629db should be inspected; the \
                 `restricted` path is restricted.",
            ],
        );
    }

    #[test]
    fn test_restricted_path_warning_topic() {
        let check = RestrictedPath::builder()
            .path("restricted")
            .required(false)
            .build()
            .unwrap();
        let result = run_topic_check("test_restricted_path_warning_topic", BAD_TOPIC, check);
        test_result_warnings(result, &["the `restricted` path is restricted."]);
    }

    #[test]
    fn test_restricted_path_topic_fixed() {
        let check = RestrictedPath::builder()
            .path("restricted")
            .build()
            .unwrap();
        run_topic_check_ok("test_restricted_path_topic_fixed", FIX_TOPIC, check);
    }
}