Skip to content
130 linesCodeBlameRaw
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
10use g1t_contracts::time::rfc3339;
11use g1t_contracts::work::*;
12use g1t_kit::now_ms;
13use serde::Deserialize;
14use worker::Result;
15
16use crate::Work;
17
18#[derive(Deserialize)]
19struct PairRow {
20 low: u32,
21 high: u32,
22}
23
24/// A pair, lower number first.
25fn pair(a: u32, b: u32) -> (u32, u32) {
26 (a.min(b), a.max(b))
27}
28
29impl 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(&current, &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)]
122mod 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}