use derive_builder::Builder;
use git_checks_core::impl_prelude::*;
use thiserror::Error;
#[derive(Debug, Error)]
enum FastForwardError {
#[error(
"failed to get the merge-base for {} against a target branch {} ({:?}): {}",
commit,
base,
code,
output
)]
MergeBase {
commit: CommitId,
base: CommitId,
code: Option<i32>,
output: String,
},
}
impl FastForwardError {
fn merge_base(commit: CommitId, base: CommitId, code: Option<i32>, output: &[u8]) -> Self {
Self::MergeBase {
commit,
base,
code,
output: String::from_utf8_lossy(output).into(),
}
}
}
#[derive(Builder, Debug, Clone)]
#[builder(field(private))]
pub struct FastForward {
#[builder(setter(into))]
branch: CommitId,
#[builder(default = "false")]
required: bool,
}
impl FastForward {
pub fn builder() -> FastForwardBuilder {
FastForwardBuilder::default()
}
}
impl BranchCheck for FastForward {
fn name(&self) -> &str {
"fast-forward"
}
fn check(
&self,
ctx: &CheckGitContext,
commit: &CommitId,
) -> Result<CheckResult, Box<dyn Error>> {
let merge_base = ctx
.git()
.arg("merge-base")
.arg("--is-ancestor")
.arg(self.branch.as_str())
.arg(commit.as_str())
.output()
.map_err(|err| GitError::subcommand("merge-base", err))?;
let ok = match merge_base.status.code() {
Some(0) => true,
Some(1) => false,
code => {
return Err(FastForwardError::merge_base(
commit.clone(),
self.branch.clone(),
code,
&merge_base.stderr,
)
.into());
},
};
let mut result = CheckResult::new();
if !ok {
if self.required {
result.add_error(format!(
"This branch is ineligible for the fast-forward merging into the `{}` branch; \
it needs to be rebased.",
self.branch,
));
} else {
result.add_warning(format!(
"Not eligible for fast-forward merging into `{}`.",
self.branch,
));
}
}
Ok(result)
}
}
#[cfg(feature = "config")]
pub(crate) mod config {
use git_checks_config::{register_checks, BranchCheckConfig, IntoCheck};
use git_workarea::CommitId;
use serde::Deserialize;
#[cfg(test)]
use serde_json::json;
#[cfg(test)]
use crate::test;
use crate::FastForward;
#[derive(Deserialize, Debug)]
pub struct FastForwardConfig {
branch: String,
#[serde(default)]
required: Option<bool>,
}
impl IntoCheck for FastForwardConfig {
type Check = FastForward;
fn into_check(self) -> Self::Check {
let mut builder = FastForward::builder();
builder.branch(CommitId::new(self.branch));
if let Some(required) = self.required {
builder.required(required);
}
builder
.build()
.expect("configuration mismatch for `FastForward`")
}
}
register_checks! {
FastForwardConfig {
"fast_forward" => BranchCheckConfig,
},
}
#[test]
fn test_fast_forward_config_empty() {
let json = json!({});
let err = serde_json::from_value::<FastForwardConfig>(json).unwrap_err();
test::check_missing_json_field(err, "branch");
}
#[test]
fn test_fast_forward_config_branch_is_required() {
let json = json!({});
let err = serde_json::from_value::<FastForwardConfig>(json).unwrap_err();
test::check_missing_json_field(err, "branch");
}
#[test]
fn test_fast_forward_config_minimum_fields() {
let exp_branch = CommitId::new("v1.x");
let json = json!({
"branch": exp_branch.as_str(),
});
let check: FastForwardConfig = serde_json::from_value(json).unwrap();
assert_eq!(check.branch, exp_branch.as_str());
assert_eq!(check.required, None);
let check = check.into_check();
assert_eq!(check.branch, exp_branch);
assert!(!check.required);
}
#[test]
fn test_fast_forward_config_all_fields() {
let exp_branch = CommitId::new("v1.x");
let json = json!({
"branch": exp_branch.as_str(),
"required": true,
});
let check: FastForwardConfig = serde_json::from_value(json).unwrap();
assert_eq!(check.branch, exp_branch.as_str());
assert_eq!(check.required, Some(true));
let check = check.into_check();
assert_eq!(check.branch, exp_branch);
assert!(check.required);
}
}
#[cfg(test)]
mod tests {
use git_checks_core::BranchCheck;
use git_workarea::CommitId;
use crate::builders::FastForwardBuilder;
use crate::test::*;
use crate::FastForward;
const RELEASE_BRANCH: &str = "3a22ca19fda09183da2faab60819ff6807568acd";
const NON_FF_TOPIC: &str = "a61fd3759b61a4a1f740f3fe656bc42151cefbdd";
const FF_TOPIC: &str = "969b794ea82ce51d1555852de33bfcb63dfec969";
#[test]
fn test_fast_forward_builder_default() {
assert!(FastForward::builder().build().is_err());
}
#[test]
fn test_fast_forward_builder_branch_is_required() {
assert!(FastForward::builder().build().is_err());
}
#[test]
fn test_fast_forward_builder_minimum_fields() {
assert!(FastForward::builder()
.branch(CommitId::new("release"))
.build()
.is_ok());
}
#[test]
fn test_fast_forward_name_branch() {
let check = FastForward::builder()
.branch(CommitId::new("release"))
.build()
.unwrap();
assert_eq!(BranchCheck::name(&check), "fast-forward");
}
fn make_fast_forward_check() -> FastForwardBuilder {
let mut builder = FastForward::builder();
builder.branch(CommitId::new(RELEASE_BRANCH));
builder
}
#[test]
fn test_fast_forward_ok() {
let check = make_fast_forward_check().build().unwrap();
run_branch_check_ok("test_fast_forward_ok", FF_TOPIC, check);
}
#[test]
fn test_fast_forward_ok_required() {
let check = make_fast_forward_check().required(true).build().unwrap();
run_branch_check_ok("test_fast_forward_ok_required", FF_TOPIC, check);
}
#[test]
fn test_fast_forward_bad() {
let check = make_fast_forward_check().build().unwrap();
let result = run_branch_check("test_fast_forward_bad", NON_FF_TOPIC, check);
test_result_warnings(
result,
&["Not eligible for fast-forward merging into \
`3a22ca19fda09183da2faab60819ff6807568acd`."],
);
}
#[test]
fn test_fast_forward_bad_required() {
let check = make_fast_forward_check().required(true).build().unwrap();
let result = run_branch_check("test_fast_forward_bad_required", NON_FF_TOPIC, check);
test_result_errors(
result,
&[
"This branch is ineligible for the fast-forward merging into the \
`3a22ca19fda09183da2faab60819ff6807568acd` branch; it needs to be rebased.",
],
);
}
}