use std::collections::BTreeSet;
use serde::{Deserialize, Serialize};
use onevcs::{
ChangeId, ChangeStanding, ChangeState, Check, CheckState, ChecksVerdict, Error, MergeOutcome,
ReadCost, Result, Sha,
};
use crate::state::HostState;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum PersonMerge {
Squash,
MergeCommit,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum HeadAfter {
Kept,
Deleted(Dependants),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Dependants {
Closed,
Retargeted,
}
const STANDINGS_BATCH: usize = 50;
const REVIEW_BATCH: usize = 150;
fn points(requests: usize) -> u32 {
u32::try_from(((requests + 50) / 100).max(1)).unwrap_or(u32::MAX)
}
const DECLARATIONS: usize = 27;
pub(crate) fn standings_charge(count: usize) -> ReadCost {
batched(count, STANDINGS_BATCH, 1)
}
pub(crate) fn review_charge(count: usize) -> ReadCost {
batched(count, REVIEW_BATCH, 54)
}
fn batched(count: usize, batch: usize, each: usize) -> ReadCost {
ReadCost {
graphql_points: (0..count)
.step_by(batch)
.map(|start| {
let declarations = if start == 0 { DECLARATIONS } else { 0 };
points(each * (count - start).min(batch) + declarations)
})
.sum(),
rest_requests: 0,
}
}
pub(crate) fn graphql(points: u32) -> ReadCost {
ReadCost {
graphql_points: points,
rest_requests: 0,
}
}
pub(crate) fn rest(count: u32) -> ReadCost {
ReadCost {
graphql_points: 0,
rest_requests: count,
}
}
pub(crate) fn checks_on_head(state: &HostState, id: &ChangeId) -> Vec<Check> {
let head = state
.changes
.iter()
.find(|change| change.id == *id)
.map(|change| change.head_sha.clone());
if let Some(checks) = head.and_then(|head| state.head_checks.get(&head)) {
return checks.clone();
}
crate::remote::current_checks(state, id)
}
fn declared(state: &HostState, base: &str) -> Option<BTreeSet<String>> {
if state.deleted_branches.contains(base) {
return None;
}
if let Some(seeded) = &state.required_checks {
return seeded
.complete()
.then(|| seeded.checks.clone())
.or_else(|| (!seeded.checks.is_empty()).then(|| seeded.checks.clone()));
}
Some(
state
.checks
.values()
.chain(state.checks_after_lift.values())
.chain(state.head_checks.values())
.chain(
state
.check_readings
.values()
.chain(state.check_readings_after_lift.values())
.flatten(),
)
.flatten()
.filter(|check| check.required)
.map(|check| check.name.clone())
.collect(),
)
}
pub(crate) fn verdict(checks: &[Check], declared: Option<&BTreeSet<String>>) -> ChecksVerdict {
let nothing = match declared {
Some(names) => names.is_empty(),
None => !checks.is_empty() && checks.iter().all(|check| !check.required),
};
let names: BTreeSet<String> = match declared {
Some(names) => names.clone(),
None if nothing => BTreeSet::new(),
None => checks
.iter()
.filter(|check| check.required)
.map(|check| check.name.clone())
.collect(),
};
let rank = |state: CheckState| match state {
CheckState::Failed => 0,
CheckState::Pending => 1,
CheckState::Passed => 2,
CheckState::NoVerdict => 3,
CheckState::Skipped => 4,
};
let standing: Vec<(String, Option<CheckState>)> = names
.into_iter()
.map(|name| {
let state = checks
.iter()
.filter(|check| check.name == name)
.map(Check::state)
.min_by_key(|state| rank(*state));
(name, state)
})
.collect();
let named = |wanted: &dyn Fn(Option<CheckState>) -> bool| -> Vec<String> {
standing
.iter()
.filter(|(_, state)| wanted(*state))
.map(|(name, _)| name.clone())
.collect()
};
let failed = named(&|state| state == Some(CheckState::Failed));
if !failed.is_empty() {
return ChecksVerdict::Failed { failed };
}
if nothing && standing.is_empty() {
return ChecksVerdict::NoneRequired;
}
let through = (nothing || !standing.is_empty())
&& standing
.iter()
.all(|(_, state)| matches!(state, Some(CheckState::Passed | CheckState::Skipped)));
if through {
let skipped = named(&|state| state == Some(CheckState::Skipped));
return if skipped.is_empty() {
ChecksVerdict::Passed
} else {
ChecksVerdict::PassedWithSkipped { skipped }
};
}
ChecksVerdict::Pending {
waiting: named(&|state| {
matches!(
state,
None | Some(CheckState::Pending | CheckState::NoVerdict)
)
}),
}
}
pub(crate) fn standing(state: &HostState, root: &str, id: &ChangeId) -> Result<ChangeStanding> {
let change = state
.changes
.iter()
.find(|change| change.id == *id)
.cloned()
.ok_or_else(|| Error::Invalid {
reason: format!(
"no change request {:?} was opened on this host, so it has no standing",
id.0
),
})?;
let base = change.base.clone();
let state_now = match state.merges.get(id) {
Some(MergeOutcome::Merged(commit)) => ChangeState::Merged {
into: base.clone(),
commit: commit.clone(),
landing: base == root,
merged_at: state.merge_times.get(id).cloned(),
},
_ if state.closed.contains(id) => ChangeState::Closed {
base_deleted: state.deleted_branches.contains(&base),
},
_ => ChangeState::Open,
};
let checks = checks_on_head(state, id);
Ok(ChangeStanding {
checks: verdict(&checks, declared(state, &base).as_ref()),
body: state.bodies.get(id).cloned().unwrap_or_default(),
head_branch: state.heads.get(id).cloned().unwrap_or_default(),
head: change.head_sha.clone(),
draft: crate::remote::drafted(state, id),
url: change.url,
base,
state: state_now,
})
}
pub(crate) fn merge_by_person(
state: &mut HostState,
id: &ChangeId,
how: PersonMerge,
at: &str,
head: HeadAfter,
) -> Result<Sha> {
let change = state
.changes
.iter()
.find(|change| change.id == *id)
.cloned()
.ok_or_else(|| Error::Invalid {
reason: format!("no change request {:?} was opened on this host", id.0),
})?;
if crate::events::moment(at).is_none() {
return Err(Error::Invalid {
reason: format!("{at:?} is not an RFC 3339 moment to merge at"),
});
}
if matches!(state.merges.get(id), Some(MergeOutcome::Merged(_))) || state.closed.contains(id) {
return Err(Error::Invalid {
reason: format!("{} is not open, so nobody can merge it", change.url),
});
}
let word = match how {
PersonMerge::Squash => "squash",
PersonMerge::MergeCommit => "merge-commit",
};
let sha = Sha(crate::events::stable_sha(&[
"person-merge",
word,
&id.0,
change.url.as_str(),
]));
state
.merges
.insert(id.clone(), MergeOutcome::Merged(sha.clone()));
state.merge_times.insert(id.clone(), at.to_owned());
if let HeadAfter::Deleted(dependants) = head {
let branch = state.heads.get(id).cloned().unwrap_or_default();
state.deleted_branches.insert(branch.clone());
let based: Vec<ChangeId> = state
.changes
.iter()
.filter(|other| {
other.base == branch
&& !state.closed.contains(&other.id)
&& !matches!(state.merges.get(&other.id), Some(MergeOutcome::Merged(_)))
})
.map(|other| other.id.clone())
.collect();
for other in based {
match dependants {
Dependants::Closed => {
state.closed.insert(other);
}
Dependants::Retargeted => retarget_in(state, &other, &change.base),
}
}
}
Ok(sha)
}
pub(crate) fn retarget_in(state: &mut HostState, id: &ChangeId, base: &str) {
if let Some(change) = state.changes.iter_mut().find(|change| change.id == *id) {
change.base = base.to_owned();
}
}