use std::path::PathBuf;
use derive_builder::Builder;
use git_checks_core::impl_prelude::*;
use thiserror::Error;
#[derive(Debug, Error)]
enum SubmoduleRewindError {
#[error("failed to get the merge-base between {} (old) and {} (new) in {}: {}", old_commit, new_commit, submodule.display(), output)]
MergeBase {
submodule: PathBuf,
old_commit: CommitId,
new_commit: CommitId,
output: String,
},
}
impl SubmoduleRewindError {
fn merge_base(
submodule: &FileName,
old_commit: CommitId,
new_commit: CommitId,
output: &[u8],
) -> Self {
SubmoduleRewindError::MergeBase {
submodule: submodule.as_path().into(),
old_commit,
new_commit,
output: String::from_utf8_lossy(output).into(),
}
}
}
#[derive(Builder, Debug, Default, Clone, Copy)]
#[builder(field(private))]
pub struct SubmoduleRewind {}
impl SubmoduleRewind {
pub fn builder() -> SubmoduleRewindBuilder {
SubmoduleRewindBuilder::default()
}
}
impl Check for SubmoduleRewind {
fn name(&self) -> &str {
"submodule-rewind"
}
fn check(&self, ctx: &CheckGitContext, commit: &Commit) -> Result<CheckResult, Box<dyn Error>> {
let mut result = CheckResult::new();
for diff in &commit.diffs {
if diff.new_mode != "160000" {
continue;
}
if diff.status == StatusChange::Deleted || diff.status == StatusChange::Added {
continue;
}
let submodule_ctx = if let Some(ctx) = SubmoduleContext::new(ctx, diff.name.as_ref()) {
ctx
} else {
continue;
};
let cat_file = submodule_ctx
.context
.git()
.arg("cat-file")
.arg("-t")
.arg(diff.new_blob.as_str())
.output()
.map_err(|err| GitError::subcommand("cat-file -t <new>", err))?;
let object_type = String::from_utf8_lossy(&cat_file.stdout);
if !cat_file.status.success() || object_type.trim() != "commit" {
continue;
}
let cat_file = submodule_ctx
.context
.git()
.arg("cat-file")
.arg("-t")
.arg(diff.old_blob.as_str())
.output()
.map_err(|err| GitError::subcommand("cat-file -t <old>", err))?;
let object_type = String::from_utf8_lossy(&cat_file.stdout);
if !cat_file.status.success() || object_type.trim() != "commit" {
continue;
}
let merge_base = submodule_ctx
.context
.git()
.arg("merge-base")
.arg(diff.old_blob.as_str())
.arg(diff.new_blob.as_str())
.output()
.map_err(|err| GitError::subcommand("merge-base", err))?;
if !merge_base.status.success() {
return Err(SubmoduleRewindError::merge_base(
&diff.name,
diff.old_blob.clone(),
diff.new_blob.clone(),
&merge_base.stderr,
)
.into());
}
let base = String::from_utf8_lossy(&merge_base.stdout);
if base.trim() == diff.new_blob.as_str() {
result.add_error(format!(
"commit {} is not allowed since it moves the submodule `{}` backwards from {} \
to {}.",
commit.sha1, submodule_ctx.path, diff.old_blob, diff.new_blob,
));
}
}
Ok(result)
}
}
#[cfg(feature = "config")]
pub(crate) mod config {
use git_checks_config::{register_checks, CommitCheckConfig, IntoCheck};
use serde::Deserialize;
#[cfg(test)]
use serde_json::json;
use crate::SubmoduleRewind;
#[derive(Deserialize, Debug)]
pub struct SubmoduleRewindConfig {}
impl IntoCheck for SubmoduleRewindConfig {
type Check = SubmoduleRewind;
fn into_check(self) -> Self::Check {
SubmoduleRewind::default()
}
}
register_checks! {
SubmoduleRewindConfig {
"submodule_rewind" => CommitCheckConfig,
},
}
#[test]
fn test_submodule_rewind_config_empty() {
let json = json!({});
let check: SubmoduleRewindConfig = serde_json::from_value(json).unwrap();
let _ = check.into_check();
}
}
#[cfg(test)]
mod tests {
use git_checks_core::Check;
use crate::test::*;
use crate::SubmoduleRewind;
const MOVE_TOPIC: &str = "2088079e35503be3be41dbdca55080ced95614e1";
const REWIND_TOPIC: &str = "39c5d0d9dc7ee6abad72cd42c90d7c1af1be169c";
const TO_UNAVAILABLE_TOPIC: &str = "1b9275caca1557611df19d1dfea687c3ef302eef";
const FROM_UNAVAILABLE_TOPIC: &str = "4d33c389cedef6fe4003ae05633fd2356bcd2acc";
const DELETE_SUBMODULE: &str = "25a69298548584f82efccd8922a1afc0a0d4182d";
#[test]
fn test_submodule_rewind_builder_default() {
assert!(SubmoduleRewind::builder().build().is_ok());
}
#[test]
fn test_submodule_rewind_name_commit() {
let check = SubmoduleRewind::default();
assert_eq!(Check::name(&check), "submodule-rewind");
}
#[test]
fn test_submodule_rewind_ok() {
let check = SubmoduleRewind::default();
let conf = make_check_conf(&check);
let result = test_check_submodule("test_submodule_rewind_ok", MOVE_TOPIC, &conf);
test_result_ok(result);
}
#[test]
fn test_submodule_rewind_to_unavailable() {
let check = SubmoduleRewind::default();
let conf = make_check_conf(&check);
let result = test_check_submodule(
"test_submodule_rewind_to_unavailable",
TO_UNAVAILABLE_TOPIC,
&conf,
);
test_result_ok(result);
}
#[test]
fn test_submodule_rewind_from_unavailable() {
let check = SubmoduleRewind::default();
let conf = make_check_conf(&check);
let result = test_check_submodule(
"test_submodule_rewind_from_unavailable",
FROM_UNAVAILABLE_TOPIC,
&conf,
);
test_result_ok(result);
}
#[test]
fn test_submodule_rewind_rewind() {
let check = SubmoduleRewind::default();
let conf = make_check_conf(&check);
let result = test_check_submodule_base(
"test_submodule_rewind_rewind",
REWIND_TOPIC,
MOVE_TOPIC,
&conf,
);
test_result_errors(result, &[
"commit 39c5d0d9dc7ee6abad72cd42c90d7c1af1be169c is not allowed since it moves the \
submodule `submodule` backwards from 8a890d8c4b89560c70a059bbdd7bc59b92b5c92b to \
2a8baa8e23bb1de5eec202dd4a29adf47feb03b1.",
]);
}
#[test]
fn test_submodule_rewind_unwatched() {
let check = SubmoduleRewind::default();
let conf = make_check_conf(&check);
let result = test_check_base(
"test_submodule_rewind_unwatched",
REWIND_TOPIC,
MOVE_TOPIC,
&conf,
);
test_result_ok(result);
}
#[test]
fn test_submodule_rewind_add() {
let check = SubmoduleRewind::default();
run_check_ok("test_submodule_rewind_add", TO_UNAVAILABLE_TOPIC, check);
}
#[test]
fn test_submodule_rewind_delete() {
let check = SubmoduleRewind::default();
let conf = make_check_conf(&check);
let result = test_check_base(
"test_submodule_rewind_delete",
DELETE_SUBMODULE,
TO_UNAVAILABLE_TOPIC,
&conf,
);
test_result_ok(result);
}
}