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.

g1t.sh
538 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}
400
401impl From<SessionRow> for SessionEntry {
402 fn from(row: SessionRow) -> Self {
403 SessionEntry {
404 seq: row.seq,
405 kind: row.kind,
406 text: row.text,
407 tool: row.tool,
408 commit: row.commit,
409 at: row.at,
410 }
411 }
412}
413
414/// A pull request whose head a push moved.
415#[derive(Deserialize)]
416pub struct MovedRow {
417 pub id: String,
418 pub repo_id: String,
419 pub number: u32,
420 pub issue_number: Option<u32>,
421 pub status: PullStatus,
422 pub author_id: String,
423 pub author_name: String,
424 #[serde(default)]
425 pub requested_by_id: Option<String>,
426 #[serde(default)]
427 pub requested_by_name: Option<String>,
428}
429
430/// A single number selected as `n`.
431#[derive(Deserialize)]
432pub struct NumberRow {
433 pub n: u32,
434}
435
436/// A single string selected as `value`.
437#[derive(Deserialize)]
438pub struct ValueRow {
439 pub value: String,
440}
441
442/// Rows as the database holds them, for tests of the rules that read them.
443#[cfg(test)]
444pub(crate) mod stored {
445 use super::*;
446
447 /// A pull request g1t made in a fork, read back as stored: g1t as its
448 /// author, and `requested_by` the person who asked, if anyone did.
449 pub(crate) fn pull(author: (&str, &str), requested_by: Option<(&str, &str)>) -> Pull {
450 let row: PullRow = serde_json::from_value(serde_json::json!({
451 "id": "pr_1", "repo_id": "rep_1", "number": 14, "issue_number": 12, "title": "Fix it",
452 "body": null, "agent": "g1t", "runtime": "hosted", "status": "open",
453 "fork_repo_id": "rep_f", "fork_namespace": "pulls", "fork_name": "pr_1",
454 "source_branch": null, "head_commit": null, "merge_base": null, "merged_by": null,
455 "merged_at": null, "superseded_by": null, "check_status": null, "files": null,
456 "assignees": "[]", "reviewers": "[]",
457 "author_id": author.0, "author_name": author.1,
458 "requested_by_id": requested_by.map(|(id, _)| id),
459 "requested_by_name": requested_by.map(|(_, name)| name),
460 "created_at": "", "updated_at": ""
461 }))
462 .unwrap();
463 row.into()
464 }
465
466 /// g1t's agent, as an author is stored.
467 pub(crate) const G1T: (&str, &str) = (g1t_contracts::identity::AGENT_ID, "g1t");
468 /// The person who asked g1t for the work.
469 pub(crate) const ASKER: (&str, &str) = ("usr_1", "syntaqx");
470}
471
472#[cfg(test)]
473mod tests {
474 use super::stored::{ASKER, G1T, pull};
475 use super::*;
476 use g1t_contracts::PrincipalKind;
477
478 #[test]
479 fn g1t_s_pull_request_reads_back_as_g1t_s_requested_by_the_person() {
480 let made = pull(G1T, Some(ASKER));
481 assert_eq!((made.author.username.as_str(), made.author.kind), ("g1t", PrincipalKind::Agent));
482 let asked = made.requested_by.as_ref().expect("who asked is kept");
483 assert_eq!((asked.id.as_str(), asked.username.as_str(), asked.kind), ("usr_1", "syntaqx", PrincipalKind::User));
484 assert_eq!(made.owner().id, "usr_1");
485 }
486
487 #[test]
488 fn anyone_else_s_reads_back_as_theirs_alone() {
489 let own = pull(ASKER, None);
490 assert_eq!(own.author.kind, PrincipalKind::User);
491 assert!(own.requested_by.is_none());
492 assert_eq!(own.owner().id, "usr_1");
493 // Half a requester is nobody.
494 assert!(requester(Some("usr_1".into()), None).is_none());
495 }
496
497 /// A session g1t started for someone acts as them (credentials::as_person)
498 /// with what it acts as kept: what it writes is g1t's, for them, and
499 /// reads back that way, so they can still edit it.
500 #[test]
501 fn a_session_s_comment_is_g1t_s_for_the_person() {
502 use g1t_contracts::credentials::{Acting, Principal};
503 let person = User { id: "usr_1".into(), username: "syntaqx".into(), ..User::default() };
504 assert_eq!(By::from(&person), By { id: "usr_1", name: "syntaqx", acting_for: None });
505 let session = User {
506 acting: Some(Box::new(Acting {
507 credential_id: "tok_1".into(),
508 agent: "g1t".into(),
509 on_behalf_of: Principal { id: "usr_1".into(), username: "syntaqx".into() },
510 scope: g1t_contracts::identity::AgentScope {
511 repo: RepoPath { namespace: "acme".into(), name: "rocket".into() },
512 operations: Vec::new(),
513 run: None,
514 },
515 })),
516 ..person.clone()
517 };
518 let by = By::from(&session);
519 assert_eq!((by.id, by.name, by.acting_for), (G1T.0, "g1t", Some(ASKER)));
520 let row: CommentRow = serde_json::from_value(serde_json::json!({
521 "id": "cmt_1", "kind": "event", "author_id": by.id, "author_name": by.name,
522 "body": "closed this as completed", "path": null, "line": null, "verdict": null,
523 "created_at": "", "acting_for_id": "usr_1", "acting_for_name": "syntaqx"
524 }))
525 .unwrap();
526 assert_eq!(row.answerable_id(), "usr_1");
527 let comment: Comment = row.into();
528 assert_eq!((comment.author.username.as_str(), comment.author.kind), ("g1t", PrincipalKind::Agent));
529 assert_eq!(comment.acting_for.map(|user| user.username).as_deref(), Some("syntaqx"));
530 }
531
532 #[test]
533 fn g1t_s_own_work_reads_back_as_g1t_itself() {
534 let own = pull(("g1t", "g1t"), None);
535 assert_eq!(own.author.kind, PrincipalKind::System);
536 assert!(own.requested_by.is_none());
537 }
538}