Skip to content
422 linesCodeBlameRaw
1//! Rows as they come out of D1, and their conversion to contract types.
2
3use g1t_contracts::User;
4use g1t_contracts::repos::RepoPath;
5use g1t_contracts::work::{
6 AgentRef, AgentVerdict, CheckStatus, Comment, CommentKind, Issue, IssueReason, MilestoneRef, Pull, PullStatus, Runtime, SessionEntry,
7 SessionEntryKind, State, Verdict,
8};
9use serde::Deserialize;
10
11/// An author or actor as stored: g1t itself when the id is its own (a
12/// security update it opened, a merge its settings made), g1t's agent by
13/// its id (what it made or filed), a person otherwise.
14pub(crate) fn user(id: String, username: String) -> User {
15 let kind = if g1t_contracts::system::is_system_id(&id) {
16 g1t_contracts::PrincipalKind::System
17 } else if id == g1t_contracts::identity::AGENT_ID {
18 g1t_contracts::PrincipalKind::Agent
19 } else {
20 g1t_contracts::PrincipalKind::User
21 };
22 User {
23 id,
24 username,
25 kind,
26 ..User::default()
27 }
28}
29
30/// Who asked g1t for the work, as stored beside its author: both columns,
31/// or nobody.
32pub(crate) fn requester(id: Option<String>, username: Option<String>) -> Option<User> {
33 Some(user(id?, username?))
34}
35
36#[derive(Deserialize)]
37pub struct IssueRow {
38 pub id: String,
39 pub repo_id: String,
40 pub number: u32,
41 pub title: String,
42 pub body: String,
43 /// JSON array of label names.
44 pub labels: String,
45 pub state: State,
46 pub reason: Option<IssueReason>,
47 pub resolved_by: Option<u32>,
48 pub author_id: String,
49 pub author_name: String,
50 #[serde(default)]
51 pub requested_by_id: Option<String>,
52 #[serde(default)]
53 pub requested_by_name: Option<String>,
54 pub created_at: String,
55 pub updated_at: String,
56 pub closed_at: Option<String>,
57 pub pull_count: u32,
58 pub comment_count: u32,
59 /// JSON array of usernames.
60 pub assignees: String,
61 /// JSON array of issue numbers.
62 pub blocked_by: String,
63 pub agent: Option<String>,
64 /// The number of its milestone, and that milestone's title.
65 #[serde(default)]
66 pub milestone: Option<u32>,
67 #[serde(default)]
68 pub milestone_title: Option<String>,
69}
70
71/// A milestone as an issue or pull request names it, from its number and
72/// title as read beside it.
73pub(crate) fn milestone_ref(number: Option<u32>, title: Option<String>) -> Option<MilestoneRef> {
74 Some(MilestoneRef { number: number?, title: title? })
75}
76
77impl From<IssueRow> for Issue {
78 fn from(row: IssueRow) -> Self {
79 Issue {
80 id: row.id,
81 repo_id: row.repo_id,
82 number: row.number,
83 title: row.title,
84 body: row.body,
85 labels: serde_json::from_str(&row.labels).unwrap_or_default(),
86 state: row.state,
87 reason: row.reason,
88 resolved_by: row.resolved_by,
89 author: user(row.author_id, row.author_name),
90 requested_by: requester(row.requested_by_id, row.requested_by_name),
91 created_at: row.created_at,
92 updated_at: row.updated_at,
93 closed_at: row.closed_at,
94 pull_count: row.pull_count,
95 comment_count: row.comment_count,
96 assignees: serde_json::from_str(&row.assignees).unwrap_or_default(),
97 blocked_by: serde_json::from_str(&row.blocked_by).unwrap_or_default(),
98 agent: row.agent,
99 milestone: milestone_ref(row.milestone, row.milestone_title),
100 }
101 }
102}
103
104/// The remembered assessment of a pull request, read beside its row.
105#[derive(Deserialize)]
106pub struct Snapshot {
107 pub stage: Option<g1t_contracts::work::Stage>,
108 pub stage_detail: Option<String>,
109 #[serde(default)]
110 pub revisions: u32,
111 /// Whether g1t is seeing it through at all.
112 #[serde(default)]
113 pub managed: u32,
114}
115
116/// A pull request's columns, with its milestone's title beside them: what
117/// every read of a [`PullRow`] selects.
118pub const PULL_COLUMNS: &str = "pulls.*,
119 (SELECT title FROM milestones
120 WHERE milestones.repo_id = pulls.repo_id AND milestones.number = pulls.milestone) AS milestone_title";
121
122#[derive(Deserialize)]
123pub struct PullRow {
124 pub id: String,
125 pub repo_id: String,
126 pub number: u32,
127 pub issue_number: Option<u32>,
128 pub title: String,
129 pub body: Option<String>,
130 pub agent: String,
131 pub runtime: Runtime,
132 pub status: PullStatus,
133 pub fork_repo_id: Option<String>,
134 pub fork_namespace: Option<String>,
135 pub fork_name: Option<String>,
136 pub source_branch: Option<String>,
137 pub head_commit: Option<String>,
138 pub merge_base: Option<String>,
139 pub merged_by: Option<String>,
140 pub merged_at: Option<String>,
141 pub superseded_by: Option<u32>,
142 pub check_status: Option<CheckStatus>,
143 /// JSON array of changed files; `None` until first worked out.
144 pub files: Option<String>,
145 /// JSON arrays of usernames.
146 pub assignees: String,
147 pub reviewers: String,
148 /// JSON array of `workspace/team`.
149 #[serde(default)]
150 pub team_reviewers: Option<String>,
151 pub author_id: String,
152 pub author_name: String,
153 #[serde(default)]
154 pub requested_by_id: Option<String>,
155 #[serde(default)]
156 pub requested_by_name: Option<String>,
157 pub created_at: String,
158 pub updated_at: String,
159 /// JSON array of label names.
160 #[serde(default)]
161 pub labels: Option<String>,
162 #[serde(default)]
163 pub milestone: Option<u32>,
164 #[serde(default)]
165 pub milestone_title: Option<String>,
166 /// The branch it merges into; NULL for the default branch.
167 #[serde(default)]
168 pub base_branch: Option<String>,
169 #[serde(default)]
170 pub head_pushed_by: Option<String>,
171 #[serde(default)]
172 pub head_pushed_at: Option<String>,
173}
174
175impl From<PullRow> for Pull {
176 fn from(row: PullRow) -> Self {
177 Pull {
178 id: row.id,
179 repo_id: row.repo_id,
180 number: row.number,
181 issue: row.issue_number,
182 title: row.title,
183 body: row.body,
184 agent: row.agent,
185 runtime: row.runtime,
186 status: row.status,
187 fork: row
188 .fork_namespace
189 .zip(row.fork_name)
190 .map(|(namespace, name)| RepoPath { namespace, name }),
191 fork_repo_id: row.fork_repo_id,
192 branch: row.source_branch,
193 head_commit: row.head_commit,
194 merge_base: row.merge_base,
195 merged_by: row.merged_by,
196 merged_at: row.merged_at,
197 superseded_by: row.superseded_by,
198 check_status: row.check_status,
199 files: row
200 .files
201 .as_deref()
202 .and_then(|files| serde_json::from_str(files).ok())
203 .unwrap_or_default(),
204 assignees: serde_json::from_str(&row.assignees).unwrap_or_default(),
205 reviewers: serde_json::from_str(&row.reviewers).unwrap_or_default(),
206 team_reviewers: row
207 .team_reviewers
208 .as_deref()
209 .and_then(|teams| serde_json::from_str(teams).ok())
210 .unwrap_or_default(),
211 author: user(row.author_id, row.author_name),
212 requested_by: requester(row.requested_by_id, row.requested_by_name),
213 created_at: row.created_at,
214 updated_at: row.updated_at,
215 labels: row
216 .labels
217 .as_deref()
218 .and_then(|labels| serde_json::from_str(labels).ok())
219 .unwrap_or_default(),
220 milestone: milestone_ref(row.milestone, row.milestone_title),
221 base: row.base_branch,
222 head_pushed_by: row.head_pushed_by,
223 head_pushed_at: row.head_pushed_at,
224 }
225 }
226}
227
228#[derive(Deserialize)]
229pub struct CommentRow {
230 pub id: String,
231 pub kind: CommentKind,
232 pub author_id: String,
233 pub author_name: String,
234 pub body: String,
235 pub path: Option<String>,
236 pub line: Option<u32>,
237 pub verdict: Option<Verdict>,
238 pub created_at: String,
239 #[serde(default)]
240 pub edited_at: Option<String>,
241 /// The issue or pull request it is on, where the query reads it.
242 #[serde(default)]
243 pub number: u32,
244 /// One of the workspace's agents that wrote it, as itself, and whom it
245 /// acted for (migrations/0033_agent_comments.sql).
246 #[serde(default)]
247 pub agent_id: Option<String>,
248 #[serde(default)]
249 pub agent_handle: Option<String>,
250 #[serde(default)]
251 pub agent_name: Option<String>,
252 #[serde(default)]
253 pub agent_avatar_seed: Option<String>,
254 #[serde(default)]
255 pub acting_for_id: Option<String>,
256 #[serde(default)]
257 pub acting_for_name: Option<String>,
258 /// An agent review's verdict, kept apart from `verdict`, which every
259 /// rule about approvals reads: so it is advisory.
260 #[serde(default)]
261 pub agent_verdict: Option<String>,
262}
263
264impl CommentRow {
265 /// Who answers for it: whoever an agent acted for, or its author.
266 pub fn answerable_id(&self) -> &str {
267 self.acting_for_id.as_deref().unwrap_or(&self.author_id)
268 }
269
270 /// Whether it carries a verdict, a person's or an agent's.
271 pub fn has_verdict(&self) -> bool {
272 self.verdict.is_some() || self.agent_verdict.is_some()
273 }
274}
275
276impl From<CommentRow> for Comment {
277 fn from(row: CommentRow) -> Self {
278 let agent = match (row.agent_id, row.agent_handle) {
279 (Some(id), Some(handle)) => Some(AgentRef {
280 display_name: row.agent_name.unwrap_or_else(|| handle.clone()),
281 avatar_seed: row.agent_avatar_seed.unwrap_or_else(|| handle.clone()),
282 id,
283 handle,
284 }),
285 _ => None,
286 };
287 let advisory_verdict = row.agent_verdict.as_deref().and_then(AgentVerdict::parse);
288 let mut author = user(row.author_id, row.author_name);
289 if agent.is_some() {
290 author.kind = g1t_contracts::PrincipalKind::Agent;
291 }
292 Comment {
293 id: row.id,
294 kind: row.kind,
295 author,
296 body: row.body,
297 path: row.path,
298 line: row.line,
299 // A person's verdict, or for showing, an agent's advisory one.
300 verdict: row.verdict.or(advisory_verdict.and_then(AgentVerdict::verdict)),
301 created_at: row.created_at,
302 edited_at: row.edited_at,
303 acting_for: agent.as_ref().and(requester(row.acting_for_id, row.acting_for_name)),
304 advisory: agent.is_some() && advisory_verdict.is_some(),
305 agent,
306 }
307 }
308}
309
310#[derive(Deserialize)]
311pub struct SessionRow {
312 pub seq: u32,
313 pub kind: SessionEntryKind,
314 pub text: String,
315 pub tool: Option<String>,
316 pub commit: Option<String>,
317 pub at: String,
318}
319
320impl From<SessionRow> for SessionEntry {
321 fn from(row: SessionRow) -> Self {
322 SessionEntry {
323 seq: row.seq,
324 kind: row.kind,
325 text: row.text,
326 tool: row.tool,
327 commit: row.commit,
328 at: row.at,
329 }
330 }
331}
332
333/// A pull request whose head a push moved.
334#[derive(Deserialize)]
335pub struct MovedRow {
336 pub id: String,
337 pub repo_id: String,
338 pub number: u32,
339 pub issue_number: Option<u32>,
340 pub status: PullStatus,
341 pub author_id: String,
342 pub author_name: String,
343 #[serde(default)]
344 pub requested_by_id: Option<String>,
345 #[serde(default)]
346 pub requested_by_name: Option<String>,
347}
348
349/// A single number selected as `n`.
350#[derive(Deserialize)]
351pub struct NumberRow {
352 pub n: u32,
353}
354
355/// A single string selected as `value`.
356#[derive(Deserialize)]
357pub struct ValueRow {
358 pub value: String,
359}
360
361/// Rows as the database holds them, for tests of the rules that read them.
362#[cfg(test)]
363pub(crate) mod stored {
364 use super::*;
365
366 /// A pull request g1t made in a fork, read back as stored: g1t as its
367 /// author, and `requested_by` the person who asked, if anyone did.
368 pub(crate) fn pull(author: (&str, &str), requested_by: Option<(&str, &str)>) -> Pull {
369 let row: PullRow = serde_json::from_value(serde_json::json!({
370 "id": "pr_1", "repo_id": "rep_1", "number": 14, "issue_number": 12, "title": "Fix it",
371 "body": null, "agent": "g1t", "runtime": "hosted", "status": "open",
372 "fork_repo_id": "rep_f", "fork_namespace": "pulls", "fork_name": "pr_1",
373 "source_branch": null, "head_commit": null, "merge_base": null, "merged_by": null,
374 "merged_at": null, "superseded_by": null, "check_status": null, "files": null,
375 "assignees": "[]", "reviewers": "[]",
376 "author_id": author.0, "author_name": author.1,
377 "requested_by_id": requested_by.map(|(id, _)| id),
378 "requested_by_name": requested_by.map(|(_, name)| name),
379 "created_at": "", "updated_at": ""
380 }))
381 .unwrap();
382 row.into()
383 }
384
385 /// g1t's agent, as an author is stored.
386 pub(crate) const G1T: (&str, &str) = (g1t_contracts::identity::AGENT_ID, "g1t");
387 /// The person who asked g1t for the work.
388 pub(crate) const ASKER: (&str, &str) = ("usr_1", "syntaqx");
389}
390
391#[cfg(test)]
392mod tests {
393 use super::stored::{ASKER, G1T, pull};
394 use super::*;
395 use g1t_contracts::PrincipalKind;
396
397 #[test]
398 fn g1t_s_pull_request_reads_back_as_g1t_s_requested_by_the_person() {
399 let made = pull(G1T, Some(ASKER));
400 assert_eq!((made.author.username.as_str(), made.author.kind), ("g1t", PrincipalKind::Agent));
401 let asked = made.requested_by.as_ref().expect("who asked is kept");
402 assert_eq!((asked.id.as_str(), asked.username.as_str(), asked.kind), ("usr_1", "syntaqx", PrincipalKind::User));
403 assert_eq!(made.owner().id, "usr_1");
404 }
405
406 #[test]
407 fn anyone_else_s_reads_back_as_theirs_alone() {
408 let own = pull(ASKER, None);
409 assert_eq!(own.author.kind, PrincipalKind::User);
410 assert!(own.requested_by.is_none());
411 assert_eq!(own.owner().id, "usr_1");
412 // Half a requester is nobody.
413 assert!(requester(Some("usr_1".into()), None).is_none());
414 }
415
416 #[test]
417 fn g1t_s_own_work_reads_back_as_g1t_itself() {
418 let own = pull(("g1t", "g1t"), None);
419 assert_eq!(own.author.kind, PrincipalKind::System);
420 assert!(own.requested_by.is_none());
421 }
422}