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
391 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.

Inbox: the events service tells people what needs them as events arrive1//! What an event names, for the inbox (`g1t_contracts::inbox`): the issue
2//! or pull request, its people, and the comment, read straight from the
3//! rows. The events service asks as each event arrives and decides from
4//! the answer who is told; nobody's access is checked here, so nothing it
5//! returns is shown to anyone but the people it names, or to those who can
6//! read the repository.
7
8use g1t_contracts::credentials::Principal;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar9use g1t_contracts::inbox::{InboxComment, InboxSubject, InboxSubjectArgs, SubjectKind, TeamMentioned};
Inbox: the events service tells people what needs them as events arrive10use g1t_contracts::is_valid_namespace;
11use g1t_contracts::work::{Comment, CommentKind, Issue, Pull};
12use worker::Result;
13
14use crate::Work;
15use crate::rows::CommentRow;
16
17/// The most people one comment notifies by name.
18const MAX_MENTIONS: usize = 20;
19/// About one line of a comment, shown under its title.
20const EXCERPT_CHARS: usize = 140;
21
The work, events, webhooks, actions, repos and packages services no longer run g1t's own agents. Work drops the 35 calls that queued issues for g1t, delegated them, messaged and reviewed through the agent, planned, captured memories automatically, rated confidence and capped agent spend, and its pull request lifecycle no longer revises or reviews: a pull request only advances to a merge by the repository's auto-merge setting. Merging a pull request that is behind now catches it up itself, through the repository's branch update, and lands it when the push arrives; when it can't be caught up cleanly the merge says to merge the base in and try again. Recording a session on a pull request, memory recall and remember, overlaps, the merge queue and required checks stay as they were.22/// What a comment says in its own words: fenced code blocks, code spans
23/// and quoted lines are left out, so that quoting or showing a mention
24/// does not make one.
25pub(crate) fn spoken(body: &str) -> String {
26 let mut out = String::new();
27 // The fence that opened the code block being skipped, if one is.
28 let mut fence: Option<String> = None;
29 for line in body.lines() {
30 let trimmed = line.trim_start();
31 if let Some(marker) = &fence {
32 if trimmed.starts_with(marker.as_str()) {
33 fence = None;
34 }
35 out.push('\n');
36 continue;
37 }
38 if let Some(first) = trimmed.chars().next()
39 && (first == '`' || first == '~')
40 {
41 let run = trimmed.chars().take_while(|c| *c == first).count();
42 if run >= 3 {
43 fence = Some(std::iter::repeat_n(first, run).collect());
44 out.push('\n');
45 continue;
46 }
47 }
48 if trimmed.starts_with('>') {
49 out.push('\n');
50 continue;
51 }
52 out.push_str(&without_code_spans(line));
53 out.push('\n');
54 }
55 out
56}
57
58/// A line with its code spans blanked: a run of backticks opens one, and
59/// the next run of the same length closes it. An unclosed run is text.
60fn without_code_spans(line: &str) -> String {
61 let chars: Vec<char> = line.chars().collect();
62 let run_at = |at: usize| chars[at..].iter().take_while(|c| **c == '`').count();
63 let mut out = String::new();
64 let mut i = 0;
65 while i < chars.len() {
66 if chars[i] != '`' {
67 out.push(chars[i]);
68 i += 1;
69 continue;
70 }
71 let open = run_at(i);
72 let mut j = i + open;
73 let mut closed = None;
74 while j < chars.len() {
75 if chars[j] == '`' {
76 let run = run_at(j);
77 if run == open {
78 closed = Some(j + run);
79 break;
80 }
81 j += run;
82 } else {
83 j += 1;
84 }
85 }
86 match closed {
87 Some(end) => {
88 out.push(' ');
89 i = end;
90 }
91 None => {
92 out.extend(std::iter::repeat_n('`', open));
93 i += open;
94 }
95 }
96 }
97 out
98}
99
Inbox: the events service tells people what needs them as events arrive100/// The people a comment mentions by name in its own words (not in code or
101/// a quote), lowercased, each once. Not an email address, a package scope
102/// or a path, and never a reserved name such as `g1t`.
103pub(crate) fn people_mentioned(body: &str) -> Vec<String> {
104 let text = spoken(body);
105 let chars: Vec<char> = text.chars().collect();
106 let mut people: Vec<String> = Vec::new();
107 let mut at = 0;
108 while at < chars.len() && people.len() < MAX_MENTIONS {
109 if chars[at] != '@' {
110 at += 1;
111 continue;
112 }
113 let starts_clean = at == 0 || {
114 let before = chars[at - 1];
115 !(before.is_alphanumeric() || "._%+-/\\@`=".contains(before))
116 };
117 let name: String = chars[at + 1..]
118 .iter()
119 .take_while(|c| c.is_ascii_alphanumeric() || **c == '-')
120 .collect();
121 let end = at + 1 + name.chars().count();
122 // A path or a package such as `@scope/name`, or a domain.
123 let ends_clean = match chars.get(end) {
124 None => true,
125 Some(c) if "_@/\\".contains(*c) => false,
126 Some('.') => chars.get(end + 1).is_none_or(|c| !c.is_alphanumeric()),
127 Some(_) => true,
128 };
129 let name = name.to_lowercase();
130 if starts_clean && ends_clean && is_valid_namespace(&name) && !people.contains(&name) {
131 people.push(name);
132 }
133 at = end.max(at + 1);
134 }
135 people
136}
137
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar138/// The teams a comment mentions in its own words, as `workspace/slug`,
139/// each once: `@acme/backend`, not in code, a quote, an address or a path
140/// such as `@acme/backend/x` or `npm i @scope/pkg@1`.
141pub(crate) fn teams_mentioned(body: &str) -> Vec<String> {
142 let text = spoken(body);
143 let chars: Vec<char> = text.chars().collect();
144 let mut teams: Vec<String> = Vec::new();
145 let mut at = 0;
146 while at < chars.len() && teams.len() < MAX_MENTIONS {
147 if chars[at] != '@' {
148 at += 1;
149 continue;
150 }
151 let starts_clean = at == 0 || {
152 let before = chars[at - 1];
153 !(before.is_alphanumeric() || "._%+-/\\@`=".contains(before))
154 };
155 let name: String = chars[at + 1..]
156 .iter()
157 .take_while(|c| c.is_ascii_alphanumeric() || **c == '-' || **c == '/')
158 .collect();
159 let end = at + 1 + name.chars().count();
160 let ends_clean = match chars.get(end) {
161 None => true,
162 Some(c) if "_@\\".contains(*c) => false,
163 Some('.') => chars.get(end + 1).is_none_or(|c| !c.is_alphanumeric()),
164 Some(_) => true,
165 };
166 if starts_clean
167 && ends_clean
168 && name.matches('/').count() == 1
169 && let Some(team) = crate::team_reviews::team_name(&name)
170 && !teams.contains(&team)
171 {
172 teams.push(team);
173 }
174 at = end.max(at + 1);
175 }
176 teams
177}
178
179/// Who each team mentioned tells: everyone in it and its child teams, when
180/// the writer may see the team and it has notifications on.
181pub(crate) fn told_of(teams: &[g1t_contracts::teams::ResolvedTeam], writer: &str) -> Vec<TeamMentioned> {
182 teams
183 .iter()
184 .map(|team| TeamMentioned {
185 team: format!("{}/{}", team.workspace, team.slug),
186 members: if team.notify && team.asker_sees {
187 team.everyone()
188 .map(|person| person.username.clone())
189 .filter(|name| !name.eq_ignore_ascii_case(writer))
190 .collect()
191 } else {
192 Vec::new()
193 },
194 })
195 .collect()
196}
197
Inbox: the events service tells people what needs them as events arrive198/// The first line a comment says something on, cut to about a line.
199pub(crate) fn excerpt(body: &str) -> String {
200 let line = body
201 .lines()
202 .map(str::trim)
203 .find(|line| !line.is_empty() && !line.starts_with("```") && !line.starts_with('>'))
204 .unwrap_or_default();
205 let line = line.split_whitespace().collect::<Vec<_>>().join(" ");
206 if line.chars().count() <= EXCERPT_CHARS {
207 return line;
208 }
209 let cut: String = line.chars().take(EXCERPT_CHARS - 1).collect();
210 format!("{}…", cut.trim_end())
211}
212
213fn principal(id: &str, username: &str) -> Principal {
214 Principal {
215 id: id.to_owned(),
216 username: username.to_lowercase(),
217 }
218}
219
220fn of_issue(issue: Issue) -> InboxSubject {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar221 let body = issue.body.clone();
Inbox: the events service tells people what needs them as events arrive222 InboxSubject {
223 kind: Some(SubjectKind::Issue),
224 title: issue.title,
225 author: principal(&issue.author.id, &issue.author.username),
226 requested_by: issue.requested_by.map(|user| principal(&user.id, &user.username)),
227 assignees: issue.assignees,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar228 mentions: people_mentioned(&body),
Inbox: the events service tells people what needs them as events arrive229 ..InboxSubject::default()
230 }
231}
232
233fn of_pull(pull: Pull, issue: Option<Issue>) -> InboxSubject {
234 InboxSubject {
235 kind: Some(SubjectKind::Pull),
236 title: pull.title,
237 author: principal(&pull.author.id, &pull.author.username),
238 requested_by: pull.requested_by.map(|user| principal(&user.id, &user.username)),
239 assignees: pull.assignees,
240 reviewers: pull.reviewers,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar241 mentions: people_mentioned(pull.body.as_deref().unwrap_or_default()),
242 team_mentions: Vec::new(),
Inbox: the events service tells people what needs them as events arrive243 issue: issue.map(|issue| Box::new(of_issue(issue))),
244 comment: None,
245 }
246}
247
248fn of_comment(comment: Comment) -> InboxComment {
249 let event = comment.kind == CommentKind::Event;
250 InboxComment {
251 author: principal(&comment.author.id, &comment.author.username),
252 excerpt: excerpt(&comment.body),
253 mentions: if event { Vec::new() } else { people_mentioned(&comment.body) },
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar254 team_mentions: Vec::new(),
Inbox: the events service tells people what needs them as events arrive255 verdict: comment.verdict.map(|verdict| verdict.as_str().to_owned()),
256 event,
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar257 acting_for: comment.acting_for.as_ref().map(|user| principal(&user.id, &user.username)),
258 advisory: comment.advisory,
259 agent: comment.agent,
Inbox: the events service tells people what needs them as events arrive260 }
261}
262
263impl Work {
264 /// The issue or pull request numbered `number`, with its people, and
265 /// the comment asked about. None when there is no such issue or pull
266 /// request.
267 pub(crate) async fn inbox_subject(&self, a: InboxSubjectArgs) -> Result<Option<InboxSubject>> {
268 let comment = async {
269 let Some(id) = &a.comment_id else {
270 return Ok::<_, worker::Error>(None);
271 };
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar272 let Some(row) = self
Inbox: the events service tells people what needs them as events arrive273 .db
274 .prepare("SELECT * FROM comments WHERE id = ? AND repo_id = ?")
275 .bind(&[id.as_str().into(), a.repo_id.as_str().into()])?
276 .first::<CommentRow>(None)
277 .await?
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar278 else {
279 return Ok(None);
280 };
281 let comment = Comment::from(row);
282 let teams = if comment.kind == CommentKind::Event { Vec::new() } else { teams_mentioned(&comment.body) };
283 let mut shown = of_comment(comment.clone());
284 shown.team_mentions = self.team_mentions(&a.repo_id, &teams, &comment.author.id, &comment.author.username).await?;
285 Ok(Some(shown))
Inbox: the events service tells people what needs them as events arrive286 };
287 let (pull, comment) = futures_util::future::try_join(self.pull(&a.repo_id, a.number), comment).await?;
288 let mut subject = match pull {
289 Some(pull) => {
290 let issue = match pull.issue {
291 Some(number) => self.issue(&a.repo_id, number).await?,
292 None => None,
293 };
294 of_pull(pull, issue)
295 }
296 None => match self.issue(&a.repo_id, a.number).await? {
297 Some(issue) => of_issue(issue),
298 None => return Ok(None),
299 },
300 };
301 subject.comment = comment;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar302 // A description's team mentions, for when it is opened.
303 if a.comment_id.is_none() {
304 let body = match subject.kind {
305 Some(SubjectKind::Pull) => self.pull(&a.repo_id, a.number).await?.and_then(|pull| pull.body),
306 _ => self.issue(&a.repo_id, a.number).await?.map(|issue| issue.body),
307 };
308 let teams = teams_mentioned(body.as_deref().unwrap_or_default());
309 let author = subject.author.clone();
310 subject.team_mentions = self.team_mentions(&a.repo_id, &teams, &author.id, &author.username).await?;
311 }
Inbox: the events service tells people what needs them as events arrive312 Ok(Some(subject))
313 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar314
315 /// The teams named, resolved, with who each mention tells.
316 async fn team_mentions(&self, repo_id: &str, teams: &[String], writer_id: &str, writer: &str) -> Result<Vec<TeamMentioned>> {
317 if teams.is_empty() {
318 return Ok(Vec::new());
319 }
320 let resolved: Vec<g1t_contracts::teams::ResolvedTeam> = g1t_kit::call(
321 &self.identity,
322 "resolve_teams",
323 &g1t_contracts::teams::ResolveTeamsArgs {
324 teams: teams.to_vec(),
325 repo_id: Some(repo_id.to_owned()),
326 asker: Some(writer_id.to_owned()),
327 },
328 )
329 .await?;
330 Ok(told_of(&resolved, writer))
331 }
Inbox: the events service tells people what needs them as events arrive332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337
338 #[test]
339 fn people_are_mentioned_by_name_in_their_own_words() {
340 assert_eq!(people_mentioned("@ana can you look? cc @Bob-1."), vec!["ana", "bob-1"]);
341 // Once each, never g1t, never in code or a quote.
342 assert_eq!(people_mentioned("@ana @ana @g1t `@carl`\n> @dee said"), vec!["ana"]);
343 // Not an address, a package, a path or a domain.
344 assert!(people_mentioned("ops@ana.dev, @scope/pkg, a/@b, @ana.dev").is_empty());
345 assert_eq!(people_mentioned("(@ana)"), vec!["ana"]);
346 }
347
348 #[test]
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar349 fn teams_are_mentioned_as_workspace_slash_team() {
350 assert_eq!(teams_mentioned("cc @acme/backend and @Acme/Web-UI."), vec!["acme/backend", "acme/web-ui"]);
351 // Once each; never in code or a quote; not a person.
352 assert_eq!(teams_mentioned("@acme/backend @acme/backend `@acme/ops`\n> @acme/sre\n@ana"), vec!["acme/backend"]);
353 // Not an address, a deeper path or a versioned package.
354 assert!(teams_mentioned("ops@acme/backend a/@acme/x @acme/backend/src npm i @acme/backend@1").is_empty());
355 // And a person is still a person.
356 assert_eq!(people_mentioned("@ana and @acme/backend"), vec!["ana"]);
357 }
358
359 #[test]
360 fn a_team_mention_tells_its_people_when_it_may() {
361 use g1t_contracts::teams::{ResolvedTeam, TeamPerson};
362 let person = |name: &str| TeamPerson {
363 id: format!("usr_{name}"),
364 username: name.to_owned(),
365 };
366 let team = |notify: bool, sees: bool| ResolvedTeam {
367 workspace: "acme".into(),
368 slug: "backend".into(),
369 notify,
370 asker_sees: sees,
371 members: vec![person("ana"), person("bo")],
372 child_members: vec![person("cy")],
373 ..ResolvedTeam::default()
374 };
375 let told = told_of(&[team(true, true)], "bo");
376 assert_eq!(told[0].team, "acme/backend");
377 assert_eq!(told[0].members, vec!["ana", "cy"]);
378 assert!(told_of(&[team(false, true)], "zed")[0].members.is_empty());
379 assert!(told_of(&[team(true, false)], "zed")[0].members.is_empty());
380 }
381
382 #[test]
Inbox: the events service tells people what needs them as events arrive383 fn an_excerpt_is_the_first_line_said() {
384 assert_eq!(excerpt("\n\n```\ncode\n```"), "code");
385 assert_eq!(excerpt("> quoted\n Looks good \nmore"), "Looks good");
386 let long = "word ".repeat(60);
387 let cut = excerpt(&long);
388 assert!(cut.ends_with('…'));
389 assert_eq!(cut.chars().count(), EXCERPT_CHARS);
390 }
391}