use std::path::Path;
use serde_json::{json, Map, Value};
use crate::error::{Error, Result};
use crate::event::EventKind;
use crate::git;
use crate::host::Hosting;
use crate::rules::BaseMerge;
use crate::stream::Stream;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Verdict {
Merged,
Skipped,
Deferred,
}
impl Verdict {
fn spell(self) -> &'static str {
match self {
Verdict::Merged => "merged",
Verdict::Skipped => "skipped",
Verdict::Deferred => "deferred",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Decision {
pub verdict: Verdict,
pub because: String,
}
impl Decision {
fn merge(because: impl Into<String>) -> Self {
Self {
verdict: Verdict::Merged,
because: because.into(),
}
}
fn skip(because: impl Into<String>) -> Self {
Self {
verdict: Verdict::Skipped,
because: because.into(),
}
}
pub(crate) fn defer(because: impl Into<String>) -> Self {
Self {
verdict: Verdict::Deferred,
because: because.into(),
}
}
pub(crate) fn merges(&self) -> bool {
self.verdict == Verdict::Merged
}
pub(crate) fn payload(&self) -> Map<String, Value> {
let Value::Object(payload) = json!({
"decision": self.verdict.spell(),
"because": self.because,
}) else {
unreachable!("a JSON object literal is an object")
};
payload
}
}
pub(crate) fn record(stream: &mut Stream, branch: &str, base: &str, decision: &Decision) {
let mut payload = Map::new();
payload.insert("branch".to_owned(), json!(branch));
payload.insert("base".to_owned(), json!(base));
payload.extend(decision.payload());
stream.emit(EventKind::BaseMerge, payload);
}
pub(crate) struct Merging<'a> {
pub repo: &'a Path,
pub integrated: &'a str,
pub base: &'a str,
pub branch: &'a str,
pub identity: &'a str,
pub hosting: &'a dyn Hosting,
}
pub(crate) fn decide(setting: BaseMerge, asked: &Merging<'_>) -> Result<Decision> {
let base = asked.integrated;
if git::is_ancestor(asked.repo, base, asked.branch)? {
return Ok(Decision::skip(format!("the branch already carries {base}")));
}
if setting == BaseMerge::Always {
return Ok(Decision::merge(format!(
"base_merge is always, so {base} is merged whenever it moved"
)));
}
if git::merge_would_conflict(asked.repo, base, asked.branch)? {
return Ok(Decision::merge(format!(
"{base} conflicts with the branch, so the conflict is resolved before anything is \
published"
)));
}
match requires_up_to_date(asked) {
Ok(Some(true)) => Ok(Decision::merge(format!(
"the host requires a branch to be up to date with {} before it merges",
asked.base
))),
Ok(Some(false)) => Ok(Decision::skip(format!(
"{base} merges cleanly and the host does not require an up-to-date branch, so the \
branch stays at its tip"
))),
Ok(None) => Ok(Decision::skip(format!(
"{base} merges cleanly and this identity has no host to require an up-to-date \
branch, so the branch stays at its tip"
))),
Err(unread) => Ok(Decision::merge(format!(
"whether the host requires an up-to-date branch could not be read, so {base} is \
merged as it always was: {unread}"
))),
}
}
fn requires_up_to_date(asked: &Merging<'_>) -> Result<Option<bool>> {
let slug = match crate::publish::change_host(asked.identity) {
Ok(slug) => slug,
Err(Error::Invalid { .. }) => return Ok(None),
Err(unhosted) => return Err(unhosted),
};
asked
.hosting
.for_repo(&slug)?
.requires_up_to_date(asked.base)
.map(Some)
}