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