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#[builder(field(private))]
45pub struct SubmoduleRewind {}
46
47impl SubmoduleRewind {
48 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 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 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 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 #[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 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 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 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 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}