1use derive_builder::Builder;
10use git_checks_core::impl_prelude::*;
11
12#[derive(Builder, Debug, Default, Clone, Copy)]
14#[builder(field(private))]
15pub struct SubmoduleWatch {
16 #[builder(default = "false")]
21 reject_additions: bool,
22 #[builder(default = "false")]
27 reject_removals: bool,
28}
29
30impl SubmoduleWatch {
31 pub fn builder() -> SubmoduleWatchBuilder {
33 Default::default()
34 }
35}
36
37impl Check for SubmoduleWatch {
38 fn name(&self) -> &str {
39 "submodule-watch"
40 }
41
42 fn check(&self, ctx: &CheckGitContext, commit: &Commit) -> Result<CheckResult, Box<dyn Error>> {
43 let mut result = CheckResult::new();
44
45 for diff in &commit.diffs {
46 let added = diff.new_mode == "160000";
47 let removed = diff.old_mode == "160000";
48
49 if !added && !removed {
51 continue;
52 }
53
54 let is_configured = SubmoduleContext::new(ctx, diff.name.as_ref()).is_some();
55
56 if added && removed {
58 if !is_configured {
60 result.add_warning(format!(
61 "commit {} modifies an unconfigured submodule at `{}`.",
62 commit.sha1, diff.name,
63 ));
64
65 result.make_temporary();
67 }
68
69 continue;
70 }
71
72 if added && !is_configured {
73 if self.reject_additions {
74 result.add_error(format!(
75 "commit {} adds a submodule at `{}` which is not allowed.",
76 commit.sha1, diff.name,
77 ));
78 } else {
79 result.add_alert(
80 format!(
81 "commit {} adds a submodule at `{}`.",
82 commit.sha1, diff.name,
83 ),
84 false,
85 );
86 }
87
88 result.make_temporary();
90 }
91
92 if removed {
93 if self.reject_removals {
94 result.add_error(format!(
95 "commit {} removes the submodule at `{}` which is not allowed.",
96 commit.sha1, diff.name,
97 ));
98 } else {
99 result.add_alert(
100 format!(
101 "commit {} removes the submodule at `{}`.",
102 commit.sha1, diff.name,
103 ),
104 false,
105 );
106 }
107 }
108 }
109
110 Ok(result)
111 }
112}
113
114#[cfg(feature = "config")]
115pub(crate) mod config {
116 use git_checks_config::{register_checks, CommitCheckConfig, IntoCheck};
117 use serde::Deserialize;
118 #[cfg(test)]
119 use serde_json::json;
120
121 use crate::SubmoduleWatch;
122
123 #[derive(Deserialize, Debug)]
139 pub struct SubmoduleWatchConfig {
140 #[serde(default)]
141 reject_additions: Option<bool>,
142 #[serde(default)]
143 reject_removals: Option<bool>,
144 }
145
146 impl IntoCheck for SubmoduleWatchConfig {
147 type Check = SubmoduleWatch;
148
149 fn into_check(self) -> Self::Check {
150 let mut builder = SubmoduleWatch::builder();
151
152 if let Some(reject_additions) = self.reject_additions {
153 builder.reject_additions(reject_additions);
154 }
155
156 if let Some(reject_removals) = self.reject_removals {
157 builder.reject_removals(reject_removals);
158 }
159
160 builder
161 .build()
162 .expect("configuration mismatch for `SubmoduleWatch`")
163 }
164 }
165
166 register_checks! {
167 SubmoduleWatchConfig {
168 "submodule_watch" => CommitCheckConfig,
169 },
170 }
171
172 #[test]
173 fn test_submodule_watch_config_empty() {
174 let json = json!({});
175 let check: SubmoduleWatchConfig = serde_json::from_value(json).unwrap();
176
177 assert_eq!(check.reject_additions, None);
178 assert_eq!(check.reject_removals, None);
179
180 let check = check.into_check();
181
182 assert!(!check.reject_additions);
183 assert!(!check.reject_removals);
184 }
185
186 #[test]
187 fn test_submodule_watch_config_all_fields() {
188 let json = json!({
189 "reject_additions": true,
190 "reject_removals": true,
191 });
192 let check: SubmoduleWatchConfig = serde_json::from_value(json).unwrap();
193
194 assert_eq!(check.reject_additions, Some(true));
195 assert_eq!(check.reject_removals, Some(true));
196
197 let check = check.into_check();
198
199 assert!(check.reject_additions);
200 assert!(check.reject_removals);
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use git_checks_core::Check;
207
208 use crate::test::*;
209 use crate::SubmoduleWatch;
210
211 const ADD_SUBMODULE_TOPIC: &str = "fe90ee22ae3ce4b4dc41f8d0876e59355ff1e21c";
212 const REMOVE_SUBMODULE_TOPIC: &str = "336dbaa31d512033fe77eaba7f92ebfecbd17a39";
213 const REMOVE_SUBMODULE_AS_FILE: &str = "24573935ac8f352893022e454d03a6450a9e5fe5";
214 const ADD_SUBMODULE_FROM_FILE: &str = "dab435c23d367c6288540cd97017a0dcd3ac042d";
215 const MOVE_SUBMODULE: &str = "2088079e35503be3be41dbdca55080ced95614e1";
216
217 #[test]
218 fn test_submodule_watch_builder_default() {
219 assert!(SubmoduleWatch::builder().build().is_ok());
220 }
221
222 #[test]
223 fn test_submodule_watch_name_commit() {
224 let check = SubmoduleWatch::default();
225 assert_eq!(Check::name(&check), "submodule-watch");
226 }
227
228 #[test]
229 fn test_submodule_watch_add() {
230 let check = SubmoduleWatch::default();
231 let result = run_check("test_submodule_watch_add", ADD_SUBMODULE_TOPIC, check);
232
233 assert_eq!(result.warnings().len(), 0);
234 assert_eq!(result.alerts().len(), 1);
235 assert_eq!(
236 result.alerts()[0],
237 "commit fe90ee22ae3ce4b4dc41f8d0876e59355ff1e21c adds a submodule at `submodule`.",
238 );
239 assert_eq!(result.errors().len(), 0);
240 assert!(result.temporary());
241 assert!(!result.allowed());
242 assert!(result.pass());
243 }
244
245 #[test]
246 fn test_submodule_watch_add_from_file() {
247 let check = SubmoduleWatch::default();
248 let conf = make_check_conf(&check);
249
250 let result = test_check_base(
251 "test_submodule_watch_add_from_file",
252 ADD_SUBMODULE_FROM_FILE,
253 REMOVE_SUBMODULE_AS_FILE,
254 &conf,
255 );
256
257 assert_eq!(result.warnings().len(), 0);
258 assert_eq!(result.alerts().len(), 1);
259 assert_eq!(
260 result.alerts()[0],
261 "commit dab435c23d367c6288540cd97017a0dcd3ac042d adds a submodule at `submodule`.",
262 );
263 assert_eq!(result.errors().len(), 0);
264 assert!(result.temporary());
265 assert!(!result.allowed());
266 assert!(result.pass());
267 }
268
269 #[test]
270 fn test_submodule_watch_add_reject() {
271 let check = SubmoduleWatch::builder()
272 .reject_additions(true)
273 .build()
274 .unwrap();
275 let result = run_check(
276 "test_submodule_watch_add_reject",
277 ADD_SUBMODULE_TOPIC,
278 check,
279 );
280
281 assert_eq!(result.warnings().len(), 0);
282 assert_eq!(result.alerts().len(), 0);
283 assert_eq!(result.errors().len(), 1);
284 assert_eq!(
285 result.errors()[0],
286 "commit fe90ee22ae3ce4b4dc41f8d0876e59355ff1e21c adds a submodule at `submodule` \
287 which is not allowed.",
288 );
289 assert!(result.temporary());
290 assert!(!result.allowed());
291 assert!(!result.pass());
292 }
293
294 #[test]
295 fn test_submodule_watch_add_from_file_reject() {
296 let check = SubmoduleWatch::builder()
297 .reject_additions(true)
298 .build()
299 .unwrap();
300 let conf = make_check_conf(&check);
301
302 let result = test_check_base(
303 "test_submodule_watch_add_from_file_reject",
304 ADD_SUBMODULE_FROM_FILE,
305 REMOVE_SUBMODULE_AS_FILE,
306 &conf,
307 );
308
309 assert_eq!(result.warnings().len(), 0);
310 assert_eq!(result.alerts().len(), 0);
311 assert_eq!(result.errors().len(), 1);
312 assert_eq!(
313 result.errors()[0],
314 "commit dab435c23d367c6288540cd97017a0dcd3ac042d adds a submodule at `submodule` \
315 which is not allowed.",
316 );
317 assert!(result.temporary());
318 assert!(!result.allowed());
319 assert!(!result.pass());
320 }
321
322 #[test]
323 fn test_submodule_watch_add_configured() {
324 let check = SubmoduleWatch::default();
325 let conf = make_check_conf(&check);
326
327 let result = test_check_submodule_configure(
328 "test_submodule_watch_add_configured",
329 ADD_SUBMODULE_TOPIC,
330 &conf,
331 "submodule",
332 );
333 test_result_ok(result);
334 }
335
336 #[test]
337 fn test_submodule_watch_add_from_file_configured() {
338 let check = SubmoduleWatch::default();
339 let conf = make_check_conf(&check);
340
341 let result = test_check_submodule_base_configure(
342 "test_submodule_watch_add_from_file_configured",
343 ADD_SUBMODULE_FROM_FILE,
344 REMOVE_SUBMODULE_AS_FILE,
345 &conf,
346 "submodule",
347 );
348 test_result_ok(result);
349 }
350
351 #[test]
352 fn test_submodule_watch_add_configured_reject() {
353 let check = SubmoduleWatch::builder()
354 .reject_additions(true)
355 .build()
356 .unwrap();
357 let conf = make_check_conf(&check);
358
359 let result = test_check_submodule_configure(
360 "test_submodule_watch_add_configured_reject",
361 ADD_SUBMODULE_TOPIC,
362 &conf,
363 "submodule",
364 );
365 test_result_ok(result);
366 }
367
368 #[test]
369 fn test_submodule_watch_add_from_file_configured_reject() {
370 let check = SubmoduleWatch::builder()
371 .reject_additions(true)
372 .build()
373 .unwrap();
374 let conf = make_check_conf(&check);
375
376 let result = test_check_submodule_base_configure(
377 "test_submodule_watch_add_from_file_configured_reject",
378 ADD_SUBMODULE_FROM_FILE,
379 REMOVE_SUBMODULE_AS_FILE,
380 &conf,
381 "submodule",
382 );
383 test_result_ok(result);
384 }
385
386 #[test]
387 fn test_submodule_watch_remove() {
388 let check = SubmoduleWatch::default();
389 let conf = make_check_conf(&check);
390
391 let result = test_check_submodule_base(
392 "test_submodule_watch_remove",
393 REMOVE_SUBMODULE_TOPIC,
394 ADD_SUBMODULE_TOPIC,
395 &conf,
396 );
397
398 assert_eq!(result.warnings().len(), 0);
399 assert_eq!(result.alerts().len(), 1);
400 assert_eq!(
401 result.alerts()[0],
402 "commit 336dbaa31d512033fe77eaba7f92ebfecbd17a39 removes the submodule at `submodule`.",
403 );
404 assert_eq!(result.errors().len(), 0);
405 assert!(!result.temporary());
406 assert!(!result.allowed());
407 assert!(result.pass());
408 }
409
410 #[test]
411 fn test_submodule_watch_remove_as_file() {
412 let check = SubmoduleWatch::default();
413 let conf = make_check_conf(&check);
414
415 let result = test_check_submodule_base(
416 "test_submodule_watch_remove_as_file",
417 REMOVE_SUBMODULE_AS_FILE,
418 ADD_SUBMODULE_TOPIC,
419 &conf,
420 );
421
422 assert_eq!(result.warnings().len(), 0);
423 assert_eq!(result.alerts().len(), 1);
424 assert_eq!(
425 result.alerts()[0],
426 "commit 24573935ac8f352893022e454d03a6450a9e5fe5 removes the submodule at `submodule`.",
427 );
428 assert_eq!(result.errors().len(), 0);
429 assert!(!result.temporary());
430 assert!(!result.allowed());
431 assert!(result.pass());
432 }
433
434 #[test]
435 fn test_submodule_watch_remove_reject() {
436 let check = SubmoduleWatch::builder()
437 .reject_removals(true)
438 .build()
439 .unwrap();
440 let conf = make_check_conf(&check);
441
442 let result = test_check_submodule_base(
443 "test_submodule_watch_remove_reject",
444 REMOVE_SUBMODULE_TOPIC,
445 ADD_SUBMODULE_TOPIC,
446 &conf,
447 );
448 test_result_errors(result, &[
449 "commit 336dbaa31d512033fe77eaba7f92ebfecbd17a39 removes the submodule at `submodule` \
450 which is not allowed.",
451 ]);
452 }
453
454 #[test]
455 fn test_submodule_watch_remove_as_file_reject() {
456 let check = SubmoduleWatch::builder()
457 .reject_removals(true)
458 .build()
459 .unwrap();
460 let conf = make_check_conf(&check);
461
462 let result = test_check_submodule_base(
463 "test_submodule_watch_remove_as_file_reject",
464 REMOVE_SUBMODULE_AS_FILE,
465 ADD_SUBMODULE_TOPIC,
466 &conf,
467 );
468 test_result_errors(result, &[
469 "commit 24573935ac8f352893022e454d03a6450a9e5fe5 removes the submodule at `submodule` \
470 which is not allowed.",
471 ]);
472 }
473
474 #[test]
475 fn test_submodule_watch_modified() {
476 let check = SubmoduleWatch::builder()
477 .reject_removals(true)
478 .build()
479 .unwrap();
480 let conf = make_check_conf(&check);
481
482 let result = test_check_base(
483 "test_submodule_watch_modified",
484 MOVE_SUBMODULE,
485 ADD_SUBMODULE_TOPIC,
486 &conf,
487 );
488
489 assert_eq!(result.warnings().len(), 1);
490 assert_eq!(
491 result.warnings()[0],
492 "commit 2088079e35503be3be41dbdca55080ced95614e1 modifies an unconfigured submodule \
493 at `submodule`.",
494 );
495 assert_eq!(result.alerts().len(), 0);
496 assert_eq!(result.errors().len(), 0);
497 assert!(result.temporary());
498 assert!(!result.allowed());
499 assert!(result.pass());
500 }
501
502 #[test]
503 fn test_submodule_watch_configure_modified() {
504 let check = SubmoduleWatch::builder()
505 .reject_removals(true)
506 .build()
507 .unwrap();
508 let conf = make_check_conf(&check);
509
510 let result = test_check_submodule_base_configure(
511 "test_submodule_watch_configure_modified",
512 MOVE_SUBMODULE,
513 ADD_SUBMODULE_TOPIC,
514 &conf,
515 "submodule",
516 );
517 test_result_ok(result);
518 }
519
520 }