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 .flatten()
162 .filter(|check| check.required)
163 .map(|check| check.name.clone())
164 .collect(),
165 )
166}
167
168pub(crate) fn verdict(checks: &[Check], declared: Option<&BTreeSet<String>>) -> ChecksVerdict {
170 let nothing = match declared {
173 Some(names) => names.is_empty(),
174 None => !checks.is_empty() && checks.iter().all(|check| !check.required),
175 };
176 let names: BTreeSet<String> = match declared {
177 Some(names) => names.clone(),
178 None if nothing => BTreeSet::new(),
179 None => checks
180 .iter()
181 .filter(|check| check.required)
182 .map(|check| check.name.clone())
183 .collect(),
184 };
185 let rank = |state: CheckState| match state {
186 CheckState::Failed => 0,
187 CheckState::Pending => 1,
188 CheckState::Passed => 2,
189 CheckState::NoVerdict => 3,
190 CheckState::Skipped => 4,
191 };
192 let standing: Vec<(String, Option<CheckState>)> = names
193 .into_iter()
194 .map(|name| {
195 let state = checks
196 .iter()
197 .filter(|check| check.name == name)
198 .map(Check::state)
199 .min_by_key(|state| rank(*state));
200 (name, state)
201 })
202 .collect();
203 let named = |wanted: &dyn Fn(Option<CheckState>) -> bool| -> Vec<String> {
204 standing
205 .iter()
206 .filter(|(_, state)| wanted(*state))
207 .map(|(name, _)| name.clone())
208 .collect()
209 };
210 let failed = named(&|state| state == Some(CheckState::Failed));
211 if !failed.is_empty() {
212 return ChecksVerdict::Failed { failed };
213 }
214 if nothing && standing.is_empty() {
215 return ChecksVerdict::NoneRequired;
216 }
217 let through = (nothing || !standing.is_empty())
218 && standing
219 .iter()
220 .all(|(_, state)| matches!(state, Some(CheckState::Passed | CheckState::Skipped)));
221 if through {
222 let skipped = named(&|state| state == Some(CheckState::Skipped));
223 return if skipped.is_empty() {
224 ChecksVerdict::Passed
225 } else {
226 ChecksVerdict::PassedWithSkipped { skipped }
227 };
228 }
229 ChecksVerdict::Pending {
230 waiting: named(&|state| {
231 matches!(
232 state,
233 None | Some(CheckState::Pending | CheckState::NoVerdict)
234 )
235 }),
236 }
237}
238
239pub(crate) fn standing(state: &HostState, root: &str, id: &ChangeId) -> Result<ChangeStanding> {
241 let change = state
242 .changes
243 .iter()
244 .find(|change| change.id == *id)
245 .cloned()
246 .ok_or_else(|| Error::Invalid {
247 reason: format!(
248 "no change request {:?} was opened on this host, so it has no standing",
249 id.0
250 ),
251 })?;
252 let base = change.base.clone();
253 let state_now = match state.merges.get(id) {
254 Some(MergeOutcome::Merged(commit)) => ChangeState::Merged {
255 into: base.clone(),
256 commit: commit.clone(),
257 landing: base == root,
258 merged_at: state.merge_times.get(id).cloned(),
259 },
260 _ if state.closed.contains(id) => ChangeState::Closed {
261 base_deleted: state.deleted_branches.contains(&base),
262 },
263 _ => ChangeState::Open,
264 };
265 let checks = checks_on_head(state, id);
266 Ok(ChangeStanding {
267 checks: verdict(&checks, declared(state, &base).as_ref()),
268 body: state.bodies.get(id).cloned().unwrap_or_default(),
269 head_branch: state.heads.get(id).cloned().unwrap_or_default(),
270 head: change.head_sha.clone(),
271 draft: crate::remote::drafted(state, id),
272 url: change.url,
273 base,
274 state: state_now,
275 })
276}
277
278pub(crate) fn merge_by_person(
281 state: &mut HostState,
282 id: &ChangeId,
283 how: PersonMerge,
284 at: &str,
285 head: HeadAfter,
286) -> Result<Sha> {
287 let change = state
288 .changes
289 .iter()
290 .find(|change| change.id == *id)
291 .cloned()
292 .ok_or_else(|| Error::Invalid {
293 reason: format!("no change request {:?} was opened on this host", id.0),
294 })?;
295 if crate::events::moment(at).is_none() {
296 return Err(Error::Invalid {
297 reason: format!("{at:?} is not an RFC 3339 moment to merge at"),
298 });
299 }
300 if matches!(state.merges.get(id), Some(MergeOutcome::Merged(_))) || state.closed.contains(id) {
301 return Err(Error::Invalid {
302 reason: format!("{} is not open, so nobody can merge it", change.url),
303 });
304 }
305 let word = match how {
306 PersonMerge::Squash => "squash",
307 PersonMerge::MergeCommit => "merge-commit",
308 };
309 let sha = Sha(crate::events::stable_sha(&[
310 "person-merge",
311 word,
312 &id.0,
313 change.url.as_str(),
314 ]));
315 state
316 .merges
317 .insert(id.clone(), MergeOutcome::Merged(sha.clone()));
318 state.merge_times.insert(id.clone(), at.to_owned());
319 if let HeadAfter::Deleted(dependants) = head {
320 let branch = state.heads.get(id).cloned().unwrap_or_default();
321 state.deleted_branches.insert(branch.clone());
322 let based: Vec<ChangeId> = state
323 .changes
324 .iter()
325 .filter(|other| {
326 other.base == branch
327 && !state.closed.contains(&other.id)
328 && !matches!(state.merges.get(&other.id), Some(MergeOutcome::Merged(_)))
329 })
330 .map(|other| other.id.clone())
331 .collect();
332 for other in based {
333 match dependants {
334 Dependants::Closed => {
335 state.closed.insert(other);
336 }
337 Dependants::Retargeted => retarget_in(state, &other, &change.base),
338 }
339 }
340 }
341 Ok(sha)
342}
343
344pub(crate) fn retarget_in(state: &mut HostState, id: &ChangeId, base: &str) {
346 if let Some(change) = state.changes.iter_mut().find(|change| change.id == *id) {
347 change.base = base.to_owned();
348 }
349}