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