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.

491 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, MilestoneRef, Pull, PullStatus, Runtime, SessionEntry, SessionEntryKind,
7 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/// Who wrote a comment or did what an event notes, as stored: g1t, and
37/// whom it acted for, when a session g1t started for someone did it (as
38/// `work::authorship` records what it opens); whoever it was otherwise.
39#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40pub(crate) struct By<'a> {
41 pub id: &'a str,
42 pub name: &'a str,
43 /// The person's id and username.
44 pub acting_for: Option<(&'a str, &'a str)>,
45}
46
47impl<'a> From<(&'a str, &'a str)> for By<'a> {
48 fn from((id, name): (&'a str, &'a str)) -> Self {
49 By { id, name, acting_for: None }
50 }
51}
52
53impl<'a> From<(&'a String, &'a String)> for By<'a> {
54 fn from((id, name): (&'a String, &'a String)) -> Self {
55 By { id, name, acting_for: None }
56 }
57}
58
59impl<'a> From<&'a User> for By<'a> {
60 fn from(actor: &'a User) -> Self {
61 if actor.kind == g1t_contracts::PrincipalKind::Agent || actor.acting.is_some() {
62 let acting_for = actor
63 .acting
64 .as_deref()
65 .map(|acting| &acting.on_behalf_of)
66 .filter(|person| !g1t_contracts::system::is_system_id(&person.id))
67 .map(|person| (person.id.as_str(), person.username.as_str()));
68 return By { id: g1t_contracts::identity::AGENT_ID, name: g1t_contracts::identity::AGENT_NAME, acting_for };
69 }
70 By { id: &actor.id, name: &actor.username, acting_for: None }
71 }
72}
73
74/// A milestone as a work item or pull request names it, from its number
75/// and title as read beside it.
76pub(crate) fn milestone_ref(number: Option<u32>, title: Option<String>) -> Option<MilestoneRef> {
77 Some(MilestoneRef { number: number?, title: title? })
78}
79
80/// The remembered assessment of a pull request, read beside its row.
81#[derive(Deserialize)]
82pub struct Snapshot {
83 pub stage: Option<g1t_contracts::work::Stage>,
84 pub stage_detail: Option<String>,
85 #[serde(default)]
86 pub revisions: u32,
87 /// Whether g1t is seeing it through at all.
88 #[serde(default)]
89 pub managed: u32,
90}
91
92/// A pull request's columns, with what it is read with: its workspace
93/// milestone's title, and its work item and those it closes and is part of
94/// by number. What every read of a [`PullRow`] selects.
95pub const PULL_COLUMNS: &str = "pulls.*,
96 (SELECT title FROM milestones
97 WHERE milestones.workspace = (SELECT workspace FROM repo_workspaces WHERE repo_workspaces.repo_id = pulls.repo_id)
98 AND milestones.number = pulls.milestone) AS milestone_title,
99 (SELECT number FROM work_items WHERE work_items.id = pulls.item_id) AS item_number,
100 (SELECT json_group_array(number) FROM
101 (SELECT w.number AS number FROM json_each(pulls.closes_items) j JOIN work_items w ON w.id = j.value
102 WHERE w.number IS NOT NULL ORDER BY j.key)) AS closes_numbers,
103 (SELECT json_group_array(number) FROM
104 (SELECT w.number AS number FROM json_each(pulls.parts) j JOIN work_items w ON w.id = j.value
105 WHERE w.number IS NOT NULL ORDER BY j.key)) AS parts_numbers";
106
107#[derive(Deserialize)]
108pub struct PullRow {
109 pub id: String,
110 pub repo_id: String,
111 pub number: u32,
112 /// Its work item's number in the workspace.
113 #[serde(default)]
114 pub item_number: Option<u32>,
115 pub title: String,
116 pub body: Option<String>,
117 pub agent: String,
118 pub runtime: Runtime,
119 pub status: PullStatus,
120 pub fork_repo_id: Option<String>,
121 pub fork_namespace: Option<String>,
122 pub fork_name: Option<String>,
123 pub source_branch: Option<String>,
124 /// The fork whose branch it is, for one opened from a fork
125 /// (migrations/0041_pull_head_repo.sql).
126 #[serde(default)]
127 pub head_repo_id: Option<String>,
128 #[serde(default)]
129 pub head_namespace: Option<String>,
130 #[serde(default)]
131 pub head_name: Option<String>,
132 pub head_commit: Option<String>,
133 pub merge_base: Option<String>,
134 pub merged_by: Option<String>,
135 pub merged_at: Option<String>,
136 pub superseded_by: Option<u32>,
137 pub check_status: Option<CheckStatus>,
138 /// JSON array of changed files; `None` until first worked out.
139 pub files: Option<String>,
140 /// JSON arrays of usernames.
141 pub assignees: String,
142 pub reviewers: String,
143 /// JSON array of `workspace/team`.
144 #[serde(default)]
145 pub team_reviewers: Option<String>,
146 pub author_id: String,
147 pub author_name: String,
148 #[serde(default)]
149 pub requested_by_id: Option<String>,
150 #[serde(default)]
151 pub requested_by_name: Option<String>,
152 pub created_at: String,
153 pub updated_at: String,
154 /// JSON array of label names.
155 #[serde(default)]
156 pub labels: Option<String>,
157 #[serde(default)]
158 pub milestone: Option<u32>,
159 #[serde(default)]
160 pub milestone_title: Option<String>,
161 /// The branch it merges into; NULL for the default branch.
162 #[serde(default)]
163 pub base_branch: Option<String>,
164 #[serde(default)]
165 pub head_pushed_by: Option<String>,
166 #[serde(default)]
167 pub head_pushed_at: Option<String>,
168 /// How it merged and what its merge left the base at; its auto-merge
169 /// (JSON), the work items it closes and is part of (JSON arrays of
170 /// numbers, as read) and whether its branch was deleted
171 /// (migrations/0042_merge_settings.sql, 0049_work_items.sql).
172 #[serde(default)]
173 pub merge_method: Option<String>,
174 #[serde(default)]
175 pub merge_commit: Option<String>,
176 #[serde(default)]
177 pub auto_merge: Option<String>,
178 #[serde(default)]
179 pub closes_numbers: Option<String>,
180 #[serde(default)]
181 pub parts_numbers: Option<String>,
182 #[serde(default)]
183 pub head_branch_deleted: Option<u8>,
184}
185
186impl From<PullRow> for Pull {
187 fn from(row: PullRow) -> Self {
188 Pull {
189 id: row.id,
190 repo_id: row.repo_id,
191 number: row.number,
192 item: row.item_number,
193 title: row.title,
194 body: row.body,
195 agent: row.agent,
196 runtime: row.runtime,
197 status: row.status,
198 fork: row
199 .fork_namespace
200 .zip(row.fork_name)
201 .map(|(namespace, name)| RepoPath { namespace, name }),
202 fork_repo_id: row.fork_repo_id,
203 branch: row.source_branch,
204 head_repo: row
205 .head_namespace
206 .zip(row.head_name)
207 .map(|(namespace, name)| RepoPath { namespace, name }),
208 head_repo_id: row.head_repo_id,
209 head_commit: row.head_commit,
210 merge_base: row.merge_base,
211 merged_by: row.merged_by,
212 merged_at: row.merged_at,
213 superseded_by: row.superseded_by,
214 check_status: row.check_status,
215 files: row
216 .files
217 .as_deref()
218 .and_then(|files| serde_json::from_str(files).ok())
219 .unwrap_or_default(),
220 assignees: serde_json::from_str(&row.assignees).unwrap_or_default(),
221 reviewers: serde_json::from_str(&row.reviewers).unwrap_or_default(),
222 team_reviewers: row
223 .team_reviewers
224 .as_deref()
225 .and_then(|teams| serde_json::from_str(teams).ok())
226 .unwrap_or_default(),
227 author: user(row.author_id, row.author_name),
228 requested_by: requester(row.requested_by_id, row.requested_by_name),
229 created_at: row.created_at,
230 updated_at: row.updated_at,
231 labels: row
232 .labels
233 .as_deref()
234 .and_then(|labels| serde_json::from_str(labels).ok())
235 .unwrap_or_default(),
236 milestone: milestone_ref(row.milestone, row.milestone_title),
237 base: row.base_branch,
238 head_pushed_by: row.head_pushed_by,
239 head_pushed_at: row.head_pushed_at,
240 merge_method: row
241 .merge_method
242 .and_then(|name| serde_json::from_value(serde_json::Value::String(name)).ok()),
243 merge_commit: row.merge_commit,
244 auto_merge: row.auto_merge.as_deref().and_then(|json| serde_json::from_str(json).ok()),
245 closes: row.closes_numbers.as_deref().and_then(|json| serde_json::from_str(json).ok()).unwrap_or_default(),
246 parts: row.parts_numbers.as_deref().and_then(|json| serde_json::from_str(json).ok()).unwrap_or_default(),
247 head_branch_deleted: row.head_branch_deleted.is_some_and(|deleted| deleted != 0),
248 }
249 }
250}
251
252#[derive(Deserialize)]
253pub struct CommentRow {
254 pub id: String,
255 pub kind: CommentKind,
256 pub author_id: String,
257 pub author_name: String,
258 pub body: String,
259 pub path: Option<String>,
260 pub line: Option<u32>,
261 pub verdict: Option<Verdict>,
262 pub created_at: String,
263 #[serde(default)]
264 pub edited_at: Option<String>,
265 /// The pull request it is on, where the query reads it.
266 #[serde(default)]
267 pub number: Option<u32>,
268 /// The work item it is on.
269 #[serde(default)]
270 pub item_id: Option<String>,
271 /// One of the workspace's agents that wrote it, as itself, and whom it
272 /// acted for (migrations/0033_agent_comments.sql).
273 #[serde(default)]
274 pub agent_id: Option<String>,
275 #[serde(default)]
276 pub agent_handle: Option<String>,
277 #[serde(default)]
278 pub agent_name: Option<String>,
279 #[serde(default)]
280 pub agent_avatar_seed: Option<String>,
281 #[serde(default)]
282 pub acting_for_id: Option<String>,
283 #[serde(default)]
284 pub acting_for_name: Option<String>,
285 /// An agent review's verdict, kept apart from `verdict`, which every
286 /// rule about approvals reads: so it is advisory.
287 #[serde(default)]
288 pub agent_verdict: Option<String>,
289}
290
291impl CommentRow {
292 /// Who answers for it: whoever an agent acted for, or its author.
293 pub fn answerable_id(&self) -> &str {
294 self.acting_for_id.as_deref().unwrap_or(&self.author_id)
295 }
296
297 /// Whether it carries a verdict, a person's or an agent's.
298 pub fn has_verdict(&self) -> bool {
299 self.verdict.is_some() || self.agent_verdict.is_some()
300 }
301}
302
303impl From<CommentRow> for Comment {
304 fn from(row: CommentRow) -> Self {
305 let agent = match (row.agent_id, row.agent_handle) {
306 (Some(id), Some(handle)) => Some(AgentRef {
307 display_name: row.agent_name.unwrap_or_else(|| handle.clone()),
308 avatar_seed: row.agent_avatar_seed.unwrap_or_else(|| handle.clone()),
309 id,
310 handle,
311 }),
312 _ => None,
313 };
314 let advisory_verdict = row.agent_verdict.as_deref().and_then(AgentVerdict::parse);
315 // Whom it acted for: a former workspace agent's, or g1t's in a
316 // session someone started.
317 let acted = agent.is_some() || row.author_id == g1t_contracts::identity::AGENT_ID;
318 let mut author = user(row.author_id, row.author_name);
319 if agent.is_some() {
320 author.kind = g1t_contracts::PrincipalKind::Agent;
321 }
322 Comment {
323 id: row.id,
324 kind: row.kind,
325 author,
326 body: row.body,
327 path: row.path,
328 line: row.line,
329 // A person's verdict, or for showing, an agent's advisory one.
330 verdict: row.verdict.or(advisory_verdict.and_then(AgentVerdict::verdict)),
331 created_at: row.created_at,
332 edited_at: row.edited_at,
333 acting_for: if acted { requester(row.acting_for_id, row.acting_for_name) } else { None },
334 advisory: agent.is_some() && advisory_verdict.is_some(),
335 agent,
336 reactions: Vec::new(),
337 }
338 }
339}
340
341#[derive(Deserialize)]
342pub struct SessionRow {
343 pub seq: u32,
344 pub kind: SessionEntryKind,
345 pub text: String,
346 pub tool: Option<String>,
347 pub commit: Option<String>,
348 pub at: String,
349 /// Whose harness session recorded it, where the query says.
350 #[serde(default)]
351 pub actor_id: Option<String>,
352}
353
354impl From<SessionRow> for SessionEntry {
355 fn from(row: SessionRow) -> Self {
356 SessionEntry {
357 seq: row.seq,
358 kind: row.kind,
359 text: row.text,
360 tool: row.tool,
361 commit: row.commit,
362 at: row.at,
363 withheld: false,
364 }
365 }
366}
367
368/// A pull request whose head a push moved.
369#[derive(Deserialize)]
370pub struct MovedRow {
371 pub id: String,
372 pub repo_id: String,
373 pub number: u32,
374 pub status: PullStatus,
375 pub author_id: String,
376 pub author_name: String,
377 #[serde(default)]
378 pub requested_by_id: Option<String>,
379 #[serde(default)]
380 pub requested_by_name: Option<String>,
381}
382
383/// A single number selected as `n`.
384#[derive(Deserialize)]
385pub struct NumberRow {
386 pub n: u32,
387}
388
389/// A single string selected as `value`.
390#[derive(Deserialize)]
391pub struct ValueRow {
392 pub value: String,
393}
394
395/// Rows as the database holds them, for tests of the rules that read them.
396#[cfg(test)]
397pub(crate) mod stored {
398 use super::*;
399
400 /// A pull request g1t made in a fork, read back as stored: g1t as its
401 /// author, and `requested_by` the person who asked, if anyone did.
402 pub(crate) fn pull(author: (&str, &str), requested_by: Option<(&str, &str)>) -> Pull {
403 let row: PullRow = serde_json::from_value(serde_json::json!({
404 "id": "pr_1", "repo_id": "rep_1", "number": 14, "item_number": 12, "title": "Fix it",
405 "body": null, "agent": "g1t", "runtime": "hosted", "status": "open",
406 "fork_repo_id": "rep_f", "fork_namespace": "pulls", "fork_name": "pr_1",
407 "source_branch": null, "head_commit": null, "merge_base": null, "merged_by": null,
408 "merged_at": null, "superseded_by": null, "check_status": null, "files": null,
409 "assignees": "[]", "reviewers": "[]",
410 "author_id": author.0, "author_name": author.1,
411 "requested_by_id": requested_by.map(|(id, _)| id),
412 "requested_by_name": requested_by.map(|(_, name)| name),
413 "created_at": "", "updated_at": ""
414 }))
415 .unwrap();
416 row.into()
417 }
418
419 /// g1t's agent, as an author is stored.
420 pub(crate) const G1T: (&str, &str) = (g1t_contracts::identity::AGENT_ID, "g1t");
421 /// The person who asked g1t for the work.
422 pub(crate) const ASKER: (&str, &str) = ("usr_1", "syntaqx");
423}
424
425#[cfg(test)]
426mod tests {
427 use super::stored::{ASKER, G1T, pull};
428 use super::*;
429 use g1t_contracts::PrincipalKind;
430
431 #[test]
432 fn g1t_s_pull_request_reads_back_as_g1t_s_requested_by_the_person() {
433 let made = pull(G1T, Some(ASKER));
434 assert_eq!((made.author.username.as_str(), made.author.kind), ("g1t", PrincipalKind::Agent));
435 let asked = made.requested_by.as_ref().expect("who asked is kept");
436 assert_eq!((asked.id.as_str(), asked.username.as_str(), asked.kind), ("usr_1", "syntaqx", PrincipalKind::User));
437 assert_eq!(made.owner().id, "usr_1");
438 }
439
440 #[test]
441 fn anyone_else_s_reads_back_as_theirs_alone() {
442 let own = pull(ASKER, None);
443 assert_eq!(own.author.kind, PrincipalKind::User);
444 assert!(own.requested_by.is_none());
445 assert_eq!(own.owner().id, "usr_1");
446 // Half a requester is nobody.
447 assert!(requester(Some("usr_1".into()), None).is_none());
448 }
449
450 /// A session g1t started for someone acts as them (credentials::as_person)
451 /// with what it acts as kept: what it writes is g1t's, for them, and
452 /// reads back that way, so they can still edit it.
453 #[test]
454 fn a_session_s_comment_is_g1t_s_for_the_person() {
455 use g1t_contracts::credentials::{Acting, Principal};
456 let person = User { id: "usr_1".into(), username: "syntaqx".into(), ..User::default() };
457 assert_eq!(By::from(&person), By { id: "usr_1", name: "syntaqx", acting_for: None });
458 let session = User {
459 acting: Some(Box::new(Acting {
460 credential_id: "tok_1".into(),
461 agent: "g1t".into(),
462 on_behalf_of: Principal { id: "usr_1".into(), username: "syntaqx".into() },
463 scope: g1t_contracts::identity::AgentScope {
464 repo: RepoPath { namespace: "acme".into(), name: "rocket".into() },
465 operations: Vec::new(),
466 run: None,
467 },
468 })),
469 ..person.clone()
470 };
471 let by = By::from(&session);
472 assert_eq!((by.id, by.name, by.acting_for), (G1T.0, "g1t", Some(ASKER)));
473 let row: CommentRow = serde_json::from_value(serde_json::json!({
474 "id": "cmt_1", "kind": "event", "author_id": by.id, "author_name": by.name,
475 "body": "closed this as completed", "path": null, "line": null, "verdict": null,
476 "created_at": "", "acting_for_id": "usr_1", "acting_for_name": "syntaqx"
477 }))
478 .unwrap();
479 assert_eq!(row.answerable_id(), "usr_1");
480 let comment: Comment = row.into();
481 assert_eq!((comment.author.username.as_str(), comment.author.kind), ("g1t", PrincipalKind::Agent));
482 assert_eq!(comment.acting_for.map(|user| user.username).as_deref(), Some("syntaqx"));
483 }
484
485 #[test]
486 fn g1t_s_own_work_reads_back_as_g1t_itself() {
487 let own = pull(("g1t", "g1t"), None);
488 assert_eq!(own.author.kind, PrincipalKind::System);
489 assert!(own.requested_by.is_none());
490 }
491}