use derive_builder::Builder;
use git_checks_core::impl_prelude::*;
use thiserror::Error;
#[derive(Debug, Error)]
enum BadCommitError {
#[error("failed to list topic refs from {} to {}: {}", base, commit, output)]
RevList {
commit: CommitId,
base: CommitId,
output: String,
},
}
impl BadCommitError {
fn rev_list(commit: CommitId, base: CommitId, output: &[u8]) -> Self {
BadCommitError::RevList {
commit,
base,
output: String::from_utf8_lossy(output).into(),
}
}
}
#[derive(Builder, Debug, Clone)]
#[builder(field(private))]
pub struct BadCommit {
#[builder(setter(into))]
commit: CommitId,
#[builder(setter(into))]
reason: String,
}
impl BadCommit {
pub fn builder() -> BadCommitBuilder {
BadCommitBuilder::default()
}
fn apply(&self, result: &mut CheckResult) {
result
.add_error(format!(
"commit {} is not allowed {}.",
self.commit, self.reason,
))
.add_alert(
format!("commit {} was pushed to the server.", self.commit),
true,
);
}
}
impl Check for BadCommit {
fn name(&self) -> &str {
"bad-commit"
}
fn check(&self, _: &CheckGitContext, commit: &Commit) -> Result<CheckResult, Box<dyn Error>> {
let mut result = CheckResult::new();
if self.commit == commit.sha1 {
self.apply(&mut result);
}
Ok(result)
}
}
impl TopicCheck for BadCommit {
fn name(&self) -> &str {
"bad-commit-topic"
}
fn check(&self, ctx: &CheckGitContext, topic: &Topic) -> Result<CheckResult, Box<dyn Error>> {
let rev_list = ctx
.git()
.arg("rev-list")
.arg("--reverse")
.arg("--topo-order")
.arg(topic.sha1.as_str())
.arg(&format!("^{}", topic.base))
.output()
.map_err(|err| GitError::subcommand("rev-list", err))?;
if !rev_list.status.success() {
return Err(BadCommitError::rev_list(
topic.sha1.clone(),
topic.base.clone(),
&rev_list.stderr,
)
.into());
}
let refs = String::from_utf8_lossy(&rev_list.stdout);
let mut result = CheckResult::new();
if refs.lines().any(|rev| rev == self.commit.as_str()) {
self.apply(&mut result)
}
Ok(result)
}
}
#[cfg(feature = "config")]
pub(crate) mod config {
use git_checks_config::{register_checks, CommitCheckConfig, IntoCheck, TopicCheckConfig};
use git_workarea::CommitId;
use serde::Deserialize;
#[cfg(test)]
use serde_json::json;
#[cfg(test)]
use crate::test;
use crate::BadCommit;
#[derive(Deserialize, Debug)]
pub struct BadCommitConfig {
commit: String,
reason: String,
}
impl IntoCheck for BadCommitConfig {
type Check = BadCommit;
fn into_check(self) -> Self::Check {
let mut builder = BadCommit::builder();
builder.commit(CommitId::new(self.commit));
builder.reason(self.reason);
builder
.build()
.expect("configuration mismatch for `BadCommit`")
}
}
register_checks! {
BadCommitConfig {
"bad_commit" => CommitCheckConfig,
"bad_commit/topic" => TopicCheckConfig,
},
}
#[test]
fn test_bad_commit_config_empty() {
let json = json!({});
let err = serde_json::from_value::<BadCommitConfig>(json).unwrap_err();
test::check_missing_json_field(err, "commit");
}
#[test]
fn test_bad_commit_config_commit_is_required() {
let reason = "because";
let json = json!({
"reason": reason,
});
let err = serde_json::from_value::<BadCommitConfig>(json).unwrap_err();
test::check_missing_json_field(err, "commit");
}
#[test]
fn test_bad_commit_config_reason_is_required() {
let commit = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
let json = json!({
"commit": commit,
});
let err = serde_json::from_value::<BadCommitConfig>(json).unwrap_err();
test::check_missing_json_field(err, "reason");
}
#[test]
fn test_bad_commit_config_minimum_fields() {
let commit = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
let reason = "because";
let json = json!({
"commit": commit,
"reason": reason,
});
let check: BadCommitConfig = serde_json::from_value(json).unwrap();
assert_eq!(check.commit, commit);
assert_eq!(check.reason, reason);
let check = check.into_check();
assert_eq!(check.commit, CommitId::new(commit));
assert_eq!(check.reason, reason);
}
}
#[cfg(test)]
mod tests {
use git_checks_core::{Check, TopicCheck};
use git_workarea::CommitId;
use crate::test::*;
use crate::BadCommit;
const NO_EXIST_COMMIT: &str = "0000000000000000000000000000000000000000";
const GOOD_COMMIT: &str = "7b0c51ed98a23a32718ed7014d6d4a813423f1bd";
const BAD_COMMIT: &str = "029a00428913ee915ce5ee7250c023abfbc2aca3";
const BAD_TOPIC: &str = "3d535904b40868dcba6465cf2c3ce4358501880a";
#[test]
fn test_bad_commit_builder_default() {
assert!(BadCommit::builder().build().is_err());
}
#[test]
fn test_bad_commit_builder_reason_is_required() {
assert!(BadCommit::builder()
.commit(CommitId::new(NO_EXIST_COMMIT))
.build()
.is_err());
}
#[test]
fn test_bad_commit_builder_commit_is_required() {
assert!(BadCommit::builder().reason("because").build().is_err());
}
#[test]
fn test_bad_commit_builder_minimum_fields() {
assert!(BadCommit::builder()
.commit(CommitId::new(NO_EXIST_COMMIT))
.reason("because")
.build()
.is_ok());
}
#[test]
fn test_bad_commit_name_commit() {
let check = BadCommit::builder()
.commit(CommitId::new(NO_EXIST_COMMIT))
.reason("because")
.build()
.unwrap();
assert_eq!(Check::name(&check), "bad-commit");
}
#[test]
fn test_bad_commit_name_topic() {
let check = BadCommit::builder()
.commit(CommitId::new(NO_EXIST_COMMIT))
.reason("because")
.build()
.unwrap();
assert_eq!(TopicCheck::name(&check), "bad-commit-topic");
}
#[test]
fn test_bad_commit_good_commit() {
let check = BadCommit::builder()
.commit(CommitId::new(BAD_COMMIT))
.reason("because")
.build()
.unwrap();
run_check_ok("test_bad_commit_good_commit", GOOD_COMMIT, check);
}
#[test]
fn test_bad_commit_no_bad_commit() {
let check = BadCommit::builder()
.commit(CommitId::new(NO_EXIST_COMMIT))
.reason("because")
.build()
.unwrap();
run_check_ok("test_bad_commit_no_bad_commit", BAD_TOPIC, check);
}
#[test]
fn test_bad_commit_already_in_history() {
let check = BadCommit::builder()
.commit(CommitId::new(FILLER_COMMIT))
.reason("because")
.build()
.unwrap();
run_check_ok("test_bad_commit_already_in_history", BAD_TOPIC, check);
}
#[test]
fn test_bad_commit_not_already_in_history() {
let check = BadCommit::builder()
.commit(CommitId::new(BAD_COMMIT))
.reason("because")
.build()
.unwrap();
let result = run_check("test_bad_commit_not_already_in_history", BAD_TOPIC, check);
assert_eq!(result.warnings().len(), 0);
assert_eq!(result.alerts().len(), 1);
assert_eq!(
result.alerts()[0],
"commit 029a00428913ee915ce5ee7250c023abfbc2aca3 was pushed to the server.",
);
assert_eq!(result.errors().len(), 1);
assert_eq!(
result.errors()[0],
"commit 029a00428913ee915ce5ee7250c023abfbc2aca3 is not allowed because.",
);
assert!(!result.temporary());
assert!(!result.allowed());
assert!(!result.pass());
}
#[test]
fn test_bad_commit_topic_good_commit() {
let check = BadCommit::builder()
.commit(CommitId::new(BAD_COMMIT))
.reason("because")
.build()
.unwrap();
run_topic_check_ok("test_bad_commit_topic_good_commit", GOOD_COMMIT, check);
}
#[test]
fn test_bad_commit_topic_no_bad_commit() {
let check = BadCommit::builder()
.commit(CommitId::new(NO_EXIST_COMMIT))
.reason("because")
.build()
.unwrap();
run_topic_check_ok("test_bad_commit_topic_no_bad_commit", BAD_TOPIC, check);
}
#[test]
fn test_bad_commit_topic_already_in_history() {
let check = BadCommit::builder()
.commit(CommitId::new(FILLER_COMMIT))
.reason("because")
.build()
.unwrap();
run_topic_check_ok("test_bad_commit_topic_already_in_history", BAD_TOPIC, check);
}
#[test]
fn test_bad_commit_topic_not_already_in_history() {
let check = BadCommit::builder()
.commit(CommitId::new(BAD_COMMIT))
.reason("because")
.build()
.unwrap();
let result = run_topic_check(
"test_bad_commit_topic_not_already_in_history",
BAD_TOPIC,
check,
);
assert_eq!(result.warnings().len(), 0);
assert_eq!(result.alerts().len(), 1);
assert_eq!(
result.alerts()[0],
"commit 029a00428913ee915ce5ee7250c023abfbc2aca3 was pushed to the server.",
);
assert_eq!(result.errors().len(), 1);
assert_eq!(
result.errors()[0],
"commit 029a00428913ee915ce5ee7250c023abfbc2aca3 is not allowed because.",
);
assert!(!result.temporary());
assert!(!result.allowed());
assert!(!result.pass());
}
}