use std::fmt::{self, Debug};
use std::iter;
use std::slice;
use git_workarea::{CommitId, GitContext, GitError, Identity, WorkAreaError};
use log::{debug, error};
use rayon::prelude::*;
use thiserror::Error;
use crate::check::{BranchCheck, Check, CheckResult, TopicCheck};
use crate::commit::{Commit, CommitError, Topic};
use crate::context::CheckGitContext;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum RunError {
#[error("git error: {}", source)]
Git {
#[from]
source: GitError,
},
#[error("git workarea error: {}", source)]
WorkArea {
#[from]
source: WorkAreaError,
},
#[error("commit error: {}", source)]
Commit {
#[from]
source: CommitError,
},
#[error("run check error: failed to update the {} ref: {}", base_ref, output)]
UpdateRef {
base_ref: CommitId,
output: String,
},
#[error(
"run check error: failed to list refs from {} to {}",
base_ref,
new_ref
)]
RevList {
base_ref: CommitId,
new_ref: CommitId,
output: String,
},
}
impl RunError {
fn update_ref(base_ref: CommitId, output: &[u8]) -> Self {
RunError::UpdateRef {
base_ref,
output: String::from_utf8_lossy(output).into(),
}
}
fn rev_list(base_ref: CommitId, new_ref: CommitId, output: &[u8]) -> Self {
RunError::RevList {
base_ref,
new_ref,
output: String::from_utf8_lossy(output).into(),
}
}
}
#[derive(Default, Clone)]
pub struct GitCheckConfiguration<'a> {
checks: Vec<&'a dyn Check>,
checks_branch: Vec<&'a dyn BranchCheck>,
checks_topic: Vec<&'a dyn TopicCheck>,
}
#[derive(Debug)]
pub struct TopicCheckResult {
commit_results: Vec<(CommitId, CheckResult)>,
topic_result: CheckResult,
}
impl TopicCheckResult {
pub fn commit_results(&self) -> slice::Iter<(CommitId, CheckResult)> {
self.commit_results.iter()
}
pub fn topic_result(&self) -> &CheckResult {
&self.topic_result
}
}
impl From<TopicCheckResult> for CheckResult {
fn from(res: TopicCheckResult) -> Self {
res.commit_results
.into_iter()
.map(|(_, result)| result)
.chain(iter::once(res.topic_result))
.fold(Self::new(), Self::combine)
}
}
impl<'a> GitCheckConfiguration<'a> {
pub fn new() -> Self {
GitCheckConfiguration {
checks: Vec::new(),
checks_branch: Vec::new(),
checks_topic: Vec::new(),
}
}
pub fn add_check(&mut self, check: &'a dyn Check) -> &mut Self {
self.checks.push(check);
self
}
pub fn add_branch_check(&mut self, check: &'a dyn BranchCheck) -> &mut Self {
self.checks_branch.push(check);
self
}
pub fn add_topic_check(&mut self, check: &'a dyn TopicCheck) -> &mut Self {
self.checks_topic.push(check);
self
}
fn list(
&self,
ctx: &GitContext,
reason: &str,
base_branch: &CommitId,
topic: &CommitId,
) -> Result<Vec<CommitId>, RunError> {
let (new_ref, base_ref) = ctx.reserve_refs(format!("check/{}", reason), topic)?;
let update_ref = ctx
.git()
.arg("update-ref")
.args(["-m", reason])
.arg(&base_ref)
.arg(base_branch.as_str())
.output()
.map_err(|err| GitError::subcommand("update-ref", err))?;
if !update_ref.status.success() {
return Err(RunError::update_ref(
CommitId::new(base_ref),
&update_ref.stderr,
));
}
let rev_list = ctx
.git()
.arg("rev-list")
.arg("--reverse")
.arg("--topo-order")
.arg(&new_ref)
.arg(&format!("^{}", base_ref))
.output()
.map_err(|err| GitError::subcommand("rev-list", err))?;
if !rev_list.status.success() {
return Err(RunError::rev_list(
CommitId::new(base_ref),
CommitId::new(new_ref),
&rev_list.stderr,
));
}
let refs = String::from_utf8_lossy(&rev_list.stdout);
Ok(refs.lines().map(CommitId::new).collect())
}
fn run_check(ctx: &CheckGitContext, check: &dyn Check, commit: &Commit) -> CheckResult {
debug!(
target: "git-checks",
"running check {} on commit {}",
check.name(),
commit.sha1,
);
check.check(ctx, commit).unwrap_or_else(|err| {
error!(
target: "git-checks",
"check {} failed on commit {}: {:?}",
check.name(),
commit.sha1,
err,
);
let mut res = CheckResult::new();
res.add_alert(
format!(
"failed to run the {} check on commit {}",
check.name(),
commit.sha1,
),
true,
);
res
})
}
fn run_branch_check(
ctx: &CheckGitContext,
check: &dyn BranchCheck,
commit: &CommitId,
) -> CheckResult {
debug!(target: "git-checks", "running check {}", check.name());
check.check(ctx, commit).unwrap_or_else(|err| {
error!(
target: "git-checks",
"branch check {}: {:?}",
check.name(),
err,
);
let mut res = CheckResult::new();
res.add_alert(
format!("failed to run the {} branch check", check.name()),
true,
);
res
})
}
fn run_topic_check(
ctx: &CheckGitContext,
check: &dyn TopicCheck,
topic: &Topic,
) -> CheckResult {
debug!(target: "git-checks", "running check {}", check.name());
check.check(ctx, topic).unwrap_or_else(|err| {
error!(
target: "git-checks",
"topic check {}: {:?}",
check.name(),
err,
);
let mut res = CheckResult::new();
res.add_alert(
format!("failed to run the {} topic check", check.name()),
true,
);
res
})
}
fn run_topic_impl(
&self,
ctx: &GitContext,
base: &CommitId,
refs: Vec<CommitId>,
owner: &Identity,
) -> Result<TopicCheckResult, RunError> {
let topic_result = refs.last().map_or_else(
|| Ok(CheckResult::new()) as Result<_, RunError>,
|head_commit| {
if self.checks_branch.is_empty() && self.checks_topic.is_empty() {
return Ok(CheckResult::new());
}
let workarea = ctx.prepare(head_commit)?;
let check_ctx = CheckGitContext::new(workarea, owner.clone());
let topic = Topic::new(ctx, base, head_commit)?;
Ok(self
.checks_branch
.par_iter()
.map(|&check| Self::run_branch_check(&check_ctx, check, head_commit))
.chain(
self.checks_topic
.par_iter()
.map(|&check| Self::run_topic_check(&check_ctx, check, &topic)),
)
.reduce(CheckResult::new, CheckResult::combine))
},
)?;
let commit_results = refs
.into_par_iter()
.map(|sha1| {
self.run_commit(ctx, &sha1, owner)
.map(|result| (sha1, result))
})
.collect::<Vec<Result<_, RunError>>>()
.into_iter()
.collect::<Result<Vec<_>, RunError>>()?;
Ok(TopicCheckResult {
commit_results,
topic_result,
})
}
pub fn run_commit(
&self,
ctx: &GitContext,
commit: &CommitId,
owner: &Identity,
) -> Result<CheckResult, RunError> {
if self.checks.is_empty() {
return Ok(CheckResult::new());
}
let workarea = ctx.prepare(commit)?;
let check_ctx = CheckGitContext::new(workarea, owner.clone());
let commit = Commit::new(ctx, commit)?;
Ok(self
.checks
.par_iter()
.map(|&check| Self::run_check(&check_ctx, check, &commit))
.reduce(CheckResult::new, CheckResult::combine))
}
pub fn run_topic<R>(
&self,
ctx: &GitContext,
reason: R,
base_branch: &CommitId,
topic: &CommitId,
owner: &Identity,
) -> Result<TopicCheckResult, RunError>
where
R: AsRef<str>,
{
let refs = self.list(ctx, reason.as_ref(), base_branch, topic)?;
self.run_topic_impl(ctx, base_branch, refs, owner)
}
}
impl<'a> Debug for GitCheckConfiguration<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"GitCheckConfiguration {{ {} commit checks, {} branch checks, {} topic checks }}",
self.checks.len(),
self.checks_branch.len(),
self.checks_topic.len(),
)
}
}
#[cfg(test)]
mod test {
use std::iter;
use std::path::Path;
use git_workarea::{CommitId, GitContext, Identity};
use crate::run::{CheckResult, GitCheckConfiguration, TopicCheckResult};
mod checks {
use thiserror::Error;
use crate::impl_prelude::*;
#[derive(Debug)]
pub struct FailingCheck {}
#[derive(Debug, Error)]
#[error("the failing check did its thing")]
struct FailingCheckError;
impl Check for FailingCheck {
fn name(&self) -> &str {
"test-failing-check-commit"
}
fn check(
&self,
_: &CheckGitContext,
_: &Commit,
) -> Result<CheckResult, Box<dyn Error>> {
Err(FailingCheckError.into())
}
}
impl BranchCheck for FailingCheck {
fn name(&self) -> &str {
"test-failing-check-branch"
}
fn check(
&self,
_: &CheckGitContext,
_: &CommitId,
) -> Result<CheckResult, Box<dyn Error>> {
Err(FailingCheckError.into())
}
}
impl TopicCheck for FailingCheck {
fn name(&self) -> &str {
"test-failing-check-topic"
}
fn check(&self, _: &CheckGitContext, _: &Topic) -> Result<CheckResult, Box<dyn Error>> {
Err(FailingCheckError.into())
}
}
}
#[test]
fn test_configuration_debug() {
let config = GitCheckConfiguration::new();
assert_eq!(
format!("{:?}", config),
"GitCheckConfiguration { 0 commit checks, 0 branch checks, 0 topic checks }",
);
}
const TARGET_COMMIT: &str = "27ff3ef5532d76afa046f76f4dd8f588dc3e83c3";
const TOPIC_COMMIT: &str = "a61fd3759b61a4a1f740f3fe656bc42151cefbdd";
const TOPIC2_COMMIT: &str = "112e9b34401724bff57f68cf47c5065d4342b263";
fn git_context() -> GitContext {
let gitdir = Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/../.git"));
if !gitdir.exists() {
panic!("The tests must be run from a git checkout.");
}
GitContext::new(gitdir)
}
fn run_commit(config: &GitCheckConfiguration) -> CheckResult {
let ctx = git_context();
config
.run_commit(
&ctx,
&CommitId::new(TOPIC_COMMIT),
&Identity::new(
"Rust Git Checks Core Tests",
"rust-git-checks@example.invalid",
),
)
.unwrap()
}
fn run_topic(
test_name: &str,
config: &GitCheckConfiguration,
commit: &str,
) -> TopicCheckResult {
let ctx = git_context();
config
.run_topic(
&ctx,
test_name,
&CommitId::new(TARGET_COMMIT),
&CommitId::new(commit),
&Identity::new(
"Rust Git Checks Core Tests",
"rust-git-checks@example.invalid",
),
)
.unwrap()
}
fn no_strings<'a>() -> iter::Empty<&'a String> {
iter::empty()
}
fn check_result(result: &CheckResult, errors: &[&str]) {
itertools::assert_equal(result.warnings(), no_strings());
itertools::assert_equal(result.alerts(), errors);
itertools::assert_equal(result.errors(), no_strings());
assert!(!result.temporary());
assert!(!result.allowed());
assert!(!result.pass());
}
fn check_result_ok(result: &CheckResult) {
itertools::assert_equal(result.warnings(), no_strings());
itertools::assert_equal(result.alerts(), no_strings());
itertools::assert_equal(result.errors(), no_strings());
assert!(!result.temporary());
assert!(!result.allowed());
assert!(result.pass());
}
#[test]
fn test_commit_check_errors_commit() {
let check = self::checks::FailingCheck {};
let mut config = GitCheckConfiguration::new();
config.add_check(&check);
let result = run_commit(&config);
check_result(
&result,
&[
"failed to run the test-failing-check-commit check on commit \
a61fd3759b61a4a1f740f3fe656bc42151cefbdd",
],
);
}
#[test]
fn test_branch_check_errors_commit() {
let check = self::checks::FailingCheck {};
let mut config = GitCheckConfiguration::new();
config.add_branch_check(&check);
let result = run_commit(&config);
check_result_ok(&result);
}
#[test]
fn test_topic_check_errors_commit() {
let check = self::checks::FailingCheck {};
let mut config = GitCheckConfiguration::new();
config.add_topic_check(&check);
let result = run_commit(&config);
check_result_ok(&result);
}
#[test]
fn test_commit_check_errors_topic() {
let check = self::checks::FailingCheck {};
let mut config = GitCheckConfiguration::new();
config.add_check(&check);
let result = run_topic("test_commit_check_errors_topic", &config, TOPIC_COMMIT);
let mut commit_results = result.commit_results();
let (commit, commit_result) = commit_results.next().unwrap();
assert_eq!(commit.as_str(), "a61fd3759b61a4a1f740f3fe656bc42151cefbdd");
check_result(
commit_result,
&[
"failed to run the test-failing-check-commit check on commit \
a61fd3759b61a4a1f740f3fe656bc42151cefbdd",
],
);
if let Some(res) = commit_results.next() {
panic!(
"multiple commits returned from a single-commit topic: {:?}",
res,
);
}
check_result_ok(result.topic_result());
check_result(
&result.into(),
&[
"failed to run the test-failing-check-commit check on commit \
a61fd3759b61a4a1f740f3fe656bc42151cefbdd",
],
);
}
#[test]
fn test_branch_check_errors_topic() {
let check = self::checks::FailingCheck {};
let mut config = GitCheckConfiguration::new();
config.add_branch_check(&check);
let result = run_topic("test_branch_check_errors_topic", &config, TOPIC_COMMIT);
let mut commit_results = result.commit_results();
let (commit, commit_result) = commit_results.next().unwrap();
assert_eq!(commit.as_str(), "a61fd3759b61a4a1f740f3fe656bc42151cefbdd");
check_result_ok(commit_result);
if let Some(res) = commit_results.next() {
panic!(
"multiple commits returned from a single-commit topic: {:?}",
res,
);
}
check_result(
result.topic_result(),
&["failed to run the test-failing-check-branch branch check"],
);
check_result(
&result.into(),
&["failed to run the test-failing-check-branch branch check"],
);
}
#[test]
fn test_topic_check_errors_topic() {
let check = self::checks::FailingCheck {};
let mut config = GitCheckConfiguration::new();
config.add_topic_check(&check);
let result = run_topic("test_topic_check_errors_topic", &config, TOPIC_COMMIT);
let mut commit_results = result.commit_results();
let (commit, commit_result) = commit_results.next().unwrap();
assert_eq!(commit.as_str(), "a61fd3759b61a4a1f740f3fe656bc42151cefbdd");
check_result_ok(commit_result);
if let Some(res) = commit_results.next() {
panic!(
"multiple commits returned from a single-commit topic: {:?}",
res,
);
}
check_result(
result.topic_result(),
&["failed to run the test-failing-check-topic topic check"],
);
check_result(
&result.into(),
&["failed to run the test-failing-check-topic topic check"],
);
}
#[test]
fn test_multiple_check_commit() {
let check = self::checks::FailingCheck {};
let mut config = GitCheckConfiguration::new();
config.add_check(&check);
config.add_check(&check);
config.add_branch_check(&check);
config.add_branch_check(&check);
config.add_topic_check(&check);
config.add_topic_check(&check);
let result = run_commit(&config);
check_result(
&result,
&[
"failed to run the test-failing-check-commit check on commit \
a61fd3759b61a4a1f740f3fe656bc42151cefbdd",
"failed to run the test-failing-check-commit check on commit \
a61fd3759b61a4a1f740f3fe656bc42151cefbdd",
],
);
}
#[test]
fn test_multiple_check_topic() {
let check = self::checks::FailingCheck {};
let mut config = GitCheckConfiguration::new();
config.add_check(&check);
config.add_check(&check);
config.add_branch_check(&check);
config.add_branch_check(&check);
config.add_topic_check(&check);
config.add_topic_check(&check);
let result = run_topic("test_multiple_check_topic", &config, TOPIC_COMMIT);
let mut commit_results = result.commit_results();
let (commit, commit_result) = commit_results.next().unwrap();
assert_eq!(commit.as_str(), "a61fd3759b61a4a1f740f3fe656bc42151cefbdd");
check_result(
commit_result,
&[
"failed to run the test-failing-check-commit check on commit \
a61fd3759b61a4a1f740f3fe656bc42151cefbdd",
"failed to run the test-failing-check-commit check on commit \
a61fd3759b61a4a1f740f3fe656bc42151cefbdd",
],
);
if let Some(res) = commit_results.next() {
panic!(
"multiple commits returned from a single-commit topic: {:?}",
res,
);
}
check_result(
result.topic_result(),
&[
"failed to run the test-failing-check-branch branch check",
"failed to run the test-failing-check-branch branch check",
"failed to run the test-failing-check-topic topic check",
"failed to run the test-failing-check-topic topic check",
],
);
check_result(
&result.into(),
&[
"failed to run the test-failing-check-commit check on commit \
a61fd3759b61a4a1f740f3fe656bc42151cefbdd",
"failed to run the test-failing-check-commit check on commit \
a61fd3759b61a4a1f740f3fe656bc42151cefbdd",
"failed to run the test-failing-check-branch branch check",
"failed to run the test-failing-check-branch branch check",
"failed to run the test-failing-check-topic topic check",
"failed to run the test-failing-check-topic topic check",
],
);
}
#[test]
fn test_check_multiple_topic() {
let check = self::checks::FailingCheck {};
let mut config = GitCheckConfiguration::new();
config.add_check(&check);
config.add_branch_check(&check);
config.add_topic_check(&check);
let result = run_topic("test_check_multiple_topic", &config, TOPIC2_COMMIT);
let mut commit_results = result.commit_results();
let (commit, commit_result) = commit_results.next().unwrap();
assert_eq!(commit.as_str(), "a61fd3759b61a4a1f740f3fe656bc42151cefbdd");
check_result(
commit_result,
&[
"failed to run the test-failing-check-commit check on commit \
a61fd3759b61a4a1f740f3fe656bc42151cefbdd",
],
);
let (commit, commit_result) = commit_results.next().unwrap();
assert_eq!(commit.as_str(), "112e9b34401724bff57f68cf47c5065d4342b263");
check_result(
commit_result,
&[
"failed to run the test-failing-check-commit check on commit \
112e9b34401724bff57f68cf47c5065d4342b263",
],
);
if let Some(res) = commit_results.next() {
panic!(
"multiple commits returned from a single-commit topic: {:?}",
res,
);
}
check_result(
result.topic_result(),
&[
"failed to run the test-failing-check-branch branch check",
"failed to run the test-failing-check-topic topic check",
],
);
check_result(
&result.into(),
&[
"failed to run the test-failing-check-commit check on commit \
a61fd3759b61a4a1f740f3fe656bc42151cefbdd",
"failed to run the test-failing-check-commit check on commit \
112e9b34401724bff57f68cf47c5065d4342b263",
"failed to run the test-failing-check-branch branch check",
"failed to run the test-failing-check-topic topic check",
],
);
}
}