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