1use 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#[derive(Builder, Debug, Default, Clone, Copy)]
44#[non_exhaustive]
45#[builder(field(private))]
46pub struct SubmoduleRewind {}
47
48impl SubmoduleRewind {
49 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 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 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 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 #[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 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 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 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 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}