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.

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