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::*;
use crates::regex::Regex;

/// A check which denies paths which look like merge conflict resolution paths.
///
/// Sometimes after a merge, the files written to assist in resolving the conflict will be added
/// accidentally.
#[derive(Builder, Debug, Clone, Copy)]
#[builder(field(private))]
pub struct RejectConflictPaths {
    /// Require that the base file exist for the check to enforce paths.
    ///
    /// If the base exists, then the conflict path patterns will always fire. However, this misses
    /// the case where the base file was deleted and the conflict files left behind. Passing `true`
    /// here rejects any path matching the conflict patterns.
    ///
    /// Configuration: Optional
    /// Default: `true`
    #[builder(default = "true")]
    require_base_exist: bool,
}

lazy_static! {
    static ref CONFLICT_FILE_PATH: Regex = Regex::new(
        "^(?P<base>.*)\
         (?P<kind>_(BACKUP|BASE|LOCAL|REMOTE))\
         (?P<pid>_[0-9]+)\
         (?P<ext>(\\..*)?)$",
    )
    .unwrap();
}
const ORIG_SUFFIX: &str = ".orig";

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

    fn check_conflict_path_name(
        self,
        ctx: &CheckGitContext,
        path: &str,
    ) -> Result<bool, CommitError> {
        if let Some(file_path) = CONFLICT_FILE_PATH.captures(path) {
            if !self.require_base_exist {
                return Ok(true);
            }

            let base = file_path
                .name("base")
                .expect("the conflict file path regex should have a 'base' group");
            let ext = file_path
                .name("ext")
                .expect("the conflict file path regex should have a 'ext' group");

            let basepath = format!("{}{}", base.as_str(), ext.as_str());
            Self::check_for_path(ctx, &basepath)
        } else {
            Ok(false)
        }
    }

    fn check_orig_path_name(self, ctx: &CheckGitContext, path: &str) -> Result<bool, CommitError> {
        if path.ends_with(ORIG_SUFFIX) {
            if !self.require_base_exist {
                return Ok(true);
            }

            let basepath = path.trim_end_matches(ORIG_SUFFIX);
            Self::check_for_path(ctx, basepath)
        } else {
            Ok(false)
        }
    }

    fn check_for_path(ctx: &CheckGitContext, path: &str) -> Result<bool, CommitError> {
        let cat_file = ctx
            .git()
            .arg("cat-file")
            .arg("-e")
            .arg(format!(":{}", path))
            .output()
            .map_err(|err| GitError::subcommand("cat-file -e", err))?;
        Ok(cat_file.status.success())
    }
}

impl Default for RejectConflictPaths {
    fn default() -> Self {
        RejectConflictPaths {
            require_base_exist: true,
        }
    }
}

impl ContentCheck for RejectConflictPaths {
    fn name(&self) -> &str {
        "reject-conflict-paths"
    }

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

        for diff in content.diffs() {
            match diff.status {
                StatusChange::Added | StatusChange::Modified(_) => (),
                _ => continue,
            }

            if self.check_conflict_path_name(ctx, diff.name.as_str())? {
                result.add_error(format!(
                    "{}it appears as though `{}` is a merge conflict \
                     resolution file and cannot be added.",
                    commit_prefix_str(content, "not allowed;"),
                    diff.name,
                ));
            }

            if self.check_orig_path_name(ctx, diff.name.as_str())? {
                result.add_error(format!(
                    "{}it appears as though `{}` is a merge conflict \
                     backup file and cannot be added.",
                    commit_prefix_str(content, "not allowed;"),
                    diff.name,
                ));
            }
        }

        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;

    use RejectConflictPaths;

    /// Configuration for the `CheckEndOfLine` check.
    ///
    /// No configuration available.
    ///
    /// This check is registered as a commit check with the name `"check_end_of_line"` and a topic
    /// check with the name `"check_end_of_line/topic"`.
    #[derive(Deserialize, Debug)]
    pub struct RejectConflictPathsConfig {
        #[serde(default)]
        require_base_exist: Option<bool>,
    }

    impl IntoCheck for RejectConflictPathsConfig {
        type Check = RejectConflictPaths;

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

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

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

    register_checks! {
        RejectConflictPathsConfig {
            "reject_conflict_paths" => CommitCheckConfig,
            "reject_conflict_paths/topic" => TopicCheckConfig,
        },
    }

    #[test]
    fn test_reject_conflict_paths_config_empty() {
        let json = json!({});
        serde_json::from_value::<RejectConflictPathsConfig>(json).unwrap();
    }

    #[test]
    fn test_reject_conflict_paths_config_all_fields() {
        let json = json!({
            "require_base_exist": false,
        });
        serde_json::from_value::<RejectConflictPathsConfig>(json).unwrap();
    }
}

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

    const MERGE_CONFLICT_NO_BASE: &str = "52710df4a433731545ef99440edeb431b3160fc6";
    const MERGE_CONFLICT_ORIG_NO_BASE: &str = "672a6a32b045bec6f93b9b1b5252611f40437a07";

    const MERGE_CONFLICT_NO_EXT: &str = "f31b2e2cb75083d174097db7054cbc9e5836bad7";
    const MERGE_CONFLICT_WITH_EXT: &str = "59f02bdb6c404c8f7cfb32700645c149148c089b";
    const MERGE_CONFLICT_TWO_EXT: &str = "e9421eadfcac3c67a090444ef2ac859e86a8a2e0";
    const MERGE_CONFLICT_ORIG_EXT: &str = "4abcf323b81c757e668ce1936f475b085d6852e8";

    const MERGE_CONFLICT_NO_EXT_FIXED: &str = "63c923b5be729f672eddcd4b1e979a7d3d22606f";
    const MERGE_CONFLICT_WITH_EXT_FIXED: &str = "8fcdd0d920ef47d0294229fdad4b3abd3ebdc43b";
    const MERGE_CONFLICT_TWO_EXT_FIXED: &str = "0bf291f1f8a320abfb63776c56e1d1497231e24e";
    const MERGE_CONFLICT_ORIG_EXT_FIXED: &str = "e1fd80490cd7f6556120aad6309d8fb95818b6ef";

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

    #[test]
    fn test_reject_conflict_paths_no_base() {
        let check = RejectConflictPaths::default();
        run_check_ok(
            "test_reject_conflict_paths_no_base",
            MERGE_CONFLICT_NO_BASE,
            check,
        );
    }

    #[test]
    fn test_reject_conflict_paths_no_base_topic() {
        let check = RejectConflictPaths::default();
        run_topic_check_ok(
            "test_reject_conflict_paths_no_base_topic",
            MERGE_CONFLICT_NO_BASE,
            check,
        );
    }

    #[test]
    fn test_reject_conflict_paths_no_base_require() {
        let check = RejectConflictPaths::builder()
            .require_base_exist(false)
            .build()
            .unwrap();
        let result = run_check(
            "test_reject_conflict_paths_no_base_require",
            MERGE_CONFLICT_NO_BASE,
            check,
        );
        test_result_errors(result, &[
            "commit 52710df4a433731545ef99440edeb431b3160fc6 not allowed; it appears as though \
             `no_base_BACKUP_12345.ext` is a merge conflict resolution file and cannot be added.",
            "commit 52710df4a433731545ef99440edeb431b3160fc6 not allowed; it appears as though \
             `no_base_BASE_12345.ext` is a merge conflict resolution file and cannot be added.",
            "commit 52710df4a433731545ef99440edeb431b3160fc6 not allowed; it appears as though \
             `no_base_LOCAL_12345.ext` is a merge conflict resolution file and cannot be added.",
            "commit 52710df4a433731545ef99440edeb431b3160fc6 not allowed; it appears as though \
             `no_base_REMOTE_12345.ext` is a merge conflict resolution file and cannot be added.",
        ]);
    }

    #[test]
    fn test_reject_conflict_paths_no_base_require_topic() {
        let check = RejectConflictPaths::builder()
            .require_base_exist(false)
            .build()
            .unwrap();
        let result = run_topic_check(
            "test_reject_conflict_paths_no_base_require_topic",
            MERGE_CONFLICT_NO_BASE,
            check,
        );
        test_result_errors(result, &[
            "it appears as though `no_base_BACKUP_12345.ext` is a merge conflict resolution file \
             and cannot be added.",
            "it appears as though `no_base_BASE_12345.ext` is a merge conflict resolution file \
             and cannot be added.",
            "it appears as though `no_base_LOCAL_12345.ext` is a merge conflict resolution file \
             and cannot be added.",
            "it appears as though `no_base_REMOTE_12345.ext` is a merge conflict resolution file \
             and cannot be added.",
        ]);
    }

    #[test]
    fn test_reject_conflict_paths_orig_no_base() {
        let check = RejectConflictPaths::default();
        run_check_ok(
            "test_reject_conflict_paths_orig_no_base",
            MERGE_CONFLICT_ORIG_NO_BASE,
            check,
        );
    }

    #[test]
    fn test_reject_conflict_paths_orig_no_base_topic() {
        let check = RejectConflictPaths::default();
        run_topic_check_ok(
            "test_reject_conflict_paths_orig_no_base_topic",
            MERGE_CONFLICT_ORIG_NO_BASE,
            check,
        );
    }

    #[test]
    fn test_reject_conflict_paths_orig_no_base_require() {
        let check = RejectConflictPaths::builder()
            .require_base_exist(false)
            .build()
            .unwrap();
        let result = run_check(
            "test_reject_conflict_paths_orig_no_base_require",
            MERGE_CONFLICT_ORIG_NO_BASE,
            check,
        );
        test_result_errors(result, &[
            "commit 672a6a32b045bec6f93b9b1b5252611f40437a07 not allowed; it appears as though \
             `orig_file_valid.orig` is a merge conflict backup file and cannot be added.",
        ]);
    }

    #[test]
    fn test_reject_conflict_paths_orig_no_base_require_topic() {
        let check = RejectConflictPaths::builder()
            .require_base_exist(false)
            .build()
            .unwrap();
        let result = run_topic_check(
            "test_reject_conflict_paths_orig_no_base_require_topic",
            MERGE_CONFLICT_ORIG_NO_BASE,
            check,
        );
        test_result_errors(
            result,
            &[
                "it appears as though `orig_file_valid.orig` is a merge conflict backup file and \
                 cannot be added.",
            ],
        );
    }

    #[test]
    fn test_reject_conflict_paths_no_ext() {
        let check = RejectConflictPaths::default();
        let result = run_check(
            "test_reject_conflict_paths_no_ext",
            MERGE_CONFLICT_NO_EXT,
            check,
        );
        test_result_errors(result, &[
            "commit f31b2e2cb75083d174097db7054cbc9e5836bad7 not allowed; it appears as though \
             `no_ext_BACKUP_12345` is a merge conflict resolution file and cannot be added.",
            "commit f31b2e2cb75083d174097db7054cbc9e5836bad7 not allowed; it appears as though \
             `no_ext_BASE_12345` is a merge conflict resolution file and cannot be added.",
            "commit f31b2e2cb75083d174097db7054cbc9e5836bad7 not allowed; it appears as though \
             `no_ext_LOCAL_12345` is a merge conflict resolution file and cannot be added.",
            "commit f31b2e2cb75083d174097db7054cbc9e5836bad7 not allowed; it appears as though \
             `no_ext_REMOTE_12345` is a merge conflict resolution file and cannot be added.",
        ]);
    }

    #[test]
    fn test_reject_conflict_paths_no_ext_topic() {
        let check = RejectConflictPaths::default();
        let result = run_topic_check(
            "test_reject_conflict_paths_no_ext_topic",
            MERGE_CONFLICT_NO_EXT,
            check,
        );
        test_result_errors(result, &[
            "it appears as though `no_ext_BACKUP_12345` is a merge conflict resolution file and \
             cannot be added.",
            "it appears as though `no_ext_BASE_12345` is a merge conflict resolution file and \
             cannot be added.",
            "it appears as though `no_ext_LOCAL_12345` is a merge conflict resolution file and \
             cannot be added.",
            "it appears as though `no_ext_REMOTE_12345` is a merge conflict resolution file and \
             cannot be added.",
        ]);
    }

    #[test]
    fn test_reject_conflict_paths_no_ext_topic_fixed() {
        let check = RejectConflictPaths::default();
        run_topic_check_ok(
            "test_reject_conflict_paths_no_ext_topic_fixed",
            MERGE_CONFLICT_NO_EXT_FIXED,
            check,
        );
    }

    #[test]
    fn test_reject_conflict_paths_with_ext() {
        let check = RejectConflictPaths::default();
        let result = run_check(
            "test_reject_conflict_paths_with_ext",
            MERGE_CONFLICT_WITH_EXT,
            check,
        );
        test_result_errors(result, &[
            "commit 59f02bdb6c404c8f7cfb32700645c149148c089b not allowed; it appears as though \
             `conflict_with_BACKUP_12345.ext` is a merge conflict resolution file and cannot be \
             added.",
            "commit 59f02bdb6c404c8f7cfb32700645c149148c089b not allowed; it appears as though \
             `conflict_with_BASE_12345.ext` is a merge conflict resolution file and cannot be \
             added.",
            "commit 59f02bdb6c404c8f7cfb32700645c149148c089b not allowed; it appears as though \
             `conflict_with_LOCAL_12345.ext` is a merge conflict resolution file and cannot be \
             added.",
            "commit 59f02bdb6c404c8f7cfb32700645c149148c089b not allowed; it appears as though \
             `conflict_with_REMOTE_12345.ext` is a merge conflict resolution file and cannot be \
             added.",
        ]);
    }

    #[test]
    fn test_reject_conflict_paths_with_ext_topic() {
        let check = RejectConflictPaths::default();
        let result = run_topic_check(
            "test_reject_conflict_paths_with_ext_topic",
            MERGE_CONFLICT_WITH_EXT,
            check,
        );
        test_result_errors(result, &[
            "it appears as though `conflict_with_BACKUP_12345.ext` is a merge conflict resolution \
             file and cannot be added.",
            "it appears as though `conflict_with_BASE_12345.ext` is a merge conflict resolution \
             file and cannot be added.",
            "it appears as though `conflict_with_LOCAL_12345.ext` is a merge conflict resolution \
             file and cannot be added.",
            "it appears as though `conflict_with_REMOTE_12345.ext` is a merge conflict resolution \
             file and cannot be added.",
        ]);
    }

    #[test]
    fn test_reject_conflict_paths_with_ext_topic_fixed() {
        let check = RejectConflictPaths::default();
        run_topic_check_ok(
            "test_reject_conflict_paths_with_ext_topic_fixed",
            MERGE_CONFLICT_WITH_EXT_FIXED,
            check,
        );
    }

    #[test]
    fn test_reject_conflict_paths_two_ext() {
        let check = RejectConflictPaths::default();
        let result = run_check(
            "test_reject_conflict_paths_two_ext",
            MERGE_CONFLICT_TWO_EXT,
            check,
        );
        test_result_errors(result, &[
            "commit e9421eadfcac3c67a090444ef2ac859e86a8a2e0 not allowed; it appears as though \
             `conflict_with.two_BACKUP_12345.ext` is a merge conflict resolution file and cannot \
             be added.",
            "commit e9421eadfcac3c67a090444ef2ac859e86a8a2e0 not allowed; it appears as though \
             `conflict_with.two_BASE_12345.ext` is a merge conflict resolution file and cannot be \
             added.",
            "commit e9421eadfcac3c67a090444ef2ac859e86a8a2e0 not allowed; it appears as though \
             `conflict_with.two_LOCAL_12345.ext` is a merge conflict resolution file and cannot be \
             added.",
            "commit e9421eadfcac3c67a090444ef2ac859e86a8a2e0 not allowed; it appears as though \
             `conflict_with.two_REMOTE_12345.ext` is a merge conflict resolution file and cannot \
             be added.",
        ]);
    }

    #[test]
    fn test_reject_conflict_paths_two_ext_topic() {
        let check = RejectConflictPaths::default();
        let result = run_topic_check(
            "test_reject_conflict_paths_two_ext_topic",
            MERGE_CONFLICT_TWO_EXT,
            check,
        );
        test_result_errors(
            result,
            &[
                "it appears as though `conflict_with.two_BACKUP_12345.ext` is a merge conflict \
                 resolution file and cannot be added.",
                "it appears as though `conflict_with.two_BASE_12345.ext` is a merge conflict \
                 resolution file and cannot be added.",
                "it appears as though `conflict_with.two_LOCAL_12345.ext` is a merge conflict \
                 resolution file and cannot be added.",
                "it appears as though `conflict_with.two_REMOTE_12345.ext` is a merge conflict \
                 resolution file and cannot be added.",
            ],
        );
    }

    #[test]
    fn test_reject_conflict_paths_two_ext_topic_fixed() {
        let check = RejectConflictPaths::default();
        run_topic_check_ok(
            "test_reject_conflict_paths_two_ext_topic_fixed",
            MERGE_CONFLICT_TWO_EXT_FIXED,
            check,
        );
    }

    #[test]
    fn test_reject_conflict_paths_orig_ext() {
        let check = RejectConflictPaths::default();
        let result = run_check(
            "test_reject_conflict_paths_orig_ext",
            MERGE_CONFLICT_ORIG_EXT,
            check,
        );
        test_result_errors(result, &[
            "commit 4abcf323b81c757e668ce1936f475b085d6852e8 not allowed; it appears as though \
             `orig_file.ext.orig` is a merge conflict backup file and cannot be added.",
        ]);
    }

    #[test]
    fn test_reject_conflict_paths_orig_ext_topic() {
        let check = RejectConflictPaths::default();
        let result = run_topic_check(
            "test_reject_conflict_paths_orig_ext_topic",
            MERGE_CONFLICT_ORIG_EXT,
            check,
        );
        test_result_errors(result, &[
            "it appears as though `orig_file.ext.orig` is a merge conflict backup file and cannot \
             be added.",
        ]);
    }

    #[test]
    fn test_reject_conflict_paths_orig_ext_topic_fixed() {
        let check = RejectConflictPaths::default();
        run_topic_check_ok(
            "test_reject_conflict_paths_orig_ext_topic_fixed",
            MERGE_CONFLICT_ORIG_EXT_FIXED,
            check,
        );
    }
}