use std::fmt::{self, Debug};
use std::iter;
use std::slice;
use crates::git_workarea::{CommitId, GitContext, GitError, Identity, WorkAreaError};
use crates::rayon::prelude::*;
use crates::thiserror::Error;
use check::{BranchCheck, Check, CheckResult, TopicCheck};
use commit::{Commit, CommitError, Topic};
use context::CheckGitContext;
#[derive(Debug, Error)]
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,
},
#[doc(hidden)]
#[error("unreachable...")]
_NonExhaustive,
}
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![],
checks_branch: vec![],
checks_topic: vec![],
}
}
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 }}",
self.checks.len(),
self.checks_branch.len(),
)
}
}