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