1use std::collections::BTreeSet;
27
28use serde::{Deserialize, Serialize};
29
30use onevcs::{
31 ChangeId, ChangeStanding, ChangeState, Check, CheckState, ChecksVerdict, Error, MergeOutcome,
32 ReadCost, Result, Sha,
33};
34
35use crate::state::HostState;
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
39#[serde(rename_all = "kebab-case")]
40pub enum PersonMerge {
41 Squash,
43 MergeCommit,
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
49#[serde(rename_all = "kebab-case")]
50pub enum HeadAfter {
51 Kept,
53 Deleted(Dependants),
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
60#[serde(rename_all = "kebab-case")]
61pub enum Dependants {
62 Closed,
65 Retargeted,
67}
68
69const STANDINGS_BATCH: usize = 50;
71const REVIEW_BATCH: usize = 150;
73
74fn points(requests: usize) -> u32 {
77 u32::try_from(((requests + 50) / 100).max(1)).unwrap_or(u32::MAX)
78}
79
80const DECLARATIONS: usize = 27;
84
85pub(crate) fn standings_charge(count: usize) -> ReadCost {
89 batched(count, STANDINGS_BATCH, 1)
90}
91
92pub(crate) fn review_charge(count: usize) -> ReadCost {
97 batched(count, REVIEW_BATCH, 54)
98}
99
100fn batched(count: usize, batch: usize, each: usize) -> ReadCost {
101 ReadCost {
102 graphql_points: (0..count)
103 .step_by(batch)
104 .map(|start| {
105 let declarations = if start == 0 { DECLARATIONS } else { 0 };
106 points(each * (count - start).min(batch) + declarations)
107 })
108 .sum(),
109 rest_requests: 0,
110 }
111}
112
113pub(crate) fn graphql(points: u32) -> ReadCost {
115 ReadCost {
116 graphql_points: points,
117 rest_requests: 0,
118 }
119}
120
121pub(crate) fn rest(count: u32) -> ReadCost {
123 ReadCost {
124 graphql_points: 0,
125 rest_requests: count,
126 }
127}
128
129pub(crate) fn checks_on_head(state: &HostState, id: &ChangeId) -> Vec<Check> {
132 let head = state
133 .changes
134 .iter()
135 .find(|change| change.id == *id)
136 .map(|change| change.head_sha.clone());
137 if let Some(checks) = head.and_then(|head| state.head_checks.get(&head)) {
138 return checks.clone();
139 }
140 crate::remote::current_checks(state, id)
141}
142
143fn declared(state: &HostState, base: &str) -> Option<BTreeSet<String>> {
146 if state.deleted_branches.contains(base) {
147 return None;
148 }
149 if let Some(seeded) = &state.required_checks {
150 return seeded
151 .complete()
152 .then(|| seeded.checks.clone())
153 .or_else(|| (!seeded.checks.is_empty()).then(|| seeded.checks.clone()));
154 }
155 Some(
156 state
157 .checks
158 .values()
159 .chain(state.checks_after_lift.values())
160 .chain(state.head_checks.values())
161 .chain(
162 state
163 .check_readings
164 .values()
165 .chain(state.check_readings_after_lift.values())
166 .flatten(),
167 )
168 .flatten()
169 .filter(|check| check.required)
170 .map(|check| check.name.clone())
171 .collect(),
172 )
173}
174
175pub(crate) fn verdict(checks: &[Check], declared: Option<&BTreeSet<String>>) -> ChecksVerdict {
177 let nothing = match declared {
180 Some(names) => names.is_empty(),
181 None => !checks.is_empty() && checks.iter().all(|check| !check.required),
182 };
183 let names: BTreeSet<String> = match declared {
184 Some(names) => names.clone(),
185 None if nothing => BTreeSet::new(),
186 None => checks
187 .iter()
188 .filter(|check| check.required)
189 .map(|check| check.name.clone())
190 .collect(),
191 };
192 let rank = |state: CheckState| match state {
193 CheckState::Failed => 0,
194 CheckState::Pending => 1,
195 CheckState::Passed => 2,
196 CheckState::NoVerdict => 3,
197 CheckState::Skipped => 4,
198 };
199 let standing: Vec<(String, Option<CheckState>)> = names
200 .into_iter()
201 .map(|name| {
202 let state = checks
203 .iter()
204 .filter(|check| check.name == name)
205 .map(Check::state)
206 .min_by_key(|state| rank(*state));
207 (name, state)
208 })
209 .collect();
210 let named = |wanted: &dyn Fn(Option<CheckState>) -> bool| -> Vec<String> {
211 standing
212 .iter()
213 .filter(|(_, state)| wanted(*state))
214 .map(|(name, _)| name.clone())
215 .collect()
216 };
217 let failed = named(&|state| state == Some(CheckState::Failed));
218 if !failed.is_empty() {
219 return ChecksVerdict::Failed { failed };
220 }
221 if nothing && standing.is_empty() {
222 return ChecksVerdict::NoneRequired;
223 }
224 let through = (nothing || !standing.is_empty())
225 && standing
226 .iter()
227 .all(|(_, state)| matches!(state, Some(CheckState::Passed | CheckState::Skipped)));
228 if through {
229 let skipped = named(&|state| state == Some(CheckState::Skipped));
230 return if skipped.is_empty() {
231 ChecksVerdict::Passed
232 } else {
233 ChecksVerdict::PassedWithSkipped { skipped }
234 };
235 }
236 ChecksVerdict::Pending {
237 waiting: named(&|state| {
238 matches!(
239 state,
240 None | Some(CheckState::Pending | CheckState::NoVerdict)
241 )
242 }),
243 }
244}
245
246pub(crate) fn standing(state: &HostState, root: &str, id: &ChangeId) -> Result<ChangeStanding> {
248 let change = state
249 .changes
250 .iter()
251 .find(|change| change.id == *id)
252 .cloned()
253 .ok_or_else(|| Error::Invalid {
254 reason: format!(
255 "no change request {:?} was opened on this host, so it has no standing",
256 id.0
257 ),
258 })?;
259 let base = change.base.clone();
260 let state_now = match state.merges.get(id) {
261 Some(MergeOutcome::Merged(commit)) => ChangeState::Merged {
262 into: base.clone(),
263 commit: commit.clone(),
264 landing: base == root,
265 merged_at: state.merge_times.get(id).cloned(),
266 },
267 _ if state.closed.contains(id) => ChangeState::Closed {
268 base_deleted: state.deleted_branches.contains(&base),
269 },
270 _ => ChangeState::Open,
271 };
272 let checks = checks_on_head(state, id);
273 Ok(ChangeStanding {
274 checks: verdict(&checks, declared(state, &base).as_ref()),
275 body: state.bodies.get(id).cloned().unwrap_or_default(),
276 head_branch: state.heads.get(id).cloned().unwrap_or_default(),
277 head: change.head_sha.clone(),
278 draft: crate::remote::drafted(state, id),
279 url: change.url,
280 base,
281 state: state_now,
282 })
283}
284
285pub(crate) fn merge_by_person(
288 state: &mut HostState,
289 id: &ChangeId,
290 how: PersonMerge,
291 at: &str,
292 head: HeadAfter,
293) -> Result<Sha> {
294 let change = state
295 .changes
296 .iter()
297 .find(|change| change.id == *id)
298 .cloned()
299 .ok_or_else(|| Error::Invalid {
300 reason: format!("no change request {:?} was opened on this host", id.0),
301 })?;
302 if crate::events::moment(at).is_none() {
303 return Err(Error::Invalid {
304 reason: format!("{at:?} is not an RFC 3339 moment to merge at"),
305 });
306 }
307 if matches!(state.merges.get(id), Some(MergeOutcome::Merged(_))) || state.closed.contains(id) {
308 return Err(Error::Invalid {
309 reason: format!("{} is not open, so nobody can merge it", change.url),
310 });
311 }
312 let word = match how {
313 PersonMerge::Squash => "squash",
314 PersonMerge::MergeCommit => "merge-commit",
315 };
316 let sha = Sha(crate::events::stable_sha(&[
317 "person-merge",
318 word,
319 &id.0,
320 change.url.as_str(),
321 ]));
322 state
323 .merges
324 .insert(id.clone(), MergeOutcome::Merged(sha.clone()));
325 state.merge_times.insert(id.clone(), at.to_owned());
326 if let HeadAfter::Deleted(dependants) = head {
327 let branch = state.heads.get(id).cloned().unwrap_or_default();
328 state.deleted_branches.insert(branch.clone());
329 let based: Vec<ChangeId> = state
330 .changes
331 .iter()
332 .filter(|other| {
333 other.base == branch
334 && !state.closed.contains(&other.id)
335 && !matches!(state.merges.get(&other.id), Some(MergeOutcome::Merged(_)))
336 })
337 .map(|other| other.id.clone())
338 .collect();
339 for other in based {
340 match dependants {
341 Dependants::Closed => {
342 state.closed.insert(other);
343 }
344 Dependants::Retargeted => retarget_in(state, &other, &change.base),
345 }
346 }
347 }
348 Ok(sha)
349}
350
351pub(crate) fn retarget_in(state: &mut HostState, id: &ChangeId, base: &str) {
353 if let Some(change) = state.changes.iter_mut().find(|change| change.id == *id) {
354 change.base = base.to_owned();
355 }
356}