Skip to content
2,976 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.

Fix a hydration mismatch in output that starts with a blank line1//! The work service: issues, pull requests, comments and sessions.
2//!
3//! Other services reach it over `POST /rpc/<method>`; see
4//! `g1t_contracts::work` for the methods and their arguments. It also
5//! consumes its queue of events from the bus.
6
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar7mod agent_comments;
Agents and memory, checks and conflicts, profiles, slug renames, custom domains8mod authored;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API9mod capture;
Fix a hydration mismatch in output that starts with a blank line10mod checks;
Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037)11mod ghost;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar12mod codeowners;
Merge checks: statuses and check runs on every commit13mod commit_checks;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look14mod compute;
Chat controls, public profiles, shadcn selects, and no Docs tab in a project15mod contributions;
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step16mod confidence;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API17mod guardrails;
Inbox: the events service tells people what needs them as events arrive18mod inbox;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar19mod labels;
Fix a hydration mismatch in output that starts with a blank line20mod lifecycle;
Agents and memory, checks and conflicts, profiles, slug renames, custom domains21mod memory;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API22mod mentions;
Agents and memory, checks and conflicts, profiles, slug renames, custom domains23mod mergeability;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar24mod milestones;
Fix a hydration mismatch in output that starts with a blank line25mod plans;
26mod messages;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily27mod prefetch;
Fix a hydration mismatch in output that starts with a blank line28mod queue;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look29mod retired;
Fix a hydration mismatch in output that starts with a blank line30mod reviews;
31mod rows;
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge32mod rulesets;
Agents and memory, checks and conflicts, profiles, slug renames, custom domains33mod runs;
Fix a hydration mismatch in output that starts with a blank line34mod settings;
GitHub Actions on g1t, part two: running workflows35mod statuses;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar36mod team_reviews;
Fix a hydration mismatch in output that starts with a blank line37
38use g1t_contracts::events::{
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts39 ChangedFrom, CommentChanges, CommentCreated, CommentDeleted, CommentEdited, DeletedComment, Event, IssueEvent,
40 NewEvent, Publish, PullEvent, SessionAppended,
Fix a hydration mismatch in output that starts with a blank line41};
42use g1t_contracts::identity::UsernameArgs;
Catching up with main takes seconds when the two sides touched different files43use g1t_contracts::repos::{
Fast pages, required checks on the branch, self-hosted runners, honest incidents44 ForkArgs, GetArgs, HeadArgs, LandArgs, Landed, NeedsAgentReason, PullBranchUpdate, ReadableArgs, Repo, RepoPath,
Catching up with main takes seconds when the two sides touched different files45 UpdatePullBranchArgs,
46};
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look47use g1t_contracts::access::{self, Capability, Denied};
Fix a hydration mismatch in output that starts with a blank line48use g1t_contracts::time::rfc3339;
49use g1t_contracts::work::*;
50use futures_util::future::{try_join, try_join3, try_join_all};
51use g1t_contracts::{FailureCode, Outcome, User, Viewer, new_id};
52use g1t_kit::{args, now_ms, reply, rpc_method};
53use serde::Serialize;
54use worker::wasm_bindgen::JsValue;
55use worker::{
56 Context, D1Database, Env, Fetcher, MessageBatch, MessageExt, Request, Response, Result, event,
57};
58
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look59use retired::writable;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar60use rows::{CommentRow, IssueRow, MovedRow, NumberRow, PULL_COLUMNS, PullRow, SessionRow, Snapshot};
Fix a hydration mismatch in output that starts with a blank line61
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge62pub(crate) const SOURCE: &str = "work";
Fix a hydration mismatch in output that starts with a blank line63const MAX_ENTRY_BATCH: usize = 200;
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar64pub(crate) const MAX_ENTRY_CHARS: usize = 64_000;
Fix a hydration mismatch in output that starts with a blank line65const MAX_TITLE_CHARS: usize = 200;
66const SESSION_PAGE: u32 = 500;
67const LIST_PAGE: u32 = 100;
68const MAX_ASSIGNEES: usize = 10;
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge69pub(crate) const UNVERIFIED: &str = "Confirm your email address first. Check your inbox, or resend the link from the banner on g1t.sh.";
Fix a hydration mismatch in output that starts with a blank line70
71const ISSUE_COLUMNS: &str = "issues.*,
72 (SELECT count(*) FROM pulls WHERE pulls.issue_id = issues.id) AS pull_count,
73 (SELECT agent FROM pulls
74 WHERE pulls.issue_id = issues.id AND pulls.status IN ('draft', 'open')
75 AND pulls.fork_repo_id IS NOT NULL
76 ORDER BY pulls.number DESC LIMIT 1) AS agent,
77 (SELECT count(*) FROM comments
78 WHERE comments.repo_id = issues.repo_id AND comments.number = issues.number
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar79 AND comments.kind = 'comment') AS comment_count,
80 (SELECT title FROM milestones
81 WHERE milestones.repo_id = issues.repo_id AND milestones.number = issues.milestone) AS milestone_title";
Fix a hydration mismatch in output that starts with a blank line82
83fn no_issue<T>() -> Outcome<T> {
84 Outcome::fail(FailureCode::NotFound, "Issue not found.")
85}
86
87fn no_pull<T>() -> Outcome<T> {
88 Outcome::fail(FailureCode::NotFound, "Pull request not found.")
89}
90
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily91/// Whether a pull request was behind when its mergeability was last
92/// worked out, and for which pair of commits (mergeability.rs).
93#[derive(serde::Deserialize)]
94struct StoredBehind {
95 #[serde(default)]
96 behind: Option<u8>,
97 #[serde(default)]
98 mergeable_key: Option<String>,
99}
100
101impl StoredBehind {
102 /// The stored answer, if it was worked out for `head`.
103 fn for_head(&self, head: Option<&str>) -> Option<bool> {
104 let (worked_for, _) = self.mergeable_key.as_deref()?.split_once("..")?;
105 (Some(worked_for) == head).then_some(self.behind? != 0)
106 }
107}
108
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look109/// Refuses `actor` unless their role on `repo` has `capability`: not found
110/// when they cannot read it, forbidden with the role it needs otherwise.
111pub(crate) fn allowed(actor: Option<&User>, repo: &Repo, capability: Capability) -> Outcome<()> {
112 match access::check(actor, repo, capability) {
113 Ok(()) => Outcome::Ok(()),
114 Err(Denied::NotFound) => Outcome::fail(FailureCode::NotFound, "Repository not found."),
115 Err(Denied::Forbidden) => Outcome::fail(
116 FailureCode::Forbidden,
117 access::needs(capability, &format!("{}/{}", repo.namespace, repo.name)),
118 ),
119 }
120}
121
Fix a hydration mismatch in output that starts with a blank line122fn optional(value: &Option<String>) -> JsValue {
123 value.as_deref().map_or(JsValue::NULL, JsValue::from)
124}
125
126fn optional_number(value: Option<u32>) -> JsValue {
127 value.map_or(JsValue::NULL, JsValue::from)
128}
129
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar130/// Names the branch a pull request merges into when it is the default
131/// branch, which is stored as none so that it follows a change of default.
132pub(crate) fn fill_base(pull: &mut Pull, repo: &Repo) {
133 if pull.base.as_deref().is_none_or(str::is_empty) {
134 pull.base = Some(repo.default_branch.clone());
135 }
136}
137
138/// The branch a pull request is stored as merging into: none for the
139/// default branch.
140fn stored_base(base: &str, repo: &Repo) -> Option<String> {
141 let base = base.trim();
142 (!base.is_empty() && base != repo.default_branch).then(|| base.to_owned())
143}
144
Fix a hydration mismatch in output that starts with a blank line145/// The lowercase name a `State` is stored and sent as.
146fn state_name(state: Option<State>) -> Option<&'static str> {
147 state.map(|state| match state {
148 State::Open => "open",
149 State::Closed => "closed",
150 })
151}
152
153/// A trimmed title, or why it cannot be used.
154fn valid_title(title: &str) -> std::result::Result<&str, &'static str> {
155 let title = title.trim();
156 if title.is_empty() {
157 Err("A title is required.")
158 } else if title.chars().count() > MAX_TITLE_CHARS {
159 Err("That title is too long.")
160 } else {
161 Ok(title)
162 }
163}
164
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts165/// What a closed pull request was when it was closed.
166#[derive(serde::Deserialize)]
167struct ClosedFrom {
168 #[serde(default)]
169 closed_from: Option<String>,
170}
171
172/// Why a pull request in `status` cannot be reopened, if it cannot: only a
173/// closed one can, never a merged one.
174fn reopen_refusal(status: PullStatus) -> Option<&'static str> {
175 match status {
176 PullStatus::Closed => None,
177 PullStatus::Merged => Some("This pull request was merged; it cannot be reopened."),
178 PullStatus::Draft | PullStatus::Open => Some("This pull request is already open."),
179 }
180}
181
182/// What a closed pull request is reopened as: the draft it was, when it
183/// was closed as one, else ready for review.
184fn reopened_status(closed_from: Option<&str>) -> PullStatus {
185 if closed_from == Some("draft") { PullStatus::Draft } else { PullStatus::Open }
186}
187
188/// Why a pull request in `status` cannot be turned into a draft, if it
189/// cannot: only one that is open, ready for review, can.
190fn draft_refusal(status: PullStatus) -> Option<&'static str> {
191 match status {
192 PullStatus::Open => None,
193 PullStatus::Draft => Some("This pull request is already a draft."),
194 PullStatus::Merged => Some("This pull request is already merged."),
195 PullStatus::Closed => Some("This pull request is closed. Reopen it first."),
196 }
197}
198
Fix a hydration mismatch in output that starts with a blank line199/// Unwraps an `Outcome`, returning its failure from the enclosing method.
200macro_rules! check {
201 ($outcome:expr) => {
202 match $outcome {
203 Outcome::Ok(value) => value,
204 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
205 }
206 };
207}
208
209struct Work {
210 db: D1Database,
211 identity: Fetcher,
212 repos: Fetcher,
213 events: Fetcher,
Sidebar: the panels really slide214 /// GitHub Actions: runs a merge queue's `merge_group` workflows.
215 actions: Fetcher,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily216 /// Where this request's time went, for its `Server-Timing`.
217 timing: g1t_kit::d1::Timing,
218 /// A pull request's rows read in one batch for this request
219 /// (prefetch.rs), which the helpers below read instead of the database.
220 prefetched: std::cell::RefCell<Option<std::rc::Rc<prefetch::Prefetched>>>,
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge221 /// Repositories read in this request, by id, for rules that need one
222 /// where only a pull request is at hand (rulesets.rs `repo_for`).
223 known_repos: std::cell::RefCell<std::collections::HashMap<String, Repo>>,
Fix a hydration mismatch in output that starts with a blank line224}
225
226impl Work {
227 async fn publish<T: Serialize>(
228 &self,
229 kind: &'static str,
230 repo_id: &str,
231 actor: &User,
232 data: T,
233 ) -> Result<()> {
Merge branch 'worktree-agent-a3abfcce648e87dca'234 // What a workflow job's token did is marked, so it starts no
235 // workflows (`g1t_contracts::events::CAUSED_BY_JOB`).
236 self.publish_as(kind, repo_id, Some(actor.id.clone()), g1t_contracts::events::marked(data, Some(actor)))
Fix a hydration mismatch in output that starts with a blank line237 .await
238 }
239
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights240 /// A pull request's owner (whoever asked g1t for it, or its author) as
241 /// a viewer who can read its repository and source. Stored people carry
242 /// no memberships, so a private repository would otherwise look missing
243 /// to them. The membership given reads and nothing more: it is for
244 /// looking, never for acting.
245 pub(crate) async fn owner_viewer(&self, pull: &Pull) -> Result<Viewer> {
Agents move along on private repositories too246 let path: Option<RepoPath> = g1t_kit::call(
247 &self.repos,
248 "path_by_id",
249 &g1t_contracts::repos::PathByIdArgs { id: pull.repo_id.clone() },
250 )
251 .await?;
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights252 let mut owner = pull.owner().clone();
Agents move along on private repositories too253 if let Some(path) = path
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights254 && !owner.is_member(&path.namespace.to_lowercase())
Agents move along on private repositories too255 {
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights256 owner.workspaces.push(g1t_contracts::Membership {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look257 base_permission: Some(access::BasePermission::Read),
258 ..g1t_contracts::Membership::member(path.namespace.to_lowercase())
259 });
Agents move along on private repositories too260 }
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights261 Ok(Some(owner))
Agents move along on private repositories too262 }
263
Fix a hydration mismatch in output that starts with a blank line264 /// Publishes an event caused by `actor`, or by g1t itself.
265 async fn publish_as<T: Serialize>(
266 &self,
267 kind: &'static str,
268 repo_id: &str,
269 actor: Option<String>,
270 data: T,
271 ) -> Result<()> {
272 let event = NewEvent {
273 kind,
274 source: SOURCE,
275 repo_id: Some(repo_id.to_owned()),
276 actor,
277 data,
278 };
279 g1t_kit::call(
280 &self.events,
281 "publish",
282 &Publish {
283 events: vec![event],
284 },
285 )
286 .await
287 }
288
289 /// The repository, if the viewer may see it. Whether they may is
290 /// decided by the repos service.
291 async fn repo(&self, path: &RepoPath, viewer: &Viewer) -> Result<Outcome<Repo>> {
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge292 let found: Outcome<Repo> = self
293 .timing
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily294 .rpc(g1t_kit::call(
295 &self.repos,
296 "get",
297 &GetArgs {
298 path: path.clone(),
299 viewer: viewer.clone(),
300 },
301 ))
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge302 .await?;
303 if let Outcome::Ok(repo) = &found {
304 self.known_repos.borrow_mut().insert(repo.id.clone(), repo.clone());
305 }
306 Ok(found)
Fix a hydration mismatch in output that starts with a blank line307 }
308
309 /// The next number in the repository's sequence. Taking it is one
310 /// statement, so concurrent opens cannot be given the same number.
311 async fn next_number(&self, repo_id: &str) -> Result<u32> {
312 let row = self
313 .db
314 .prepare(
315 "INSERT INTO counters (repo_id, last) VALUES (?, 1)
316 ON CONFLICT (repo_id) DO UPDATE SET last = last + 1
317 RETURNING last AS n",
318 )
319 .bind(&[repo_id.into()])?
320 .first::<NumberRow>(None)
321 .await?;
322 row.map(|row| row.n)
323 .ok_or_else(|| worker::Error::RustError("no number was assigned".into()))
324 }
325
326 async fn issue(&self, repo_id: &str, number: u32) -> Result<Option<Issue>> {
327 Ok(self
328 .db
329 .prepare(format!(
330 "SELECT {ISSUE_COLUMNS} FROM issues WHERE repo_id = ? AND number = ?"
331 ))
332 .bind(&[repo_id.into(), number.into()])?
333 .first::<IssueRow>(None)
334 .await?
335 .map(Issue::from))
336 }
337
338 async fn pull(&self, repo_id: &str, number: u32) -> Result<Option<Pull>> {
339 Ok(self
340 .db
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step341 .prepare(format!("SELECT {PULL_COLUMNS} FROM pulls WHERE repo_id = ? AND number = ?"))
Fix a hydration mismatch in output that starts with a blank line342 .bind(&[repo_id.into(), number.into()])?
343 .first::<PullRow>(None)
344 .await?
345 .map(Pull::from))
346 }
347
348 /// The repository and one of its issues, as seen by `viewer`.
349 async fn issue_at(
350 &self,
351 path: &RepoPath,
352 number: u32,
353 viewer: &Viewer,
354 ) -> Result<Outcome<(Repo, Issue)>> {
355 let Outcome::Ok(repo) = self.repo(path, viewer).await? else {
356 return Ok(no_issue());
357 };
358 Ok(match self.issue(&repo.id, number).await? {
359 Some(issue) => Outcome::Ok((repo, issue)),
360 None => no_issue(),
361 })
362 }
363
364 /// The repository and one of its pull requests, as seen by `viewer`.
365 async fn pull_at(
366 &self,
367 path: &RepoPath,
368 number: u32,
369 viewer: &Viewer,
370 ) -> Result<Outcome<(Repo, Pull)>> {
371 let Outcome::Ok(repo) = self.repo(path, viewer).await? else {
372 return Ok(no_pull());
373 };
374 Ok(match self.pull(&repo.id, number).await? {
375 Some(pull) => Outcome::Ok((repo, pull)),
376 None => no_pull(),
377 })
378 }
379
380 /// Records something that happened to an issue or a pull request, so
381 /// that it shows in the conversation where it happened. `text` is what
382 /// `author` did, as the rest of a sentence starting with their name.
383 pub(crate) async fn note(
384 &self,
385 repo_id: &str,
386 number: u32,
387 author: (&str, &str),
388 text: &str,
389 ) -> Result<()> {
390 let now = now_ms();
391 self.db
392 .prepare(
393 "INSERT INTO comments
394 (id, repo_id, number, author_id, author_name, body, kind, created_at)
395 VALUES (?, ?, ?, ?, ?, ?, 'event', ?)",
396 )
397 .bind(&[
398 new_id("cmt", now).into(),
399 repo_id.into(),
400 number.into(),
401 author.0.into(),
402 author.1.into(),
403 text.into(),
404 rfc3339(now).into(),
405 ])?
406 .run()
407 .await?;
408 Ok(())
409 }
410
411 /// Notes who was added to and removed from a list of people, such as
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent412 /// "assigned ana" or "requested a review from g1t".
Fix a hydration mismatch in output that starts with a blank line413 async fn note_changes(
414 &self,
415 repo_id: &str,
416 number: u32,
417 actor: &User,
418 before: &[String],
419 after: &[String],
420 (added, removed): (&str, &str),
421 ) -> Result<()> {
422 let joined = |names: Vec<&String>| {
423 names
424 .into_iter()
425 .map(String::as_str)
426 .collect::<Vec<_>>()
427 .join(", ")
428 };
429 let new: Vec<&String> = after.iter().filter(|name| !before.contains(name)).collect();
430 let gone: Vec<&String> = before.iter().filter(|name| !after.contains(name)).collect();
431 let who = (actor.id.as_str(), actor.username.as_str());
432 if !new.is_empty() {
433 // Taking something on oneself reads better said that way.
434 let text = if added == "assigned" && new == [&actor.username] {
435 "self-assigned this".to_owned()
436 } else {
437 format!("{added} {}", joined(new))
438 };
439 self.note(repo_id, number, who, &text).await?;
440 }
441 if !gone.is_empty() {
442 self.note(repo_id, number, who, &format!("{removed} {}", joined(gone)))
443 .await?;
444 }
445 Ok(())
446 }
447
448 fn issue_event(issue: &Issue) -> IssueEvent {
449 IssueEvent {
450 issue_id: issue.id.clone(),
451 repo_id: issue.repo_id.clone(),
452 number: issue.number,
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights453 author: Some((&issue.author).into()),
454 requested_by: issue.requested_by.as_ref().map(Into::into),
Fix a hydration mismatch in output that starts with a blank line455 ..IssueEvent::default()
456 }
457 }
458
Workflows run when an agent's pull request is marked ready459 /// The commit a pull request's change is at in git right now: its
460 /// fork's default branch, or its branch.
461 async fn live_head(&self, pull: &Pull) -> Result<Option<String>> {
462 g1t_kit::call(
463 &self.repos,
464 "head",
465 &HeadArgs {
466 repo_id: pull.fork_repo_id.clone().unwrap_or_else(|| pull.repo_id.clone()),
467 branch: pull.branch.clone().unwrap_or_default(),
468 },
469 )
470 .await
471 }
472
Fix a hydration mismatch in output that starts with a blank line473 fn pull_event(pull: &Pull) -> PullEvent {
474 PullEvent {
475 pull_id: pull.id.clone(),
476 repo_id: pull.repo_id.clone(),
477 number: pull.number,
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights478 author: Some((&pull.author).into()),
479 requested_by: pull.requested_by.as_ref().map(Into::into),
Fix a hydration mismatch in output that starts with a blank line480 issue: pull.issue,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step481 confidence: pull.confidence.clone(),
Fix a hydration mismatch in output that starts with a blank line482 ..PullEvent::default()
483 }
484 }
485
486 // --- Issues ------------------------------------------------------------
487
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent488 /// Opens an issue for g1t to take at once: refused before
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step489 /// anything is opened unless the actor may put agents to work here. The
490 /// runner's `delegate` starts the agent on it.
491 async fn delegate_issue(&self, a: DelegateIssueArgs) -> Result<Outcome<Issue>> {
492 let repo = check!(self.repo(&a.repo, &Some(a.actor.clone())).await?);
493 check!(writable(&repo));
494 check!(allowed(Some(&a.actor), &repo, Capability::Run));
Merge branch 'worktree-agent-ad8a36dfcd4176015' into spend-guardrails495 if let Some(refused) = mentions::refuse_job_token(&a.actor) {
496 return Ok(refused);
497 }
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step498 self.open_issue(OpenIssueArgs {
499 actor: a.actor,
500 repo: a.repo,
501 title: a.title,
502 body: a.body,
503 labels: a.labels,
504 checks: a.checks,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar505 milestone: None,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step506 })
507 .await
508 }
509
Fix a hydration mismatch in output that starts with a blank line510 async fn open_issue(&self, a: OpenIssueArgs) -> Result<Outcome<Issue>> {
511 if !a.actor.verified {
512 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
513 }
514 let title = match valid_title(&a.title) {
515 Ok(title) => title,
516 Err(message) => return Ok(Outcome::fail(FailureCode::Invalid, message)),
517 };
518 let Some(labels) = normalize_labels(&a.labels) else {
519 return Ok(Outcome::fail(
520 FailureCode::Invalid,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar521 format!("An issue can have up to {MAX_LABELS} labels of up to {MAX_LABEL_CHARS} characters each."),
Fix a hydration mismatch in output that starts with a blank line522 ));
523 };
524 let repo = check!(self.repo(&a.repo, &Some(a.actor.clone())).await?);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look525 check!(writable(&repo));
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar526 // Labels it does not have yet are made for someone who may triage;
527 // anyone else picks from those there are.
528 let colors = check!(self.ensure_labels(&a.actor, &repo, &labels).await?);
529 let milestone = match a.milestone.filter(|number| *number > 0) {
530 Some(number) => {
531 check!(allowed(Some(&a.actor), &repo, Capability::Triage));
532 check!(self.milestone_ref(&repo.id, number).await?)
533 }
534 None => None,
535 };
Fast pages, required checks on the branch, self-hosted runners, honest incidents536 // Commands given the old way are words for the agent now: added to
537 // the body under "Definition of done". What has to pass to merge is
538 // the branch's required checks.
539 let body = with_definition_of_done(&a.body, &commands_pass(&a.checks));
Fix a hydration mismatch in output that starts with a blank line540
541 let now = now_ms();
542 let id = new_id("iss", now);
543 let number = self.next_number(&repo.id).await?;
544 let timestamp = rfc3339(now);
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights545 // What g1t's agent files at work is g1t's, for the person it works for.
546 let (author, requested_by) = authorship(&a.actor, false);
Fix a hydration mismatch in output that starts with a blank line547 self.db
548 .prepare(
549 "INSERT INTO issues
550 (id, repo_id, number, title, body, labels, checks, author_id, author_name,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar551 requested_by_id, requested_by_name, created_at, updated_at, milestone)
552 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
Fix a hydration mismatch in output that starts with a blank line553 )
554 .bind(&[
555 id.as_str().into(),
556 repo.id.as_str().into(),
557 number.into(),
558 title.into(),
Fast pages, required checks on the branch, self-hosted runners, honest incidents559 body.into(),
Fix a hydration mismatch in output that starts with a blank line560 serde_json::to_string(&labels)?.into(),
Fast pages, required checks on the branch, self-hosted runners, honest incidents561 "[]".into(),
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights562 author.id.as_str().into(),
563 author.username.as_str().into(),
564 optional(&requested_by.as_ref().map(|user| user.id.clone())),
565 optional(&requested_by.as_ref().map(|user| user.username.clone())),
Fix a hydration mismatch in output that starts with a blank line566 timestamp.as_str().into(),
567 timestamp.as_str().into(),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar568 optional_number(milestone.as_ref().map(|milestone| milestone.number)),
Fix a hydration mismatch in output that starts with a blank line569 ])?
570 .run()
571 .await?;
572 let Some(issue) = self.issue(&repo.id, number).await? else {
573 return Ok(no_issue());
574 };
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API575 self.apply_label_rule(&a.actor, &issue, &[]).await?;
Fix a hydration mismatch in output that starts with a blank line576 self.publish(
577 "issue.opened",
578 &repo.id,
579 &a.actor,
580 IssueEvent {
581 title: Some(issue.title.clone()),
582 ..Self::issue_event(&issue)
583 },
584 )
585 .await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar586 // Opened with labels and a milestone: each is said, as it would be
587 // if they were added afterwards, without notes in the conversation.
588 for label in &issue.labels {
589 let color = colors.iter().find(|(name, _)| name == label).map_or_else(|| label_color_for(label), |(_, c)| c.clone());
590 let label = Some(g1t_contracts::events::EventLabel { name: label.clone(), color });
591 self.publish("issue.labeled", &repo.id, &a.actor, IssueEvent { label, ..Self::issue_event(&issue) })
592 .await?;
593 }
594 if let Some(milestone) = milestone {
595 self.publish(
596 "issue.milestoned",
597 &repo.id,
598 &a.actor,
599 IssueEvent { milestone: Some(milestone), ..Self::issue_event(&issue) },
600 )
601 .await?;
602 }
Fix a hydration mismatch in output that starts with a blank line603 Ok(Outcome::Ok(issue))
604 }
605
606 async fn list_issues(&self, a: ListIssuesArgs) -> Result<Outcome<Vec<Issue>>> {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily607 let state = state_name(a.state);
Fix a hydration mismatch in output that starts with a blank line608 let label = a
609 .label
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar610 .map(|label| label.split_whitespace().collect::<Vec<_>>().join(" ").to_lowercase())
Fix a hydration mismatch in output that starts with a blank line611 .filter(|label| !label.is_empty());
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar612 let milestone = a.milestone;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily613 let list = |repo_id: String| {
614 let label = label.clone();
615 async move {
616 let state = state.map_or(JsValue::NULL, JsValue::from);
617 let query = self
618 .db
619 .prepare(format!(
620 "SELECT {ISSUE_COLUMNS} FROM issues
621 WHERE repo_id = ? AND (? IS NULL OR state = ?)
622 AND (? IS NULL OR EXISTS
623 (SELECT 1 FROM json_each(issues.labels) WHERE json_each.value = ?))
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar624 AND (? IS NULL OR milestone = ?)
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily625 ORDER BY number DESC LIMIT ?"
626 ))
627 .bind(&[
628 repo_id.into(),
629 state.clone(),
630 state,
631 optional(&label),
632 optional(&label),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar633 optional_number(milestone),
634 optional_number(milestone),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily635 LIST_PAGE.into(),
636 ])?;
637 self.timing.db(1, query.all()).await?.results::<IssueRow>()
638 }
639 };
640 let (_, rows) = check!(self.repo_then(&a.repo, &a.viewer, list).await?);
Fix a hydration mismatch in output that starts with a blank line641 Ok(Outcome::Ok(rows.into_iter().map(Issue::from).collect()))
642 }
643
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily644 /// An issue, the pull requests for it and its comments: one batch,
645 /// started beside the access check (prefetch.rs).
Fix a hydration mismatch in output that starts with a blank line646 async fn get_issue(&self, a: ViewArgs) -> Result<Outcome<IssueDetail>> {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily647 let number = a.number;
648 let read = |repo_id: String| async move {
649 let key = [JsValue::from(repo_id.as_str()), JsValue::from(number)];
650 let statements = vec![
651 self.db
652 .prepare(format!("SELECT {ISSUE_COLUMNS} FROM issues WHERE repo_id = ?1 AND number = ?2"))
653 .bind(&key)?,
654 self.db
655 .prepare(format!(
656 "SELECT {PULL_COLUMNS} FROM pulls
657 WHERE issue_id = (SELECT id FROM issues WHERE repo_id = ?1 AND number = ?2)
658 ORDER BY number"
659 ))
660 .bind(&key)?,
661 self.db
662 .prepare("SELECT * FROM comments WHERE repo_id = ?1 AND number = ?2 ORDER BY id LIMIT 500")
663 .bind(&key)?,
664 ];
665 let results = self.timing.db(3, self.db.batch(statements)).await?;
666 let rows = |index: usize| results.get(index).ok_or_else(|| worker::Error::RustError("short batch".into()));
667 Ok::<_, worker::Error>((
668 rows(0)?.results::<IssueRow>()?.into_iter().next().map(Issue::from),
669 rows(1)?.results::<PullRow>()?.into_iter().map(Pull::from).collect::<Vec<_>>(),
670 rows(2)?.results::<CommentRow>()?.into_iter().map(Comment::from).collect::<Vec<_>>(),
671 ))
672 };
673 let Outcome::Ok((_, (Some(issue), pulls, comments))) = self.repo_then(&a.repo, &a.viewer, read).await? else {
674 return Ok(no_issue());
675 };
676 Ok(Outcome::Ok(IssueDetail { comments, pulls, issue }))
Fix a hydration mismatch in output that starts with a blank line677 }
678
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look679 /// The issue, if `actor` wrote it or may triage the repository's issues.
Fix a hydration mismatch in output that starts with a blank line680 async fn manageable_issue(
681 &self,
682 actor: &User,
683 path: &RepoPath,
684 number: u32,
685 ) -> Result<Outcome<Issue>> {
686 let (repo, issue) = check!(self.issue_at(path, number, &Some(actor.clone())).await?);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look687 check!(writable(&repo));
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights688 if issue.owner().id != actor.id {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look689 check!(allowed(Some(actor), &repo, Capability::Triage));
Fix a hydration mismatch in output that starts with a blank line690 }
691 Ok(Outcome::Ok(issue))
692 }
693
694 async fn update_issue(&self, a: UpdateIssueArgs) -> Result<Outcome<Issue>> {
695 let issue = check!(self.manageable_issue(&a.actor, &a.repo, a.number).await?);
696 let title = match a.title.as_deref().map(valid_title) {
697 Some(Err(message)) => return Ok(Outcome::fail(FailureCode::Invalid, message)),
698 Some(Ok(title)) => Some(title.to_owned()),
699 None => None,
700 };
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar701 if a.labels.as_deref().is_some_and(|labels| normalize_labels(labels).is_none()) {
702 return Ok(Outcome::fail(
703 FailureCode::Invalid,
704 format!("An issue can have up to {MAX_LABELS} labels of up to {MAX_LABEL_CHARS} characters each."),
705 ));
706 }
Fix a hydration mismatch in output that starts with a blank line707 let assignees = match a.assignees {
708 Some(names) => Some(check!(self.valid_assignees(names).await?)),
709 None => None,
710 };
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar711 // Its labels and milestone first: either can be refused, and then
712 // nothing else changes.
713 if a.milestone.is_some() || a.labels.is_some() {
714 let repo = check!(self.repo(&a.repo, &Some(a.actor.clone())).await?);
715 if let Some(number) = a.milestone {
716 check!(self.set_milestone(&a.actor, &repo, &labels::Item::Issue(issue.clone()), number).await?);
717 }
718 if let Some(labels) = &a.labels {
719 check!(self.relabel(&a.actor, &repo, &labels::Item::Issue(issue.clone()), labels).await?);
720 }
721 }
Fix a hydration mismatch in output that starts with a blank line722 let assigned = assignees.as_ref().map(serde_json::to_string).transpose()?;
723 let body = a.body.map(|body| body.trim().to_owned());
724 self.db
725 .prepare(
726 "UPDATE issues
727 SET title = COALESCE(?, title), body = COALESCE(?, body),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar728 assignees = COALESCE(?, assignees), updated_at = ?
Fix a hydration mismatch in output that starts with a blank line729 WHERE id = ?",
730 )
731 .bind(&[
732 optional(&title),
733 optional(&body),
734 optional(&assigned),
735 rfc3339(now_ms()).into(),
736 issue.id.as_str().into(),
737 ])?
738 .run()
739 .await?;
740 let before = issue.assignees.clone();
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API741 let labels_before = issue.labels.clone();
Fix a hydration mismatch in output that starts with a blank line742 let Some(issue) = self.issue(&issue.repo_id, issue.number).await? else {
743 return Ok(no_issue());
744 };
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API745 self.apply_label_rule(&a.actor, &issue, &labels_before).await?;
Fix a hydration mismatch in output that starts with a blank line746 self.publish(
747 "issue.updated",
748 &issue.repo_id,
749 &a.actor,
750 Self::issue_event(&issue),
751 )
752 .await?;
753 if let Some(assignees) = assignees {
754 self.note_changes(
755 &issue.repo_id,
756 issue.number,
757 &a.actor,
758 &before,
759 &assignees,
760 ("assigned", "unassigned"),
761 )
762 .await?;
Events: review requests, assignments, stops and deployments are published763 let added: Vec<String> = assignees.iter().filter(|name| !before.contains(name)).cloned().collect();
Fix a hydration mismatch in output that starts with a blank line764 self.publish(
765 "issue.assigned",
766 &issue.repo_id,
767 &a.actor,
768 IssueEvent {
769 assignees: Some(assignees),
Events: review requests, assignments, stops and deployments are published770 added: Some(added),
Fix a hydration mismatch in output that starts with a blank line771 ..Self::issue_event(&issue)
772 },
773 )
774 .await?;
775 }
776 Ok(Outcome::Ok(issue))
777 }
778
779 /// Usernames as given, tidied, if each names an account.
780 async fn valid_assignees(&self, names: Vec<String>) -> Result<Outcome<Vec<String>>> {
781 let mut assignees: Vec<String> = Vec::new();
782 for name in names {
783 let name = name.trim().trim_start_matches('@').to_lowercase();
784 if name.is_empty() || assignees.contains(&name) {
785 continue;
786 }
787 if assignees.len() == MAX_ASSIGNEES {
788 return Ok(Outcome::fail(
789 FailureCode::Invalid,
790 format!("An issue can be assigned to at most {MAX_ASSIGNEES} people."),
791 ));
792 }
793 let account: Viewer = g1t_kit::call(
794 &self.identity,
795 "user_by_username",
796 &UsernameArgs {
797 username: name.clone(),
798 },
799 )
800 .await?;
801 if account.is_none() {
802 return Ok(Outcome::fail(
803 FailureCode::Invalid,
804 format!("There is no account named {name}."),
805 ));
806 }
807 assignees.push(name);
808 }
809 Ok(Outcome::Ok(assignees))
810 }
811
812 /// Open issues assigned to the viewer, in every repository. Callers
813 /// show only those in repositories the viewer can still see.
814 async fn list_assigned_issues(&self, a: ViewerArgs) -> Result<Vec<Issue>> {
815 let Some(viewer) = a.viewer else {
816 return Ok(Vec::new());
817 };
818 let rows = self
819 .db
820 .prepare(format!(
821 "SELECT {ISSUE_COLUMNS} FROM issues
822 WHERE state = 'open' AND EXISTS (
823 SELECT 1 FROM json_each(issues.assignees) WHERE json_each.value = ?)
824 ORDER BY updated_at DESC LIMIT 50"
825 ))
826 .bind(&[viewer.username.into()])?
827 .all()
828 .await?
829 .results::<IssueRow>()?;
830 Ok(rows.into_iter().map(Issue::from).collect())
831 }
832
833 async fn close_issue(&self, a: IssueActionArgs) -> Result<Outcome<Issue>> {
834 let mut issue = check!(self.manageable_issue(&a.actor, &a.repo, a.number).await?);
835 if issue.state == State::Closed {
836 return Ok(Outcome::fail(
837 FailureCode::Conflict,
838 "This issue is already closed.",
839 ));
840 }
841 let reason = a.reason.unwrap_or(IssueReason::Completed);
842 let now = rfc3339(now_ms());
843 self.db
844 .prepare(
845 "UPDATE issues SET state = 'closed', reason = ?, closed_at = ?, updated_at = ?
846 WHERE id = ?",
847 )
848 .bind(&[
849 reason.as_str().into(),
850 now.as_str().into(),
851 now.as_str().into(),
852 issue.id.as_str().into(),
853 ])?
854 .run()
855 .await?;
856 self.publish(
857 "issue.closed",
858 &issue.repo_id,
859 &a.actor,
860 IssueEvent {
861 reason: Some(reason.as_str()),
862 ..Self::issue_event(&issue)
863 },
864 )
865 .await?;
866 self.note(
867 &issue.repo_id,
868 issue.number,
869 (&a.actor.id, &a.actor.username),
870 match reason {
871 IssueReason::Completed => "closed this as completed",
872 IssueReason::NotPlanned => "closed this as not planned",
873 },
874 )
875 .await?;
876 issue.state = State::Closed;
877 issue.reason = Some(reason);
878 issue.closed_at = Some(now.clone());
879 issue.updated_at = now;
880 Ok(Outcome::Ok(issue))
881 }
882
883 async fn reopen_issue(&self, a: IssueActionArgs) -> Result<Outcome<Issue>> {
884 let mut issue = check!(self.manageable_issue(&a.actor, &a.repo, a.number).await?);
885 if issue.state == State::Open {
886 return Ok(Outcome::fail(
887 FailureCode::Conflict,
888 "This issue is already open.",
889 ));
890 }
891 let now = rfc3339(now_ms());
892 self.db
893 .prepare(
894 "UPDATE issues
895 SET state = 'open', reason = NULL, resolved_by = NULL, closed_at = NULL,
896 updated_at = ?
897 WHERE id = ?",
898 )
899 .bind(&[now.as_str().into(), issue.id.as_str().into()])?
900 .run()
901 .await?;
902 self.publish(
903 "issue.reopened",
904 &issue.repo_id,
905 &a.actor,
906 Self::issue_event(&issue),
907 )
908 .await?;
909 self.note(
910 &issue.repo_id,
911 issue.number,
912 (&a.actor.id, &a.actor.username),
913 "reopened this",
914 )
915 .await?;
916 issue.state = State::Open;
917 issue.reason = None;
918 issue.resolved_by = None;
919 issue.closed_at = None;
920 issue.updated_at = now;
921 Ok(Outcome::Ok(issue))
922 }
923
924
925 async fn counts(&self, a: ViewArgs) -> Result<Outcome<Counts>> {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily926 let read = |repo_id: String| async move {
927 let query = self
928 .db
929 .prepare(
930 "SELECT
931 (SELECT count(*) FROM issues WHERE repo_id = ?1 AND state = 'open') AS issues,
932 (SELECT count(*) FROM pulls
933 WHERE repo_id = ?1 AND status IN ('draft', 'open')) AS pulls",
934 )
935 .bind(&[repo_id.into()])?;
936 self.timing.db(1, query.first::<Counts>(None)).await
937 };
938 let (_, counts) = check!(self.repo_then(&a.repo, &a.viewer, read).await?);
Fix a hydration mismatch in output that starts with a blank line939 Ok(Outcome::Ok(counts.unwrap_or(Counts {
940 issues: 0,
941 pulls: 0,
942 })))
943 }
944
945 // --- Comments ----------------------------------------------------------
946
947 async fn add_comment(&self, a: AddCommentArgs) -> Result<Outcome<Comment>> {
948 if !a.actor.verified {
949 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
950 }
951 let body = a.body.trim();
952 // An approval speaks for itself; anything else has to say something.
953 if body.is_empty() && a.verdict != Some(Verdict::Approve) {
954 return Ok(Outcome::fail(
955 FailureCode::Invalid,
956 "A comment cannot be empty.",
957 ));
958 }
959 let path = a
960 .path
961 .as_deref()
962 .map(str::trim)
963 .filter(|path| !path.is_empty());
964 let line = a.line.filter(|line| *line > 0 && path.is_some());
965 if body.chars().count() > MAX_ENTRY_CHARS {
966 return Ok(Outcome::fail(
967 FailureCode::Invalid,
968 "That comment is too long.",
969 ));
970 }
971 let repo = check!(self.repo(&a.repo, &Some(a.actor.clone())).await?);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look972 check!(writable(&repo));
Fix a hydration mismatch in output that starts with a blank line973 // The number names an issue or a pull request, never both.
974 let mut pull_id = None;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar975 // A command to g1t on a dependency update it opened (`@g1t rebase`)
976 // is the security service's to act on, not a mention for an agent.
977 let mut update_command = false;
Fix a hydration mismatch in output that starts with a blank line978 let table = if self.issue(&repo.id, a.number).await?.is_some() {
979 if path.is_some() || a.verdict.is_some() {
980 return Ok(Outcome::fail(
981 FailureCode::Invalid,
982 "Only a pull request can be reviewed or commented on by line.",
983 ));
984 }
985 "issues"
986 } else if let Some(pull) = self.pull(&repo.id, a.number).await? {
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights987 if a.verdict.is_some() && pull.is_owned_by(&a.actor.id) {
Fix a hydration mismatch in output that starts with a blank line988 return Ok(Outcome::fail(
989 FailureCode::Forbidden,
990 "You cannot approve or request changes on your own pull request.",
991 ));
992 }
993 pull_id = Some(pull.id.clone());
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar994 update_command = pull.author.is_system()
995 && g1t_contracts::updates::update_command(body).is_some();
Fix a hydration mismatch in output that starts with a blank line996 "pulls"
997 } else {
998 return Ok(Outcome::fail(
999 FailureCode::NotFound,
1000 "No issue or pull request has that number.",
1001 ));
1002 };
1003
1004 let now = now_ms();
1005 let comment = Comment {
1006 kind: CommentKind::Comment,
1007 id: new_id("cmt", now),
1008 author: a.actor.clone(),
1009 body: body.to_owned(),
1010 path: path.map(str::to_owned),
1011 line,
1012 verdict: a.verdict,
1013 created_at: rfc3339(now),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1014 edited_at: None,
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar1015 agent: None,
1016 acting_for: None,
1017 advisory: false,
Fix a hydration mismatch in output that starts with a blank line1018 };
1019 self.db
1020 .batch(vec![
1021 self.db
1022 .prepare(
1023 "INSERT INTO comments
1024 (id, repo_id, number, author_id, author_name, body, path, line,
1025 verdict, created_at)
1026 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
1027 )
1028 .bind(&[
1029 comment.id.as_str().into(),
1030 repo.id.as_str().into(),
1031 a.number.into(),
1032 a.actor.id.as_str().into(),
1033 a.actor.username.as_str().into(),
1034 body.into(),
1035 optional(&comment.path),
1036 optional_number(line),
1037 a.verdict
1038 .map_or(JsValue::NULL, |verdict| verdict.as_str().into()),
1039 comment.created_at.as_str().into(),
1040 ])?,
1041 self.db
1042 .prepare(format!(
1043 "UPDATE {table} SET updated_at = ? WHERE repo_id = ? AND number = ?"
1044 ))
1045 .bind(&[
1046 comment.created_at.as_str().into(),
1047 repo.id.as_str().into(),
1048 a.number.into(),
1049 ])?,
1050 ])
1051 .await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1052 if !update_command {
1053 self.note_mention(&a.actor, &repo, a.number, &comment, pull_id.as_deref()).await?;
1054 }
Fix a hydration mismatch in output that starts with a blank line1055 self.publish(
1056 "comment.created",
1057 &repo.id,
1058 &a.actor,
1059 CommentCreated {
1060 comment_id: comment.id.clone(),
1061 repo_id: repo.id.clone(),
1062 number: a.number,
1063 pull_id,
1064 verdict: a.verdict,
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar1065 ..CommentCreated::default()
Fix a hydration mismatch in output that starts with a blank line1066 },
1067 )
1068 .await?;
1069 Ok(Outcome::Ok(comment))
1070 }
1071
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1072 /// A comment in the repository, with the repository and the pull
1073 /// request it is on (if it is on one), when `actor` may edit it, or
1074 /// with `deleting`, delete it (`may_change_comment`).
1075 async fn changeable_comment(
1076 &self,
1077 a: &CommentActionArgs,
1078 deleting: bool,
1079 ) -> Result<Outcome<(Repo, CommentRow, Option<String>)>> {
1080 let repo = check!(self.repo(&a.repo, &Some(a.actor.clone())).await?);
1081 check!(writable(&repo));
1082 let Some(row) = self
1083 .db
1084 .prepare("SELECT * FROM comments WHERE id = ? AND repo_id = ?")
1085 .bind(&[a.comment_id.trim().into(), repo.id.as_str().into()])?
1086 .first::<CommentRow>(None)
1087 .await?
1088 else {
1089 return Ok(Outcome::fail(FailureCode::NotFound, "Comment not found."));
1090 };
1091 let maintains = access::can(Some(&a.actor), &repo, Capability::ManageSettings);
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar1092 // What an agent wrote as itself is answered for by whoever it acted for.
1093 let author_id = row.answerable_id().to_owned();
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1094 if let Err(refusal) = may_change_comment(
1095 row.kind,
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar1096 row.has_verdict(),
1097 &author_id,
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1098 &a.actor.id,
1099 maintains,
1100 deleting,
1101 ) {
1102 // Someone who may change it otherwise was refused for what it is.
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar1103 let code = if author_id == a.actor.id || maintains { FailureCode::Conflict } else { FailureCode::Forbidden };
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1104 return Ok(Outcome::fail(code, refusal));
1105 }
1106 let pull_id = self.pull(&repo.id, row.number).await?.map(|pull| pull.id);
1107 Ok(Outcome::Ok((repo, row, pull_id)))
1108 }
1109
1110 /// Changes the text of a comment: its author's to do, or a
1111 /// maintainer's. Publishes `comment.edited` with what it said before.
1112 async fn edit_comment(&self, a: CommentActionArgs) -> Result<Outcome<Comment>> {
1113 let body = a.body.trim().to_owned();
1114 if body.chars().count() > MAX_ENTRY_CHARS {
1115 return Ok(Outcome::fail(FailureCode::Invalid, "That comment is too long."));
1116 }
1117 let (repo, row, pull_id) = check!(self.changeable_comment(&a, false).await?);
1118 // An approval speaks for itself; anything else has to say something.
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar1119 if body.is_empty() && row.verdict != Some(Verdict::Approve) && row.agent_verdict.as_deref() != Some("approve") {
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1120 return Ok(Outcome::fail(FailureCode::Invalid, "A comment cannot be empty."));
1121 }
1122 let number = row.number;
1123 let mut comment = Comment::from(row);
1124 if comment.body == body {
1125 return Ok(Outcome::Ok(comment));
1126 }
1127 let now = rfc3339(now_ms());
1128 self.db
1129 .prepare("UPDATE comments SET body = ?, edited_at = ? WHERE id = ?")
1130 .bind(&[body.as_str().into(), now.as_str().into(), comment.id.as_str().into()])?
1131 .run()
1132 .await?;
1133 let before = std::mem::replace(&mut comment.body, body);
1134 comment.edited_at = Some(now);
1135 self.publish(
1136 "comment.edited",
1137 &repo.id,
1138 &a.actor,
1139 CommentEdited {
1140 comment_id: comment.id.clone(),
1141 repo_id: repo.id.clone(),
1142 number,
1143 pull_id,
1144 changes: CommentChanges { body: ChangedFrom { from: before } },
1145 },
1146 )
1147 .await?;
1148 Ok(Outcome::Ok(comment))
1149 }
1150
1151 /// Deletes a comment: its author's to do, or a maintainer's. A review
1152 /// that gave a verdict stays. Publishes `comment.deleted` with the
1153 /// comment as it was.
1154 async fn delete_comment(&self, a: CommentActionArgs) -> Result<Outcome<bool>> {
1155 let (repo, row, pull_id) = check!(self.changeable_comment(&a, true).await?);
1156 self.db
1157 .prepare("DELETE FROM comments WHERE id = ?")
1158 .bind(&[row.id.as_str().into()])?
1159 .run()
1160 .await?;
1161 let number = row.number;
1162 let comment = Comment::from(row);
1163 self.publish(
1164 "comment.deleted",
1165 &repo.id,
1166 &a.actor,
1167 CommentDeleted {
1168 comment_id: comment.id.clone(),
1169 repo_id: repo.id.clone(),
1170 number,
1171 pull_id,
1172 comment: DeletedComment {
1173 id: comment.id,
1174 body: comment.body,
1175 author: (&comment.author).into(),
1176 created_at: comment.created_at,
1177 path: comment.path,
1178 line: comment.line,
1179 },
1180 },
1181 )
1182 .await?;
1183 Ok(Outcome::Ok(true))
1184 }
1185
Fix a hydration mismatch in output that starts with a blank line1186 // --- Pull requests -----------------------------------------------------
1187
1188 async fn open_pull(&self, a: OpenPullArgs) -> Result<Outcome<Pull>> {
1189 if !a.actor.verified {
1190 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
1191 }
1192 let repo = check!(self.repo(&a.repo, &Some(a.actor.clone())).await?);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1193 check!(writable(&repo));
1194 // g1t's own agent at work spends the workspace's compute; a pull
1195 // request anyone else's agent makes is like any other.
1196 if matches!(a.runtime, Runtime::Hosted) {
1197 check!(allowed(Some(&a.actor), &repo, Capability::Run));
1198 }
Fix a hydration mismatch in output that starts with a blank line1199 let issue = match a.issue {
1200 Some(number) => match self.issue(&repo.id, number).await? {
1201 Some(issue) if issue.state == State::Open => Some(issue),
1202 Some(_) => {
1203 return Ok(Outcome::fail(
1204 FailureCode::Conflict,
1205 "This issue is closed.",
1206 ));
1207 }
1208 None => return Ok(no_issue()),
1209 },
1210 None => None,
1211 };
1212 // A pull request for an issue takes the issue's title unless given one.
1213 let title = match (a.title.trim(), &issue) {
1214 ("", Some(issue)) => issue.title.clone(),
1215 (title, _) => match valid_title(title) {
1216 Ok(title) => title.to_owned(),
1217 Err(message) => return Ok(Outcome::fail(FailureCode::Invalid, message)),
1218 },
1219 };
Pull requests: unnamed, a pull request is its author's, not an agent's1220 // Unnamed, the change is its author's, unless an agent opened it.
Fix a hydration mismatch in output that starts with a blank line1221 let agent = match a.agent.trim() {
Pull requests: unnamed, a pull request is its author's, not an agent's1222 "" if g1t_contracts::rules::is_agent(&a.actor) => "agent",
1223 "" => a.actor.username.as_str(),
Fix a hydration mismatch in output that starts with a blank line1224 agent => agent,
1225 };
1226 let runtime = match a.runtime {
1227 Runtime::Hosted => "hosted",
1228 Runtime::External => "external",
1229 };
1230
1231 let now = now_ms();
1232 let id = new_id("pr", now);
1233 let branch = a
1234 .branch
1235 .as_deref()
1236 .map(str::trim)
1237 .filter(|branch| !branch.is_empty());
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1238 // The branch it merges into: the default branch unless another is
1239 // asked for, which has to exist.
1240 let base = a.base.as_deref().and_then(|base| stored_base(base, &repo));
1241 if let Some(base) = &base {
1242 if branch == Some(base.as_str()) {
1243 return Ok(Outcome::fail(
1244 FailureCode::Invalid,
1245 format!("A pull request cannot merge {base} into itself. Choose another base."),
1246 ));
1247 }
1248 let exists: Option<String> = g1t_kit::call(
1249 &self.repos,
1250 "head",
1251 &HeadArgs { repo_id: repo.id.clone(), branch: base.clone() },
1252 )
1253 .await?;
1254 if exists.is_none() {
1255 return Ok(Outcome::fail(FailureCode::NotFound, format!("There is no branch named {base} to merge into.")));
1256 }
1257 }
1258 let base_name = base.clone().unwrap_or_else(|| repo.default_branch.clone());
Fix a hydration mismatch in output that starts with a blank line1259 // The change is on a branch already pushed to the repository, or
1260 // will be made in a fork created for this pull request.
1261 let (fork, head) = match branch {
1262 Some(branch) => {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1263 if branch == base_name {
Fix a hydration mismatch in output that starts with a blank line1264 return Ok(Outcome::fail(
1265 FailureCode::Invalid,
1266 format!("Choose a branch other than {branch}."),
1267 ));
1268 }
1269 let head: Option<String> = g1t_kit::call(
1270 &self.repos,
1271 "head",
1272 &HeadArgs {
1273 repo_id: repo.id.clone(),
1274 branch: branch.to_owned(),
1275 },
1276 )
1277 .await?;
1278 let Some(head) = head else {
1279 return Ok(Outcome::fail(
1280 FailureCode::NotFound,
1281 format!("There is no branch named {branch}. Push it first."),
1282 ));
1283 };
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1284 // One open pull request for each branch and base.
Fix a hydration mismatch in output that starts with a blank line1285 let existing = self
1286 .db
1287 .prepare(
1288 "SELECT number AS n FROM pulls
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1289 WHERE repo_id = ? AND source_branch = ? AND status IN ('draft', 'open')
1290 AND base_branch IS ?",
Fix a hydration mismatch in output that starts with a blank line1291 )
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1292 .bind(&[repo.id.as_str().into(), branch.into(), optional(&base)])?
Fix a hydration mismatch in output that starts with a blank line1293 .first::<NumberRow>(None)
1294 .await?;
1295 if let Some(existing) = existing {
1296 return Ok(Outcome::fail(
1297 FailureCode::Conflict,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1298 format!("Pull request #{} is already open from {branch} into {base_name}.", existing.n),
Fix a hydration mismatch in output that starts with a blank line1299 ));
1300 }
1301 (None, Some(head))
1302 }
1303 None => {
1304 let fork: Outcome<Repo> = g1t_kit::call(
1305 &self.repos,
1306 "fork_for_pull",
1307 &ForkArgs {
1308 source_id: repo.id.clone(),
1309 pull_id: id.clone(),
1310 actor: a.actor.clone(),
1311 },
1312 )
1313 .await?;
1314 (Some(check!(fork)), None)
1315 }
1316 };
1317 // A branch already holds the work, so its pull request is ready for
1318 // review from the start; one with a fork starts as a draft.
Open a pull request as a draft: it can't merge, and agents' review routines wait, until it is marked ready1319 // Asked for as a draft, it waits until it is marked ready.
1320 let status = if branch.is_some() && !a.draft { "open" } else { "draft" };
Fix a hydration mismatch in output that starts with a blank line1321 let body = Some(a.body.trim().to_owned()).filter(|body| !body.is_empty());
1322
1323 let number = self.next_number(&repo.id).await?;
1324 let timestamp = rfc3339(now);
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights1325 // A change g1t makes is g1t's, for whoever asked for it. This is
1326 // what lifecycle::made_by_g1t reads back.
1327 let by_g1t = matches!(a.runtime, Runtime::Hosted) && agent == reviews::AGENT_NAME && fork.is_some();
1328 let (author, requested_by) = authorship(&a.actor, by_g1t);
Fix a hydration mismatch in output that starts with a blank line1329 self.db
1330 .prepare(
1331 "INSERT INTO pulls
1332 (id, repo_id, number, issue_id, issue_number, title, body, agent, runtime,
1333 status, fork_repo_id, fork_namespace, fork_name, source_branch, head_commit,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1334 author_id, author_name, requested_by_id, requested_by_name, created_at, updated_at,
1335 base_branch)
1336 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
Fix a hydration mismatch in output that starts with a blank line1337 )
1338 .bind(&[
1339 id.as_str().into(),
1340 repo.id.as_str().into(),
1341 number.into(),
1342 optional(&issue.as_ref().map(|issue| issue.id.clone())),
1343 optional_number(issue.as_ref().map(|issue| issue.number)),
1344 title.into(),
1345 optional(&body),
1346 agent.into(),
1347 runtime.into(),
1348 status.into(),
1349 optional(&fork.as_ref().map(|fork| fork.id.clone())),
1350 optional(&fork.as_ref().map(|fork| fork.namespace.clone())),
1351 optional(&fork.as_ref().map(|fork| fork.name.clone())),
1352 optional(&branch.map(str::to_owned)),
1353 optional(&head),
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights1354 author.id.as_str().into(),
1355 author.username.as_str().into(),
1356 optional(&requested_by.as_ref().map(|user| user.id.clone())),
1357 optional(&requested_by.as_ref().map(|user| user.username.clone())),
Fix a hydration mismatch in output that starts with a blank line1358 timestamp.as_str().into(),
1359 timestamp.as_str().into(),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1360 optional(&base),
Fix a hydration mismatch in output that starts with a blank line1361 ])?
1362 .run()
1363 .await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1364 let Some(mut pull) = self.pull(&repo.id, number).await? else {
Fix a hydration mismatch in output that starts with a blank line1365 return Ok(no_pull());
1366 };
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1367 fill_base(&mut pull, &repo);
Fix a hydration mismatch in output that starts with a blank line1368 self.manage(&pull).await?;
1369 // Someone is on it now, so it is no longer waiting for an agent.
1370 if let Some(issue) = pull.issue {
1371 self.db
1372 .prepare("UPDATE issues SET queued_by = NULL WHERE repo_id = ? AND number = ?")
1373 .bind(&[repo.id.as_str().into(), issue.into()])?
1374 .run()
1375 .await?;
1376 }
1377 if let Some(issue) = pull.issue {
1378 let text = if lifecycle::made_by_g1t(&pull) {
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent1379 format!("assigned this to g1t, which opened #{}", pull.number)
Fix a hydration mismatch in output that starts with a blank line1380 } else {
1381 format!("opened #{} for this", pull.number)
1382 };
1383 self.note(&repo.id, issue, (&a.actor.id, &a.actor.username), &text)
1384 .await?;
1385 }
1386 self.publish(
1387 "pull.opened",
1388 &repo.id,
1389 &a.actor,
1390 PullEvent {
1391 agent: Some(pull.agent.clone()),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1392 base: pull.base.clone(),
Fix a hydration mismatch in output that starts with a blank line1393 ..Self::pull_event(&pull)
1394 },
1395 )
1396 .await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1397 // Its code owners asked to review (codeowners.rs).
1398 self.refresh_code_owners(&pull).await;
1399 let pull = self.pull(&repo.id, number).await?.unwrap_or(pull);
Fix a hydration mismatch in output that starts with a blank line1400 Ok(Outcome::Ok(pull))
1401 }
1402
1403 async fn list_pulls(&self, a: ListPullsArgs) -> Result<Outcome<Vec<Pull>>> {
1404 let filter = match a.state {
1405 Some(State::Open) => "AND status IN ('draft', 'open')",
1406 Some(State::Closed) => "AND status IN ('merged', 'closed')",
1407 None => "",
1408 };
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1409 let label = a
1410 .label
1411 .map(|label| label.split_whitespace().collect::<Vec<_>>().join(" ").to_lowercase())
1412 .filter(|label| !label.is_empty());
1413 let milestone = a.milestone;
1414 let read = |repo_id: String| {
1415 let label = label.clone();
1416 async move {
1417 let query = self
1418 .db
1419 .prepare(format!(
1420 "SELECT {PULL_COLUMNS} FROM pulls WHERE repo_id = ? {filter}
1421 AND (? IS NULL OR EXISTS
1422 (SELECT 1 FROM json_each(pulls.labels) WHERE json_each.value = ?))
1423 AND (? IS NULL OR milestone = ?)
1424 ORDER BY number DESC LIMIT ?"
1425 ))
1426 .bind(&[
1427 repo_id.into(),
1428 optional(&label),
1429 optional(&label),
1430 optional_number(milestone),
1431 optional_number(milestone),
1432 LIST_PAGE.into(),
1433 ])?;
1434 self.timing.db(1, query.all()).await?.results::<PullRow>()
1435 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1436 };
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1437 let (repo, rows) = check!(self.repo_then(&a.repo, &a.viewer, read).await?);
1438 let base = a.base.map(|base| base.trim().to_owned()).filter(|base| !base.is_empty());
1439 Ok(Outcome::Ok(
1440 rows.into_iter()
1441 .map(|row| {
1442 let mut pull = Pull::from(row);
1443 fill_base(&mut pull, &repo);
1444 pull
1445 })
1446 .filter(|pull| base.as_deref().is_none_or(|base| pull.base.as_deref() == Some(base)))
1447 .collect(),
1448 ))
Fix a hydration mismatch in output that starts with a blank line1449 }
1450
Fast pages, required checks on the branch, self-hosted runners, honest incidents1451 /// `pulls_for_repos`: what `list_pulls` gives, open and closed, for many
1452 /// repositories at once: one access check with repos for all of them
1453 /// and one query, instead of two of each per repository.
1454 async fn pulls_for_repos(&self, a: PullsForReposArgs) -> Result<Vec<RepoPulls>> {
1455 let ids: Vec<String> = a.repo_ids.into_iter().take(MAX_PULLS_FOR_REPOS).collect();
1456 if ids.is_empty() {
1457 return Ok(Vec::new());
1458 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1459 let limit = a.limit.clamp(1, LIST_PAGE);
1460 // The rows are read beside the access check, for every id asked
1461 // about; those of repositories the viewer cannot read are dropped.
1462 let asked = serde_json::to_string(&ids)?;
1463 let check = ReadableArgs { ids, viewer: a.viewer };
1464 let readable = self.timing.rpc(g1t_kit::call::<_, Vec<Repo>>(&self.repos, "readable", &check));
1465 let (readable, rows) = try_join(readable, self.timing.db(1, self.newest_pulls(asked, limit))).await?;
Fast pages, required checks on the branch, self-hosted runners, honest incidents1466 if readable.is_empty() {
1467 return Ok(Vec::new());
1468 }
1469 let mut answer: Vec<RepoPulls> = readable
1470 .iter()
1471 .map(|repo| RepoPulls { repo_id: repo.id.clone(), open: Vec::new(), closed: Vec::new() })
1472 .collect();
1473 for pull in rows.into_iter().map(Pull::from) {
1474 let Some(entry) = answer.iter_mut().find(|entry| entry.repo_id == pull.repo_id) else {
1475 continue;
1476 };
1477 match pull.status {
1478 PullStatus::Draft | PullStatus::Open => entry.open.push(pull),
1479 PullStatus::Merged | PullStatus::Closed => entry.closed.push(pull),
1480 }
1481 }
1482 for entry in &mut answer {
1483 entry.open.sort_by_key(|pull| std::cmp::Reverse(pull.number));
1484 entry.closed.sort_by_key(|pull| std::cmp::Reverse(pull.number));
1485 }
1486 Ok(answer)
1487 }
1488
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1489 /// The newest `limit` of each repository's open (draft or open) and
1490 /// closed (merged or closed) pull requests, for the ids in `ids` (JSON).
1491 async fn newest_pulls(&self, ids: String, limit: u32) -> Result<Vec<PullRow>> {
1492 self.db
1493 .prepare(format!(
1494 "SELECT * FROM (
1495 SELECT {PULL_COLUMNS}, ROW_NUMBER() OVER (
1496 PARTITION BY pulls.repo_id, pulls.status IN ('draft', 'open') ORDER BY pulls.number DESC
1497 ) AS place
1498 FROM pulls WHERE pulls.repo_id IN (SELECT value FROM json_each(?1))
1499 ) WHERE place <= ?2"
1500 ))
1501 .bind(&[ids.into(), limit.into()])?
1502 .all()
1503 .await?
1504 .results::<PullRow>()
1505 }
1506
Fix a hydration mismatch in output that starts with a blank line1507 async fn get_pull(&self, a: ViewArgs) -> Result<Outcome<PullDetail>> {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1508 let number = a.number;
1509 // Every row the page and the lifecycle read, in one batch started
1510 // beside the access check; the helpers below read from it.
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1511 let namespace = a.repo.namespace.clone();
1512 let read = |repo_id: String| self.prefetch_pull(repo_id, namespace.clone(), number);
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1513 let Outcome::Ok((repo, Some(found))) = self.repo_then(&a.repo, &a.viewer, read).await? else {
1514 return Ok(no_pull());
1515 };
1516 let Some(row) = found.first::<PullRow>(prefetch::Slot::Pull)? else {
1517 return Ok(no_pull());
Fix a hydration mismatch in output that starts with a blank line1518 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1519 let stored = found.first::<StoredBehind>(prefetch::Slot::Pull)?;
1520 let issue = found.first::<IssueRow>(prefetch::Slot::Issue)?.map(Issue::from);
1521 let comments: Vec<Comment> =
1522 found.rows::<CommentRow>(prefetch::Slot::Comments)?.into_iter().map(Comment::from).collect();
1523 self.keep_prefetched(Some(found));
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1524 let default_branch = repo.default_branch.clone();
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1525 let detail = self.pull_detail(repo, Pull::from(row), issue, comments, stored, &a.viewer).await;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1526 self.keep_prefetched(None);
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1527 // The branch it merges into, named, for whoever reads it.
1528 Ok(match detail? {
1529 Outcome::Ok(mut detail) => {
1530 if detail.pull.base.as_deref().is_none_or(str::is_empty) {
1531 detail.pull.base = Some(default_branch);
1532 }
1533 Outcome::Ok(detail)
1534 }
1535 failed => failed,
1536 })
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1537 }
1538
1539 async fn pull_detail(
1540 &self,
1541 repo: Repo,
1542 mut pull: Pull,
1543 issue: Option<Issue>,
1544 comments: Vec<Comment>,
1545 stored: Option<StoredBehind>,
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1546 viewer: &Viewer,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1547 ) -> Result<Outcome<PullDetail>> {
1548 // Whether it is behind, as worked out with its mergeability on the
1549 // last push to either side (mergeability.rs), when that was for
1550 // its head as it is now; otherwise asked of the repos service.
1551 let known_behind = stored.and_then(|stored| stored.for_head(pull.head_commit.as_deref()));
Fix a hydration mismatch in output that starts with a blank line1552 // Worked out on each push; this covers a pull request from before
1553 // that was recorded.
1554 if pull.files.is_empty() && pull.head_commit.is_some() {
1555 pull.files = self.refresh_files(&pull).await?;
1556 }
1557 // Everything else at once: none of it depends on the rest, and each
1558 // is a round trip of its own.
1559 let standing = async {
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1560 // Mergeability first: where g1t sees a pull request through, a
1561 // conflict decides its next step.
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1562 let behind = async {
1563 match known_behind {
1564 Some(behind) => Ok(behind),
1565 None => {
1566 let behind = self.is_behind(&repo.id, &pull).await?;
1567 // Kept for the next view when the mergeability on
1568 // record is for this head: a pull request from
1569 // before `behind` was kept asks once.
1570 if let Some(head) = pull.head_commit.as_deref()
1571 && pull.status.is_active()
1572 {
1573 self.db
1574 .prepare(
1575 "UPDATE pulls SET behind = ?1
1576 WHERE id = ?2 AND behind IS NULL AND mergeable_key LIKE ?3 || '..%'",
1577 )
1578 .bind(&[u32::from(behind).into(), pull.id.as_str().into(), head.into()])?
1579 .run()
1580 .await?;
1581 }
1582 Ok(behind)
1583 }
1584 }
1585 };
1586 let (merge, behind) = try_join(self.mergeability(&pull), behind).await?;
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1587 let assessed = self.assess_with_confidence(&pull, &issue, behind).await?;
1588 let confidence = assessed.as_ref().and_then(|(_, _, confidence)| confidence.clone());
1589 let lifecycle = assessed.map(|(lifecycle, _, _)| lifecycle);
1590 Ok::<_, worker::Error>((behind, (lifecycle, confidence), merge))
Fix a hydration mismatch in output that starts with a blank line1591 };
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1592 let (((behind, (lifecycle, confidence), (mergeable, conflicts)), (landing, stalled), comments), (checks, overlaps, review_pending)) =
Fix a hydration mismatch in output that starts with a blank line1593 try_join(
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1594 try_join3(standing, self.landing_state(&pull.id), async { Ok(comments) }),
Fix a hydration mismatch in output that starts with a blank line1595 try_join3(
1596 self.latest_checks(&pull.id),
1597 self.overlaps(&pull),
1598 self.review_pending(&pull.id),
1599 ),
1600 )
1601 .await?;
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1602 // As just worked out, rather than as it was read.
1603 if confidence.is_some() {
1604 pull.confidence = confidence;
1605 }
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1606 // The rules of the branch it merges into, as they stack, and which
1607 // of them it does not meet yet, for whoever is looking.
1608 let (statuses, settings, gate) = try_join3(
1609 self.statuses(&repo.id, pull.head_commit.as_deref()),
1610 self.settings_on(&repo, &pull),
1611 async {
1612 if pull.status.is_active() {
1613 self.merge_gate(&repo, &pull, viewer.as_ref(), false, true).await.map(Some)
1614 } else {
1615 Ok(None)
1616 }
1617 },
1618 )
1619 .await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1620 let code_owners = self.pull_code_owners(&pull, &comments, &settings).await?;
Fix a hydration mismatch in output that starts with a blank line1621 Ok(Outcome::Ok(PullDetail {
Fast pages, required checks on the branch, self-hosted runners, honest incidents1622 required_checks: required_checks(&settings.required_checks, &statuses),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1623 rules: gate.map(|gate| rulesets::merge_rules(&gate.judged, &gate.requirements, pull.base_branch(&repo.default_branch) == repo.default_branch)),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1624 code_owners,
Fix a hydration mismatch in output that starts with a blank line1625 comments,
1626 checks,
1627 overlaps,
1628 behind,
1629 review_pending,
1630 lifecycle,
1631 landing,
1632 stalled,
1633 messages: self.messages(&pull.id).await?,
Fast pages, required checks on the branch, self-hosted runners, honest incidents1634 statuses,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1635 mergeable,
1636 conflicts,
1637 earlier_checks: self.earlier_checks(&pull.id).await?,
Fix a hydration mismatch in output that starts with a blank line1638 issue,
1639 pull,
1640 }))
1641 }
1642
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1643 /// The pull request, whatever its status, if its repository is not
1644 /// archived and `actor` opened it or may triage its pull requests.
1645 async fn managed_pull(
Fix a hydration mismatch in output that starts with a blank line1646 &self,
1647 actor: &User,
1648 path: &RepoPath,
1649 number: u32,
1650 ) -> Result<Outcome<Pull>> {
1651 let (repo, pull) = check!(self.pull_at(path, number, &Some(actor.clone())).await?);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1652 check!(writable(&repo));
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights1653 if !pull.is_owned_by(&actor.id) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1654 check!(allowed(Some(actor), &repo, Capability::Triage));
Fix a hydration mismatch in output that starts with a blank line1655 }
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1656 Ok(Outcome::Ok(pull))
1657 }
1658
1659 /// The pull request, if it is still active and `actor` opened it or
1660 /// may triage the repository's pull requests.
1661 async fn manageable_pull(
1662 &self,
1663 actor: &User,
1664 path: &RepoPath,
1665 number: u32,
1666 ) -> Result<Outcome<Pull>> {
1667 let pull = check!(self.managed_pull(actor, path, number).await?);
Fix a hydration mismatch in output that starts with a blank line1668 if !pull.status.is_active() {
1669 return Ok(Outcome::fail(
1670 FailureCode::Conflict,
1671 format!("This pull request is already {}.", pull.status.as_str()),
1672 ));
1673 }
1674 Ok(Outcome::Ok(pull))
1675 }
1676
Catching up with main takes seconds when the two sides touched different files1677 /// Brings a pull request up to date with the default branch without a
1678 /// sandbox, where the repos service can do that safely. Whoever could
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights1679 /// have pushed the merge themselves may ask: whoever opened it (or asked
1680 /// g1t for it), for a fork; anyone who may push, for a branch of the
1681 /// repository. When it needs a
Catching up with main takes seconds when the two sides touched different files1682 /// real merge, says so, naming the conflicting files if a probe found
1683 /// them, and pushes nothing.
1684 async fn catch_up_pull(&self, a: PullActionArgs) -> Result<Outcome<PullBranchUpdate>> {
1685 let (repo, pull) = check!(self.pull_at(&a.repo, a.number, &Some(a.actor.clone())).await?);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1686 check!(writable(&repo));
Catching up with main takes seconds when the two sides touched different files1687 if !pull.status.is_active() {
1688 return Ok(Outcome::fail(
1689 FailureCode::Conflict,
1690 format!("This pull request is already {}.", pull.status.as_str()),
1691 ));
1692 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1693 if pull.fork_repo_id.is_some() {
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights1694 if !pull.is_owned_by(&a.actor.id) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1695 return Ok(Outcome::fail(
1696 FailureCode::Forbidden,
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights1697 "Only whoever opened this pull request, or asked g1t for it, can update it.",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1698 ));
1699 }
Catching up with main takes seconds when the two sides touched different files1700 } else {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1701 check!(allowed(Some(&a.actor), &repo, Capability::Push));
Catching up with main takes seconds when the two sides touched different files1702 }
1703 let updated: Outcome<PullBranchUpdate> = g1t_kit::call(
1704 &self.repos,
1705 "update_pull_branch",
1706 &UpdatePullBranchArgs {
1707 source_id: pull.fork_repo_id.clone().unwrap_or_else(|| repo.id.clone()),
1708 branch: pull.branch.clone(),
1709 number: pull.number,
1710 actor: a.actor,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1711 target_branch: pull.base.clone(),
Catching up with main takes seconds when the two sides touched different files1712 },
1713 )
1714 .await?;
1715 // A probe that found conflicts says more than "both changed it".
1716 if let Outcome::Ok(PullBranchUpdate::NeedsAgent { .. }) = &updated
1717 && let Some(files) = self.conflicting_files(&pull).await?
1718 && !files.is_empty()
1719 {
1720 return Ok(Outcome::Ok(PullBranchUpdate::NeedsAgent {
1721 reason: NeedsAgentReason::Conflicting,
1722 detail: "Merging it conflicts.".to_owned(),
1723 paths: files,
1724 }));
1725 }
1726 Ok(updated)
1727 }
1728
Fix a hydration mismatch in output that starts with a blank line1729 async fn update_pull(&self, a: UpdatePullArgs) -> Result<Outcome<Pull>> {
1730 let pull = check!(self.manageable_pull(&a.actor, &a.repo, a.number).await?);
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1731 // The branch it merges into, its milestone and its labels first:
1732 // each can be refused, and then nothing else changes.
1733 if a.base.is_some() || a.milestone.is_some() || a.labels.is_some() {
1734 let repo = check!(self.repo(&a.repo, &Some(a.actor.clone())).await?);
1735 if let Some(base) = &a.base {
1736 check!(self.change_base(&a.actor, &repo, &pull, base).await?);
1737 }
1738 if let Some(number) = a.milestone {
1739 check!(self.set_milestone(&a.actor, &repo, &labels::Item::Pull(pull.clone()), number).await?);
1740 }
1741 if let Some(labels) = &a.labels {
1742 check!(self.relabel(&a.actor, &repo, &labels::Item::Pull(pull.clone()), labels).await?);
1743 }
1744 }
Fix a hydration mismatch in output that starts with a blank line1745 let assignees = match a.assignees {
1746 Some(names) => Some(check!(self.valid_assignees(names).await?)),
1747 None => None,
1748 };
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1749 // Teams, named `workspace/team`, apart from the people.
1750 let (team_names, a_reviewers) = match a.reviewers {
1751 Some(names) => {
1752 let (teams, people): (Vec<String>, Vec<String>) =
1753 names.into_iter().partition(|name| team_reviews::team_name(name).is_some());
1754 (Some(teams), Some(people))
1755 }
1756 None => (None, None),
1757 };
1758 let teams = match team_names {
1759 Some(names) => Some(check!(self.valid_team_reviewers(&a.actor, &a.repo, &pull, names).await?)),
1760 None => None,
1761 };
1762 let reviewers = match a_reviewers {
Fix a hydration mismatch in output that starts with a blank line1763 Some(names) => {
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent1764 // g1t is not an account; everyone else has to be.
Fix a hydration mismatch in output that starts with a blank line1765 let agent = names
1766 .iter()
1767 .any(|name| name.trim().eq_ignore_ascii_case(reviews::AGENT_NAME));
1768 let people = names
1769 .into_iter()
1770 .filter(|name| !name.trim().eq_ignore_ascii_case(reviews::AGENT_NAME))
1771 .collect();
1772 let mut reviewers = check!(self.valid_assignees(people).await?);
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights1773 // Nobody is asked to review their own, nor what they had g1t make.
1774 reviewers.retain(|name| *name != pull.owner().username);
Fix a hydration mismatch in output that starts with a blank line1775 if agent {
1776 reviewers.insert(0, reviews::AGENT_NAME.to_owned());
1777 }
1778 Some(reviewers)
1779 }
1780 None => None,
1781 };
1782 self.db
1783 .prepare(
1784 "UPDATE pulls
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1785 SET assignees = COALESCE(?, assignees), updated_at = ?
Fix a hydration mismatch in output that starts with a blank line1786 WHERE id = ?",
1787 )
1788 .bind(&[
1789 optional(&assignees.as_ref().map(serde_json::to_string).transpose()?),
1790 rfc3339(now_ms()).into(),
1791 pull.id.as_str().into(),
1792 ])?
1793 .run()
1794 .await?;
1795 if let Some(assignees) = &assignees {
1796 self.note_changes(
1797 &pull.repo_id,
1798 pull.number,
1799 &a.actor,
1800 &pull.assignees,
1801 assignees,
1802 ("assigned", "unassigned"),
1803 )
1804 .await?;
1805 }
Events: review requests, assignments, stops and deployments are published1806 // Who was newly assigned or asked to review, and whose request was
1807 // withdrawn: the inbox tells them, and webhooks say so.
1808 let newly = |after: &[String], before: &[String]| -> Vec<String> {
1809 after.iter().filter(|name| !before.contains(name)).cloned().collect()
1810 };
1811 if let Some(assignees) = &assignees {
1812 let added = newly(assignees, &pull.assignees);
1813 if !added.is_empty() {
1814 self.publish(
1815 "pull.assigned",
1816 &pull.repo_id,
1817 &a.actor,
1818 PullEvent {
1819 assignees: Some(assignees.clone()),
1820 added: Some(added),
1821 ..Self::pull_event(&pull)
1822 },
1823 )
1824 .await?;
1825 }
1826 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1827 if reviewers.is_some() || teams.is_some() {
1828 let people = reviewers.unwrap_or_else(|| pull.reviewers.clone());
1829 let teams = teams.unwrap_or_else(|| pull.team_reviewers.clone());
1830 self.set_reviewers(&pull, people, teams, Some(&a.actor), false).await?;
Events: review requests, assignments, stops and deployments are published1831 }
Fix a hydration mismatch in output that starts with a blank line1832 Ok(match self.pull(&pull.repo_id, pull.number).await? {
1833 Some(pull) => Outcome::Ok(pull),
1834 None => no_pull(),
1835 })
1836 }
1837
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1838 /// Points an open pull request at another branch to merge into. Needs
1839 /// the Write role. What it would merge, whether it is behind, its
1840 /// mergeability and its checks are all worked out against the new base.
1841 async fn change_base(&self, actor: &User, repo: &Repo, pull: &Pull, base: &str) -> Result<Outcome<()>> {
1842 check!(allowed(Some(actor), repo, Capability::Push));
1843 let base = base.trim();
1844 if base.is_empty() {
1845 return Ok(Outcome::fail(FailureCode::Invalid, "Name the branch it should merge into."));
1846 }
1847 let before = pull.base_branch(&repo.default_branch).to_owned();
1848 if base == before {
1849 return Ok(Outcome::Ok(()));
1850 }
1851 if pull.fork_repo_id.is_none() && pull.branch.as_deref() == Some(base) {
1852 return Ok(Outcome::fail(
1853 FailureCode::Invalid,
1854 format!("A pull request cannot merge {base} into itself. Choose another base."),
1855 ));
1856 }
1857 let exists: Option<String> = g1t_kit::call(
1858 &self.repos,
1859 "head",
1860 &HeadArgs { repo_id: repo.id.clone(), branch: base.to_owned() },
1861 )
1862 .await?;
1863 if exists.is_none() {
1864 return Ok(Outcome::fail(FailureCode::NotFound, format!("There is no branch named {base} to merge into.")));
1865 }
1866 // In the merge queue it was headed for the default branch; it
1867 // leaves the queue for another base.
1868 let left_queue = self
1869 .leave(&pull.repo_id, pull, QueueState::Removed, Some("Its base branch changed."))
1870 .await?;
1871 let stored = stored_base(base, repo);
1872 self.db
1873 .prepare(
1874 "UPDATE pulls
1875 SET base_branch = ?, updated_at = ?, land_requested = NULL, land_requested_at = NULL, behind = NULL,
1876 mergeable = NULL, mergeable_key = NULL, conflicts = NULL
1877 WHERE id = ?",
1878 )
1879 .bind(&[optional(&stored), rfc3339(now_ms()).into(), pull.id.as_str().into()])?
1880 .run()
1881 .await?;
1882 self.note(
1883 &pull.repo_id,
1884 pull.number,
1885 (&actor.id, &actor.username),
1886 &format!("changed the base branch from `{before}` to `{base}`"),
1887 )
1888 .await?;
1889 self.publish(
1890 "pull.base_changed",
1891 &pull.repo_id,
1892 actor,
1893 PullEvent { base: Some(base.to_owned()), ..Self::pull_event(pull) },
1894 )
1895 .await?;
1896 if left_queue {
1897 self.publish_as(
1898 "queue.changed",
1899 &pull.repo_id,
1900 None,
1901 g1t_contracts::events::QueueChanged { repo_id: pull.repo_id.clone() },
1902 )
1903 .await?;
1904 }
1905 // Whether it merges cleanly into the new base.
1906 if let Some(moved) = self.pull_by_id(&pull.id).await?
1907 && let Err(error) = self.assess_mergeability(&moved).await
1908 {
1909 worker::console_warn!("mergeability of {}: {error}", pull.id);
1910 }
1911 Ok(Outcome::Ok(()))
1912 }
1913
Fix a hydration mismatch in output that starts with a blank line1914 /// Marks a draft ready for review, or updates the description of one
1915 /// that already is.
1916 async fn ready_pull(&self, a: PullActionArgs) -> Result<Outcome<Pull>> {
1917 let mut pull = check!(self.manageable_pull(&a.actor, &a.repo, a.number).await?);
1918 let summary = Some(a.summary.trim().to_owned()).filter(|summary| !summary.is_empty());
1919 let now = rfc3339(now_ms());
1920 self.db
1921 .prepare(
1922 "UPDATE pulls SET status = 'open', body = COALESCE(?, body), updated_at = ?
1923 WHERE id = ?",
1924 )
1925 .bind(&[
1926 optional(&summary),
1927 now.as_str().into(),
1928 pull.id.as_str().into(),
1929 ])?
1930 .run()
1931 .await?;
1932 if pull.status == PullStatus::Draft {
Workflows run when an agent's pull request is marked ready1933 // The head as it is now: the push that came just before may not
1934 // have reached `head_commit` yet, and workflows run on it.
1935 let commit = self.live_head(&pull).await?.or_else(|| pull.head_commit.clone());
Fix a hydration mismatch in output that starts with a blank line1936 self.publish(
1937 "pull.ready",
1938 &pull.repo_id,
1939 &a.actor,
Workflows run when an agent's pull request is marked ready1940 PullEvent {
1941 commit,
1942 ..Self::pull_event(&pull)
1943 },
Fix a hydration mismatch in output that starts with a blank line1944 )
1945 .await?;
1946 }
1947 if pull.status == PullStatus::Draft {
1948 self.note(
1949 &pull.repo_id,
1950 pull.number,
1951 (&a.actor.id, &a.actor.username),
1952 "marked this ready for review",
1953 )
1954 .await?;
1955 }
1956 pull.status = PullStatus::Open;
1957 pull.body = summary.or(pull.body);
1958 pull.updated_at = now;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1959 // A draft's code owners are asked once it is ready.
1960 self.refresh_code_owners(&pull).await;
1961 if let Some(fresh) = self.pull(&pull.repo_id, pull.number).await? {
1962 pull.reviewers = fresh.reviewers;
1963 pull.team_reviewers = fresh.team_reviewers;
1964 }
Fix a hydration mismatch in output that starts with a blank line1965 Ok(Outcome::Ok(pull))
1966 }
1967
1968 async fn close_pull(&self, a: PullActionArgs) -> Result<Outcome<Pull>> {
1969 let mut pull = check!(self.manageable_pull(&a.actor, &a.repo, a.number).await?);
1970 let now = rfc3339(now_ms());
1971 self.db
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1972 // What it was closed as, so that reopening brings that back.
1973 .prepare("UPDATE pulls SET status = 'closed', closed_from = ?, updated_at = ? WHERE id = ?")
1974 .bind(&[pull.status.as_str().into(), now.as_str().into(), pull.id.as_str().into()])?
Fix a hydration mismatch in output that starts with a blank line1975 .run()
1976 .await?;
1977 self.publish(
1978 "pull.closed",
1979 &pull.repo_id,
1980 &a.actor,
1981 Self::pull_event(&pull),
1982 )
1983 .await?;
1984 self.note(
1985 &pull.repo_id,
1986 pull.number,
1987 (&a.actor.id, &a.actor.username),
1988 "closed this",
1989 )
1990 .await?;
1991 // A closed pull request leaves the merge queue.
1992 if self
1993 .leave(&pull.repo_id, &pull, QueueState::Removed, Some("It was closed."))
1994 .await?
1995 {
1996 self.publish_as(
1997 "queue.changed",
1998 &pull.repo_id,
1999 None,
2000 g1t_contracts::events::QueueChanged {
2001 repo_id: pull.repo_id.clone(),
2002 },
2003 )
2004 .await?;
2005 }
2006 pull.status = PullStatus::Closed;
2007 pull.updated_at = now;
2008 Ok(Outcome::Ok(pull))
2009 }
2010
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts2011 /// Opens a closed pull request again: as the draft it was, if it was
2012 /// closed as one, else ready for review. A merged one stays merged.
2013 async fn reopen_pull(&self, a: PullActionArgs) -> Result<Outcome<Pull>> {
2014 let mut pull = check!(self.managed_pull(&a.actor, &a.repo, a.number).await?);
2015 if let Some(refusal) = reopen_refusal(pull.status) {
2016 return Ok(Outcome::fail(FailureCode::Conflict, refusal));
2017 }
2018 // The head as it is now, which workflows run on. A branch of the
2019 // repository that was deleted since leaves nothing to reopen; a
2020 // fork removed after the close is made again (repos, `pull.reopened`).
2021 let head = self.live_head(&pull).await?;
2022 if head.is_none() && pull.fork_repo_id.is_none() {
2023 return Ok(Outcome::fail(
2024 FailureCode::Conflict,
2025 format!(
2026 "The branch {} no longer exists. Push it again to reopen this pull request.",
2027 pull.branch.as_deref().unwrap_or_default()
2028 ),
2029 ));
2030 }
2031 let closed_from: Option<ClosedFrom> = self
2032 .db
2033 .prepare("SELECT closed_from FROM pulls WHERE id = ?")
2034 .bind(&[pull.id.as_str().into()])?
2035 .first(None)
2036 .await?;
2037 let status = reopened_status(closed_from.and_then(|row| row.closed_from).as_deref());
2038 let now = rfc3339(now_ms());
2039 self.db
2040 .prepare("UPDATE pulls SET status = ?, closed_from = NULL, superseded_by = NULL, updated_at = ? WHERE id = ?")
2041 .bind(&[status.as_str().into(), now.as_str().into(), pull.id.as_str().into()])?
2042 .run()
2043 .await?;
2044 self.publish(
2045 "pull.reopened",
2046 &pull.repo_id,
2047 &a.actor,
2048 PullEvent {
2049 commit: head.or_else(|| pull.head_commit.clone()),
2050 ..Self::pull_event(&pull)
2051 },
2052 )
2053 .await?;
2054 self.note(
2055 &pull.repo_id,
2056 pull.number,
2057 (&a.actor.id, &a.actor.username),
2058 "reopened this",
2059 )
2060 .await?;
2061 pull.status = status;
2062 pull.superseded_by = None;
2063 pull.updated_at = now;
2064 // Whether it still merges cleanly, now that it is open again.
2065 if let Err(error) = self.assess_mergeability(&pull).await {
2066 worker::console_warn!("mergeability of {}: {error}", pull.id);
2067 }
2068 Ok(Outcome::Ok(pull))
2069 }
2070
2071 /// Turns a pull request that is ready for review back into a draft: it
2072 /// cannot be merged until it is marked ready again, and it leaves the
2073 /// merge queue and any merge that was waiting for it to catch up.
2074 async fn convert_pull_to_draft(&self, a: PullActionArgs) -> Result<Outcome<Pull>> {
2075 let mut pull = check!(self.managed_pull(&a.actor, &a.repo, a.number).await?);
2076 if let Some(refusal) = draft_refusal(pull.status) {
2077 return Ok(Outcome::fail(FailureCode::Conflict, refusal));
2078 }
2079 let now = rfc3339(now_ms());
2080 self.db
2081 .prepare(
2082 "UPDATE pulls SET status = 'draft', land_requested = NULL, land_requested_at = NULL, updated_at = ?
2083 WHERE id = ?",
2084 )
2085 .bind(&[now.as_str().into(), pull.id.as_str().into()])?
2086 .run()
2087 .await?;
2088 self.publish(
2089 "pull.converted_to_draft",
2090 &pull.repo_id,
2091 &a.actor,
2092 Self::pull_event(&pull),
2093 )
2094 .await?;
2095 self.note(
2096 &pull.repo_id,
2097 pull.number,
2098 (&a.actor.id, &a.actor.username),
2099 "marked this as a draft",
2100 )
2101 .await?;
2102 if self
2103 .leave(&pull.repo_id, &pull, QueueState::Removed, Some("It was marked as a draft."))
2104 .await?
2105 {
2106 self.publish_as(
2107 "queue.changed",
2108 &pull.repo_id,
2109 None,
2110 g1t_contracts::events::QueueChanged {
2111 repo_id: pull.repo_id.clone(),
2112 },
2113 )
2114 .await?;
2115 }
2116 pull.status = PullStatus::Draft;
2117 pull.updated_at = now;
2118 Ok(Outcome::Ok(pull))
2119 }
2120
Fix a hydration mismatch in output that starts with a blank line2121 /// Lands the pull request on the repository's default branch. Unless
2122 /// told to keep it open, that resolves the issue it was for: the issue
2123 /// closes naming this pull request, and the others still in progress
2124 /// for it close as superseded.
2125 async fn merge_pull(&self, a: PullActionArgs) -> Result<Outcome<Pull>> {
2126 let viewer = Some(a.actor.clone());
2127 let (repo, pull) = check!(self.pull_at(&a.repo, a.number, &viewer).await?);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2128 check!(writable(&repo));
Fix a hydration mismatch in output that starts with a blank line2129 match pull.status {
2130 PullStatus::Open => {}
2131 PullStatus::Draft => {
2132 return Ok(Outcome::fail(
2133 FailureCode::Conflict,
2134 "This pull request is still a draft. Mark it ready for review first.",
2135 ));
2136 }
2137 status => {
2138 return Ok(Outcome::fail(
2139 FailureCode::Conflict,
2140 format!("This pull request is already {}.", status.as_str()),
2141 ));
2142 }
2143 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2144 let base = pull.base_branch(&repo.default_branch).to_owned();
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge2145 // The rules of the branch it merges into, as they stack: the one
2146 // gate for the merge button, the API, MCP, auto-merge and the queue,
2147 // for a person's pull request and an agent's alike. A bypass counts
2148 // when the merger asks for it, or for g1t when a ruleset lists it.
2149 let gate = self.merge_gate(&repo, &pull, Some(&a.actor), a.ignore_checks, true).await?;
2150 let bypassable = gate.bypassable();
2151 let gate = if a.bypass_rules || a.actor.is_system() { gate } else { gate.without_bypass() };
2152 let settings = rulesets::overlay(self.settings(&repo.id).await?, &gate.requirements, base == repo.default_branch);
2153 if pull.check_status == Some(CheckStatus::Failed) && !(a.ignore_checks && settings.allow_ignoring_checks) {
2154 return Ok(Outcome::fail(
2155 FailureCode::Conflict,
2156 "It failed in the merge queue; push a fix to try again.",
2157 ));
2158 }
2159 if let Some(refusal) = gate.refusal() {
2160 if access::can(Some(&a.actor), &repo, Capability::Merge) {
2161 self.record_merge_evaluations(&repo, &pull, &gate).await;
Fix a hydration mismatch in output that starts with a blank line2162 }
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge2163 let offer = if bypassable && !a.bypass_rules {
2164 " You may bypass these rules: merge again and ask to bypass them (bypass_rules)."
2165 } else {
2166 ""
2167 };
2168 return Ok(Outcome::fail(FailureCode::Conflict, format!("{refusal}{offer}")));
Fix a hydration mismatch in output that starts with a blank line2169 }
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2170 // Known ahead of time to conflict: neither a merge nor the queue
2171 // would get through, so say what has to be resolved now.
2172 if let Some(files) = self.conflicting_files(&pull).await? {
2173 let named = if files.is_empty() {
2174 String::new()
2175 } else {
2176 format!(" in {}", files.join(", "))
2177 };
2178 return Ok(Outcome::fail(
2179 FailureCode::Conflict,
2180 format!(
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2181 "This branch has conflicts with {base}{named} that must be resolved first. Have g1t resolve them, or merge {base} into it, fix them and push."
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2182 ),
2183 ));
2184 }
Fix a hydration mismatch in output that starts with a blank line2185
2186 // A repository that merges through a queue: it joins the queue, and
2187 // lands once its state together with everything ahead has passed.
2188 if settings.merge_queue {
2189 if !a.actor.verified {
2190 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
2191 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2192 check!(allowed(Some(&a.actor), &repo, Capability::Merge));
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge2193 self.record_merge_evaluations(&repo, &pull, &gate).await;
Fix a hydration mismatch in output that starts with a blank line2194 return self.enqueue(&repo, &pull, &a.actor, a.keep_issue_open).await;
2195 }
2196
2197 // The default branch has moved under it. Unless the repository
2198 // insists on that being dealt with first, bring it up to date and
2199 // land it when that is done.
2200 if self.is_behind(&repo.id, &pull).await? {
2201 if settings.require_up_to_date {
2202 return Ok(Outcome::fail(
2203 FailureCode::Conflict,
2204 format!(
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2205 "{base} has moved since this pull request was made, and this repository requires pull requests to be up to date before they merge. Catch up with {base} first."
Fix a hydration mismatch in output that starts with a blank line2206 ),
2207 ));
2208 }
2209 if !a.actor.verified {
2210 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
2211 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2212 check!(allowed(Some(&a.actor), &repo, Capability::Merge));
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge2213 self.record_merge_evaluations(&repo, &pull, &gate).await;
Fix a hydration mismatch in output that starts with a blank line2214 self.request_landing(&pull, &a.actor, a.keep_issue_open)
2215 .await?;
2216 return Ok(Outcome::Ok(pull));
2217 }
2218
2219 // Whether the actor may write to the repository is decided by repos.
2220 let landed: Outcome<Landed> = g1t_kit::call(
2221 &self.repos,
2222 "land",
2223 &LandArgs {
2224 // A pull request from a branch lands from the repository itself.
2225 source_id: pull.fork_repo_id.clone().unwrap_or_else(|| repo.id.clone()),
2226 branch: pull.branch.clone(),
2227 actor: a.actor.clone(),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2228 target_branch: Some(base.clone()),
Fix a hydration mismatch in output that starts with a blank line2229 },
2230 )
2231 .await?;
2232 let landed = check!(landed);
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge2233 self.record_merge_evaluations(&repo, &pull, &gate).await;
Fix a hydration mismatch in output that starts with a blank line2234 Ok(Outcome::Ok(
2235 self.record_merge(&repo, pull, &a.actor, a.keep_issue_open, landed)
2236 .await?,
2237 ))
2238 }
2239
2240 /// Records a pull request as merged once the default branch holds it:
2241 /// closes its issue, supersedes the others for it, and says so.
2242 pub(crate) async fn record_merge(
2243 &self,
2244 repo: &Repo,
2245 mut pull: Pull,
2246 actor: &User,
2247 keep_issue_open: bool,
2248 landed: Landed,
2249 ) -> Result<Pull> {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2250 // Only a merge into the default branch resolves the issue: into
2251 // another branch, the work has not landed yet.
2252 let keep_issue_open = keep_issue_open || !pull.targets_default(&repo.default_branch);
Fix a hydration mismatch in output that starts with a blank line2253 let issue = match pull.issue {
2254 Some(number) if !keep_issue_open => self
2255 .issue(&repo.id, number)
2256 .await?
2257 .filter(|issue| issue.state == State::Open),
2258 _ => None,
2259 };
2260 let now = rfc3339(now_ms());
2261 let mut statements = vec![
2262 self.db
2263 .prepare(
2264 "UPDATE pulls
2265 SET status = 'merged', head_commit = ?, merge_base = ?, merged_by = ?,
2266 merged_at = ?, updated_at = ?
2267 WHERE id = ?",
2268 )
2269 .bind(&[
2270 landed.commit.as_str().into(),
2271 optional(&landed.previous),
2272 actor.username.as_str().into(),
2273 now.as_str().into(),
2274 now.as_str().into(),
2275 pull.id.as_str().into(),
2276 ])?,
2277 ];
2278 if let Some(issue) = &issue {
2279 statements.push(
2280 self.db
2281 .prepare(
2282 "UPDATE issues
2283 SET state = 'closed', reason = 'completed', resolved_by = ?,
2284 closed_at = ?, updated_at = ?
2285 WHERE id = ?",
2286 )
2287 .bind(&[
2288 pull.number.into(),
2289 now.as_str().into(),
2290 now.as_str().into(),
2291 issue.id.as_str().into(),
2292 ])?,
2293 );
2294 statements.push(
2295 self.db
2296 .prepare(
2297 "UPDATE pulls SET status = 'closed', superseded_by = ?, updated_at = ?
2298 WHERE issue_id = ? AND id != ? AND status IN ('draft', 'open')",
2299 )
2300 .bind(&[
2301 pull.number.into(),
2302 now.as_str().into(),
2303 issue.id.as_str().into(),
2304 pull.id.as_str().into(),
2305 ])?,
2306 );
2307 }
2308 self.db.batch(statements).await?;
2309
2310 self.publish(
2311 "pull.merged",
2312 &repo.id,
2313 actor,
2314 PullEvent {
2315 commit: Some(landed.commit.clone()),
2316 ..Self::pull_event(&pull)
2317 },
2318 )
2319 .await?;
2320 if let Some(issue) = &issue {
2321 self.publish(
2322 "issue.closed",
2323 &repo.id,
2324 actor,
2325 IssueEvent {
2326 reason: Some(IssueReason::Completed.as_str()),
2327 resolved_by: Some(pull.number),
2328 ..Self::issue_event(issue)
2329 },
2330 )
2331 .await?;
2332 }
2333
2334 let who = (actor.id.as_str(), actor.username.as_str());
2335 self.note(&repo.id, pull.number, who, "merged this").await?;
2336 if let Some(issue) = &issue {
2337 self.note(
2338 &repo.id,
2339 issue.number,
2340 who,
2341 &format!("closed this by merging #{}", pull.number),
2342 )
2343 .await?;
2344 }
2345 pull.status = PullStatus::Merged;
2346 pull.head_commit = Some(landed.commit.clone());
2347 pull.merge_base = landed.previous;
2348 pull.merged_by = Some(actor.username.clone());
2349 pull.merged_at = Some(now.clone());
2350 pull.updated_at = now;
2351 Ok(pull)
2352 }
2353
2354 async fn list_active_pulls(&self, a: ViewerArgs) -> Result<Vec<ActivePull>> {
2355 let Some(viewer) = a.viewer else {
2356 return Ok(Vec::new());
2357 };
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights2358 // The pull requests and their issues, in one round trip: their own,
2359 // and those g1t made for them (Pull::owner).
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2360 let author = [JsValue::from(viewer.id.as_str())];
Fix a hydration mismatch in output that starts with a blank line2361 let found = self
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2362 .timing
2363 .db(
2364 2,
2365 self.db.batch(vec![
2366 self.db
2367 .prepare(format!(
2368 "SELECT {PULL_COLUMNS} FROM pulls
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights2369 WHERE COALESCE(requested_by_id, author_id) = ?1 AND status IN ('draft', 'open')
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2370 ORDER BY updated_at DESC LIMIT 50"
2371 ))
2372 .bind(&author)?,
2373 self.db
2374 .prepare(format!(
2375 "SELECT {ISSUE_COLUMNS} FROM issues WHERE issues.id IN (
2376 SELECT issue_id FROM pulls
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights2377 WHERE COALESCE(requested_by_id, author_id) = ?1 AND status IN ('draft', 'open') AND issue_id IS NOT NULL
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2378 ORDER BY updated_at DESC LIMIT 50)"
2379 ))
2380 .bind(&author)?,
2381 ]),
2382 )
Fix a hydration mismatch in output that starts with a blank line2383 .await?;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2384 let (Some(found), Some(issues)) = (found.first(), found.get(1)) else {
2385 return Ok(Vec::new());
2386 };
Fix a hydration mismatch in output that starts with a blank line2387 let snapshots = found.results::<Snapshot>()?;
2388 let pulls: Vec<Pull> = found.results::<PullRow>()?.into_iter().map(Pull::from).collect();
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2389 let issues: Vec<Issue> = issues.results::<IssueRow>()?.into_iter().map(Issue::from).collect();
2390 let issues = &issues;
2391 // Where each stands: the remembered assessment when there is one,
2392 // and worked out otherwise.
Fix a hydration mismatch in output that starts with a blank line2393 try_join_all(pulls.into_iter().zip(snapshots).map(|(pull, snapshot)| async move {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2394 let issue = pull.issue.and_then(|number| {
2395 issues
2396 .iter()
2397 .find(|issue| issue.repo_id == pull.repo_id && issue.number == number)
2398 .cloned()
2399 });
Fix a hydration mismatch in output that starts with a blank line2400 // Only a pull request g1t is seeing through has a lifecycle.
2401 let lifecycle = if !lifecycle::made_by_g1t(&pull) || snapshot.managed == 0 {
2402 None
2403 } else if let (Some(stage), Some(detail)) = (snapshot.stage, snapshot.stage_detail) {
2404 Some(Lifecycle {
2405 stage,
2406 detail,
2407 revisions: snapshot.revisions,
2408 })
2409 } else {
2410 let behind = self.is_behind(&pull.repo_id, &pull).await?;
2411 self.assess(&pull, &issue, behind)
2412 .await?
2413 .map(|(lifecycle, _)| lifecycle)
2414 };
2415 Ok::<_, worker::Error>(ActivePull {
2416 pull,
2417 issue,
2418 lifecycle,
2419 })
2420 }))
2421 .await
2422 }
2423
2424 // --- Sessions ----------------------------------------------------------
2425
2426 async fn append_session(&self, a: AppendSessionArgs) -> Result<Outcome<Appended>> {
2427 if a.entries.is_empty() {
2428 return Ok(Outcome::Ok(Appended { count: 0 }));
2429 }
2430 if a.entries.len() > MAX_ENTRY_BATCH {
2431 return Ok(Outcome::fail(
2432 FailureCode::Invalid,
2433 format!("Send at most {MAX_ENTRY_BATCH} entries at a time."),
2434 ));
2435 }
2436 let viewer = Some(a.actor.clone());
2437 let (_, pull) = check!(self.pull_at(&a.repo, a.number, &viewer).await?);
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights2438 if !pull.is_owned_by(&a.actor.id) {
Fix a hydration mismatch in output that starts with a blank line2439 return Ok(Outcome::fail(
2440 FailureCode::Forbidden,
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights2441 "Only whoever opened a pull request, or asked g1t for it, can record its session.",
Fix a hydration mismatch in output that starts with a blank line2442 ));
2443 }
2444
2445 let now = rfc3339(now_ms());
2446 let count = a.entries.len() as u32;
2447 let mut statements = Vec::with_capacity(a.entries.len() + 1);
2448 for entry in a.entries {
2449 let kind = serde_json::to_value(entry.kind)?;
2450 let text: String = entry.text.chars().take(MAX_ENTRY_CHARS).collect();
2451 // Each insert takes the next sequence number itself, so two
2452 // writers appending at once cannot collide.
2453 statements.push(
2454 self.db
2455 .prepare(
2456 "INSERT INTO session_entries (pull_id, seq, kind, text, tool, \"commit\", at)
2457 SELECT ?, COALESCE(MAX(seq), 0) + 1, ?, ?, ?, ?, ?
2458 FROM session_entries WHERE pull_id = ?",
2459 )
2460 .bind(&[
2461 pull.id.as_str().into(),
2462 kind.as_str().unwrap_or("note").into(),
2463 text.into(),
2464 optional(&entry.tool),
2465 optional(&entry.commit.or_else(|| pull.head_commit.clone())),
2466 now.as_str().into(),
2467 pull.id.as_str().into(),
2468 ])?,
2469 );
2470 }
2471 statements.push(
2472 self.db
2473 .prepare("UPDATE pulls SET updated_at = ? WHERE id = ?")
2474 .bind(&[now.as_str().into(), pull.id.as_str().into()])?,
2475 );
2476 self.db.batch(statements).await?;
2477 self.publish(
2478 "session.appended",
2479 &pull.repo_id,
2480 &a.actor,
2481 SessionAppended {
2482 pull_id: pull.id.clone(),
2483 repo_id: pull.repo_id.clone(),
2484 number: pull.number,
2485 count,
2486 },
2487 )
2488 .await?;
2489 Ok(Outcome::Ok(Appended { count }))
2490 }
2491
2492 /// Adds entries to a pull request's session, each taking the next
2493 /// sequence number, without announcing it.
2494 pub(crate) async fn append_entries(&self, pull: &Pull, entries: &[NewSessionEntry]) -> Result<()> {
2495 let now = rfc3339(now_ms());
2496 let mut statements = Vec::with_capacity(entries.len());
2497 for entry in entries {
2498 let kind = serde_json::to_value(entry.kind)?;
2499 let text: String = entry.text.chars().take(MAX_ENTRY_CHARS).collect();
2500 statements.push(
2501 self.db
2502 .prepare(
2503 "INSERT INTO session_entries (pull_id, seq, kind, text, tool, \"commit\", at)
2504 SELECT ?, COALESCE(MAX(seq), 0) + 1, ?, ?, ?, ?, ?
2505 FROM session_entries WHERE pull_id = ?",
2506 )
2507 .bind(&[
2508 pull.id.as_str().into(),
2509 kind.as_str().unwrap_or("note").into(),
2510 text.into(),
2511 optional(&entry.tool),
2512 optional(&entry.commit.clone().or_else(|| pull.head_commit.clone())),
2513 now.as_str().into(),
2514 pull.id.as_str().into(),
2515 ])?,
2516 );
2517 }
2518 self.db.batch(statements).await?;
2519 Ok(())
2520 }
2521
2522 async fn read_session(&self, a: ViewArgs) -> Result<Outcome<Vec<SessionEntry>>> {
2523 let (_, pull) = check!(self.pull_at(&a.repo, a.number, &a.viewer).await?);
2524 let rows = self
2525 .db
2526 .prepare(
2527 "SELECT seq, kind, text, tool, \"commit\", at FROM session_entries
2528 WHERE pull_id = ? AND seq > ? ORDER BY seq LIMIT ?",
2529 )
2530 .bind(&[pull.id.into(), a.after_seq.into(), SESSION_PAGE.into()])?
2531 .all()
2532 .await?
2533 .results::<SessionRow>()?;
2534 Ok(Outcome::Ok(
2535 rows.into_iter().map(SessionEntry::from).collect(),
2536 ))
2537 }
2538
2539 /// A push moves the head of the pull request it concerns: the one whose
2540 /// fork was pushed to, or the one opened from the branch that moved.
2541 async fn on_event(&self, event: &Event) -> Result<()> {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2542 // A new repository starts with the default labels.
2543 if event.kind == "repo.created"
2544 && let Some(repo_id) = event.repo_id.as_deref()
2545 {
2546 return self.seed_labels(repo_id).await;
2547 }
2548 // Pull requests into the branch that became the default merge into
2549 // the default branch, which is stored as none.
2550 if event.kind == "repo.default_branch_changed"
2551 && let (Some(repo_id), Some(to)) = (event.repo_id.as_deref(), event.data["to"].as_str())
2552 {
2553 self.db
2554 .prepare(
2555 "UPDATE pulls SET base_branch = NULL
2556 WHERE repo_id = ? AND base_branch = ? AND status IN ('draft', 'open')",
2557 )
2558 .bind(&[repo_id.into(), to.into()])?
2559 .run()
2560 .await?;
2561 return Ok(());
2562 }
Fix a hydration mismatch in output that starts with a blank line2563 if event.kind != "git.push" {
2564 return Ok(());
2565 }
2566 let (Some(repo_id), Some(after), Some(git_ref)) = (
2567 event.repo_id.as_deref(),
2568 event.data["after"].as_str(),
2569 event.data["ref"].as_str(),
2570 ) else {
2571 return Ok(());
2572 };
2573 let now = rfc3339(now_ms());
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge2574 // Who moved it, for rules about the most recent push.
2575 let pusher: JsValue = event.actor.as_deref().map_or(JsValue::NULL, Into::into);
Fix a hydration mismatch in output that starts with a blank line2576 // The head moved, so whatever the checks said no longer applies, and
2577 // whatever step g1t was waiting on has been taken.
2578 let moved = "UPDATE pulls
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge2579 SET head_commit = ?, updated_at = ?, head_pushed_by = ?, head_pushed_at = ?, check_status = NULL, check_run_id = NULL,
Fix a hydration mismatch in output that starts with a blank line2580 working_on = NULL, working_until = NULL, stalled = NULL";
2581 let active = "status IN ('draft', 'open') AND head_commit IS NOT ?";
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights2582 let returning =
2583 "RETURNING id, repo_id, number, issue_number, status, author_id, author_name, requested_by_id, requested_by_name";
Fix a hydration mismatch in output that starts with a blank line2584 let mut pulls: Vec<MovedRow> = Vec::new();
2585 // A fork carries its pull request on its default branch.
2586 if event.data["defaultBranch"].as_bool() == Some(true) {
2587 pulls.extend(
2588 self.db
2589 .prepare(format!(
2590 "{moved} WHERE fork_repo_id = ? AND {active} {returning}"
2591 ))
2592 .bind(&[
2593 after.into(),
2594 now.as_str().into(),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge2595 pusher.clone(),
2596 now.as_str().into(),
Fix a hydration mismatch in output that starts with a blank line2597 repo_id.into(),
2598 after.into(),
2599 ])?
2600 .all()
2601 .await?
2602 .results::<MovedRow>()?,
2603 );
2604 }
2605 if let Some(branch) = git_ref.strip_prefix("refs/heads/") {
2606 pulls.extend(
2607 self.db
2608 .prepare(format!(
2609 "{moved} WHERE repo_id = ? AND source_branch = ? AND {active} {returning}"
2610 ))
2611 .bind(&[
2612 after.into(),
2613 now.as_str().into(),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge2614 pusher.clone(),
2615 now.as_str().into(),
Fix a hydration mismatch in output that starts with a blank line2616 repo_id.into(),
2617 branch.into(),
2618 after.into(),
2619 ])?
2620 .all()
2621 .await?
2622 .results::<MovedRow>()?,
2623 );
2624 }
2625 // What each now changes, so overlaps show while the work is under way.
2626 for moved in &pulls {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2627 if let Some(mut pull) = self.pull_by_id(&moved.id).await? {
2628 pull.files = self.refresh_files(&pull).await?;
2629 // Owners of files it now changes are asked too.
2630 self.refresh_code_owners(&pull).await;
Fix a hydration mismatch in output that starts with a blank line2631 }
2632 }
2633 // A merge that was waiting for this push to bring it up to date.
2634 for moved in &pulls {
2635 self.land_if_requested(&moved.id).await?;
2636 }
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2637 // Whether each still merges cleanly, and, when a default branch
2638 // moved, every open pull request into it.
2639 let moved_ids: Vec<String> = pulls.iter().map(|pull| pull.id.clone()).collect();
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2640 let into = match event.data["defaultBranch"].as_bool() {
2641 Some(true) => mergeability::Moved::DefaultBranch,
2642 _ => match git_ref.strip_prefix("refs/heads/") {
2643 Some(branch) => mergeability::Moved::Branch(branch),
2644 None => mergeability::Moved::Nothing,
2645 },
2646 };
2647 self.after_push(repo_id, into, &moved_ids).await;
Fix a hydration mismatch in output that starts with a blank line2648 // A draft is announced when it is marked ready instead.
2649 for pull in pulls
2650 .into_iter()
2651 .filter(|pull| pull.status == PullStatus::Open)
2652 {
2653 self.publish_as(
2654 "pull.updated",
2655 &pull.repo_id,
2656 event.actor.clone(),
2657 PullEvent {
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights2658 author: Some(g1t_contracts::credentials::Principal { id: pull.author_id, username: pull.author_name }),
2659 requested_by: pull
2660 .requested_by_id
2661 .zip(pull.requested_by_name)
2662 .map(|(id, username)| g1t_contracts::credentials::Principal { id, username }),
Fix a hydration mismatch in output that starts with a blank line2663 pull_id: pull.id,
2664 repo_id: pull.repo_id.clone(),
2665 number: pull.number,
2666 issue: pull.issue_number,
2667 commit: Some(after.to_owned()),
2668 ..PullEvent::default()
Merge branch 'worktree-agent-a3abfcce648e87dca'2669 }
2670 .carrying(&event.data),
Fix a hydration mismatch in output that starts with a blank line2671 )
2672 .await?;
2673 }
2674 Ok(())
2675 }
2676}
2677
Merge branch 'worktree-agent-a3abfcce648e87dca'2678/// An event's data that carries on what caused the event it follows from.
2679trait Carrying: serde::Serialize + Sized {
2680 /// As JSON, marked as a workflow job's doing when `cause` was.
2681 fn carrying(self, cause: &serde_json::Value) -> serde_json::Value {
2682 g1t_contracts::events::carried(self, cause)
2683 }
2684}
2685
2686impl Carrying for PullEvent {}
2687
Fix a hydration mismatch in output that starts with a blank line2688fn service(env: &Env) -> Result<Work> {
2689 Ok(Work {
2690 db: env.d1("DB")?,
2691 identity: env.service("IDENTITY")?,
2692 repos: env.service("REPOS")?,
2693 events: env.service("EVENTS")?,
Sidebar: the panels really slide2694 actions: env.service("ACTIONS")?,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2695 timing: g1t_kit::d1::Timing::default(),
2696 prefetched: std::cell::RefCell::new(None),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge2697 known_repos: std::cell::RefCell::new(std::collections::HashMap::new()),
Fix a hydration mismatch in output that starts with a blank line2698 })
2699}
2700
2701#[event(fetch)]
2702async fn fetch(mut request: Request, env: Env, _ctx: Context) -> Result<Response> {
2703 let Some(method) = rpc_method(&request) else {
2704 return Response::error("Not found", 404);
2705 };
Fast pages, required checks on the branch, self-hosted runners, honest incidents2706 // A replica near the caller when it asks for one (crates/kit/src/d1.rs).
2707 let (db, served) = g1t_kit::d1::open(&env, "DB", &request)?;
Fix a hydration mismatch in output that starts with a blank line2708 let body: serde_json::Value = request.json().await?;
Fast pages, required checks on the branch, self-hosted runners, honest incidents2709 let mut work = service(&env)?;
2710 work.db = db;
Fix a hydration mismatch in output that starts with a blank line2711
Fast pages, required checks on the branch, self-hosted runners, honest incidents2712 let answered = match method.as_str() {
Fix a hydration mismatch in output that starts with a blank line2713 "open_issue" => reply(&work.open_issue(args(body)?).await?),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step2714 "delegate_issue" => reply(&work.delegate_issue(args(body)?).await?),
2715 "report_confidence" => reply(&work.report_confidence(args(body)?).await?),
Fix a hydration mismatch in output that starts with a blank line2716 "list_issues" => reply(&work.list_issues(args(body)?).await?),
2717 "get_issue" => reply(&work.get_issue(args(body)?).await?),
2718 "update_issue" => reply(&work.update_issue(args(body)?).await?),
2719 "close_issue" => reply(&work.close_issue(args(body)?).await?),
2720 "reopen_issue" => reply(&work.reopen_issue(args(body)?).await?),
2721 "list_labels" => reply(&work.list_labels(args(body)?).await?),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2722 "save_label" => reply(&work.save_label(args(body)?).await?),
2723 "delete_label" => reply(&work.delete_label(args(body)?).await?),
2724 "add_default_labels" => reply(&work.add_default_labels(args(body)?).await?),
2725 "set_labels" => reply(&work.set_labels(args(body)?).await?),
2726 "list_milestones" => reply(&work.list_milestones(args(body)?).await?),
2727 "get_milestone" => reply(&work.get_milestone(args(body)?).await?),
2728 "save_milestone" => reply(&work.save_milestone(args(body)?).await?),
2729 "delete_milestone" => reply(&work.delete_milestone(args(body)?).await?),
Fix a hydration mismatch in output that starts with a blank line2730 "counts" => reply(&work.counts(args(body)?).await?),
2731 "add_comment" => reply(&work.add_comment(args(body)?).await?),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts2732 "edit_comment" => reply(&work.edit_comment(args(body)?).await?),
2733 "delete_comment" => reply(&work.delete_comment(args(body)?).await?),
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar2734 // A workspace's own agents, as themselves (agent_comments.rs). For
2735 // the agents service; never reachable with a person's token.
2736 "workspace_agent_comment" => reply(&work.workspace_agent_comment(args(body)?).await?),
2737 "workspace_agent_review" => reply(&work.workspace_agent_review(args(body)?).await?),
Fix a hydration mismatch in output that starts with a blank line2738 "start_checks" => reply(&work.start_checks(args(body)?).await?),
Fast pages, required checks on the branch, self-hosted runners, honest incidents2739 "seen_checks" => reply(&work.seen_checks(args(body)?).await?),
Fix a hydration mismatch in output that starts with a blank line2740 "report_checks" => reply(&work.report_checks(args(body)?).await?),
GitHub Actions on g1t, part two: running workflows2741 "set_commit_status" => reply(&work.set_commit_status(args(body)?).await?),
Merge checks: statuses and check runs on every commit2742 "create_commit_status" => reply(&work.create_commit_status(args(body)?).await?),
2743 "commit_statuses" => reply(&work.commit_statuses(args(body)?).await?),
2744 "combined_status" => reply(&work.combined_status(args(body)?).await?),
2745 "create_check_run" => reply(&work.create_check_run(args(body)?).await?),
2746 "update_check_run" => reply(&work.update_check_run(args(body)?).await?),
2747 "get_check_run" => reply(&work.get_check_run(args(body)?).await?),
2748 "check_run_annotations" => reply(&work.check_run_annotations(args(body)?).await?),
2749 "ref_check_runs" => reply(&work.ref_check_runs(args(body)?).await?),
2750 "ref_check_suites" => reply(&work.ref_check_suites(args(body)?).await?),
2751 "get_check_suite" => reply(&work.get_check_suite(args(body)?).await?),
2752 "rerequest_check_run" => reply(&work.rerequest_check_run(args(body)?).await?),
2753 "rerequest_check_suite" => reply(&work.rerequest_check_suite(args(body)?).await?),
2754 "request_check_action" => reply(&work.request_check_action(args(body)?).await?),
2755 "commit_checks" => reply(&work.commit_checks(args(body)?).await?),
Fix a hydration mismatch in output that starts with a blank line2756 "start_review" => reply(&work.start_review(args(body)?).await?),
2757 "advance" => reply(&work.advance(args(body)?).await?),
2758 "stall" => reply(&work.stall(args(body)?).await?),
2759 "managed_pulls" => reply(&work.managed_pulls(args(body)?).await?),
2760 "queue" => reply(&work.queue(args(body)?).await?),
2761 "queue_build" => reply(&work.queue_build(args(body)?).await?),
2762 "report_queue" => reply(&work.report_queue(args(body)?).await?),
2763 "remove_from_queue" => reply(&work.remove_from_queue(args(body)?).await?),
2764 "message_agent" => reply(&work.message_agent(args(body)?).await?),
Record your own agent's sessions automatically2765 "locate_pull" => reply(&work.locate_pull(args(body)?).await?),
Inbox: the events service tells people what needs them as events arrive2766 "inbox_subject" => reply(&work.inbox_subject(args(body)?).await?),
Agents ask each other, hand each other work, and answer2767 "answer_message" => reply(&work.answer_message(args(body)?).await?),
Fix a hydration mismatch in output that starts with a blank line2768 "take_messages" => reply(&work.take_messages(args(body)?).await?),
Agents asked while not at work are woken to answer2769 "wake_for_messages" => reply(&work.wake_for_messages(args(body)?).await?),
Fix a hydration mismatch in output that starts with a blank line2770 "catch_up_job" => reply(&work.catch_up_job(args(body)?).await?),
2771 "get_settings" => reply(&work.get_settings(args(body)?).await?),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2772 "codeowners_errors" => reply(&work.codeowners_errors(args(body)?).await?),
Fix a hydration mismatch in output that starts with a blank line2773 "update_settings" => reply(&work.update_settings(args(body)?).await?),
2774 "report_review" => reply(&work.report_review(args(body)?).await?),
2775 "open_pull" => reply(&work.open_pull(args(body)?).await?),
2776 "list_pulls" => reply(&work.list_pulls(args(body)?).await?),
Fast pages, required checks on the branch, self-hosted runners, honest incidents2777 "pulls_for_repos" => reply(&work.pulls_for_repos(args(body)?).await?),
Fix a hydration mismatch in output that starts with a blank line2778 "get_pull" => reply(&work.get_pull(args(body)?).await?),
2779 "update_pull" => reply(&work.update_pull(args(body)?).await?),
Catching up with main takes seconds when the two sides touched different files2780 "catch_up_pull" => reply(&work.catch_up_pull(args(body)?).await?),
Fix a hydration mismatch in output that starts with a blank line2781 "ready_pull" => reply(&work.ready_pull(args(body)?).await?),
2782 "close_pull" => reply(&work.close_pull(args(body)?).await?),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts2783 "reopen_pull" => reply(&work.reopen_pull(args(body)?).await?),
2784 "convert_pull_to_draft" => reply(&work.convert_pull_to_draft(args(body)?).await?),
Fix a hydration mismatch in output that starts with a blank line2785 "merge_pull" => reply(&work.merge_pull(args(body)?).await?),
2786 "list_active_pulls" => reply(&work.list_active_pulls(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2787 "by_author" => reply(&work.by_author(args(body)?).await?),
Chat controls, public profiles, shadcn selects, and no Docs tab in a project2788 "contributions" => reply(&work.contributions(args(body)?).await?),
Fix a hydration mismatch in output that starts with a blank line2789 "start_plan" => reply(&work.start_plan(args(body)?).await?),
2790 "report_plan" => reply(&work.report_plan(args(body)?).await?),
2791 "get_plan" => reply(&work.get_plan(args(body)?).await?),
2792 "list_plans" => reply(&work.list_plans(args(body)?).await?),
2793 "apply_plan" => reply(&work.apply_plan(args(body)?).await?),
2794 "queue_issue" => reply(&work.queue_issue(args(body)?).await?),
2795 "ready_issues" => reply(&work.ready_issues(args(body)?).await?),
2796 "list_assigned_issues" => reply(&work.list_assigned_issues(args(body)?).await?),
2797 "append_session" => reply(&work.append_session(args(body)?).await?),
2798 "read_session" => reply(&work.read_session(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2799 // Agents at work, their sessions, and memory (runs.rs, memory.rs).
2800 "open_run" => reply(&work.open_run(args(body)?).await?),
2801 "report_run" => reply(&work.report_run(args(body)?).await?),
2802 "stop_run" => reply(&work.stop_run(args(body)?).await?),
2803 "list_runs" => reply(&work.list_runs(args(body)?).await?),
2804 "get_run" => reply(&work.get_run(args(body)?).await?),
2805 "list_sessions" => reply(&work.list_sessions(args(body)?).await?),
2806 "get_session" => reply(&work.get_session(args(body)?).await?),
2807 "list_memories" => reply(&work.list_memories(args(body)?).await?),
2808 "add_memory" => reply(&work.add_memory(args(body)?).await?),
2809 "update_memory" => reply(&work.update_memory(args(body)?).await?),
2810 "delete_memory" => reply(&work.delete_memory(args(body)?).await?),
2811 "recall" => reply(&work.recall(args(body)?).await?),
2812 "memory_context" => reply(&work.memory_context(args(body)?).await?),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API2813 // What agents may do in a sandbox (guardrails.rs).
2814 "get_guardrails" => reply(&work.get_guardrails(args(body)?).await?),
2815 "update_guardrails" => reply(&work.update_guardrails(args(body)?).await?),
2816 "run_guardrails" => reply(&work.run_guardrails(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2817 // Plan caps the runner applies (compute.rs).
2818 "active_agents" => reply(&work.active_agents(args(body)?).await?),
2819 "issue_spend" => reply(&work.issue_spend(args(body)?).await?),
2820 "wait_for_slot" => reply(&work.wait_for_slot(args(body)?).await?),
2821 "agent_comment" => reply(&work.agent_comment(args(body)?).await?),
2822 "add_wait" => reply(&work.add_wait(args(body)?).await?),
2823 "waiting_workspaces" => reply(&work.waiting_workspaces(args(body)?).await?),
2824 "take_wait" => reply(&work.take_wait(args(body)?).await?),
2825 // The runs whose sandboxes stop with their repository (retired.rs).
2826 "runs_in_repo" => reply(&work.runs_in_repo(args(body)?).await?),
2827 "run_cost" => reply(&work.run_cost(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2828 "start_mergecheck" => reply(&work.start_mergecheck(args(body)?).await?),
2829 "report_mergecheck" => reply(&work.report_mergecheck(args(body)?).await?),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API2830 // Memory that fills itself, and its review queue (capture.rs).
2831 method if capture::METHODS.contains(&method) => capture::dispatch(&work, method, body).await,
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent2832 // @g1t in comments, and the label rule (mentions.rs).
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API2833 "take_mention" => reply(&work.take_mention(args(body)?).await?),
2834 "mention_revision" => reply(&work.mention_revision(args(body)?).await?),
2835 "reply_mention" => reply(&work.reply_mention(args(body)?).await?),
2836 "get_agent_rules" => reply(&work.get_agent_rules(args(body)?).await?),
2837 "set_agent_rules" => reply(&work.set_agent_rules(args(body)?).await?),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge2838 // Rulesets (rulesets.rs): kept here, enforced here on merge and by
2839 // repos on push.
2840 "list_rulesets" => reply(&work.list_rulesets(args(body)?).await?),
2841 "get_ruleset" => reply(&work.get_ruleset(args(body)?).await?),
2842 "save_ruleset" => reply(&work.save_ruleset(args(body)?).await?),
2843 "delete_ruleset" => reply(&work.delete_ruleset(args(body)?).await?),
2844 "effective_rules" => reply(&work.effective_rules(args(body)?).await?),
2845 "rule_evaluations" => reply(&work.rule_evaluations(args(body)?).await?),
2846 "ref_rules" => reply(&work.ref_rules(args(body)?).await?),
2847 "record_evaluations" => reply(&work.record_evaluations(args(body)?).await?),
2848 "set_requires_pull_request" => reply(&work.set_requires_pull_request(args(body)?).await?),
Fix a hydration mismatch in output that starts with a blank line2849 _ => Response::error("Unknown method", 404),
Fast pages, required checks on the branch, self-hosted runners, honest incidents2850 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2851 served.finish_timed(answered, &work.timing)
Fix a hydration mismatch in output that starts with a blank line2852}
2853
2854/// Events from the bus, delivered on this service's own queue.
2855#[event(queue)]
2856async fn queue(batch: MessageBatch<Event>, env: Env, _ctx: Context) -> Result<()> {
2857 let work = service(&env)?;
2858 for message in batch.messages()? {
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2859 // A workspace renamed: its agent runs and memory move to the slug it has now.
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge2860 if g1t_kit::rename::on_event(&env, &env.d1("DB")?, message.body(), &[memory::RENAMED, guardrails::RENAMED, rulesets::RENAMED].concat()).await? {
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2861 message.ack();
2862 continue;
2863 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2864 // A repository renamed or transferred: its runs, memory, guardrails
2865 // and runs waiting for a slot follow.
2866 if g1t_kit::transfer::on_event(&env, &env.d1("DB")?, message.body(), &[memory::TRANSFERRED, guardrails::TRANSFERRED, retired::WAITS_MOVED].concat()).await? {
2867 message.ack();
2868 continue;
2869 }
2870 // A workspace deleted: what it kept for itself goes.
2871 if g1t_kit::deleted::on_event(&env.d1("DB")?, message.body(), memory::DELETED).await? {
2872 message.ack();
2873 continue;
2874 }
Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037)2875 // An account purged: what it wrote shows as ghost (ghost.rs).
2876 let ghost = ghost::statements();
2877 let ghost: Vec<&str> = ghost.iter().map(String::as_str).collect();
2878 if g1t_kit::user_deleted::on_event(&env.d1("DB")?, message.body(), &ghost).await? {
2879 message.ack();
2880 continue;
2881 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2882 // A repository deleted, archived or purged, or a branch renamed (retired.rs).
2883 if work.on_retired(message.body()).await? {
2884 message.ack();
2885 continue;
2886 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API2887 capture::on_event(&work, message.body()).await;
Fix a hydration mismatch in output that starts with a blank line2888 work.on_event(message.body()).await?;
2889 message.ack();
2890 }
2891 Ok(())
2892}
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights2893
2894/// The rules that once read a pull request's author read its owner now:
2895/// whoever asked g1t for it, or its author. For each, the person who asked
2896/// is held to what an author was, and g1t's agent (a token it works with)
2897/// gains nothing by being the author.
2898#[cfg(test)]
2899mod owner_rules {
2900 use super::*;
2901 use crate::rows::stored::{ASKER, G1T, pull};
2902 use g1t_contracts::identity::AGENT_ID;
2903
2904 const SOMEONE: &str = "usr_2";
2905
2906 #[test]
2907 fn no_self_approval() {
2908 // add_comment refuses a verdict on one that is theirs.
2909 let made = pull(G1T, Some(ASKER));
2910 assert!(made.is_owned_by(ASKER.0), "the person who asked cannot approve it");
2911 assert!(!made.is_owned_by(AGENT_ID), "g1t's review agent still gives its verdict");
2912 assert!(!made.is_owned_by(SOMEONE));
2913 }
2914
2915 #[test]
2916 fn what_an_author_could_do_without_a_role() {
2917 // manageable_pull (update, ready, close), catch_up_pull on a fork,
2918 // append_session, and steering with message_agent: theirs to do.
2919 let made = pull(G1T, Some(ASKER));
2920 assert!(made.is_owned_by(ASKER.0));
2921 assert!(!made.is_owned_by(SOMEONE), "anyone else still needs the role");
2922 assert!(!made.is_owned_by(AGENT_ID), "being its author gives g1t's tokens nothing more");
2923 }
2924
2925 #[test]
2926 fn nobody_is_asked_to_review_what_they_asked_for() {
2927 // update_pull drops the owner from the reviewers asked.
2928 let made = pull(G1T, Some(ASKER));
2929 let mut reviewers = vec!["syntaqx".to_owned(), "ana".to_owned()];
2930 reviewers.retain(|name| *name != made.owner().username);
2931 assert_eq!(reviewers, ["ana"]);
2932 }
2933
2934 #[test]
2935 fn sandboxes_act_as_whoever_asked() {
2936 // LifecycleJob, ReviewJob, MergecheckJob and the merge queue's job
2937 // carry who the sandbox's credential acts for: a real account.
2938 let made = pull(G1T, Some(ASKER));
2939 assert_eq!(made.owner().id, ASKER.0);
2940 let acts_as = made.requested_by.unwrap_or(made.author);
2941 assert_eq!(acts_as.id, ASKER.0);
2942 // g1t's own work, which nobody asked for, acts as g1t, as before.
2943 let own = pull(("g1t", "g1t"), None);
2944 assert_eq!(own.requested_by.unwrap_or(own.author).id, "g1t");
2945 }
2946
2947 #[test]
2948 fn events_name_g1t_and_whoever_asked() {
2949 let made = pull(G1T, Some(ASKER));
2950 let event = serde_json::to_value(Work::pull_event(&made)).unwrap();
2951 assert_eq!(event["author"], serde_json::json!({ "id": AGENT_ID, "username": "g1t" }));
2952 assert_eq!(event["requestedBy"], serde_json::json!({ "id": "usr_1", "username": "syntaqx" }));
2953 let own = serde_json::to_value(Work::pull_event(&pull(ASKER, None))).unwrap();
2954 assert!(own.get("requestedBy").is_none());
2955 }
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts2956
2957 #[test]
2958 fn only_a_closed_pull_request_reopens_and_only_an_open_one_turns_draft() {
2959 assert_eq!(reopen_refusal(PullStatus::Closed), None);
2960 assert!(reopen_refusal(PullStatus::Merged).is_some(), "a merge cannot be undone");
2961 assert!(reopen_refusal(PullStatus::Open).is_some());
2962 assert!(reopen_refusal(PullStatus::Draft).is_some());
2963 assert_eq!(draft_refusal(PullStatus::Open), None);
2964 assert!(draft_refusal(PullStatus::Draft).is_some());
2965 assert!(draft_refusal(PullStatus::Closed).is_some());
2966 assert!(draft_refusal(PullStatus::Merged).is_some());
2967 }
2968
2969 #[test]
2970 fn a_pull_request_reopens_as_what_it_was_closed_as() {
2971 assert_eq!(reopened_status(Some("draft")), PullStatus::Draft);
2972 assert_eq!(reopened_status(Some("open")), PullStatus::Open);
2973 // Closed before this was recorded, or by a merge of another.
2974 assert_eq!(reopened_status(None), PullStatus::Open);
2975 }
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights2976}

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