use std::path::PathBuf;
use derive_builder::Builder;
use git_checks_core::impl_prelude::*;
use thiserror::Error;
#[derive(Debug, Error)]
enum SubmoduleAvailableError {
#[error("failed to get the merge-base for {} against the tracking branch {} in {}: {}", commit, branch, submodule.display(), output)]
MergeBase {
submodule: PathBuf,
commit: CommitId,
branch: String,
output: String,
},
#[error("failed to list refs from {} to {} in {}: {}", branch, commit, submodule.display(), output)]
RevList {
submodule: PathBuf,
commit: CommitId,
branch: String,
output: String,
},
}
impl SubmoduleAvailableError {
fn merge_base(submodule: &FileName, commit: CommitId, branch: String, output: &[u8]) -> Self {
SubmoduleAvailableError::MergeBase {
submodule: submodule.as_path().into(),
commit,
branch,
output: String::from_utf8_lossy(output).into(),
}
}
fn rev_list(submodule: &FileName, commit: CommitId, branch: String, output: &[u8]) -> Self {
SubmoduleAvailableError::RevList {
submodule: submodule.as_path().into(),
commit,
branch,
output: String::from_utf8_lossy(output).into(),
}
}
}
#[derive(Builder, Debug, Default, Clone, Copy)]
#[builder(field(private))]
pub struct SubmoduleAvailable {
#[builder(default = "false")]
require_first_parent: bool,
}
impl SubmoduleAvailable {
pub fn builder() -> SubmoduleAvailableBuilder {
SubmoduleAvailableBuilder::default()
}
}
impl Check for SubmoduleAvailable {
fn name(&self) -> &str {
"submodule-available"
}
fn check(&self, ctx: &CheckGitContext, commit: &Commit) -> Result<CheckResult, Box<dyn Error>> {
let mut result = CheckResult::new();
for diff in &commit.diffs {
if let StatusChange::Deleted = diff.status {
continue;
}
if diff.new_mode != "160000" {
continue;
}
let submodule_ctx = if let Some(ctx) = SubmoduleContext::new(ctx, diff.name.as_ref()) {
ctx
} else {
result.add_alert(
format!("submodule at `{}` is not configured.", diff.name),
false,
);
continue;
};
let submodule_commit = &diff.new_blob;
let cat_file = submodule_ctx
.context
.git()
.arg("cat-file")
.arg("-t")
.arg(submodule_commit.as_str())
.output()
.map_err(|err| GitError::subcommand("cat-file -t", err))?;
let object_type = String::from_utf8_lossy(&cat_file.stdout);
if !cat_file.status.success() || object_type.trim() != "commit" {
result
.add_error(format!(
"commit {} references an unreachable commit {} at `{}`; please make the \
commit available in the {} repository on the `{}` branch first.",
commit.sha1,
submodule_commit,
submodule_ctx.path,
submodule_ctx.url,
submodule_ctx.branch,
))
.make_temporary();
continue;
}
let merge_base = submodule_ctx
.context
.git()
.arg("merge-base")
.arg(submodule_commit.as_str())
.arg(submodule_ctx.branch.as_ref())
.output()
.map_err(|err| GitError::subcommand("merge-base", err))?;
if !merge_base.status.success() {
return Err(SubmoduleAvailableError::merge_base(
&diff.name,
submodule_commit.clone(),
submodule_ctx.branch.into(),
&merge_base.stderr,
)
.into());
}
let base = String::from_utf8_lossy(&merge_base.stdout);
if base.trim() != submodule_commit.as_str() {
result
.add_error(format!(
"commit {} references the commit {} at `{}`, but it is not available on \
the tracked branch `{}`; please make the commit available from the `{}` \
branch first.",
commit.sha1,
submodule_commit,
submodule_ctx.path,
submodule_ctx.branch,
submodule_ctx.branch,
))
.make_temporary();
continue;
}
if self.require_first_parent {
let refs = submodule_ctx
.context
.git()
.arg("rev-list")
.arg("--first-parent") .arg("--reverse") .arg(submodule_ctx.branch.as_ref())
.arg(format!("^{}~", submodule_commit))
.output()
.map_err(|err| GitError::subcommand("rev-list", err))?;
if !refs.status.success() {
return Err(SubmoduleAvailableError::rev_list(
&diff.name,
submodule_commit.clone(),
submodule_ctx.branch.into(),
&refs.stderr,
)
.into());
}
let refs = String::from_utf8_lossy(&refs.stdout);
if !refs.lines().any(|rev| rev == submodule_commit.as_str()) {
result.add_error(format!(
"commit {} references the commit {} at `{}`, but it is not available as a \
first-parent of the tracked branch `{}`; please choose the commit where \
it was merged into the `{}` branch.",
commit.sha1,
submodule_commit,
submodule_ctx.path,
submodule_ctx.branch,
submodule_ctx.branch,
));
continue;
}
}
}
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::SubmoduleAvailable;
#[derive(Deserialize, Debug)]
pub struct SubmoduleAvailableConfig {
#[serde(default)]
require_first_parent: Option<bool>,
}
impl IntoCheck for SubmoduleAvailableConfig {
type Check = SubmoduleAvailable;
fn into_check(self) -> Self::Check {
let mut builder = SubmoduleAvailable::builder();
if let Some(require_first_parent) = self.require_first_parent {
builder.require_first_parent(require_first_parent);
}
builder
.build()
.expect("configuration mismatch for `SubmoduleAvailable`")
}
}
register_checks! {
SubmoduleAvailableConfig {
"submodule_available" => CommitCheckConfig,
},
}
#[test]
fn test_submodule_available_config_empty() {
let json = json!({});
let check: SubmoduleAvailableConfig = serde_json::from_value(json).unwrap();
assert_eq!(check.require_first_parent, None);
let check = check.into_check();
assert!(!check.require_first_parent);
}
#[test]
fn test_submodule_available_config_all_fields() {
let json = json!({
"require_first_parent": true,
});
let check: SubmoduleAvailableConfig = serde_json::from_value(json).unwrap();
assert_eq!(check.require_first_parent, Some(true));
let check = check.into_check();
assert!(check.require_first_parent);
}
}
#[cfg(test)]
mod tests {
use git_checks_core::Check;
use crate::test::*;
use crate::SubmoduleAvailable;
const BASE_COMMIT: &str = "fe90ee22ae3ce4b4dc41f8d0876e59355ff1e21c";
const MOVE_TOPIC: &str = "2088079e35503be3be41dbdca55080ced95614e1";
const MOVE_NOT_FIRST_PARENT_TOPIC: &str = "eb4df16a8a38f6ca30b6e67cfbca0672156b54d2";
const UNAVAILABLE_TOPIC: &str = "1b9275caca1557611df19d1dfea687c3ef302eef";
const NOT_ANCESTOR_TOPIC: &str = "07fb2ca9c1c8c0ddfcf921e762688ffcd476bc09";
const DELETE_SUBMODULE: &str = "25a69298548584f82efccd8922a1afc0a0d4182d";
#[test]
fn test_submodule_available_builder_default() {
assert!(SubmoduleAvailable::builder().build().is_ok());
}
#[test]
fn test_submodule_available_name_commit() {
let check = SubmoduleAvailable::default();
assert_eq!(Check::name(&check), "submodule-available");
}
#[test]
fn test_submodule_unconfigured() {
let check = SubmoduleAvailable::default();
let result = run_check("test_submodule_unconfigured", BASE_COMMIT, check);
assert_eq!(result.warnings().len(), 0);
assert_eq!(result.alerts().len(), 1);
assert_eq!(
result.alerts()[0],
"submodule at `submodule` is not configured.",
);
assert_eq!(result.errors().len(), 0);
assert!(!result.temporary());
assert!(!result.allowed());
assert!(result.pass());
}
#[test]
fn test_submodule_move() {
let check = SubmoduleAvailable::default();
let conf = make_check_conf(&check);
let result = test_check_submodule("test_submodule_move", MOVE_TOPIC, &conf);
test_result_ok(result);
}
#[test]
fn test_submodule_move_not_first_parent() {
let check = SubmoduleAvailable::default();
let conf = make_check_conf(&check);
let result = test_check_submodule(
"test_submodule_move_not_first_parent",
MOVE_NOT_FIRST_PARENT_TOPIC,
&conf,
);
test_result_ok(result);
}
#[test]
fn test_submodule_move_not_first_parent_reject() {
let check = SubmoduleAvailable::builder()
.require_first_parent(true)
.build()
.unwrap();
let conf = make_check_conf(&check);
let result = test_check_submodule(
"test_submodule_move_not_first_parent_reject",
MOVE_NOT_FIRST_PARENT_TOPIC,
&conf,
);
test_result_errors(result, &[
"commit eb4df16a8a38f6ca30b6e67cfbca0672156b54d2 references the commit \
c2bd427807b40b1715b8d1441fe92f50e8ad1769 at `submodule`, but it is not available as a \
first-parent of the tracked branch `master`; please choose the commit where it was \
merged into the `master` branch.",
]);
}
#[test]
fn test_submodule_unavailable() {
let check = SubmoduleAvailable::default();
let conf = make_check_conf(&check);
let result = test_check_submodule("test_submodule_unavailable", UNAVAILABLE_TOPIC, &conf);
assert_eq!(result.warnings().len(), 0);
assert_eq!(result.alerts().len(), 0);
assert_eq!(result.errors().len(), 1);
assert_eq!(
result.errors()[0],
"commit 1b9275caca1557611df19d1dfea687c3ef302eef references an unreachable commit \
4b029c2e0f186d681caa071fa4dd7eb1f0f033f6 at `submodule`; please make the commit \
available in the https://gitlab.kitware.com/utils/test-repo.git repository on the \
`master` branch first.",
);
assert!(result.temporary());
assert!(!result.allowed());
assert!(!result.pass());
}
#[test]
fn test_submodule_not_ancestor() {
let check = SubmoduleAvailable::default();
let conf = make_check_conf(&check);
let result = test_check_submodule("test_submodule_not_ancestor", NOT_ANCESTOR_TOPIC, &conf);
assert_eq!(result.warnings().len(), 0);
assert_eq!(result.alerts().len(), 0);
assert_eq!(result.errors().len(), 1);
assert_eq!(
result.errors()[0],
"commit 07fb2ca9c1c8c0ddfcf921e762688ffcd476bc09 references the commit \
bd89a556b6ab6f378a776713439abbc1c1f15b6d at `submodule`, but it is not available on \
the tracked branch `master`; please make the commit available from the `master` \
branch first."
);
assert!(result.temporary());
assert!(!result.allowed());
assert!(!result.pass());
}
#[test]
fn test_submodule_delete() {
let check = SubmoduleAvailable::default();
let conf = make_check_conf(&check);
let result = test_check_base(
"test_submodule_delete",
DELETE_SUBMODULE,
UNAVAILABLE_TOPIC,
&conf,
);
test_result_ok(result);
}
}