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
422 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Docs: inbox threads, reasons, subscriptions, watching, email, API and MCP1//! 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::{
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar6 AgentRef, AgentVerdict, CheckStatus, Comment, CommentKind, Issue, IssueReason, MilestoneRef, Pull, PullStatus, Runtime, SessionEntry,
Docs: inbox threads, reasons, subscriptions, watching, email, API and MCP7 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 pub author_id: String,
49 pub author_name: String,
50 #[serde(default)]
51 pub requested_by_id: Option<String>,
52 #[serde(default)]
53 pub requested_by_name: Option<String>,
54 pub created_at: String,
55 pub updated_at: String,
56 pub closed_at: Option<String>,
57 pub pull_count: u32,
58 pub comment_count: u32,
59 /// JSON array of usernames.
60 pub assignees: String,
61 /// JSON array of issue numbers.
62 pub blocked_by: String,
63 pub agent: Option<String>,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar64 /// The number of its milestone, and that milestone's title.
65 #[serde(default)]
66 pub milestone: Option<u32>,
67 #[serde(default)]
68 pub milestone_title: Option<String>,
Docs: inbox threads, reasons, subscriptions, watching, email, API and MCP69}
70
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar71/// A milestone as an issue or pull request names it, from its number and
72/// title as read beside it.
73pub(crate) fn milestone_ref(number: Option<u32>, title: Option<String>) -> Option<MilestoneRef> {
74 Some(MilestoneRef { number: number?, title: title? })
75}
76
Docs: inbox threads, reasons, subscriptions, watching, email, API and MCP77impl From<IssueRow> for Issue {
78 fn from(row: IssueRow) -> Self {
79 Issue {
80 id: row.id,
81 repo_id: row.repo_id,
82 number: row.number,
83 title: row.title,
84 body: row.body,
85 labels: serde_json::from_str(&row.labels).unwrap_or_default(),
86 state: row.state,
87 reason: row.reason,
88 resolved_by: row.resolved_by,
89 author: user(row.author_id, row.author_name),
90 requested_by: requester(row.requested_by_id, row.requested_by_name),
91 created_at: row.created_at,
92 updated_at: row.updated_at,
93 closed_at: row.closed_at,
94 pull_count: row.pull_count,
95 comment_count: row.comment_count,
96 assignees: serde_json::from_str(&row.assignees).unwrap_or_default(),
97 blocked_by: serde_json::from_str(&row.blocked_by).unwrap_or_default(),
98 agent: row.agent,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar99 milestone: milestone_ref(row.milestone, row.milestone_title),
Docs: inbox threads, reasons, subscriptions, watching, email, API and MCP100 }
101 }
102}
103
104/// The remembered assessment of a pull request, read beside its row.
105#[derive(Deserialize)]
106pub struct Snapshot {
107 pub stage: Option<g1t_contracts::work::Stage>,
108 pub stage_detail: Option<String>,
109 #[serde(default)]
110 pub revisions: u32,
111 /// Whether g1t is seeing it through at all.
112 #[serde(default)]
113 pub managed: u32,
114}
115
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.116/// A pull request's columns, with its milestone's title beside them: what
117/// every read of a [`PullRow`] selects.
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar118pub const PULL_COLUMNS: &str = "pulls.*,
119 (SELECT title FROM milestones
120 WHERE milestones.repo_id = pulls.repo_id AND milestones.number = pulls.milestone) AS milestone_title";
Docs: inbox threads, reasons, subscriptions, watching, email, API and MCP121
122#[derive(Deserialize)]
123pub struct PullRow {
124 pub id: String,
125 pub repo_id: String,
126 pub number: u32,
127 pub issue_number: Option<u32>,
128 pub title: String,
129 pub body: Option<String>,
130 pub agent: String,
131 pub runtime: Runtime,
132 pub status: PullStatus,
133 pub fork_repo_id: Option<String>,
134 pub fork_namespace: Option<String>,
135 pub fork_name: Option<String>,
136 pub source_branch: Option<String>,
137 pub head_commit: Option<String>,
138 pub merge_base: Option<String>,
139 pub merged_by: Option<String>,
140 pub merged_at: Option<String>,
141 pub superseded_by: Option<u32>,
142 pub check_status: Option<CheckStatus>,
143 /// JSON array of changed files; `None` until first worked out.
144 pub files: Option<String>,
145 /// JSON arrays of usernames.
146 pub assignees: String,
147 pub reviewers: String,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar148 /// JSON array of `workspace/team`.
149 #[serde(default)]
150 pub team_reviewers: Option<String>,
Docs: inbox threads, reasons, subscriptions, watching, email, API and MCP151 pub author_id: String,
152 pub author_name: String,
153 #[serde(default)]
154 pub requested_by_id: Option<String>,
155 #[serde(default)]
156 pub requested_by_name: Option<String>,
157 pub created_at: String,
158 pub updated_at: String,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar159 /// JSON array of label names.
160 #[serde(default)]
161 pub labels: Option<String>,
162 #[serde(default)]
163 pub milestone: Option<u32>,
164 #[serde(default)]
165 pub milestone_title: Option<String>,
166 /// The branch it merges into; NULL for the default branch.
167 #[serde(default)]
168 pub base_branch: Option<String>,
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge169 #[serde(default)]
170 pub head_pushed_by: Option<String>,
171 #[serde(default)]
172 pub head_pushed_at: Option<String>,
Docs: inbox threads, reasons, subscriptions, watching, email, API and MCP173}
174
175impl From<PullRow> for Pull {
176 fn from(row: PullRow) -> Self {
177 Pull {
178 id: row.id,
179 repo_id: row.repo_id,
180 number: row.number,
181 issue: row.issue_number,
182 title: row.title,
183 body: row.body,
184 agent: row.agent,
185 runtime: row.runtime,
186 status: row.status,
187 fork: row
188 .fork_namespace
189 .zip(row.fork_name)
190 .map(|(namespace, name)| RepoPath { namespace, name }),
191 fork_repo_id: row.fork_repo_id,
192 branch: row.source_branch,
193 head_commit: row.head_commit,
194 merge_base: row.merge_base,
195 merged_by: row.merged_by,
196 merged_at: row.merged_at,
197 superseded_by: row.superseded_by,
198 check_status: row.check_status,
199 files: row
200 .files
201 .as_deref()
202 .and_then(|files| serde_json::from_str(files).ok())
203 .unwrap_or_default(),
204 assignees: serde_json::from_str(&row.assignees).unwrap_or_default(),
205 reviewers: serde_json::from_str(&row.reviewers).unwrap_or_default(),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar206 team_reviewers: row
207 .team_reviewers
208 .as_deref()
209 .and_then(|teams| serde_json::from_str(teams).ok())
210 .unwrap_or_default(),
Docs: inbox threads, reasons, subscriptions, watching, email, API and MCP211 author: user(row.author_id, row.author_name),
212 requested_by: requester(row.requested_by_id, row.requested_by_name),
213 created_at: row.created_at,
214 updated_at: row.updated_at,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar215 labels: row
216 .labels
217 .as_deref()
218 .and_then(|labels| serde_json::from_str(labels).ok())
219 .unwrap_or_default(),
220 milestone: milestone_ref(row.milestone, row.milestone_title),
221 base: row.base_branch,
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge222 head_pushed_by: row.head_pushed_by,
223 head_pushed_at: row.head_pushed_at,
Docs: inbox threads, reasons, subscriptions, watching, email, API and MCP224 }
225 }
226}
227
228#[derive(Deserialize)]
229pub struct CommentRow {
230 pub id: String,
231 pub kind: CommentKind,
232 pub author_id: String,
233 pub author_name: String,
234 pub body: String,
235 pub path: Option<String>,
236 pub line: Option<u32>,
237 pub verdict: Option<Verdict>,
238 pub created_at: String,
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts239 #[serde(default)]
240 pub edited_at: Option<String>,
241 /// The issue or pull request it is on, where the query reads it.
242 #[serde(default)]
243 pub number: u32,
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar244 /// One of the workspace's agents that wrote it, as itself, and whom it
245 /// acted for (migrations/0033_agent_comments.sql).
246 #[serde(default)]
247 pub agent_id: Option<String>,
248 #[serde(default)]
249 pub agent_handle: Option<String>,
250 #[serde(default)]
251 pub agent_name: Option<String>,
252 #[serde(default)]
253 pub agent_avatar_seed: Option<String>,
254 #[serde(default)]
255 pub acting_for_id: Option<String>,
256 #[serde(default)]
257 pub acting_for_name: Option<String>,
258 /// An agent review's verdict, kept apart from `verdict`, which every
259 /// rule about approvals reads: so it is advisory.
260 #[serde(default)]
261 pub agent_verdict: Option<String>,
262}
263
264impl CommentRow {
265 /// Who answers for it: whoever an agent acted for, or its author.
266 pub fn answerable_id(&self) -> &str {
267 self.acting_for_id.as_deref().unwrap_or(&self.author_id)
268 }
269
270 /// Whether it carries a verdict, a person's or an agent's.
271 pub fn has_verdict(&self) -> bool {
272 self.verdict.is_some() || self.agent_verdict.is_some()
273 }
Docs: inbox threads, reasons, subscriptions, watching, email, API and MCP274}
275
276impl From<CommentRow> for Comment {
277 fn from(row: CommentRow) -> Self {
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar278 let agent = match (row.agent_id, row.agent_handle) {
279 (Some(id), Some(handle)) => Some(AgentRef {
280 display_name: row.agent_name.unwrap_or_else(|| handle.clone()),
281 avatar_seed: row.agent_avatar_seed.unwrap_or_else(|| handle.clone()),
282 id,
283 handle,
284 }),
285 _ => None,
286 };
287 let advisory_verdict = row.agent_verdict.as_deref().and_then(AgentVerdict::parse);
288 let mut author = user(row.author_id, row.author_name);
289 if agent.is_some() {
290 author.kind = g1t_contracts::PrincipalKind::Agent;
291 }
Docs: inbox threads, reasons, subscriptions, watching, email, API and MCP292 Comment {
293 id: row.id,
294 kind: row.kind,
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar295 author,
Docs: inbox threads, reasons, subscriptions, watching, email, API and MCP296 body: row.body,
297 path: row.path,
298 line: row.line,
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar299 // A person's verdict, or for showing, an agent's advisory one.
300 verdict: row.verdict.or(advisory_verdict.and_then(AgentVerdict::verdict)),
Docs: inbox threads, reasons, subscriptions, watching, email, API and MCP301 created_at: row.created_at,
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts302 edited_at: row.edited_at,
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar303 acting_for: agent.as_ref().and(requester(row.acting_for_id, row.acting_for_name)),
304 advisory: agent.is_some() && advisory_verdict.is_some(),
305 agent,
Docs: inbox threads, reasons, subscriptions, watching, email, API and MCP306 }
307 }
308}
309
310#[derive(Deserialize)]
311pub struct SessionRow {
312 pub seq: u32,
313 pub kind: SessionEntryKind,
314 pub text: String,
315 pub tool: Option<String>,
316 pub commit: Option<String>,
317 pub at: String,
318}
319
320impl From<SessionRow> for SessionEntry {
321 fn from(row: SessionRow) -> Self {
322 SessionEntry {
323 seq: row.seq,
324 kind: row.kind,
325 text: row.text,
326 tool: row.tool,
327 commit: row.commit,
328 at: row.at,
329 }
330 }
331}
332
333/// A pull request whose head a push moved.
334#[derive(Deserialize)]
335pub struct MovedRow {
336 pub id: String,
337 pub repo_id: String,
338 pub number: u32,
339 pub issue_number: Option<u32>,
340 pub status: PullStatus,
341 pub author_id: String,
342 pub author_name: String,
343 #[serde(default)]
344 pub requested_by_id: Option<String>,
345 #[serde(default)]
346 pub requested_by_name: Option<String>,
347}
348
349/// A single number selected as `n`.
350#[derive(Deserialize)]
351pub struct NumberRow {
352 pub n: u32,
353}
354
355/// A single string selected as `value`.
356#[derive(Deserialize)]
357pub struct ValueRow {
358 pub value: String,
359}
360
361/// Rows as the database holds them, for tests of the rules that read them.
362#[cfg(test)]
363pub(crate) mod stored {
364 use super::*;
365
366 /// A pull request g1t made in a fork, read back as stored: g1t as its
367 /// author, and `requested_by` the person who asked, if anyone did.
368 pub(crate) fn pull(author: (&str, &str), requested_by: Option<(&str, &str)>) -> Pull {
369 let row: PullRow = serde_json::from_value(serde_json::json!({
370 "id": "pr_1", "repo_id": "rep_1", "number": 14, "issue_number": 12, "title": "Fix it",
371 "body": null, "agent": "g1t", "runtime": "hosted", "status": "open",
372 "fork_repo_id": "rep_f", "fork_namespace": "pulls", "fork_name": "pr_1",
373 "source_branch": null, "head_commit": null, "merge_base": null, "merged_by": null,
374 "merged_at": null, "superseded_by": null, "check_status": null, "files": null,
375 "assignees": "[]", "reviewers": "[]",
376 "author_id": author.0, "author_name": author.1,
377 "requested_by_id": requested_by.map(|(id, _)| id),
378 "requested_by_name": requested_by.map(|(_, name)| name),
379 "created_at": "", "updated_at": ""
380 }))
381 .unwrap();
382 row.into()
383 }
384
385 /// g1t's agent, as an author is stored.
386 pub(crate) const G1T: (&str, &str) = (g1t_contracts::identity::AGENT_ID, "g1t");
387 /// The person who asked g1t for the work.
388 pub(crate) const ASKER: (&str, &str) = ("usr_1", "syntaqx");
389}
390
391#[cfg(test)]
392mod tests {
393 use super::stored::{ASKER, G1T, pull};
394 use super::*;
395 use g1t_contracts::PrincipalKind;
396
397 #[test]
398 fn g1t_s_pull_request_reads_back_as_g1t_s_requested_by_the_person() {
399 let made = pull(G1T, Some(ASKER));
400 assert_eq!((made.author.username.as_str(), made.author.kind), ("g1t", PrincipalKind::Agent));
401 let asked = made.requested_by.as_ref().expect("who asked is kept");
402 assert_eq!((asked.id.as_str(), asked.username.as_str(), asked.kind), ("usr_1", "syntaqx", PrincipalKind::User));
403 assert_eq!(made.owner().id, "usr_1");
404 }
405
406 #[test]
407 fn anyone_else_s_reads_back_as_theirs_alone() {
408 let own = pull(ASKER, None);
409 assert_eq!(own.author.kind, PrincipalKind::User);
410 assert!(own.requested_by.is_none());
411 assert_eq!(own.owner().id, "usr_1");
412 // Half a requester is nobody.
413 assert!(requester(Some("usr_1".into()), None).is_none());
414 }
415
416 #[test]
417 fn g1t_s_own_work_reads_back_as_g1t_itself() {
418 let own = pull(("g1t", "g1t"), None);
419 assert_eq!(own.author.kind, PrincipalKind::System);
420 assert!(own.requested_by.is_none());
421 }
422}

This file's history is long; its oldest lines are credited to the oldest commit read.