use std::path::Path;
use serde_json::{json, Map, Value};
use time::format_description::well_known::Rfc3339;
use time::OffsetDateTime;
use crate::event::Phase;
use crate::host::Check;
use crate::{git, ids};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Gate {
PrePush,
RequiredChecks,
}
impl Gate {
fn word(self) -> &'static str {
match self {
Gate::PrePush => "pre-push",
Gate::RequiredChecks => "required-checks",
}
}
pub(crate) fn phase(self) -> Phase {
match self {
Gate::PrePush => Phase::Integrate,
Gate::RequiredChecks => Phase::Review,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Ruling {
Passed,
PassedWithSkipped,
Failed,
NoVerdict,
}
impl Ruling {
fn word(self) -> &'static str {
match self {
Ruling::Passed => "passed",
Ruling::PassedWithSkipped => "passed-with-skipped",
Ruling::Failed => "failed",
Ruling::NoVerdict => "no-verdict",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) struct Moment(OffsetDateTime);
impl Moment {
pub(crate) fn now() -> Self {
Self::at(OffsetDateTime::now_utc())
}
fn at(when: OffsetDateTime) -> Self {
let whole = when.nanosecond() - when.nanosecond() % 1_000_000;
Self(when.replace_nanosecond(whole).unwrap_or(when))
}
pub(crate) fn reported(spelled: &str) -> Option<Self> {
let parsed = OffsetDateTime::parse(spelled.trim(), &Rfc3339).ok()?;
(parsed.year() > 1).then(|| Self::at(parsed))
}
pub(crate) fn stamp(self) -> String {
ids::stamp(self.0)
}
fn milliseconds_since(self, earlier: Moment) -> i128 {
(self.0 - earlier.0).whole_milliseconds()
}
}
pub(crate) struct Run {
pub(crate) gate: Gate,
pub(crate) started: Moment,
pub(crate) ended: Moment,
pub(crate) verdict: Ruling,
pub(crate) checks: Vec<Value>,
}
impl Run {
pub(crate) fn payload(&self, attempt: u32) -> Map<String, Value> {
let milliseconds = self.ended.milliseconds_since(self.started);
let object = json!({
"gate": self.gate.word(),
"attempt": attempt,
"started_at": self.started.stamp(),
"ended_at": self.ended.stamp(),
"seconds": milliseconds as f64 / 1000.0,
"verdict": self.verdict.word(),
"checks": self.checks,
});
match object {
Value::Object(fields) => fields,
_ => unreachable!("a JSON object literal is an object"),
}
}
}
pub(crate) fn listed(name: &str, check: Option<&Check>) -> Value {
json!({
"name": name,
"required": true,
"started_at": check.and_then(|check| check.started_at.clone()),
"completed_at": check.and_then(|check| check.completed_at.clone()),
"conclusion": check.and_then(|check| check.conclusion.clone()),
})
}
pub(crate) fn passed(skipped: bool) -> Ruling {
if skipped {
Ruling::PassedWithSkipped
} else {
Ruling::Passed
}
}
pub(crate) fn runs_pre_push(cwd: &Path) -> bool {
git::hooks_dir(cwd)
.map(|hooks| executable(&hooks.join("pre-push")))
.unwrap_or(false)
}
#[cfg(unix)]
fn executable(path: &Path) -> bool {
use std::os::unix::fs::PermissionsExt;
std::fs::metadata(path)
.is_ok_and(|meta| meta.is_file() && meta.permissions().mode() & 0o111 != 0)
}
#[cfg(not(unix))]
fn executable(path: &Path) -> bool {
path.is_file()
}
pub(crate) fn landed(payload: &mut Map<String, Value>, at: Option<Moment>, landing: &str) {
payload.insert(
"landed_at".to_owned(),
at.map_or(Value::Null, |at| Value::String(at.stamp())),
);
payload.insert("landing".to_owned(), Value::String(landing.to_owned()));
}