Skip to content
166 linesCodeBlameRaw
1//! What g1t adds around a pull request for many agents working at once:
2//! which files it changes, which other pull requests are changing the same
3//! files, and whether it has fallen behind.
4
5use std::collections::HashSet;
6
7use g1t_contracts::repos::{BehindArgs, Comparison, GetByIdArgs, Repo, RepoPath};
8use g1t_contracts::work::*;
9use g1t_contracts::{FailureCode, Outcome};
10use serde::Deserialize;
11use worker::Result;
12
13use crate::Work;
14use crate::rows::{PULL_COLUMNS, PullRow};
15
16/// Beyond this a change is too large for per-file bookkeeping to be useful.
17const MAX_FILES: usize = 300;
18/// g1t's agent, which wrote reviews and pull requests before g1t stopped
19/// hosting agents: shown as `g1t`, which nobody can register or sign in as.
20pub(crate) use g1t_contracts::identity::{AGENT_ID, AGENT_NAME};
21
22#[derive(Deserialize)]
23struct OtherRow {
24 number: u32,
25 title: String,
26 issue_number: Option<u32>,
27 files: Option<String>,
28}
29
30impl Work {
31 pub(crate) async fn pull_by_id(&self, id: &str) -> Result<Option<Pull>> {
32 Ok(self
33 .db
34 .prepare(format!("SELECT {PULL_COLUMNS} FROM pulls WHERE id = ?"))
35 .bind(&[id.into()])?
36 .first::<PullRow>(None)
37 .await?
38 .map(Pull::from))
39 }
40
41 /// Works out which files a pull request changes and records them, so
42 /// that overlaps between pull requests can be found without comparing
43 /// every one of them each time.
44 pub(crate) async fn refresh_files(&self, pull: &Pull) -> Result<Vec<ChangedFile>> {
45 // Its owner (whoever asked g1t for it, or its author) can read both
46 // the repository and the pull request's source.
47 let viewer = self.owner_viewer(pull).await?;
48 let compared: Outcome<Comparison> =
49 g1t_kit::call(&self.repos, "compare", &pull.comparison(&viewer)).await?;
50 let files: Vec<ChangedFile> = match compared {
51 Outcome::Ok(comparison) => comparison
52 .files
53 .into_iter()
54 .take(MAX_FILES)
55 .map(|file| ChangedFile {
56 path: file.path,
57 additions: file.additions,
58 deletions: file.deletions,
59 })
60 .collect(),
61 // Nothing pushed yet, or nothing to compare against.
62 Outcome::Fail(_) => Vec::new(),
63 };
64 self.db
65 .prepare("UPDATE pulls SET files = ? WHERE id = ?")
66 .bind(&[
67 serde_json::to_string(&files)?.into(),
68 pull.id.as_str().into(),
69 ])?
70 .run()
71 .await?;
72 Ok(files)
73 }
74
75 /// Other pull requests in progress that change files this one changes.
76 pub(crate) async fn overlaps(&self, pull: &Pull) -> Result<Vec<Overlap>> {
77 if pull.files.is_empty() {
78 return Ok(Vec::new());
79 }
80 let mine: HashSet<&str> = pull.files.iter().map(|file| file.path.as_str()).collect();
81 let others = match self.prefetched_pull(&pull.id) {
82 Some(found) => found.rows::<OtherRow>(crate::prefetch::Slot::Others)?,
83 None => self.other_pulls(pull).await?,
84 };
85 Ok(others
86 .into_iter()
87 .filter_map(|other| {
88 let files: Vec<ChangedFile> =
89 serde_json::from_str(other.files.as_deref().unwrap_or("[]")).ok()?;
90 let paths: Vec<String> = files
91 .into_iter()
92 .map(|file| file.path)
93 .filter(|path| mine.contains(path.as_str()))
94 .collect();
95 (!paths.is_empty()).then_some(Overlap {
96 number: other.number,
97 title: other.title,
98 issue: other.issue_number,
99 paths,
100 })
101 })
102 .collect())
103 }
104
105 /// The other pull requests in progress in its repository.
106 async fn other_pulls(&self, pull: &Pull) -> Result<Vec<OtherRow>> {
107 self
108 .db
109 .prepare(
110 "SELECT number, title, issue_number, files FROM pulls
111 WHERE repo_id = ? AND id != ? AND status IN ('draft', 'open')
112 ORDER BY number LIMIT 200",
113 )
114 .bind(&[pull.repo_id.as_str().into(), pull.id.as_str().into()])?
115 .all()
116 .await?
117 .results::<OtherRow>()
118 }
119
120 /// Whether the branch a pull request would merge into has commits the
121 /// pull request does not, so that it must catch up before it can merge.
122 pub(crate) async fn is_behind(&self, repo_id: &str, pull: &Pull) -> Result<bool> {
123 if !pull.status.is_active() || pull.head_commit.is_none() {
124 return Ok(false);
125 }
126 let asked = BehindArgs {
127 source_id: pull
128 .fork_repo_id
129 .clone()
130 .unwrap_or_else(|| repo_id.to_owned()),
131 branch: pull.branch.clone(),
132 target_branch: pull.base.clone().filter(|base| !base.is_empty()),
133 };
134 self.timing.rpc(g1t_kit::call(&self.repos, "behind", &asked)).await
135 }
136
137 /// Where a pull request lives, for a tool that knows only its id (its
138 /// fork is named after it).
139 pub(crate) async fn locate_pull(&self, a: LocatePullArgs) -> Result<Outcome<LocatedPull>> {
140 let missing = || Outcome::fail(FailureCode::NotFound, "Pull request not found.");
141 let Some(pull) = self.pull_by_id(&a.id).await? else {
142 return Ok(missing());
143 };
144 let repo: Outcome<Repo> = g1t_kit::call(
145 &self.repos,
146 "get_by_id",
147 &GetByIdArgs {
148 id: pull.repo_id.clone(),
149 viewer: a.viewer,
150 },
151 )
152 .await?;
153 let Outcome::Ok(repo) = repo else {
154 return Ok(missing());
155 };
156 Ok(Outcome::Ok(LocatedPull {
157 repo: RepoPath {
158 namespace: repo.namespace,
159 name: repo.name,
160 },
161 number: pull.number,
162 title: pull.title,
163 status: pull.status,
164 }))
165 }
166}