use derive_builder::Builder;
use git_checks_core::impl_prelude::*;
use thiserror::Error;
#[derive(Debug, Error)]
enum ReleaseBranchError {
#[error(
"failed to get the merge-base for {} against a release branch {}: {}",
commit,
base,
output
)]
MergeBase {
commit: CommitId,
base: CommitId,
output: String,
},
}
impl ReleaseBranchError {
fn merge_base(commit: CommitId, base: CommitId, output: &[u8]) -> Self {
ReleaseBranchError::MergeBase {
commit,
base,
output: String::from_utf8_lossy(output).into(),
}
}
}
#[derive(Builder, Debug, Clone)]
#[builder(field(private))]
pub struct ReleaseBranch {
#[builder(setter(into))]
branch: String,
#[builder(setter(into))]
disallowed_commit: CommitId,
#[builder(default = "false")]
required: bool,
}
impl ReleaseBranch {
pub fn builder() -> ReleaseBranchBuilder {
ReleaseBranchBuilder::default()
}
}
impl BranchCheck for ReleaseBranch {
fn name(&self) -> &str {
"release-branch"
}
fn check(
&self,
ctx: &CheckGitContext,
commit: &CommitId,
) -> Result<CheckResult, Box<dyn Error>> {
let merge_base = ctx
.git()
.arg("merge-base")
.arg("--all")
.arg(commit.as_str())
.arg(self.disallowed_commit.as_str())
.output()
.map_err(|err| GitError::subcommand("merge-base", err))?;
if !merge_base.status.success() {
return Err(ReleaseBranchError::merge_base(
commit.clone(),
self.disallowed_commit.clone(),
&merge_base.stderr,
)
.into());
}
let merge_bases = String::from_utf8_lossy(&merge_base.stdout);
let is_eligible = merge_bases
.lines()
.all(|merge_base| merge_base != self.disallowed_commit.as_str());
let mut result = CheckResult::new();
if is_eligible && !self.required {
result.add_warning(format!("Eligible for the {} branch.", self.branch));
} else if !is_eligible && self.required {
result.add_error(format!(
"This branch is ineligible for the {} branch; it needs to \
be based on a commit before {}.",
self.branch, self.disallowed_commit,
));
}
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::ReleaseBranch;
#[derive(Deserialize, Debug)]
pub struct ReleaseBranchConfig {
branch: String,
disallowed_commit: String,
#[serde(default)]
required: Option<bool>,
}
impl IntoCheck for ReleaseBranchConfig {
type Check = ReleaseBranch;
fn into_check(self) -> Self::Check {
let mut builder = ReleaseBranch::builder();
builder
.branch(self.branch)
.disallowed_commit(CommitId::new(self.disallowed_commit));
if let Some(required) = self.required {
builder.required(required);
}
builder
.build()
.expect("configuration mismatch for `ReleaseBranch`")
}
}
register_checks! {
ReleaseBranchConfig {
"release_branch" => BranchCheckConfig,
},
}
#[test]
fn test_release_branch_config_empty() {
let json = json!({});
let err = serde_json::from_value::<ReleaseBranchConfig>(json).unwrap_err();
test::check_missing_json_field(err, "branch");
}
#[test]
fn test_release_branch_config_branch_is_required() {
let exp_disallowed_commit = "post-branch commit hash";
let json = json!({
"disallowed_commit": exp_disallowed_commit,
});
let err = serde_json::from_value::<ReleaseBranchConfig>(json).unwrap_err();
test::check_missing_json_field(err, "branch");
}
#[test]
fn test_release_branch_config_disallowed_commit_is_required() {
let exp_branch = "v1.x";
let json = json!({
"branch": exp_branch,
});
let err = serde_json::from_value::<ReleaseBranchConfig>(json).unwrap_err();
test::check_missing_json_field(err, "disallowed_commit");
}
#[test]
fn test_release_branch_config_minimum_fields() {
let exp_branch = "v1.x";
let exp_disallowed_commit = "post-branch commit hash";
let json = json!({
"branch": exp_branch,
"disallowed_commit": exp_disallowed_commit,
});
let check: ReleaseBranchConfig = serde_json::from_value(json).unwrap();
assert_eq!(check.branch, exp_branch);
assert_eq!(check.disallowed_commit, exp_disallowed_commit);
assert_eq!(check.required, None);
let check = check.into_check();
assert_eq!(check.branch, exp_branch);
assert_eq!(
check.disallowed_commit,
CommitId::new(exp_disallowed_commit),
);
assert!(!check.required);
}
#[test]
fn test_release_branch_config_all_fields() {
let exp_branch = "v1.x";
let exp_disallowed_commit = "post-branch commit hash";
let json = json!({
"branch": exp_branch,
"disallowed_commit": exp_disallowed_commit,
"required": true,
});
let check: ReleaseBranchConfig = serde_json::from_value(json).unwrap();
assert_eq!(check.branch, exp_branch);
assert_eq!(check.disallowed_commit, exp_disallowed_commit);
assert_eq!(check.required, Some(true));
let check = check.into_check();
assert_eq!(check.branch, exp_branch);
assert_eq!(
check.disallowed_commit,
CommitId::new(exp_disallowed_commit),
);
assert!(check.required);
}
}
#[cfg(test)]
mod tests {
use git_checks_core::BranchCheck;
use git_workarea::CommitId;
use crate::builders::ReleaseBranchBuilder;
use crate::test::*;
use crate::ReleaseBranch;
const RELEASE_BRANCH: &str = "3a22ca19fda09183da2faab60819ff6807568acd";
const POST_RELEASE_COMMIT: &str = "d02f015907371738253a22b9a7fec78607a969b2";
const POST_RELEASE_BRANCH: &str = "a61fd3759b61a4a1f740f3fe656bc42151cefbdd";
const POST_RELEASE_BRANCH_MERGE: &str = "c58dd19d9976722d82aa6bc6a52a2a01a52bd9e8";
#[test]
fn test_release_branch_builder_default() {
assert!(ReleaseBranch::builder().build().is_err());
}
#[test]
fn test_release_branch_builder_branch_is_required() {
assert!(ReleaseBranch::builder()
.disallowed_commit(CommitId::new(POST_RELEASE_COMMIT))
.build()
.is_err());
}
#[test]
fn test_release_branch_builder_commit_is_required() {
assert!(ReleaseBranch::builder().branch("release").build().is_err());
}
#[test]
fn test_release_branch_builder_minimum_fields() {
assert!(ReleaseBranch::builder()
.branch("release")
.disallowed_commit(CommitId::new(POST_RELEASE_COMMIT))
.build()
.is_ok());
}
#[test]
fn test_release_branch_name_branch() {
let check = ReleaseBranch::builder()
.branch("release")
.disallowed_commit(CommitId::new(POST_RELEASE_COMMIT))
.build()
.unwrap();
assert_eq!(BranchCheck::name(&check), "release-branch");
}
fn make_release_branch_check() -> ReleaseBranchBuilder {
let mut builder = ReleaseBranch::builder();
builder
.branch("release")
.disallowed_commit(CommitId::new(POST_RELEASE_COMMIT));
builder
}
#[test]
fn test_release_branch_ok() {
let check = make_release_branch_check().build().unwrap();
let result = run_branch_check("test_release_branch_ok", RELEASE_BRANCH, check);
test_result_warnings(result, &["Eligible for the release branch."]);
}
#[test]
fn test_release_branch_ok_required() {
let check = make_release_branch_check().required(true).build().unwrap();
run_branch_check_ok("test_release_branch_ok_required", RELEASE_BRANCH, check);
}
#[test]
fn test_post_release_branch() {
let check = make_release_branch_check().build().unwrap();
run_branch_check_ok("test_post_release_branch", POST_RELEASE_BRANCH, check);
}
#[test]
fn test_post_release_branch_required() {
let check = make_release_branch_check().required(true).build().unwrap();
let result = run_branch_check(
"test_post_release_branch_required",
POST_RELEASE_BRANCH,
check,
);
test_result_errors(result, &[
"This branch is ineligible for the release branch; it needs to be based on a commit \
before d02f015907371738253a22b9a7fec78607a969b2.",
]);
}
#[test]
fn test_post_release_branch_merge() {
let check = make_release_branch_check().build().unwrap();
run_branch_check_ok(
"test_post_release_branch_merge",
POST_RELEASE_BRANCH_MERGE,
check,
);
}
#[test]
fn test_post_release_branch_merge_required() {
let check = make_release_branch_check().required(true).build().unwrap();
let result = run_branch_check(
"test_post_release_branch_merge_required",
POST_RELEASE_BRANCH_MERGE,
check,
);
test_result_errors(result, &[
"This branch is ineligible for the release branch; it needs to be based on a commit \
before d02f015907371738253a22b9a7fec78607a969b2.",
]);
}
}