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.

189 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(
111 "SELECT number, title, (SELECT number FROM work_items WHERE work_items.id = pulls.item_id) AS item_number,
112 files FROM pulls
113 WHERE repo_id = ? AND id != ? AND status IN ('draft', 'open')
114 ORDER BY number LIMIT 200",
115 )
116 .bind(&[pull.repo_id.as_str().into(), pull.id.as_str().into()])?
117 .all()
118 .await?
119 .results::<OtherRow>()
120 }
121
122 /// Whether the branch a pull request would merge into has commits the
123 /// pull request does not, so that it must catch up before it can merge.
124 pub(crate) async fn is_behind(&self, pull: &Pull) -> Result<bool> {
125 if !pull.status.is_active() || pull.head_commit.is_none() {
126 return Ok(false);
127 }
128 let asked = BehindArgs {
129 source_id: pull.source_id().to_owned(),
130 branch: pull.branch.clone(),
131 target_branch: pull.base.clone().filter(|base| !base.is_empty()),
132 target_id: pull.target_id(),
133 };
134 self.timing.rpc(g1t_kit::call(&self.repos, "behind", &asked)).await
135 }
136
137 /// Whether a pull request is behind, as stored with its mergeability
138 /// when that was worked out for its head as it is now (the last push
139 /// to either side); otherwise asked of the repos service.
140 pub(crate) async fn behind_now(&self, pull: &Pull) -> Result<bool> {
141 if !pull.status.is_active() || pull.head_commit.is_none() {
142 return Ok(false);
143 }
144 let stored = self
145 .timing
146 .db(
147 1,
148 self.db
149 .prepare("SELECT behind, mergeable_key FROM pulls WHERE id = ?")
150 .bind(&[pull.id.as_str().into()])?
151 .first::<crate::StoredBehind>(None),
152 )
153 .await?;
154 match stored.and_then(|stored| stored.for_head(pull.head_commit.as_deref())) {
155 Some(behind) => Ok(behind),
156 None => self.is_behind(pull).await,
157 }
158 }
159
160 /// Where a pull request lives, for a tool that knows only its id (its
161 /// fork is named after it).
162 pub(crate) async fn locate_pull(&self, a: LocatePullArgs) -> Result<Outcome<LocatedPull>> {
163 let missing = || Outcome::fail(FailureCode::NotFound, "Pull request not found.");
164 let Some(pull) = self.pull_by_id(&a.id).await? else {
165 return Ok(missing());
166 };
167 let repo: Outcome<Repo> = g1t_kit::call(
168 &self.repos,
169 "get_by_id",
170 &GetByIdArgs {
171 id: pull.repo_id.clone(),
172 viewer: a.viewer,
173 },
174 )
175 .await?;
176 let Outcome::Ok(repo) = repo else {
177 return Ok(missing());
178 };
179 Ok(Outcome::Ok(LocatedPull {
180 repo: RepoPath {
181 namespace: repo.namespace,
182 name: repo.name,
183 },
184 number: pull.number,
185 title: pull.title,
186 status: pull.status,
187 }))
188 }
189}