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 for files which lack an end-of-line at the end of the file.
#[derive(Builder, Debug, Default, Clone, Copy)]
pub struct CheckEndOfLine {}

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

impl ContentCheck for CheckEndOfLine {
    fn name(&self) -> &str {
        "check-end-of-line"
    }

    fn check(
        &self,
        _: &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,
            }

            // Ignore symlinks; they only end with newlines if they point to a file with a newline
            // at the end of its name.
            if diff.new_mode == "120000" {
                continue;
            }

            let patch = match content.path_diff(&diff.name) {
                Ok(s) => s,
                Err(err) => {
                    result.add_alert(
                        format!(
                            "{}failed to get the diff for file `{}`: {}",
                            commit_prefix(content),
                            diff.name,
                            err,
                        ),
                        true,
                    );
                    continue;
                },
            };

            let has_missing_newline = patch
                .lines()
                .last()
                .map_or(false, |line| line == "\\ No newline at end of file");

            if has_missing_newline {
                result.add_error(format!(
                    "{}missing newline at the end of file in `{}`.",
                    commit_prefix_str(content, "is not allowed;"),
                    diff.name,
                ));
            }
        }

        Ok(result)
    }
}

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

    use crate::CheckEndOfLine;

    /// 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 CheckEndOfLineConfig {}

    impl IntoCheck for CheckEndOfLineConfig {
        type Check = CheckEndOfLine;

        fn into_check(self) -> Self::Check {
            CheckEndOfLine::default()
        }
    }

    register_checks! {
        CheckEndOfLineConfig {
            "check_end_of_line" => CommitCheckConfig,
            "check_end_of_line/topic" => TopicCheckConfig,
        },
    }

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

        let _ = check.into_check();
    }
}

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

    use crate::test::*;
    use crate::CheckEndOfLine;

    const BAD_COMMIT: &str = "829cdf8cb069b8f8a634a034d3f85089271601cf";
    const SYMLINK_COMMIT: &str = "00ffdf352196c16a453970de022a8b4343610ccf";
    const FIX_COMMIT: &str = "767dd1c173175d85e0f7de23dcd286f5a83617b1";
    const DELETE_COMMIT: &str = "74828dc2e957f883cc520f0c0fc5a73efc4c0fca";

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

    #[test]
    fn test_check_end_of_line_name_commit() {
        let check = CheckEndOfLine::default();
        assert_eq!(Check::name(&check), "check-end-of-line");
    }

    #[test]
    fn test_check_end_of_line_name_topic() {
        let check = CheckEndOfLine::default();
        assert_eq!(TopicCheck::name(&check), "check-end-of-line");
    }

    #[test]
    fn test_check_end_of_line() {
        let check = CheckEndOfLine::default();
        let result = run_check("test_check_end_of_line", BAD_COMMIT, check);
        test_result_errors(result, &[
            "commit 829cdf8cb069b8f8a634a034d3f85089271601cf is not allowed; missing newline at \
             the end of file in `missing-newline-eof`.",
        ]);
    }

    #[test]
    fn test_check_end_of_line_topic() {
        let check = CheckEndOfLine::default();
        let result = run_topic_check("test_check_end_of_line_topic", BAD_COMMIT, check);
        test_result_errors(
            result,
            &["missing newline at the end of file in `missing-newline-eof`."],
        );
    }

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

        let result = test_check_base(
            "test_check_end_of_line_removal",
            FIX_COMMIT,
            BAD_COMMIT,
            &conf,
        );
        test_result_ok(result);
    }

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

        let result = test_check_base(
            "test_check_end_of_line_delete_file",
            DELETE_COMMIT,
            BAD_COMMIT,
            &conf,
        );
        test_result_ok(result);
    }

    #[test]
    fn test_check_end_of_line_topic_fixed() {
        let check = CheckEndOfLine::default();
        run_topic_check_ok("test_check_end_of_line_topic_fixed", FIX_COMMIT, check);
    }

    #[test]
    fn test_check_end_of_line_topic_delete_file() {
        let check = CheckEndOfLine::default();
        run_topic_check_ok(
            "test_check_end_of_line_topic_delete_file",
            DELETE_COMMIT,
            check,
        );
    }

    #[test]
    fn test_check_end_of_line_ignore_symlinks() {
        let check = CheckEndOfLine::default();
        run_check_ok(
            "test_check_end_of_line_ignore_symlinks",
            SYMLINK_COMMIT,
            check,
        );
    }

    #[test]
    fn test_check_end_of_line_ignore_symlinks_topic() {
        let check = CheckEndOfLine::default();
        run_topic_check_ok(
            "test_check_end_of_line_ignore_symlinks_topic",
            SYMLINK_COMMIT,
            check,
        );
    }
}