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.

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