Skip to main content

git_checks/
submodule_rewind.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 std::path::PathBuf;
10
11use derive_builder::Builder;
12use git_checks_core::impl_prelude::*;
13use thiserror::Error;
14
15#[derive(Debug, Error)]
16enum SubmoduleRewindError {
17    #[error("failed to get the merge-base between {} (old) and {} (new) in {}: {}", old_commit, new_commit, submodule.display(), output)]
18    MergeBase {
19        submodule: PathBuf,
20        old_commit: CommitId,
21        new_commit: CommitId,
22        output: String,
23    },
24}
25
26impl SubmoduleRewindError {
27    fn merge_base(
28        submodule: &FileName,
29        old_commit: CommitId,
30        new_commit: CommitId,
31        output: &[u8],
32    ) -> Self {
33        SubmoduleRewindError::MergeBase {
34            submodule: submodule.as_path().into(),
35            old_commit,
36            new_commit,
37            output: String::from_utf8_lossy(output).into(),
38        }
39    }
40}
41
42/// Check that submodules are not rewound to older revisions.
43#[derive(Builder, Debug, Default, Clone, Copy)]
44#[builder(field(private))]
45pub struct SubmoduleRewind {}
46
47impl SubmoduleRewind {
48    /// Create a new builder.
49    pub fn builder() -> SubmoduleRewindBuilder {
50        Default::default()
51    }
52}
53
54impl ContentCheck for SubmoduleRewind {
55    fn name(&self) -> &str {
56        "submodule-rewind"
57    }
58
59    fn check(
60        &self,
61        ctx: &CheckGitContext,
62        content: &dyn Content,
63    ) -> Result<CheckResult, Box<dyn Error>> {
64        let mut result = CheckResult::new();
65
66        for diff in content.diffs() {
67            // Ignore diffs which are not submodules on the new side and submodules without a
68            // history.
69            if diff.new_mode != "160000" || diff.status == StatusChange::Added {
70                continue;
71            }
72
73            let submodule_ctx = if let Some(ctx) = SubmoduleContext::new(ctx, diff.name.as_ref()) {
74                ctx
75            } else {
76                continue;
77            };
78
79            let cat_file = submodule_ctx
80                .context
81                .git()
82                .arg("cat-file")
83                .arg("-t")
84                .arg(diff.new_blob.as_str())
85                .output()
86                .map_err(|err| GitError::subcommand("cat-file -t <new>", err))?;
87            let object_type = String::from_utf8_lossy(&cat_file.stdout);
88            if !cat_file.status.success() || object_type.trim() != "commit" {
89                // The commit is updating to a submodule reference which we can't find; we can't do
90                // our work here.
91                continue;
92            }
93
94            let cat_file = submodule_ctx
95                .context
96                .git()
97                .arg("cat-file")
98                .arg("-t")
99                .arg(diff.old_blob.as_str())
100                .output()
101                .map_err(|err| GitError::subcommand("cat-file -t <old>", err))?;
102            let object_type = String::from_utf8_lossy(&cat_file.stdout);
103            if !cat_file.status.success() || object_type.trim() != "commit" {
104                // The commit is updating a submodule reference which we can't find; we can't do
105                // our work here.
106                continue;
107            }
108
109            let merge_base = submodule_ctx
110                .context
111                .git()
112                .arg("merge-base")
113                .arg(diff.old_blob.as_str())
114                .arg(diff.new_blob.as_str())
115                .output()
116                .map_err(|err| GitError::subcommand("merge-base", err))?;
117            if !merge_base.status.success() {
118                return Err(SubmoduleRewindError::merge_base(
119                    &diff.name,
120                    diff.old_blob.clone(),
121                    diff.new_blob.clone(),
122                    &merge_base.stderr,
123                )
124                .into());
125            }
126            let base = String::from_utf8_lossy(&merge_base.stdout);
127
128            if base.trim() == diff.new_blob.as_str() {
129                result.add_error(format!(
130                    "{}is not allowed since it moves the submodule `{}` backwards from {} to {}.",
131                    commit_prefix(content),
132                    submodule_ctx.path,
133                    diff.old_blob,
134                    diff.new_blob,
135                ));
136            }
137        }
138
139        Ok(result)
140    }
141}
142
143#[cfg(feature = "config")]
144pub(crate) mod config {
145    use git_checks_config::{register_checks, CommitCheckConfig, IntoCheck, TopicCheckConfig};
146    use serde::Deserialize;
147    #[cfg(test)]
148    use serde_json::json;
149
150    use crate::SubmoduleRewind;
151
152    /// Configuration for the `SubmoduleRewind` check.
153    ///
154    /// No configuration available.
155    ///
156    /// This check is registered as a commit check with the name `"submodule_rewind"`.
157    #[derive(Deserialize, Debug)]
158    pub struct SubmoduleRewindConfig {}
159
160    impl IntoCheck for SubmoduleRewindConfig {
161        type Check = SubmoduleRewind;
162
163        fn into_check(self) -> Self::Check {
164            Default::default()
165        }
166    }
167
168    register_checks! {
169        SubmoduleRewindConfig {
170            "submodule_rewind" => CommitCheckConfig,
171            "submodule_rewind/topic" => TopicCheckConfig,
172        },
173    }
174
175    #[test]
176    fn test_submodule_rewind_config_empty() {
177        let json = json!({});
178        let check: SubmoduleRewindConfig = serde_json::from_value(json).unwrap();
179
180        let _ = check.into_check();
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use git_checks_core::{Check, TopicCheck};
187
188    use crate::test::*;
189    use crate::SubmoduleRewind;
190
191    const MOVE_TOPIC: &str = "2088079e35503be3be41dbdca55080ced95614e1";
192    const REWIND_TOPIC: &str = "39c5d0d9dc7ee6abad72cd42c90d7c1af1be169c";
193    const REWIND_TOPIC_FIXED: &str = "915796f7985ceda23f731c4766752a247309fc87";
194    const TO_UNAVAILABLE_TOPIC: &str = "1b9275caca1557611df19d1dfea687c3ef302eef";
195    const FROM_UNAVAILABLE_TOPIC: &str = "4d33c389cedef6fe4003ae05633fd2356bcd2acc";
196    const DELETE_SUBMODULE: &str = "25a69298548584f82efccd8922a1afc0a0d4182d";
197
198    #[test]
199    fn test_submodule_rewind_builder_default() {
200        assert!(SubmoduleRewind::builder().build().is_ok());
201    }
202
203    #[test]
204    fn test_submodule_rewind_name_commit() {
205        let check = SubmoduleRewind::default();
206        assert_eq!(Check::name(&check), "submodule-rewind");
207    }
208
209    #[test]
210    fn test_submodule_rewind_name_topic() {
211        let check = SubmoduleRewind::default();
212        assert_eq!(TopicCheck::name(&check), "submodule-rewind");
213    }
214
215    #[test]
216    fn test_submodule_rewind_ok() {
217        let check = SubmoduleRewind::default();
218        let conf = make_check_conf(&check);
219
220        let result = test_check_submodule("test_submodule_rewind_ok", MOVE_TOPIC, &conf);
221        test_result_ok(result);
222    }
223
224    #[test]
225    fn test_submodule_rewind_to_unavailable() {
226        let check = SubmoduleRewind::default();
227        let conf = make_check_conf(&check);
228
229        let result = test_check_submodule(
230            "test_submodule_rewind_to_unavailable",
231            TO_UNAVAILABLE_TOPIC,
232            &conf,
233        );
234
235        // No errors because we can't give an answer due to not having the referenced commits
236        // locally..
237        test_result_ok(result);
238    }
239
240    #[test]
241    fn test_submodule_rewind_to_unavailable_topic() {
242        let check = SubmoduleRewind::default();
243        let conf = make_topic_check_conf(&check);
244
245        let result = test_check_submodule(
246            "test_submodule_rewind_to_unavailable_topic",
247            TO_UNAVAILABLE_TOPIC,
248            &conf,
249        );
250
251        // No errors because we can't give an answer due to not having the referenced commits
252        // locally..
253        test_result_ok(result);
254    }
255
256    #[test]
257    fn test_submodule_rewind_from_unavailable() {
258        let check = SubmoduleRewind::default();
259        let conf = make_check_conf(&check);
260
261        let result = test_check_submodule(
262            "test_submodule_rewind_from_unavailable",
263            FROM_UNAVAILABLE_TOPIC,
264            &conf,
265        );
266
267        // No errors because we can't give an answer due to not having the referenced commits
268        // locally..
269        test_result_ok(result);
270    }
271
272    #[test]
273    fn test_submodule_rewind_from_unavailable_topic() {
274        let check = SubmoduleRewind::default();
275        let conf = make_topic_check_conf(&check);
276
277        let result = test_check_submodule(
278            "test_submodule_rewind_from_unavailable_topic",
279            FROM_UNAVAILABLE_TOPIC,
280            &conf,
281        );
282
283        // No errors because we can't give an answer due to not having the referenced commits
284        // locally..
285        test_result_ok(result);
286    }
287
288    #[test]
289    fn test_submodule_rewind_rewind() {
290        let check = SubmoduleRewind::default();
291        let conf = make_check_conf(&check);
292
293        let result = test_check_submodule_base(
294            "test_submodule_rewind_rewind",
295            REWIND_TOPIC,
296            MOVE_TOPIC,
297            &conf,
298        );
299        test_result_errors(result, &[
300            "commit 39c5d0d9dc7ee6abad72cd42c90d7c1af1be169c is not allowed since it moves the \
301             submodule `submodule` backwards from 8a890d8c4b89560c70a059bbdd7bc59b92b5c92b to \
302             2a8baa8e23bb1de5eec202dd4a29adf47feb03b1.",
303        ]);
304    }
305
306    #[test]
307    fn test_submodule_rewind_rewind_topic() {
308        let check = SubmoduleRewind::default();
309        let conf = make_topic_check_conf(&check);
310
311        let result = test_check_submodule_base(
312            "test_submodule_rewind_rewind_topic",
313            REWIND_TOPIC,
314            MOVE_TOPIC,
315            &conf,
316        );
317        test_result_errors(
318            result,
319            &[
320                "is not allowed since it moves the submodule `submodule` backwards from \
321                 8a890d8c4b89560c70a059bbdd7bc59b92b5c92b to \
322                 2a8baa8e23bb1de5eec202dd4a29adf47feb03b1.",
323            ],
324        );
325    }
326
327    #[test]
328    fn test_submodule_rewind_rewind_topic_fixed() {
329        let check = SubmoduleRewind::default();
330        let conf = make_topic_check_conf(&check);
331
332        let result = test_check_submodule_base(
333            "test_submodule_rewind_rewind_topic_fixed",
334            REWIND_TOPIC_FIXED,
335            MOVE_TOPIC,
336            &conf,
337        );
338        test_result_ok(result);
339    }
340
341    #[test]
342    fn test_submodule_rewind_unwatched() {
343        let check = SubmoduleRewind::default();
344        let conf = make_check_conf(&check);
345
346        let result = test_check_base(
347            "test_submodule_rewind_unwatched",
348            REWIND_TOPIC,
349            MOVE_TOPIC,
350            &conf,
351        );
352        test_result_ok(result);
353    }
354
355    #[test]
356    fn test_submodule_rewind_unwatched_topic() {
357        let check = SubmoduleRewind::default();
358        let conf = make_topic_check_conf(&check);
359
360        let result = test_check_base(
361            "test_submodule_rewind_unwatched_topic",
362            REWIND_TOPIC,
363            MOVE_TOPIC,
364            &conf,
365        );
366        test_result_ok(result);
367    }
368
369    #[test]
370    fn test_submodule_rewind_add() {
371        let check = SubmoduleRewind::default();
372
373        run_check_ok("test_submodule_rewind_add", TO_UNAVAILABLE_TOPIC, check);
374    }
375
376    #[test]
377    fn test_submodule_rewind_add_topic() {
378        let check = SubmoduleRewind::default();
379
380        run_topic_check_ok(
381            "test_submodule_rewind_add_topic",
382            TO_UNAVAILABLE_TOPIC,
383            check,
384        );
385    }
386
387    #[test]
388    fn test_submodule_rewind_delete() {
389        let check = SubmoduleRewind::default();
390        let conf = make_check_conf(&check);
391
392        let result = test_check_base(
393            "test_submodule_rewind_delete",
394            DELETE_SUBMODULE,
395            TO_UNAVAILABLE_TOPIC,
396            &conf,
397        );
398        test_result_ok(result);
399    }
400
401    #[test]
402    fn test_submodule_rewind_delete_topic() {
403        let check = SubmoduleRewind::default();
404        let conf = make_topic_check_conf(&check);
405
406        let result = test_check_base(
407            "test_submodule_rewind_delete",
408            DELETE_SUBMODULE,
409            TO_UNAVAILABLE_TOPIC,
410            &conf,
411        );
412        test_result_ok(result);
413    }
414}