git_checks/
check_whitespace.rs1use derive_builder::Builder;
10use git_checks_core::impl_prelude::*;
11use itertools::Itertools;
12
13const CR_LF_ENDING: &str = "\r\n";
15const CARRIAGE_RETURN_SYMBOL: &str = "\u{23ce}";
17
18#[derive(Builder, Debug, Default, Clone, Copy)]
23#[non_exhaustive]
24#[builder(field(private))]
25pub struct CheckWhitespace {}
26
27impl CheckWhitespace {
28 pub fn builder() -> CheckWhitespaceBuilder {
30 Default::default()
31 }
32
33 fn diff_tree(
34 ctx: &CheckGitContext,
35 args: &[&str],
36 content: &dyn Content,
37 ) -> Result<CheckResult, Box<dyn Error>> {
38 let mut result = CheckResult::new();
39
40 let diff_tree = ctx
41 .git()
42 .arg("diff-tree")
43 .arg("--no-commit-id")
44 .arg("--root")
45 .arg("-c")
46 .arg("--check")
47 .args(args)
48 .output()
49 .map_err(|err| GitError::subcommand("diff-tree", err))?;
50 if !diff_tree.status.success() {
51 Self::add_error(&mut result, &diff_tree.stdout, content);
52 }
53
54 Ok(result)
55 }
56
57 fn add_error(result: &mut CheckResult, output: &[u8], content: &dyn Content) {
58 let output = String::from_utf8_lossy(output);
61 let crlf_msg = if output.contains(CR_LF_ENDING) {
62 " including CR/LF line endings"
63 } else {
64 ""
65 };
66 let formatted_output = output
67 .split('\n')
68 .dropping_back(1)
70 .map(|line| format!(" {line}\n"))
71 .join("")
72 .replace('\r', CARRIAGE_RETURN_SYMBOL);
73
74 result.add_error(format!(
75 "{}adds bad whitespace{crlf_msg}:\n\n{formatted_output}",
76 commit_prefix(content),
77 ));
78 }
79}
80
81impl Check for CheckWhitespace {
82 fn name(&self) -> &str {
83 "check-whitespace"
84 }
85
86 fn check(&self, ctx: &CheckGitContext, commit: &Commit) -> Result<CheckResult, Box<dyn Error>> {
87 Self::diff_tree(ctx, &[commit.sha1.as_str()], commit)
88 }
89}
90
91impl TopicCheck for CheckWhitespace {
92 fn name(&self) -> &str {
93 "check-whitespace"
94 }
95
96 fn check(&self, ctx: &CheckGitContext, topic: &Topic) -> Result<CheckResult, Box<dyn Error>> {
97 Self::diff_tree(ctx, &[topic.base.as_str(), topic.sha1.as_str()], topic)
98 }
99}
100
101#[cfg(feature = "config")]
102pub(crate) mod config {
103 use git_checks_config::{register_checks, CommitCheckConfig, IntoCheck, TopicCheckConfig};
104 use serde::Deserialize;
105 #[cfg(test)]
106 use serde_json::json;
107
108 use crate::CheckWhitespace;
109
110 #[derive(Deserialize, Debug)]
117 pub struct CheckWhitespaceConfig {}
118
119 impl IntoCheck for CheckWhitespaceConfig {
120 type Check = CheckWhitespace;
121
122 fn into_check(self) -> Self::Check {
123 Default::default()
124 }
125 }
126
127 register_checks! {
128 CheckWhitespaceConfig {
129 "check_whitespace" => CommitCheckConfig,
130 "check_whitespace/topic" => TopicCheckConfig,
131 },
132 }
133
134 #[test]
135 fn test_check_whitespace_config_empty() {
136 let json = json!({});
137 let check: CheckWhitespaceConfig = serde_json::from_value(json).unwrap();
138
139 let _ = check.into_check();
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use git_checks_core::{Check, TopicCheck};
146
147 use crate::test::*;
148 use crate::CheckWhitespace;
149
150 const DEFAULT_TOPIC: &str = "829cdf8cb069b8f8a634a034d3f85089271601cf";
151 const NOCR_TOPIC: &str = "5db0c24d032d972ba5bf50eca99016adbfdd3e87";
152 const ALL_IGNORED_TOPIC: &str = "3a87e0f3f7430bbb81ebbd8ae8764b7f26384f1c";
153 const ALL_IGNORED_BLANKET_TOPIC: &str = "92cac7579a26f7d8449512476bd64b3000688fd5";
154
155 #[test]
156 fn test_check_whitespace_builder_default() {
157 assert!(CheckWhitespace::builder().build().is_ok());
158 }
159
160 #[test]
161 fn test_check_whitespace_name_commit() {
162 let check = CheckWhitespace::default();
163 assert_eq!(Check::name(&check), "check-whitespace");
164 }
165
166 #[test]
167 fn test_check_whitespace_name_topic() {
168 let check = CheckWhitespace::default();
169 assert_eq!(TopicCheck::name(&check), "check-whitespace");
170 }
171
172 #[test]
173 fn test_check_whitespace_defaults() {
174 let check = CheckWhitespace::default();
175 let result = run_check("test_check_whitespace_defaults", DEFAULT_TOPIC, check);
176 test_result_errors(
177 result,
178 &[
179 "commit 829cdf8cb069b8f8a634a034d3f85089271601cf adds bad whitespace including \
180 CR/LF line endings:\n\
181 \n \
182 crlf-file:1: trailing whitespace.\n \
183 +This file contains CRLF lines.\u{23ce}\n \
184 crlf-file:2: trailing whitespace.\n \
185 +\u{23ce}\n \
186 crlf-file:3: trailing whitespace.\n \
187 +line1\u{23ce}\n \
188 crlf-file:4: trailing whitespace.\n \
189 +line2\u{23ce}\n \
190 crlf-mixed-file:3: trailing whitespace.\n \
191 +crlf\u{23ce}\n \
192 extra-newlines:2: new blank line at EOF.\n \
193 mixed-tabs-spaces:3: space before tab in indent.\n \
194 + \tmixed indent\n \
195 trailing-spaces:3: trailing whitespace.\n \
196 +trailing \n \
197 trailing-tab:3: trailing whitespace.\n \
198 +trailing\t\n",
199 ],
200 );
201 }
202
203 #[test]
204 fn test_check_whitespace_nocr() {
205 let check = CheckWhitespace::default();
206 let result = run_check("test_check_whitespace_nocr", NOCR_TOPIC, check);
207 test_result_errors(
208 result,
209 &[
210 "commit 5db0c24d032d972ba5bf50eca99016adbfdd3e87 adds bad whitespace:\n\
211 \n \
212 extra-newlines:2: new blank line at EOF.\n \
213 mixed-tabs-spaces:3: space before tab in indent.\n \
214 + \tmixed indent\n \
215 trailing-spaces:3: trailing whitespace.\n \
216 +trailing \n \
217 trailing-tab:3: trailing whitespace.\n \
218 +trailing\t\n",
219 ],
220 );
221 }
222
223 #[test]
224 fn test_check_whitespace_all_ignored() {
225 let check = CheckWhitespace::default();
226 run_check_ok(
227 "test_check_whitespace_all_ignored",
228 ALL_IGNORED_TOPIC,
229 check,
230 );
231 }
232
233 #[test]
234 fn test_check_whitespace_all_ignored_blanket() {
235 let check = CheckWhitespace::default();
236 run_check_ok(
237 "test_check_whitespace_all_ignored_blanket",
238 ALL_IGNORED_BLANKET_TOPIC,
239 check,
240 );
241 }
242
243 #[test]
244 fn test_check_whitespace_defaults_topic() {
245 let check = CheckWhitespace::default();
246 let result = run_topic_check("test_check_whitespace_defaults_topic", DEFAULT_TOPIC, check);
247 test_result_errors(
248 result,
249 &["adds bad whitespace including CR/LF line endings:\n\
250 \n \
251 crlf-file:1: trailing whitespace.\n \
252 +This file contains CRLF lines.\u{23ce}\n \
253 crlf-file:2: trailing whitespace.\n \
254 +\u{23ce}\n \
255 crlf-file:3: trailing whitespace.\n \
256 +line1\u{23ce}\n \
257 crlf-file:4: trailing whitespace.\n \
258 +line2\u{23ce}\n \
259 crlf-mixed-file:3: trailing whitespace.\n \
260 +crlf\u{23ce}\n \
261 extra-newlines:2: new blank line at EOF.\n \
262 mixed-tabs-spaces:3: space before tab in indent.\n \
263 + \tmixed indent\n \
264 trailing-spaces:3: trailing whitespace.\n \
265 +trailing \n \
266 trailing-tab:3: trailing whitespace.\n \
267 +trailing\t\n"],
268 );
269 }
270
271 #[test]
272 fn test_check_whitespace_nocr_topic() {
273 let check = CheckWhitespace::default();
274 let result = run_topic_check("test_check_whitespace_nocr_topic", NOCR_TOPIC, check);
275 test_result_errors(
276 result,
277 &["adds bad whitespace:\n\
278 \n \
279 extra-newlines:2: new blank line at EOF.\n \
280 mixed-tabs-spaces:3: space before tab in indent.\n \
281 + \tmixed indent\n \
282 trailing-spaces:3: trailing whitespace.\n \
283 +trailing \n \
284 trailing-tab:3: trailing whitespace.\n \
285 +trailing\t\n"],
286 );
287 }
288
289 #[test]
290 fn test_check_whitespace_all_ignored_topic() {
291 let check = CheckWhitespace::default();
292 run_topic_check_ok(
293 "test_check_whitespace_all_ignored_topic",
294 ALL_IGNORED_TOPIC,
295 check,
296 );
297 }
298
299 #[test]
300 fn test_check_whitespace_all_ignored_blanket_topic() {
301 let check = CheckWhitespace::default();
302 run_topic_check_ok(
303 "test_check_whitespace_all_ignored_blanket_topic",
304 ALL_IGNORED_BLANKET_TOPIC,
305 check,
306 );
307 }
308}