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        ctx: &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 diff_attr = ctx.check_attr("diff", diff.name.as_path())?;
48            if let AttributeState::Unset = diff_attr {
49                // Binary files should not be handled here.
50                continue;
51            }
52
53            let text_attr = ctx.check_attr("text", diff.name.as_path())?;
54            if let AttributeState::Unset = text_attr {
55                // Binary files should not be handled here.
56                continue;
57            }
58
59            let patch = match content.path_diff(&diff.name) {
60                Ok(s) => s,
61                Err(err) => {
62                    result.add_alert(
63                        format!(
64                            "{}failed to get the diff for file `{}`: {}",
65                            commit_prefix(content),
66                            diff.name,
67                            err,
68                        ),
69                        true,
70                    );
71                    continue;
72                },
73            };
74
75            let has_missing_newline = patch.lines().last() == Some("\\ No newline at end of file");
76
77            if has_missing_newline {
78                result.add_error(format!(
79                    "{}missing newline at the end of file in `{}`.",
80                    commit_prefix_str(content, "is not allowed;"),
81                    diff.name,
82                ));
83            }
84        }
85
86        Ok(result)
87    }
88}
89
90#[cfg(feature = "config")]
91pub(crate) mod config {
92    use git_checks_config::{register_checks, CommitCheckConfig, IntoCheck, TopicCheckConfig};
93    use serde::Deserialize;
94    #[cfg(test)]
95    use serde_json::json;
96
97    use crate::CheckEndOfLine;
98
99    /// Configuration for the `CheckEndOfLine` check.
100    ///
101    /// No configuration available.
102    ///
103    /// This check is registered as a commit check with the name `"check_end_of_line"` and a topic
104    /// check with the name `"check_end_of_line/topic"`.
105    #[derive(Deserialize, Debug)]
106    pub struct CheckEndOfLineConfig {}
107
108    impl IntoCheck for CheckEndOfLineConfig {
109        type Check = CheckEndOfLine;
110
111        fn into_check(self) -> Self::Check {
112            Default::default()
113        }
114    }
115
116    register_checks! {
117        CheckEndOfLineConfig {
118            "check_end_of_line" => CommitCheckConfig,
119            "check_end_of_line/topic" => TopicCheckConfig,
120        },
121    }
122
123    #[test]
124    fn test_check_end_of_line_config_empty() {
125        let json = json!({});
126        let check: CheckEndOfLineConfig = serde_json::from_value(json).unwrap();
127
128        let _ = check.into_check();
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use git_checks_core::{Check, TopicCheck};
135
136    use crate::test::*;
137    use crate::CheckEndOfLine;
138
139    const BAD_COMMIT: &str = "829cdf8cb069b8f8a634a034d3f85089271601cf";
140    const SYMLINK_COMMIT: &str = "00ffdf352196c16a453970de022a8b4343610ccf";
141    const FIX_COMMIT: &str = "767dd1c173175d85e0f7de23dcd286f5a83617b1";
142    const DELETE_COMMIT: &str = "74828dc2e957f883cc520f0c0fc5a73efc4c0fca";
143    const NO_DIFF_COMMIT: &str = "665ee8fbdd0e260e0416f1c3b199aa2e34a1ab92";
144    const NO_TEXT_COMMIT: &str = "86e09d587be3d1d760a67d16eead0ccce716b5a1";
145    const BINARY_COMMIT: &str = "83cd07b3ca3f3e616ed69fbe01f3cc5731467823";
146
147    #[test]
148    fn test_check_end_of_line_builder_default() {
149        assert!(CheckEndOfLine::builder().build().is_ok());
150    }
151
152    #[test]
153    fn test_check_end_of_line_name_commit() {
154        let check = CheckEndOfLine::default();
155        assert_eq!(Check::name(&check), "check-end-of-line");
156    }
157
158    #[test]
159    fn test_check_end_of_line_name_topic() {
160        let check = CheckEndOfLine::default();
161        assert_eq!(TopicCheck::name(&check), "check-end-of-line");
162    }
163
164    #[test]
165    fn test_check_end_of_line() {
166        let check = CheckEndOfLine::default();
167        let result = run_check("test_check_end_of_line", BAD_COMMIT, check);
168        test_result_errors(result, &[
169            "commit 829cdf8cb069b8f8a634a034d3f85089271601cf is not allowed; missing newline at \
170             the end of file in `missing-newline-eof`.",
171        ]);
172    }
173
174    #[test]
175    fn test_check_end_of_line_topic() {
176        let check = CheckEndOfLine::default();
177        let result = run_topic_check("test_check_end_of_line_topic", BAD_COMMIT, check);
178        test_result_errors(
179            result,
180            &["missing newline at the end of file in `missing-newline-eof`."],
181        );
182    }
183
184    #[test]
185    fn test_check_end_of_line_removal() {
186        let check = CheckEndOfLine::default();
187        let conf = make_check_conf(&check);
188
189        let result = test_check_base(
190            "test_check_end_of_line_removal",
191            FIX_COMMIT,
192            BAD_COMMIT,
193            &conf,
194        );
195        test_result_ok(result);
196    }
197
198    #[test]
199    fn test_check_end_of_line_delete_file() {
200        let check = CheckEndOfLine::default();
201        let conf = make_check_conf(&check);
202
203        let result = test_check_base(
204            "test_check_end_of_line_delete_file",
205            DELETE_COMMIT,
206            BAD_COMMIT,
207            &conf,
208        );
209        test_result_ok(result);
210    }
211
212    #[test]
213    fn test_check_end_of_line_topic_fixed() {
214        let check = CheckEndOfLine::default();
215        run_topic_check_ok("test_check_end_of_line_topic_fixed", FIX_COMMIT, check);
216    }
217
218    #[test]
219    fn test_check_end_of_line_topic_delete_file() {
220        let check = CheckEndOfLine::default();
221        run_topic_check_ok(
222            "test_check_end_of_line_topic_delete_file",
223            DELETE_COMMIT,
224            check,
225        );
226    }
227
228    #[test]
229    fn test_check_end_of_line_ignore_symlinks() {
230        let check = CheckEndOfLine::default();
231        run_check_ok(
232            "test_check_end_of_line_ignore_symlinks",
233            SYMLINK_COMMIT,
234            check,
235        );
236    }
237
238    #[test]
239    fn test_check_end_of_line_ignore_symlinks_topic() {
240        let check = CheckEndOfLine::default();
241        run_topic_check_ok(
242            "test_check_end_of_line_ignore_symlinks_topic",
243            SYMLINK_COMMIT,
244            check,
245        );
246    }
247
248    #[test]
249    fn test_check_end_of_line_ignore_via_diff() {
250        let check = CheckEndOfLine::default();
251        run_check_ok(
252            "test_check_end_of_line_ignore_via_diff",
253            NO_DIFF_COMMIT,
254            check,
255        );
256    }
257
258    #[test]
259    fn test_check_end_of_line_ignore_via_diff_topic() {
260        let check = CheckEndOfLine::default();
261        run_topic_check_ok(
262            "test_check_end_of_line_ignore_via_diff_topic",
263            NO_DIFF_COMMIT,
264            check,
265        );
266    }
267
268    #[test]
269    fn test_check_end_of_line_ignore_via_text() {
270        let check = CheckEndOfLine::default();
271        run_check_ok(
272            "test_check_end_of_line_ignore_via_text",
273            NO_TEXT_COMMIT,
274            check,
275        );
276    }
277
278    #[test]
279    fn test_check_end_of_line_ignore_via_text_topic() {
280        let check = CheckEndOfLine::default();
281        run_topic_check_ok(
282            "test_check_end_of_line_ignore_via_text_topic",
283            NO_TEXT_COMMIT,
284            check,
285        );
286    }
287
288    #[test]
289    fn test_check_end_of_line_ignore_via_binary() {
290        let check = CheckEndOfLine::default();
291        run_check_ok(
292            "test_check_end_of_line_ignore_via_binary",
293            BINARY_COMMIT,
294            check,
295        );
296    }
297
298    #[test]
299    fn test_check_end_of_line_ignore_via_binary_topic() {
300        let check = CheckEndOfLine::default();
301        run_topic_check_ok(
302            "test_check_end_of_line_ignore_via_binary_topic",
303            BINARY_COMMIT,
304            check,
305        );
306    }
307}