git_checks/
submodule_rewind.rs1use 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 Check for SubmoduleRewind {
55 fn name(&self) -> &str {
56 "submodule-rewind"
57 }
58
59 fn check(&self, ctx: &CheckGitContext, commit: &Commit) -> Result<CheckResult, Box<dyn Error>> {
60 let mut result = CheckResult::new();
61
62 for diff in &commit.diffs {
63 if diff.new_mode != "160000" || diff.status == StatusChange::Added {
66 continue;
67 }
68
69 let submodule_ctx = if let Some(ctx) = SubmoduleContext::new(ctx, diff.name.as_ref()) {
70 ctx
71 } else {
72 continue;
73 };
74
75 let cat_file = submodule_ctx
76 .context
77 .git()
78 .arg("cat-file")
79 .arg("-t")
80 .arg(diff.new_blob.as_str())
81 .output()
82 .map_err(|err| GitError::subcommand("cat-file -t <new>", err))?;
83 let object_type = String::from_utf8_lossy(&cat_file.stdout);
84 if !cat_file.status.success() || object_type.trim() != "commit" {
85 continue;
88 }
89
90 let cat_file = submodule_ctx
91 .context
92 .git()
93 .arg("cat-file")
94 .arg("-t")
95 .arg(diff.old_blob.as_str())
96 .output()
97 .map_err(|err| GitError::subcommand("cat-file -t <old>", err))?;
98 let object_type = String::from_utf8_lossy(&cat_file.stdout);
99 if !cat_file.status.success() || object_type.trim() != "commit" {
100 continue;
103 }
104
105 let merge_base = submodule_ctx
106 .context
107 .git()
108 .arg("merge-base")
109 .arg(diff.old_blob.as_str())
110 .arg(diff.new_blob.as_str())
111 .output()
112 .map_err(|err| GitError::subcommand("merge-base", err))?;
113 if !merge_base.status.success() {
114 return Err(SubmoduleRewindError::merge_base(
115 &diff.name,
116 diff.old_blob.clone(),
117 diff.new_blob.clone(),
118 &merge_base.stderr,
119 )
120 .into());
121 }
122 let base = String::from_utf8_lossy(&merge_base.stdout);
123
124 if base.trim() == diff.new_blob.as_str() {
125 result.add_error(format!(
126 "commit {} is not allowed since it moves the submodule `{}` backwards from {} \
127 to {}.",
128 commit.sha1, submodule_ctx.path, diff.old_blob, diff.new_blob,
129 ));
130 }
131 }
132
133 Ok(result)
134 }
135}
136
137#[cfg(feature = "config")]
138pub(crate) mod config {
139 use git_checks_config::{register_checks, CommitCheckConfig, IntoCheck};
140 use serde::Deserialize;
141 #[cfg(test)]
142 use serde_json::json;
143
144 use crate::SubmoduleRewind;
145
146 #[derive(Deserialize, Debug)]
152 pub struct SubmoduleRewindConfig {}
153
154 impl IntoCheck for SubmoduleRewindConfig {
155 type Check = SubmoduleRewind;
156
157 fn into_check(self) -> Self::Check {
158 Default::default()
159 }
160 }
161
162 register_checks! {
163 SubmoduleRewindConfig {
164 "submodule_rewind" => CommitCheckConfig,
165 },
166 }
167
168 #[test]
169 fn test_submodule_rewind_config_empty() {
170 let json = json!({});
171 let check: SubmoduleRewindConfig = serde_json::from_value(json).unwrap();
172
173 let _ = check.into_check();
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use git_checks_core::Check;
180
181 use crate::test::*;
182 use crate::SubmoduleRewind;
183
184 const MOVE_TOPIC: &str = "2088079e35503be3be41dbdca55080ced95614e1";
185 const REWIND_TOPIC: &str = "39c5d0d9dc7ee6abad72cd42c90d7c1af1be169c";
186 const TO_UNAVAILABLE_TOPIC: &str = "1b9275caca1557611df19d1dfea687c3ef302eef";
187 const FROM_UNAVAILABLE_TOPIC: &str = "4d33c389cedef6fe4003ae05633fd2356bcd2acc";
188 const DELETE_SUBMODULE: &str = "25a69298548584f82efccd8922a1afc0a0d4182d";
189
190 #[test]
191 fn test_submodule_rewind_builder_default() {
192 assert!(SubmoduleRewind::builder().build().is_ok());
193 }
194
195 #[test]
196 fn test_submodule_rewind_name_commit() {
197 let check = SubmoduleRewind::default();
198 assert_eq!(Check::name(&check), "submodule-rewind");
199 }
200
201 #[test]
202 fn test_submodule_rewind_ok() {
203 let check = SubmoduleRewind::default();
204 let conf = make_check_conf(&check);
205
206 let result = test_check_submodule("test_submodule_rewind_ok", MOVE_TOPIC, &conf);
207 test_result_ok(result);
208 }
209
210 #[test]
211 fn test_submodule_rewind_to_unavailable() {
212 let check = SubmoduleRewind::default();
213 let conf = make_check_conf(&check);
214
215 let result = test_check_submodule(
216 "test_submodule_rewind_to_unavailable",
217 TO_UNAVAILABLE_TOPIC,
218 &conf,
219 );
220
221 test_result_ok(result);
224 }
225
226 #[test]
227 fn test_submodule_rewind_from_unavailable() {
228 let check = SubmoduleRewind::default();
229 let conf = make_check_conf(&check);
230
231 let result = test_check_submodule(
232 "test_submodule_rewind_from_unavailable",
233 FROM_UNAVAILABLE_TOPIC,
234 &conf,
235 );
236
237 test_result_ok(result);
240 }
241
242 #[test]
243 fn test_submodule_rewind_rewind() {
244 let check = SubmoduleRewind::default();
245 let conf = make_check_conf(&check);
246
247 let result = test_check_submodule_base(
248 "test_submodule_rewind_rewind",
249 REWIND_TOPIC,
250 MOVE_TOPIC,
251 &conf,
252 );
253 test_result_errors(result, &[
254 "commit 39c5d0d9dc7ee6abad72cd42c90d7c1af1be169c is not allowed since it moves the \
255 submodule `submodule` backwards from 8a890d8c4b89560c70a059bbdd7bc59b92b5c92b to \
256 2a8baa8e23bb1de5eec202dd4a29adf47feb03b1.",
257 ]);
258 }
259
260 #[test]
261 fn test_submodule_rewind_unwatched() {
262 let check = SubmoduleRewind::default();
263 let conf = make_check_conf(&check);
264
265 let result = test_check_base(
266 "test_submodule_rewind_unwatched",
267 REWIND_TOPIC,
268 MOVE_TOPIC,
269 &conf,
270 );
271 test_result_ok(result);
272 }
273
274 #[test]
275 fn test_submodule_rewind_add() {
276 let check = SubmoduleRewind::default();
277
278 run_check_ok("test_submodule_rewind_add", TO_UNAVAILABLE_TOPIC, check);
279 }
280
281 #[test]
282 fn test_submodule_rewind_delete() {
283 let check = SubmoduleRewind::default();
284 let conf = make_check_conf(&check);
285
286 let result = test_check_base(
287 "test_submodule_rewind_delete",
288 DELETE_SUBMODULE,
289 TO_UNAVAILABLE_TOPIC,
290 &conf,
291 );
292 test_result_ok(result);
293 }
294}