| 1 | //! Announcing pull requests that start changing the same files. |
| 2 | //! |
| 3 | //! Each push works out what a pull request changes (reviews.rs) and so |
| 4 | //! which other open or draft pull requests it overlaps. A pair is announced |
| 5 | //! as `overlap.detected` the first time it overlaps, once for each side so |
| 6 | //! that whoever follows either hears of it, and remembered in |
| 7 | //! `overlap_pairs`; it is announced again only after a push shows it |
| 8 | //! stopped overlapping and it was forgotten. |
| 9 | |
| 10 | use g1t_contracts::time::rfc3339; |
| 11 | use g1t_contracts::work::*; |
| 12 | use g1t_kit::now_ms; |
| 13 | use serde::Deserialize; |
| 14 | use worker::Result; |
| 15 | |
| 16 | use crate::Work; |
| 17 | |
| 18 | #[derive(Deserialize)] |
| 19 | struct PairRow { |
| 20 | low: u32, |
| 21 | high: u32, |
| 22 | } |
| 23 | |
| 24 | /// A pair, lower number first. |
| 25 | fn pair(a: u32, b: u32) -> (u32, u32) { |
| 26 | (a.min(b), a.max(b)) |
| 27 | } |
| 28 | |
| 29 | impl Work { |
| 30 | /// Announces the overlaps `pull` (with its files just worked out) has |
| 31 | /// that were not announced before, and forgets the ones it no longer |
| 32 | /// has. Never fails the push it follows: a problem is logged. |
| 33 | pub(crate) async fn announce_overlaps(&self, pull: &Pull, actor: Option<String>) { |
| 34 | if let Err(error) = self.announce_overlaps_or_fail(pull, actor).await { |
| 35 | worker::console_error!("#{} overlaps were not announced: {error}", pull.number); |
| 36 | } |
| 37 | } |
| 38 | |
| 39 | async fn announce_overlaps_or_fail(&self, pull: &Pull, actor: Option<String>) -> Result<()> { |
| 40 | if !pull.status.is_active() { |
| 41 | return Ok(()); |
| 42 | } |
| 43 | let overlaps = self.overlaps(pull).await?; |
| 44 | let announced: Vec<u32> = self |
| 45 | .db |
| 46 | .prepare("SELECT low, high FROM overlap_pairs WHERE repo_id = ?1 AND (low = ?2 OR high = ?2)") |
| 47 | .bind(&[pull.repo_id.as_str().into(), pull.number.into()])? |
| 48 | .all() |
| 49 | .await? |
| 50 | .results::<PairRow>()? |
| 51 | .into_iter() |
| 52 | .map(|row| if row.low == pull.number { row.high } else { row.low }) |
| 53 | .collect(); |
| 54 | let current: Vec<u32> = overlaps.iter().map(|overlap| overlap.number).collect(); |
| 55 | let (new, cleared) = overlap_changes(¤t, &announced); |
| 56 | if new.is_empty() && cleared.is_empty() { |
| 57 | return Ok(()); |
| 58 | } |
| 59 | let at = rfc3339(now_ms()); |
| 60 | let mut statements = Vec::new(); |
| 61 | for other in &cleared { |
| 62 | let (low, high) = pair(pull.number, *other); |
| 63 | statements.push( |
| 64 | self.db |
| 65 | .prepare("DELETE FROM overlap_pairs WHERE repo_id = ? AND low = ? AND high = ?") |
| 66 | .bind(&[pull.repo_id.as_str().into(), low.into(), high.into()])?, |
| 67 | ); |
| 68 | } |
| 69 | let first_new = statements.len(); |
| 70 | for other in &new { |
| 71 | let (low, high) = pair(pull.number, *other); |
| 72 | let files = overlaps.iter().find(|overlap| overlap.number == *other).map(|overlap| overlap.paths.clone()).unwrap_or_default(); |
| 73 | statements.push( |
| 74 | self.db |
| 75 | .prepare( |
| 76 | "INSERT INTO overlap_pairs (repo_id, low, high, files, noticed_at) VALUES (?, ?, ?, ?, ?) |
| 77 | ON CONFLICT (repo_id, low, high) DO NOTHING RETURNING low", |
| 78 | ) |
| 79 | .bind(&[ |
| 80 | pull.repo_id.as_str().into(), |
| 81 | low.into(), |
| 82 | high.into(), |
| 83 | serde_json::to_string(&files)?.into(), |
| 84 | at.as_str().into(), |
| 85 | ])?, |
| 86 | ); |
| 87 | } |
| 88 | let results = self.db.batch(statements).await?; |
| 89 | for (result, other) in results.into_iter().skip(first_new).zip(&new) { |
| 90 | // Announced by the other side's push at the same moment. |
| 91 | if result.results::<PairRow>()?.is_empty() { |
| 92 | continue; |
| 93 | } |
| 94 | let Some(overlap) = overlaps.iter().find(|overlap| overlap.number == *other) else { |
| 95 | continue; |
| 96 | }; |
| 97 | let ours = OverlapDetected { |
| 98 | repo_id: pull.repo_id.clone(), |
| 99 | number: pull.number, |
| 100 | pull: pull.number, |
| 101 | other_pull: overlap.number, |
| 102 | files: overlap.paths.clone(), |
| 103 | issue: pull.issue, |
| 104 | other_issue: overlap.issue, |
| 105 | }; |
| 106 | let theirs = OverlapDetected { |
| 107 | number: overlap.number, |
| 108 | pull: overlap.number, |
| 109 | other_pull: pull.number, |
| 110 | issue: overlap.issue, |
| 111 | other_issue: pull.issue, |
| 112 | ..ours.clone() |
| 113 | }; |
| 114 | self.publish_as("overlap.detected", &pull.repo_id, actor.clone(), ours).await?; |
| 115 | self.publish_as("overlap.detected", &pull.repo_id, actor.clone(), theirs).await?; |
| 116 | } |
| 117 | Ok(()) |
| 118 | } |
| 119 | } |
| 120 | |
| 121 | #[cfg(test)] |
| 122 | mod tests { |
| 123 | use super::*; |
| 124 | |
| 125 | #[test] |
| 126 | fn a_pair_is_the_same_from_either_side() { |
| 127 | assert_eq!(pair(9, 4), (4, 9)); |
| 128 | assert_eq!(pair(4, 9), (4, 9)); |
| 129 | } |
| 130 | } |