Skip to main content

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