Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

191 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 #[serde(default)]
27 item_number: Option<u32>,
28 files: Option<String>,
29}
30
31impl Work {
32 pub(crate) async fn pull_by_id(&self, id: &str) -> Result<Option<Pull>> {
33 Ok(self
34 .db
35 .prepare(format!("SELECT {PULL_COLUMNS} FROM pulls WHERE id = ?"))
36 .bind(&[id.into()])?
37 .first::<PullRow>(None)
38 .await?
39 .map(Pull::from))
40 }
41
42 /// Works out which files a pull request changes and records them, so
43 /// that overlaps between pull requests can be found without comparing
44 /// every one of them each time.
45 pub(crate) async fn refresh_files(&self, pull: &Pull) -> Result<Vec<ChangedFile>> {
46 // Its owner (whoever asked g1t for it, or its author) can read both
47 // the repository and the pull request's source.
48 let viewer = self.owner_viewer(pull).await?;
49 let compared: Outcome<Comparison> =
50 g1t_kit::call(&self.repos, "compare", &pull.comparison(&viewer)).await?;
51 let files: Vec<ChangedFile> = match compared {
52 Outcome::Ok(comparison) => comparison
53 .files
54 .into_iter()
55 .take(MAX_FILES)
56 .map(|file| ChangedFile {
57 path: file.path,
58 additions: file.additions,
59 deletions: file.deletions,
60 })
61 .collect(),
62 // Nothing pushed yet, or nothing to compare against.
63 Outcome::Fail(_) => Vec::new(),
64 };
65 self.db
66 .prepare("UPDATE pulls SET files = ? WHERE id = ?")
67 .bind(&[
68 serde_json::to_string(&files)?.into(),
69 pull.id.as_str().into(),
70 ])?
71 .run()
72 .await?;
73 Ok(files)
74 }
75
76 /// Other pull requests in progress that change files this one changes.
77 pub(crate) async fn overlaps(&self, pull: &Pull) -> Result<Vec<Overlap>> {
78 if pull.files.is_empty() {
79 return Ok(Vec::new());
80 }
81 let mine: HashSet<&str> = pull.files.iter().map(|file| file.path.as_str()).collect();
82 let others = match self.prefetched_pull(&pull.id) {
83 Some(found) => found.rows::<OtherRow>(crate::prefetch::Slot::Others)?,
84 None => self.other_pulls(pull).await?,
85 };
86 Ok(others
87 .into_iter()
88 .filter_map(|other| {
89 let files: Vec<ChangedFile> =
90 serde_json::from_str(other.files.as_deref().unwrap_or("[]")).ok()?;
91 let paths: Vec<String> = files
92 .into_iter()
93 .map(|file| file.path)
94 .filter(|path| mine.contains(path.as_str()))
95 .collect();
96 (!paths.is_empty()).then_some(Overlap {
97 number: other.number,
98 title: other.title,
99 item: other.item_number,
100 paths,
101 })
102 })
103 .collect())
104 }
105
106 /// The other pull requests in progress in its repository.
107 async fn other_pulls(&self, pull: &Pull) -> Result<Vec<OtherRow>> {
108 self
109 .db
110 .prepare(concat!(
111 "SELECT number, title, ",
112 crate::items::sql::item_number!("pulls.item_id"),
113 " AS item_number,
114 files FROM pulls
115 WHERE repo_id = ? AND id != ? AND status IN ('draft', 'open')
116 ORDER BY number LIMIT 200",
117 ))
118 .bind(&[pull.repo_id.as_str().into(), pull.id.as_str().into()])?
119 .all()
120 .await?
121 .results::<OtherRow>()
122 }
123
124 /// Whether the branch a pull request would merge into has commits the
125 /// pull request does not, so that it must catch up before it can merge.
126 pub(crate) async fn is_behind(&self, pull: &Pull) -> Result<bool> {
127 if !pull.status.is_active() || pull.head_commit.is_none() {
128 return Ok(false);
129 }
130 let asked = BehindArgs {
131 source_id: pull.source_id().to_owned(),
132 branch: pull.branch.clone(),
133 target_branch: pull.base.clone().filter(|base| !base.is_empty()),
134 target_id: pull.target_id(),
135 };
136 self.timing.rpc(g1t_kit::call(&self.repos, "behind", &asked)).await
137 }
138
139 /// Whether a pull request is behind, as stored with its mergeability
140 /// when that was worked out for its head as it is now (the last push
141 /// to either side); otherwise asked of the repos service.
142 pub(crate) async fn behind_now(&self, pull: &Pull) -> Result<bool> {
143 if !pull.status.is_active() || pull.head_commit.is_none() {
144 return Ok(false);
145 }
146 let stored = self
147 .timing
148 .db(
149 1,
150 self.db
151 .prepare("SELECT behind, mergeable_key FROM pulls WHERE id = ?")
152 .bind(&[pull.id.as_str().into()])?
153 .first::<crate::StoredBehind>(None),
154 )
155 .await?;
156 match stored.and_then(|stored| stored.for_head(pull.head_commit.as_deref())) {
157 Some(behind) => Ok(behind),
158 None => self.is_behind(pull).await,
159 }
160 }
161
162 /// Where a pull request lives, for a tool that knows only its id (its
163 /// fork is named after it).
164 pub(crate) async fn locate_pull(&self, a: LocatePullArgs) -> Result<Outcome<LocatedPull>> {
165 let missing = || Outcome::fail(FailureCode::NotFound, "Pull request not found.");
166 let Some(pull) = self.pull_by_id(&a.id).await? else {
167 return Ok(missing());
168 };
169 let repo: Outcome<Repo> = g1t_kit::call(
170 &self.repos,
171 "get_by_id",
172 &GetByIdArgs {
173 id: pull.repo_id.clone(),
174 viewer: a.viewer,
175 },
176 )
177 .await?;
178 let Outcome::Ok(repo) = repo else {
179 return Ok(missing());
180 };
181 Ok(Outcome::Ok(LocatedPull {
182 repo: RepoPath {
183 namespace: repo.namespace,
184 name: repo.name,
185 },
186 number: pull.number,
187 title: pull.title,
188 status: pull.status,
189 }))
190 }
191}