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