git_checks/
submodule_watch.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 that submodules are reachable from a given branch and available.
13#[derive(Builder, Debug, Default, Clone, Copy)]
14#[builder(field(private))]
15pub struct SubmoduleWatch {
16    /// Whether to reject new submodules or not.
17    ///
18    /// Configuration: Optional
19    /// Default: `false`
20    #[builder(default = "false")]
21    reject_additions: bool,
22    /// Whether to reject deletion of submodules or not.
23    ///
24    /// Configuration: Optional
25    /// Default: `false`
26    #[builder(default = "false")]
27    reject_removals: bool,
28}
29
30impl SubmoduleWatch {
31    /// Checks that submodules in the project are available.
32    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            // Ignore diffs which are not submodules.
50            if !added && !removed {
51                continue;
52            }
53
54            let is_configured = SubmoduleContext::new(ctx, diff.name.as_ref()).is_some();
55
56            // Ignore changes which don't change the submodule status.
57            if added && removed {
58                // However, check if the submodule is not configured and warn if it isn't.
59                if !is_configured {
60                    result.add_warning(format!(
61                        "commit {} modifies an unconfigured submodule at `{}`.",
62                        commit.sha1, diff.name,
63                    ));
64
65                    // Configuring the submodule can resolve the problem.
66                    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                // Configuring the submodule can resolve the problem.
89                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    /// Configuration for the `SubmoduleWatch` check.
124    ///
125    /// The `reject_additions` and `reject_removals` keys are both booleans which default to
126    /// `false`.
127    ///
128    /// This check is registered as a commit check with the name `"submodule_watch"`.
129    ///
130    /// # Example
131    ///
132    /// ```json
133    /// {
134    ///     "reject_additions": false,
135    ///     "reject_removals": false
136    /// }
137    /// ```
138    #[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    // TODO: Test submodule.path setting changes.
521}