git_checks/
check_end_of_line.rs

1// Copyright Kitware, Inc.
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
6// option. This file may not be copied, modified, or distributed
7// except according to those terms.
8
9use derive_builder::Builder;
10use git_checks_core::impl_prelude::*;
11
12/// Check for files which lack an end-of-line at the end of the file.
13#[derive(Builder, Debug, Default, Clone, Copy)]
14pub struct CheckEndOfLine {}
15
16impl CheckEndOfLine {
17    /// Create a new builder.
18    pub fn builder() -> CheckEndOfLineBuilder {
19        Default::default()
20    }
21}
22
23impl ContentCheck for CheckEndOfLine {
24    fn name(&self) -> &str {
25        "check-end-of-line"
26    }
27
28    fn check(
29        &self,
30        _: &CheckGitContext,
31        content: &dyn Content,
32    ) -> Result<CheckResult, Box<dyn Error>> {
33        let mut result = CheckResult::new();
34
35        for diff in content.diffs() {
36            match diff.status {
37                StatusChange::Added | StatusChange::Modified(_) => (),
38                _ => continue,
39            }
40
41            // Ignore symlinks; they only end with newlines if they point to a file with a newline
42            // at the end of its name.
43            if diff.new_mode == "120000" {
44                continue;
45            }
46
47            let patch = match content.path_diff(&diff.name) {
48                Ok(s) => s,
49                Err(err) => {
50                    result.add_alert(
51                        format!(
52                            "{}failed to get the diff for file `{}`: {}",
53                            commit_prefix(content),
54                            diff.name,
55                            err,
56                        ),
57                        true,
58                    );
59                    continue;
60                },
61            };
62
63            let has_missing_newline = patch.lines().last() == Some("\\ No newline at end of file");
64
65            if has_missing_newline {
66                result.add_error(format!(
67                    "{}missing newline at the end of file in `{}`.",
68                    commit_prefix_str(content, "is not allowed;"),
69                    diff.name,
70                ));
71            }
72        }
73
74        Ok(result)
75    }
76}
77
78#[cfg(feature = "config")]
79pub(crate) mod config {
80    use git_checks_config::{register_checks, CommitCheckConfig, IntoCheck, TopicCheckConfig};
81    use serde::Deserialize;
82    #[cfg(test)]
83    use serde_json::json;
84
85    use crate::CheckEndOfLine;
86
87    /// Configuration for the `CheckEndOfLine` check.
88    ///
89    /// No configuration available.
90    ///
91    /// This check is registered as a commit check with the name `"check_end_of_line"` and a topic
92    /// check with the name `"check_end_of_line/topic"`.
93    #[derive(Deserialize, Debug)]
94    pub struct CheckEndOfLineConfig {}
95
96    impl IntoCheck for CheckEndOfLineConfig {
97        type Check = CheckEndOfLine;
98
99        fn into_check(self) -> Self::Check {
100            Default::default()
101        }
102    }
103
104    register_checks! {
105        CheckEndOfLineConfig {
106            "check_end_of_line" => CommitCheckConfig,
107            "check_end_of_line/topic" => TopicCheckConfig,
108        },
109    }
110
111    #[test]
112    fn test_check_end_of_line_config_empty() {
113        let json = json!({});
114        let check: CheckEndOfLineConfig = serde_json::from_value(json).unwrap();
115
116        let _ = check.into_check();
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use git_checks_core::{Check, TopicCheck};
123
124    use crate::test::*;
125    use crate::CheckEndOfLine;
126
127    const BAD_COMMIT: &str = "829cdf8cb069b8f8a634a034d3f85089271601cf";
128    const SYMLINK_COMMIT: &str = "00ffdf352196c16a453970de022a8b4343610ccf";
129    const FIX_COMMIT: &str = "767dd1c173175d85e0f7de23dcd286f5a83617b1";
130    const DELETE_COMMIT: &str = "74828dc2e957f883cc520f0c0fc5a73efc4c0fca";
131
132    #[test]
133    fn test_check_end_of_line_builder_default() {
134        assert!(CheckEndOfLine::builder().build().is_ok());
135    }
136
137    #[test]
138    fn test_check_end_of_line_name_commit() {
139        let check = CheckEndOfLine::default();
140        assert_eq!(Check::name(&check), "check-end-of-line");
141    }
142
143    #[test]
144    fn test_check_end_of_line_name_topic() {
145        let check = CheckEndOfLine::default();
146        assert_eq!(TopicCheck::name(&check), "check-end-of-line");
147    }
148
149    #[test]
150    fn test_check_end_of_line() {
151        let check = CheckEndOfLine::default();
152        let result = run_check("test_check_end_of_line", BAD_COMMIT, check);
153        test_result_errors(result, &[
154            "commit 829cdf8cb069b8f8a634a034d3f85089271601cf is not allowed; missing newline at \
155             the end of file in `missing-newline-eof`.",
156        ]);
157    }
158
159    #[test]
160    fn test_check_end_of_line_topic() {
161        let check = CheckEndOfLine::default();
162        let result = run_topic_check("test_check_end_of_line_topic", BAD_COMMIT, check);
163        test_result_errors(
164            result,
165            &["missing newline at the end of file in `missing-newline-eof`."],
166        );
167    }
168
169    #[test]
170    fn test_check_end_of_line_removal() {
171        let check = CheckEndOfLine::default();
172        let conf = make_check_conf(&check);
173
174        let result = test_check_base(
175            "test_check_end_of_line_removal",
176            FIX_COMMIT,
177            BAD_COMMIT,
178            &conf,
179        );
180        test_result_ok(result);
181    }
182
183    #[test]
184    fn test_check_end_of_line_delete_file() {
185        let check = CheckEndOfLine::default();
186        let conf = make_check_conf(&check);
187
188        let result = test_check_base(
189            "test_check_end_of_line_delete_file",
190            DELETE_COMMIT,
191            BAD_COMMIT,
192            &conf,
193        );
194        test_result_ok(result);
195    }
196
197    #[test]
198    fn test_check_end_of_line_topic_fixed() {
199        let check = CheckEndOfLine::default();
200        run_topic_check_ok("test_check_end_of_line_topic_fixed", FIX_COMMIT, check);
201    }
202
203    #[test]
204    fn test_check_end_of_line_topic_delete_file() {
205        let check = CheckEndOfLine::default();
206        run_topic_check_ok(
207            "test_check_end_of_line_topic_delete_file",
208            DELETE_COMMIT,
209            check,
210        );
211    }
212
213    #[test]
214    fn test_check_end_of_line_ignore_symlinks() {
215        let check = CheckEndOfLine::default();
216        run_check_ok(
217            "test_check_end_of_line_ignore_symlinks",
218            SYMLINK_COMMIT,
219            check,
220        );
221    }
222
223    #[test]
224    fn test_check_end_of_line_ignore_symlinks_topic() {
225        let check = CheckEndOfLine::default();
226        run_topic_check_ok(
227            "test_check_end_of_line_ignore_symlinks_topic",
228            SYMLINK_COMMIT,
229            check,
230        );
231    }
232}