git-checks 4.2.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 derive_builder::Builder;
use git_checks_core::impl_prelude::*;

/// Check that submodules are reachable from a given branch and available.
#[derive(Builder, Debug, Default, Clone, Copy)]
#[builder(field(private))]
pub struct SubmoduleWatch {
    /// Whether to reject new submodules or not.
    ///
    /// Configuration: Optional
    /// Default: `false`
    #[builder(default = "false")]
    reject_additions: bool,
    /// Whether to reject deletion of submodules or not.
    ///
    /// Configuration: Optional
    /// Default: `false`
    #[builder(default = "false")]
    reject_removals: bool,
}

impl SubmoduleWatch {
    /// Checks that submodules in the project are available.
    pub fn builder() -> SubmoduleWatchBuilder {
        SubmoduleWatchBuilder::default()
    }
}

impl Check for SubmoduleWatch {
    fn name(&self) -> &str {
        "submodule-watch"
    }

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

        for diff in &commit.diffs {
            let added = diff.new_mode == "160000";
            let removed = diff.old_mode == "160000";

            // Ignore diffs which are not submodules.
            if !added && !removed {
                continue;
            }

            let is_configured = SubmoduleContext::new(ctx, diff.name.as_ref()).is_some();

            // Ignore changes which don't change the submodule status.
            if added && removed {
                // However, check if the submodule is not configured and warn if it isn't.
                if !is_configured {
                    result.add_warning(format!(
                        "commit {} modifies an unconfigured submodule at `{}`.",
                        commit.sha1, diff.name,
                    ));

                    // Configuring the submodule can resolve the problem.
                    result.make_temporary();
                }

                continue;
            }

            if added && !is_configured {
                if self.reject_additions {
                    result.add_error(format!(
                        "commit {} adds a submodule at `{}` which is not allowed.",
                        commit.sha1, diff.name,
                    ));
                } else {
                    result.add_alert(
                        format!(
                            "commit {} adds a submodule at `{}`.",
                            commit.sha1, diff.name,
                        ),
                        false,
                    );
                }

                // Configuring the submodule can resolve the problem.
                result.make_temporary();
            }

            if removed {
                if self.reject_removals {
                    result.add_error(format!(
                        "commit {} removes the submodule at `{}` which is not allowed.",
                        commit.sha1, diff.name,
                    ));
                } else {
                    result.add_alert(
                        format!(
                            "commit {} removes the submodule at `{}`.",
                            commit.sha1, diff.name,
                        ),
                        false,
                    );
                }
            }
        }

        Ok(result)
    }
}

#[cfg(feature = "config")]
pub(crate) mod config {
    use git_checks_config::{register_checks, CommitCheckConfig, IntoCheck};
    use serde::Deserialize;
    #[cfg(test)]
    use serde_json::json;

    use crate::SubmoduleWatch;

    /// Configuration for the `SubmoduleWatch` check.
    ///
    /// The `reject_additions` and `reject_removals` keys are both booleans which default to
    /// `false`.
    ///
    /// This check is registered as a commit check with the name `"submodule_watch"`.
    ///
    /// # Example
    ///
    /// ```json
    /// {
    ///     "reject_additions": false,
    ///     "reject_removals": false
    /// }
    /// ```
    #[derive(Deserialize, Debug)]
    pub struct SubmoduleWatchConfig {
        #[serde(default)]
        reject_additions: Option<bool>,
        #[serde(default)]
        reject_removals: Option<bool>,
    }

    impl IntoCheck for SubmoduleWatchConfig {
        type Check = SubmoduleWatch;

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

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

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

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

    register_checks! {
        SubmoduleWatchConfig {
            "submodule_watch" => CommitCheckConfig,
        },
    }

    #[test]
    fn test_submodule_watch_config_empty() {
        let json = json!({});
        let check: SubmoduleWatchConfig = serde_json::from_value(json).unwrap();

        assert_eq!(check.reject_additions, None);
        assert_eq!(check.reject_removals, None);

        let check = check.into_check();

        assert!(!check.reject_additions);
        assert!(!check.reject_removals);
    }

    #[test]
    fn test_submodule_watch_config_all_fields() {
        let json = json!({
            "reject_additions": true,
            "reject_removals": true,
        });
        let check: SubmoduleWatchConfig = serde_json::from_value(json).unwrap();

        assert_eq!(check.reject_additions, Some(true));
        assert_eq!(check.reject_removals, Some(true));

        let check = check.into_check();

        assert!(check.reject_additions);
        assert!(check.reject_removals);
    }
}

#[cfg(test)]
mod tests {
    use git_checks_core::Check;

    use crate::test::*;
    use crate::SubmoduleWatch;

    const ADD_SUBMODULE_TOPIC: &str = "fe90ee22ae3ce4b4dc41f8d0876e59355ff1e21c";
    const REMOVE_SUBMODULE_TOPIC: &str = "336dbaa31d512033fe77eaba7f92ebfecbd17a39";
    const REMOVE_SUBMODULE_AS_FILE: &str = "24573935ac8f352893022e454d03a6450a9e5fe5";
    const ADD_SUBMODULE_FROM_FILE: &str = "dab435c23d367c6288540cd97017a0dcd3ac042d";
    const MOVE_SUBMODULE: &str = "2088079e35503be3be41dbdca55080ced95614e1";

    #[test]
    fn test_submodule_watch_builder_default() {
        assert!(SubmoduleWatch::builder().build().is_ok());
    }

    #[test]
    fn test_submodule_watch_name_commit() {
        let check = SubmoduleWatch::default();
        assert_eq!(Check::name(&check), "submodule-watch");
    }

    #[test]
    fn test_submodule_watch_add() {
        let check = SubmoduleWatch::default();
        let result = run_check("test_submodule_watch_add", ADD_SUBMODULE_TOPIC, check);

        assert_eq!(result.warnings().len(), 0);
        assert_eq!(result.alerts().len(), 1);
        assert_eq!(
            result.alerts()[0],
            "commit fe90ee22ae3ce4b4dc41f8d0876e59355ff1e21c adds a submodule at `submodule`.",
        );
        assert_eq!(result.errors().len(), 0);
        assert!(result.temporary());
        assert!(!result.allowed());
        assert!(result.pass());
    }

    #[test]
    fn test_submodule_watch_add_from_file() {
        let check = SubmoduleWatch::default();
        let conf = make_check_conf(&check);

        let result = test_check_base(
            "test_submodule_watch_add_from_file",
            ADD_SUBMODULE_FROM_FILE,
            REMOVE_SUBMODULE_AS_FILE,
            &conf,
        );

        assert_eq!(result.warnings().len(), 0);
        assert_eq!(result.alerts().len(), 1);
        assert_eq!(
            result.alerts()[0],
            "commit dab435c23d367c6288540cd97017a0dcd3ac042d adds a submodule at `submodule`.",
        );
        assert_eq!(result.errors().len(), 0);
        assert!(result.temporary());
        assert!(!result.allowed());
        assert!(result.pass());
    }

    #[test]
    fn test_submodule_watch_add_reject() {
        let check = SubmoduleWatch::builder()
            .reject_additions(true)
            .build()
            .unwrap();
        let result = run_check(
            "test_submodule_watch_add_reject",
            ADD_SUBMODULE_TOPIC,
            check,
        );

        assert_eq!(result.warnings().len(), 0);
        assert_eq!(result.alerts().len(), 0);
        assert_eq!(result.errors().len(), 1);
        assert_eq!(
            result.errors()[0],
            "commit fe90ee22ae3ce4b4dc41f8d0876e59355ff1e21c adds a submodule at `submodule` \
             which is not allowed.",
        );
        assert!(result.temporary());
        assert!(!result.allowed());
        assert!(!result.pass());
    }

    #[test]
    fn test_submodule_watch_add_from_file_reject() {
        let check = SubmoduleWatch::builder()
            .reject_additions(true)
            .build()
            .unwrap();
        let conf = make_check_conf(&check);

        let result = test_check_base(
            "test_submodule_watch_add_from_file_reject",
            ADD_SUBMODULE_FROM_FILE,
            REMOVE_SUBMODULE_AS_FILE,
            &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 dab435c23d367c6288540cd97017a0dcd3ac042d adds a submodule at `submodule` \
             which is not allowed.",
        );
        assert!(result.temporary());
        assert!(!result.allowed());
        assert!(!result.pass());
    }

    #[test]
    fn test_submodule_watch_add_configured() {
        let check = SubmoduleWatch::default();
        let conf = make_check_conf(&check);

        let result = test_check_submodule_configure(
            "test_submodule_watch_add_configured",
            ADD_SUBMODULE_TOPIC,
            &conf,
            "submodule",
        );
        test_result_ok(result);
    }

    #[test]
    fn test_submodule_watch_add_from_file_configured() {
        let check = SubmoduleWatch::default();
        let conf = make_check_conf(&check);

        let result = test_check_submodule_base_configure(
            "test_submodule_watch_add_from_file_configured",
            ADD_SUBMODULE_FROM_FILE,
            REMOVE_SUBMODULE_AS_FILE,
            &conf,
            "submodule",
        );
        test_result_ok(result);
    }

    #[test]
    fn test_submodule_watch_add_configured_reject() {
        let check = SubmoduleWatch::builder()
            .reject_additions(true)
            .build()
            .unwrap();
        let conf = make_check_conf(&check);

        let result = test_check_submodule_configure(
            "test_submodule_watch_add_configured_reject",
            ADD_SUBMODULE_TOPIC,
            &conf,
            "submodule",
        );
        test_result_ok(result);
    }

    #[test]
    fn test_submodule_watch_add_from_file_configured_reject() {
        let check = SubmoduleWatch::builder()
            .reject_additions(true)
            .build()
            .unwrap();
        let conf = make_check_conf(&check);

        let result = test_check_submodule_base_configure(
            "test_submodule_watch_add_from_file_configured_reject",
            ADD_SUBMODULE_FROM_FILE,
            REMOVE_SUBMODULE_AS_FILE,
            &conf,
            "submodule",
        );
        test_result_ok(result);
    }

    #[test]
    fn test_submodule_watch_remove() {
        let check = SubmoduleWatch::default();
        let conf = make_check_conf(&check);

        let result = test_check_submodule_base(
            "test_submodule_watch_remove",
            REMOVE_SUBMODULE_TOPIC,
            ADD_SUBMODULE_TOPIC,
            &conf,
        );

        assert_eq!(result.warnings().len(), 0);
        assert_eq!(result.alerts().len(), 1);
        assert_eq!(
            result.alerts()[0],
            "commit 336dbaa31d512033fe77eaba7f92ebfecbd17a39 removes the submodule at `submodule`.",
        );
        assert_eq!(result.errors().len(), 0);
        assert!(!result.temporary());
        assert!(!result.allowed());
        assert!(result.pass());
    }

    #[test]
    fn test_submodule_watch_remove_as_file() {
        let check = SubmoduleWatch::default();
        let conf = make_check_conf(&check);

        let result = test_check_submodule_base(
            "test_submodule_watch_remove_as_file",
            REMOVE_SUBMODULE_AS_FILE,
            ADD_SUBMODULE_TOPIC,
            &conf,
        );

        assert_eq!(result.warnings().len(), 0);
        assert_eq!(result.alerts().len(), 1);
        assert_eq!(
            result.alerts()[0],
            "commit 24573935ac8f352893022e454d03a6450a9e5fe5 removes the submodule at `submodule`.",
        );
        assert_eq!(result.errors().len(), 0);
        assert!(!result.temporary());
        assert!(!result.allowed());
        assert!(result.pass());
    }

    #[test]
    fn test_submodule_watch_remove_reject() {
        let check = SubmoduleWatch::builder()
            .reject_removals(true)
            .build()
            .unwrap();
        let conf = make_check_conf(&check);

        let result = test_check_submodule_base(
            "test_submodule_watch_remove_reject",
            REMOVE_SUBMODULE_TOPIC,
            ADD_SUBMODULE_TOPIC,
            &conf,
        );
        test_result_errors(result, &[
            "commit 336dbaa31d512033fe77eaba7f92ebfecbd17a39 removes the submodule at `submodule` \
             which is not allowed.",
        ]);
    }

    #[test]
    fn test_submodule_watch_remove_as_file_reject() {
        let check = SubmoduleWatch::builder()
            .reject_removals(true)
            .build()
            .unwrap();
        let conf = make_check_conf(&check);

        let result = test_check_submodule_base(
            "test_submodule_watch_remove_as_file_reject",
            REMOVE_SUBMODULE_AS_FILE,
            ADD_SUBMODULE_TOPIC,
            &conf,
        );
        test_result_errors(result, &[
            "commit 24573935ac8f352893022e454d03a6450a9e5fe5 removes the submodule at `submodule` \
             which is not allowed.",
        ]);
    }

    #[test]
    fn test_submodule_watch_modified() {
        let check = SubmoduleWatch::builder()
            .reject_removals(true)
            .build()
            .unwrap();
        let conf = make_check_conf(&check);

        let result = test_check_base(
            "test_submodule_watch_modified",
            MOVE_SUBMODULE,
            ADD_SUBMODULE_TOPIC,
            &conf,
        );

        assert_eq!(result.warnings().len(), 1);
        assert_eq!(
            result.warnings()[0],
            "commit 2088079e35503be3be41dbdca55080ced95614e1 modifies an unconfigured submodule \
             at `submodule`.",
        );
        assert_eq!(result.alerts().len(), 0);
        assert_eq!(result.errors().len(), 0);
        assert!(result.temporary());
        assert!(!result.allowed());
        assert!(result.pass());
    }

    #[test]
    fn test_submodule_watch_configure_modified() {
        let check = SubmoduleWatch::builder()
            .reject_removals(true)
            .build()
            .unwrap();
        let conf = make_check_conf(&check);

        let result = test_check_submodule_base_configure(
            "test_submodule_watch_configure_modified",
            MOVE_SUBMODULE,
            ADD_SUBMODULE_TOPIC,
            &conf,
            "submodule",
        );
        test_result_ok(result);
    }

    // TODO: Test submodule.path setting changes.
}