g1t/services/repos/src/lib.rs

1,794 lines72,812 bytesCodeBlame

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.

Rust repos service with shipping; pull requests kept in the model1//! The repos service: repository metadata, contents, forks, landing, and
2//! git over HTTPS.
3//!
4//! Other services reach it over `POST /rpc/<method>`; see
5//! `g1t_contracts::repos` for the methods and their arguments. Any other
6//! request is treated as git's smart HTTP protocol.
7
Agents as a team: lifecycle, merge queue, billing and a new shell8mod blame;
Catching up with main takes seconds when the two sides touched different files9mod catch_up;
Diffs on attempts; hosted agent presented as the g1t agent10mod diff;
Rust repos service with shipping; pull requests kept in the model11mod git_http;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look12mod git_ops;
Agents as a team: lifecycle, merge queue, billing and a new shell13mod import;
Rust repos service with shipping; pull requests kept in the model14mod land;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look15mod lifecycle;
Search across all of g1t, Explore, and a command palette16mod listing;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look17mod mirror;
Pull requests from branches18mod refs;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms19mod refs_cache;
Rust repos service with shipping; pull requests kept in the model20mod registry;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API21mod run_access;
22mod secret_scan;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms23mod shared;
Rust repos service with shipping; pull requests kept in the model24mod store;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look25mod transfer;
Rust repos service with shipping; pull requests kept in the model26
Agents and memory, checks and conflicts, profiles, slug renames, custom domains27use g1t_contracts::events::{
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look28 Event, GitPush, NewEvent, Publish, RepoCreated, RepoForked, RepoUpdated, WorkspaceDeleted,
Search across all of g1t, Explore, and a command palette29 WorkspaceRenamed,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains30};
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look31use g1t_contracts::access::{self, Capability};
Rust repos service with shipping; pull requests kept in the model32use g1t_contracts::repos::*;
RFC 3339 timestamps in identity and repos33use g1t_contracts::time::rfc3339;
Agents as a team: lifecycle, merge queue, billing and a new shell34use g1t_contracts::{FailureCode, Outcome, PrincipalKind, User, Viewer, is_valid_repo_name, new_id};
Events service in Rust, with RFC 3339 times and accurate push events35use g1t_kit::{args, now_ms, reply, rpc_method};
Diffs on attempts; hosted agent presented as the g1t agent36use std::collections::{HashMap, HashSet, VecDeque};
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms37use std::rc::Rc;
Rust repos service with shipping; pull requests kept in the model38
39use serde::Serialize;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look40use worker::{
41 Context, Env, Fetcher, MessageBatch, Method, Request, Response, Result, ScheduleContext, ScheduledEvent,
42 event,
43};
Rust repos service with shipping; pull requests kept in the model44
45use registry::{Registry, can_read, can_write, store_key};
46use store::{ArtifactsStore, GitRepo, GitStore, Scope};
47
Issues and pull requests replace intents and attempts48/// Namespace that holds every pull request's fork: `pulls/<pull id>`.
49const PULLS_NAMESPACE: &str = "pulls";
Rust repos service with shipping; pull requests kept in the model50const MAX_TEXT_BYTES: usize = 512 * 1024;
Issues and pull requests replace intents and attempts51/// How far back a pull request may have forked and still be landed.
Rust repos service with shipping; pull requests kept in the model52const MAX_ANCESTRY: u32 = 1000;
Agents as a team: lifecycle, merge queue, billing and a new shell53const MAX_DESCRIPTION_CHARS: usize = 200;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look54pub(crate) const SOURCE: &str = "repos";
55pub(crate) const UNVERIFIED: &str = "Confirm your email address first. Check your inbox, or resend the link from the banner on g1t.sh.";
Rust repos service with shipping; pull requests kept in the model56
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look57pub(crate) fn not_found<T>() -> Outcome<T> {
Rust repos service with shipping; pull requests kept in the model58 Outcome::fail(FailureCode::NotFound, "Repository not found.")
59}
60
61/// Decoded text, or `None` when the file is too large or looks binary.
Agents as a team: lifecycle, merge queue, billing and a new shell62/// Whether a ref is a full commit hash rather than a branch name.
63fn is_commit_hash(git_ref: &str) -> bool {
64 git_ref.len() == 40 && git_ref.bytes().all(|b| b.is_ascii_hexdigit())
65}
66
Rust repos service with shipping; pull requests kept in the model67fn text_of(bytes: Vec<u8>) -> Option<String> {
68 if bytes.len() > MAX_TEXT_BYTES || bytes.contains(&0) {
69 return None;
70 }
71 Some(String::from_utf8_lossy(&bytes).into_owned())
72}
73
74fn is_readme(name: &str) -> bool {
75 matches!(
76 name.to_lowercase().as_str(),
77 "readme" | "readme.md" | "readme.markdown" | "readme.txt"
78 )
79}
80
81/// Whether `ancestor` is reachable from the newest commit in `history`.
82///
83/// `history` is the first-parent chain, which is all the store lists; a fork
84/// that merged the target branch in has the target's head on a second
85/// parent, so the walk follows every parent.
86async fn descends_from<R: GitRepo>(repo: &R, history: &[Commit], ancestor: &str) -> Result<bool> {
87 let known: HashMap<&str, &[String]> = history
88 .iter()
89 .map(|commit| (commit.hash.as_str(), commit.parents.as_slice()))
90 .collect();
91 let mut seen = HashSet::new();
92 let mut queue: Vec<String> = history
93 .first()
94 .map(|c| c.hash.clone())
95 .into_iter()
96 .collect();
97 while let Some(hash) = queue.pop() {
98 if hash == ancestor {
99 return Ok(true);
100 }
101 if !seen.insert(hash.clone()) || seen.len() > MAX_ANCESTRY as usize {
102 continue;
103 }
104 match known.get(hash.as_str()) {
105 Some(parents) => queue.extend(parents.iter().cloned()),
106 None => queue.extend(repo.parents(&hash).await?.unwrap_or_default()),
107 }
108 }
109 Ok(false)
110}
111
Diffs on attempts; hosted agent presented as the g1t agent112/// The commit closest to the newest in `history` that is also in `shared`:
113/// where a fork and the repository it came from last agreed.
114async fn nearest_ancestor_in<R: GitRepo>(
115 repo: &R,
116 history: &[Commit],
117 shared: &HashSet<String>,
118) -> Result<Option<String>> {
119 let known: HashMap<&str, &[String]> = history
120 .iter()
121 .map(|commit| (commit.hash.as_str(), commit.parents.as_slice()))
122 .collect();
123 let mut seen = HashSet::new();
124 let mut queue: VecDeque<String> = history
125 .first()
126 .map(|c| c.hash.clone())
127 .into_iter()
128 .collect();
129 while let Some(hash) = queue.pop_front() {
130 if shared.contains(&hash) {
131 return Ok(Some(hash));
132 }
133 if !seen.insert(hash.clone()) || seen.len() > MAX_ANCESTRY as usize {
134 continue;
135 }
136 match known.get(hash.as_str()) {
137 Some(parents) => queue.extend(parents.iter().cloned()),
138 None => queue.extend(repo.parents(&hash).await?.unwrap_or_default()),
139 }
140 }
141 Ok(None)
142}
143
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look144pub(crate) struct Repos<S: GitStore> {
Rust repos service with shipping; pull requests kept in the model145 registry: Registry,
146 store: S,
Events service in Rust, with RFC 3339 times and accurate push events147 events: Fetcher,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API148 /// Asked during a push which secrets have been allowed.
149 security: Option<Fetcher>,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look150 /// Asked whether a workspace is on a plan, for its private storage.
151 billing: Option<Fetcher>,
152 /// Told when a repository moves, for the tokens of agents at work on it.
153 identity: Option<Fetcher>,
154 /// What a free workspace's private repositories may hold.
155 free_private_bytes: i64,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms156 /// What isolates share: answers that list refs (refs_cache.rs).
157 shared: Option<Rc<shared::Shared>>,
Rust repos service with shipping; pull requests kept in the model158}
159
160impl<S: GitStore> Repos<S> {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms161 /// Records that the refs of the repository with this id changed, once
162 /// they have, so that the answers kept that list them go stale (see
163 /// refs_cache.rs). Everything that changes a repository's refs calls
164 /// this after it (`every_ref_writer_records_the_change` checks). A
165 /// failure is logged: the change itself happened, and what was kept
166 /// expires within `refs_cache::TTL_SECONDS` regardless.
167 pub(crate) async fn refs_moved(&self, repo_id: &str) {
168 if let Err(error) = self.registry.refs_moved(repo_id).await {
169 worker::console_error!("refs of {repo_id} changed but not recorded: {error}");
170 }
171 }
172
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look173 pub(crate) async fn publish<T: Serialize>(&self, event: NewEvent<T>) -> Result<()> {
Events service in Rust, with RFC 3339 times and accurate push events174 g1t_kit::call(
175 &self.events,
176 "publish",
177 &Publish {
178 events: vec![event],
179 },
180 )
181 .await
Rust repos service with shipping; pull requests kept in the model182 }
183
Members can read a private repository's pull request forks184 /// Whether the viewer may read `repo`. A pull request's fork of a
185 /// private repository can be read by everyone who can read that
186 /// repository, so its members can review and check out the change, as
187 /// well as by whoever opened the pull request.
188 async fn may_read(&self, repo: &Repo, viewer: &Viewer) -> Result<bool> {
189 if can_read(repo, viewer) {
190 return Ok(true);
191 }
192 let Some(source_id) = &repo.fork_of else {
193 return Ok(false);
194 };
Rust repos service with shipping; pull requests kept in the model195 Ok(self
196 .registry
Members can read a private repository's pull request forks197 .by_id(source_id)
Rust repos service with shipping; pull requests kept in the model198 .await?
Members can read a private repository's pull request forks199 .is_some_and(|source| can_read(&source, viewer)))
Rust repos service with shipping; pull requests kept in the model200 }
201
Members can read a private repository's pull request forks202 /// `repo`, if there is one and the viewer may read it.
203 async fn visible(&self, repo: Option<Repo>, viewer: &Viewer) -> Result<Option<Repo>> {
204 Ok(match repo {
205 Some(repo) if self.may_read(&repo, viewer).await? => Some(repo),
206 _ => None,
207 })
208 }
209
210 /// Resolves a repo the viewer may read; private repos look missing.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look211 pub(crate) async fn readable(&self, path: &RepoPath, viewer: &Viewer) -> Result<Option<Repo>> {
Members can read a private repository's pull request forks212 self.visible(self.registry.by_path(path).await?, viewer)
213 .await
214 }
215
Rust repos service with shipping; pull requests kept in the model216 async fn get(&self, a: GetArgs) -> Result<Outcome<Repo>> {
217 Ok(self
218 .readable(&a.path, &a.viewer)
219 .await?
220 .map_or_else(not_found, Outcome::Ok))
221 }
222
223 async fn get_by_id(&self, a: GetByIdArgs) -> Result<Outcome<Repo>> {
224 Ok(self
Members can read a private repository's pull request forks225 .visible(self.registry.by_id(&a.id).await?, &a.viewer)
Rust repos service with shipping; pull requests kept in the model226 .await?
227 .map_or_else(not_found, Outcome::Ok))
228 }
229
Agents as a team: lifecycle, merge queue, billing and a new shell230 async fn update(&self, a: UpdateArgs) -> Result<Outcome<Repo>> {
231 let viewer = Some(a.actor.clone());
232 let Some(repo) = self.readable(&a.path, &viewer).await? else {
233 return Ok(not_found());
234 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look235 // Its details take Maintain; its protection, Maintain too; who can
236 // see it, Admin (below). See g1t_contracts::access.
237 let protection_changes = a.protected.is_some_and(|protected| protected != repo.protected);
238 let details_change = a.description.is_some() || a.website.is_some() || a.topics.is_some();
239 let mut needed = Vec::new();
240 if details_change || !protection_changes {
241 needed.push(Capability::ManageSettings);
242 }
243 if protection_changes {
244 needed.push(Capability::ManageProtection);
245 }
246 let full_name = format!("{}/{}", repo.namespace, repo.name);
247 if repo.fork_of.is_some() {
248 return Ok(Outcome::fail(FailureCode::Forbidden, access::needs(Capability::ManageSettings, &full_name)));
249 }
250 if let Some(missing) = needed.into_iter().find(|capability| !registry::can(&repo, &viewer, *capability)) {
251 return Ok(Outcome::fail(FailureCode::Forbidden, access::needs(missing, &full_name)));
Agents as a team: lifecycle, merge queue, billing and a new shell252 }
253 if !a.actor.verified {
254 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
255 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look256 if let Some((code, message)) = lifecycle::archived_refusal(&repo) {
257 return Ok(Outcome::fail(code, message));
258 }
Agents as a team: lifecycle, merge queue, billing and a new shell259 let description = match a.description {
260 Some(text) => Some(
261 text.trim()
262 .chars()
263 .take(MAX_DESCRIPTION_CHARS)
264 .collect::<String>(),
265 )
266 .filter(|text| !text.is_empty()),
267 None => repo.description.clone(),
268 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look269 let website = match a.website.as_deref() {
270 Some(text) => match clean_website(text) {
271 Ok(website) => website,
272 Err(reason) => return Ok(Outcome::fail(FailureCode::Invalid, reason)),
273 },
274 None => repo.website.clone(),
275 };
276 // Who can see it is an owner's to change, and a free workspace's
277 // storage may not take it private: see lifecycle.rs.
278 let wants_private = a.is_private.filter(|private| *private != repo.is_private);
279 if wants_private.is_some()
280 && let Err((code, message)) = lifecycle::admin_only(
281 lifecycle::Asker::on(&a.actor, &repo),
282 &repo.namespace,
283 "change the visibility of",
284 Capability::Administer,
285 )
286 {
287 return Ok(Outcome::fail(code, message));
288 }
289 let is_private = repo.is_private;
Agents as a team: lifecycle, merge queue, billing and a new shell290 let protected = a.protected.unwrap_or(repo.protected);
Search across all of g1t, Explore, and a command palette291 let topics = match &a.topics {
292 Some(topics) => match clean_topics(topics) {
293 Ok(topics) => topics,
294 Err(reason) => return Ok(Outcome::fail(FailureCode::Invalid, reason)),
295 },
296 None => repo.topics.clone(),
297 };
Agents as a team: lifecycle, merge queue, billing and a new shell298 self.registry
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look299 .update(&repo.id, description.as_deref(), protected, &topics, website.as_deref())
Agents as a team: lifecycle, merge queue, billing and a new shell300 .await?;
Search across all of g1t, Explore, and a command palette301 let updated = Repo {
Agents as a team: lifecycle, merge queue, billing and a new shell302 description,
303 is_private,
304 protected,
Search across all of g1t, Explore, and a command palette305 topics,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look306 website,
Agents as a team: lifecycle, merge queue, billing and a new shell307 ..repo
Search across all of g1t, Explore, and a command palette308 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look309 if let Some(private) = wants_private {
310 return self.change_visibility(updated, private, &a.actor, a.surface).await;
311 }
312 let visibility_changed = false;
Search across all of g1t, Explore, and a command palette313 // Search and anything else that shows the repository hears of it;
314 // a change of visibility is announced on its own as well, so that
315 // what was public stops being shown at once.
316 self.publish(NewEvent {
317 kind: "repo.updated",
318 source: SOURCE,
319 repo_id: Some(updated.id.clone()),
320 actor: Some(a.actor.id.clone()),
321 data: RepoUpdated {
322 repo_id: updated.id.clone(),
323 namespace: updated.namespace.clone(),
324 name: updated.name.clone(),
325 is_private,
326 visibility_changed,
327 },
328 })
329 .await?;
330 Ok(Outcome::Ok(updated))
331 }
332
333 /// The repository with this id, if it is not a fork, and its store.
334 async fn stored(&self, repo_id: &str) -> Result<Option<S::Repo>> {
335 match self.registry.by_id(repo_id).await? {
336 Some(repo) if repo.fork_of.is_none() => Ok(Some(self.store.open(&store_key(&repo)).await?)),
337 _ => Ok(None),
338 }
339 }
340
341 async fn list_files(&self, a: ListFilesArgs) -> Result<FileList> {
342 let Some(repo) = self.registry.by_id(&a.repo_id).await?.filter(|repo| repo.fork_of.is_none()) else {
343 return Ok(FileList::default());
344 };
345 let git = self.store.open(&store_key(&repo)).await?;
346 let head = a.git_ref.unwrap_or_else(|| repo.default_branch.clone());
347 listing::list(&git, None, &head, &a.skip_dirs, a.limit).await
348 }
349
350 async fn changed_files(&self, a: ChangedFilesArgs) -> Result<FileList> {
351 let Some(git) = self.stored(&a.repo_id).await? else {
352 return Ok(FileList::default());
353 };
354 listing::list(&git, a.base.as_deref(), &a.head, &a.skip_dirs, a.limit).await
355 }
356
357 async fn read_blobs(&self, a: ReadBlobsArgs) -> Result<Vec<BlobText>> {
358 let Some(git) = self.stored(&a.repo_id).await? else {
359 return Ok(Vec::new());
360 };
361 listing::read(&git, &a.hashes, a.max_bytes.min(MAX_TEXT_BYTES as u32)).await
Agents as a team: lifecycle, merge queue, billing and a new shell362 }
363
Rust repos service with shipping; pull requests kept in the model364 async fn create(&self, a: CreateArgs) -> Result<Outcome<Repo>> {
365 if !a.owner.verified {
366 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
367 }
368 let name = a.name.trim().to_lowercase();
369 if !is_valid_repo_name(&name) {
370 return Ok(Outcome::fail(
371 FailureCode::Invalid,
372 "Use letters, digits, dots, hyphens and underscores only.",
373 ));
374 }
Workspaces own repositories375 let namespace = a.namespace.trim().to_lowercase();
376 if namespace.is_empty() {
Rust repos service with shipping; pull requests kept in the model377 return Ok(Outcome::fail(
378 FailureCode::Invalid,
Workspaces own repositories379 "Say which workspace to create the repository in.",
380 ));
381 }
382 if !a.owner.is_member(&namespace) {
383 return Ok(Outcome::fail(
384 FailureCode::Forbidden,
385 "You are not a member of that workspace.",
Rust repos service with shipping; pull requests kept in the model386 ));
387 }
Workspaces own repositories388 let path = RepoPath { namespace, name };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look389 match self.registry.by_path_any(&path).await? {
390 Some((_, None)) => {
391 return Ok(Outcome::fail(
392 FailureCode::Conflict,
393 "That workspace already has a repository with that name.",
394 ));
395 }
396 Some((_, Some(_))) => {
397 return Ok(Outcome::fail(
398 FailureCode::Conflict,
399 format!(
400 "{}/{} was deleted recently and can still be restored, so its name is taken. Restore it, or delete it permanently from the workspace's Recently deleted list.",
401 path.namespace, path.name
402 ),
403 ));
404 }
405 None => {}
406 }
407 // With a credential (a GitHub App installation's token), everything
408 // is copied: every branch and tag. See mirror.rs.
409 let mut credentialed = None;
410 if let (Some(url), Some(token)) = (a.import_url.as_deref(), a.import_token.as_deref()) {
411 let Some(url) = import::clean_url(url) else {
412 return Ok(Outcome::fail(FailureCode::Invalid, "That is not an https repository address."));
413 };
414 let source = mirror::Endpoint::github(&url, token);
415 match mirror::probe(&source).await? {
416 Ok(advertised) => credentialed = Some((source, advertised)),
417 Err(reason) => return Ok(Outcome::fail(FailureCode::Invalid, reason)),
418 }
Rust repos service with shipping; pull requests kept in the model419 }
Agents as a team: lifecycle, merge queue, billing and a new shell420 // An import is fetched before anything is created, so that an
421 // address that does not work leaves nothing behind.
422 let mut imported = None;
423 if let Some(url) = a
424 .import_url
425 .as_deref()
426 .map(str::trim)
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look427 .filter(|url| !url.is_empty() && credentialed.is_none())
Agents as a team: lifecycle, merge queue, billing and a new shell428 {
429 let Some(url) = import::clean_url(url) else {
430 return Ok(Outcome::fail(
431 FailureCode::Invalid,
432 "Give the https address of a public repository, such as https://github.com/owner/repo.",
433 ));
434 };
435 let remote = match import::discover(&url).await? {
436 Ok(remote) => remote,
437 Err(reason) => return Ok(Outcome::fail(FailureCode::Invalid, reason)),
438 };
439 let pack = match import::fetch(&url, &remote.head).await? {
440 Ok(pack) => pack,
441 Err(reason) => return Ok(Outcome::fail(FailureCode::Invalid, reason)),
442 };
443 imported = Some((remote, pack));
444 }
Rust repos service with shipping; pull requests kept in the model445 let now = now_ms();
446 let repo = Repo {
447 id: new_id("rep", now),
448 namespace: path.namespace,
449 name: path.name,
450 description: a
451 .description
452 .map(|text| text.trim().to_owned())
453 .filter(|text| !text.is_empty()),
454 is_private: a.is_private,
455 owner_id: a.owner.id.clone(),
Agents as a team: lifecycle, merge queue, billing and a new shell456 default_branch: imported
457 .as_ref()
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look458 .map(|(remote, _)| remote.branch.clone())
459 .or_else(|| credentialed.as_ref().and_then(|(_, advertised)| advertised.default_branch()))
460 .unwrap_or_else(|| "main".to_owned()),
Rust repos service with shipping; pull requests kept in the model461 fork_of: None,
Agents as a team: lifecycle, merge queue, billing and a new shell462 protected: false,
RFC 3339 timestamps in identity and repos463 created_at: rfc3339(now),
Search across all of g1t, Explore, and a command palette464 topics: Vec::new(),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look465 website: None,
466 archived_at: None,
Rust repos service with shipping; pull requests kept in the model467 };
Agents and memory, checks and conflicts, profiles, slug renames, custom domains468 self.registry.claim_store_key(&repo).await?;
Rust repos service with shipping; pull requests kept in the model469 self.store
470 .create(
471 &store_key(&repo),
472 repo.description.as_deref(),
473 &repo.default_branch,
474 )
475 .await?;
476 self.registry.insert(&repo).await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look477 // A repository that was transferred away from this path stops
478 // redirecting here.
479 self.registry
480 .drop_redirect(&RepoPath {
481 namespace: repo.namespace.clone(),
482 name: repo.name.clone(),
483 })
484 .await?;
Agents as a team: lifecycle, merge queue, billing and a new shell485 let mut pushed = None;
486 if let Some((remote, pack)) = imported {
487 let access = self
488 .store
489 .open(&store_key(&repo))
490 .await?
491 .access(Scope::Write)
492 .await?;
493 let stored =
494 land::push_pack(&access, &repo.default_branch, None, &remote.head, pack).await?;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms495 self.refs_moved(&repo.id).await;
Agents as a team: lifecycle, merge queue, billing and a new shell496 if let Err(reason) = stored {
497 self.registry.remove(&repo.id).await?;
498 return Ok(Outcome::fail(
499 FailureCode::Invalid,
500 format!("The repository could not be stored: {reason}"),
501 ));
502 }
503 pushed = Some(remote.head);
504 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look505 if let Some((source, _)) = credentialed {
506 let access = self
507 .store
508 .open(&store_key(&repo))
509 .await?
510 .access(Scope::Write)
511 .await?;
512 let target = mirror::Endpoint::bearer(&access.remote, &access.token);
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms513 let copied = mirror::copy(&source, &target, mirror::Prune::Yes).await?;
514 self.refs_moved(&repo.id).await;
515 match copied {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look516 Ok(copied) => {
517 let branch = format!("refs/heads/{}", repo.default_branch);
518 pushed = copied
519 .updated
520 .into_iter()
521 .find(|(name, _, _)| *name == branch)
522 .map(|(_, _, new)| new);
523 }
524 Err(reason) => {
525 self.registry.remove(&repo.id).await?;
526 return Ok(Outcome::fail(
527 FailureCode::Invalid,
528 format!("The repository could not be copied: {reason}"),
529 ));
530 }
531 }
532 }
Rust repos service with shipping; pull requests kept in the model533 self.publish(NewEvent {
534 kind: "repo.created",
535 source: SOURCE,
536 repo_id: Some(repo.id.clone()),
537 actor: Some(a.owner.id),
538 data: RepoCreated {
539 repo_id: repo.id.clone(),
540 namespace: repo.namespace.clone(),
541 name: repo.name.clone(),
542 is_private: repo.is_private,
543 },
544 })
545 .await?;
Agents as a team: lifecycle, merge queue, billing and a new shell546 if let Some(head) = pushed {
GitHub Actions on g1t, part one: reading workflows547 self.publish_push(
548 &repo,
549 &format!("refs/heads/{}", repo.default_branch),
550 None,
551 &head,
552 None,
553 )
Agents as a team: lifecycle, merge queue, billing and a new shell554 .await?;
555 }
Rust repos service with shipping; pull requests kept in the model556 Ok(Outcome::Ok(repo))
557 }
558
559 async fn tree(&self, a: TreeArgs) -> Result<Outcome<TreeView>> {
560 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
561 return Ok(not_found());
562 };
563 let git = self.store.open(&store_key(&repo)).await?;
564 let git_ref = a
565 .git_ref
566 .clone()
567 .unwrap_or_else(|| repo.default_branch.clone());
568
569 let Some(head) = git.log(&git_ref, 1).await?.into_iter().next() else {
570 // An unknown ref is an error; a repo with no commits is just empty.
571 if a.git_ref.is_some() {
572 return Ok(Outcome::fail(
573 FailureCode::NotFound,
574 "No such branch, tag or commit.",
575 ));
576 }
577 return Ok(Outcome::Ok(TreeView {
578 repo,
579 git_ref,
580 path: a.tree_path,
581 head: None,
582 entries: Vec::new(),
583 readme: None,
584 }));
585 };
586
587 let no_directory = || Outcome::fail(FailureCode::NotFound, "No such directory.");
588 let mut entries = git.read_tree(&head.tree_hash).await?;
589 for segment in a.tree_path.split('/').filter(|segment| !segment.is_empty()) {
590 let next = entries.as_ref().and_then(|entries| {
591 entries
592 .iter()
593 .find(|entry| entry.name == segment && entry.kind == EntryKind::Tree)
594 });
595 let Some(next) = next else {
596 return Ok(no_directory());
597 };
598 entries = git.read_tree(&next.hash).await?;
599 }
600 let Some(mut entries) = entries else {
601 return Ok(no_directory());
602 };
603 // Directories first, then by name.
604 entries.sort_by(|a, b| {
605 (b.kind == EntryKind::Tree)
606 .cmp(&(a.kind == EntryKind::Tree))
607 .then_with(|| a.name.cmp(&b.name))
608 });
609
610 let readme_entry = entries
611 .iter()
612 .find(|entry| entry.kind == EntryKind::Blob && is_readme(&entry.name));
613 let readme = match readme_entry {
614 Some(entry) => git.read_blob(&entry.hash).await?.map(|bytes| Readme {
615 name: entry.name.clone(),
616 text: text_of(bytes),
617 }),
618 None => None,
619 };
620 Ok(Outcome::Ok(TreeView {
621 repo,
622 git_ref,
623 path: a.tree_path,
624 head: Some(head),
625 entries,
626 readme,
627 }))
628 }
629
630 async fn blob(&self, a: BlobArgs) -> Result<Outcome<BlobView>> {
631 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
632 return Ok(not_found());
633 };
634 let bytes = if a.file_path.is_empty() {
635 None
636 } else {
637 let git = self.store.open(&store_key(&repo)).await?;
638 git.read_file(&a.git_ref, &a.file_path).await?
639 };
640 let Some(bytes) = bytes else {
641 return Ok(Outcome::fail(FailureCode::NotFound, "No such file."));
642 };
643 Ok(Outcome::Ok(BlobView {
644 repo,
645 git_ref: a.git_ref,
646 path: a.file_path,
647 size: bytes.len() as u64,
648 text: text_of(bytes),
649 }))
650 }
651
Agents as a team: lifecycle, merge queue, billing and a new shell652 async fn blame(&self, a: BlameArgs) -> Result<Outcome<Blame>> {
653 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
654 return Ok(not_found());
655 };
656 let git = self.store.open(&store_key(&repo)).await?;
657 let git_ref = a.git_ref.unwrap_or_else(|| repo.default_branch.clone());
658 Ok(match blame::blame(&git, &git_ref, &a.file_path).await? {
659 Some(blame) => Outcome::Ok(blame),
660 None => not_found(),
661 })
662 }
663
Rust repos service with shipping; pull requests kept in the model664 async fn log(&self, a: LogArgs) -> Result<Outcome<Vec<Commit>>> {
665 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
666 return Ok(not_found());
667 };
668 let git = self.store.open(&store_key(&repo)).await?;
669 let git_ref = a.git_ref.unwrap_or_else(|| repo.default_branch.clone());
670 Ok(Outcome::Ok(git.log(&git_ref, a.limit).await?))
671 }
672
Pull requests from branches673 /// The repository's branches, default branch first.
674 async fn branches(&self, a: BranchesArgs) -> Result<Outcome<Vec<Branch>>> {
675 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
676 return Ok(not_found());
677 };
678 let mut branches = self.store.open(&store_key(&repo)).await?.branches().await?;
679 branches.sort_by_key(|branch| branch.name != repo.default_branch);
680 Ok(Outcome::Ok(branches))
681 }
682
Agents as a team: lifecycle, merge queue, billing and a new shell683 /// Whether a pull request's source lacks commits that the branch it
684 /// would merge into has.
685 async fn behind(&self, a: BehindArgs) -> Result<bool> {
686 let Some(source) = self.registry.by_id(&a.source_id).await? else {
687 return Ok(false);
688 };
689 let target = match &source.fork_of {
690 Some(id) => self.registry.by_id(id).await?,
691 None => Some(source.clone()),
692 };
693 let Some(target) = target else {
694 return Ok(false);
695 };
696 let branch = a.branch.unwrap_or_else(|| target.default_branch.clone());
697 let target_head = self
698 .store
699 .open(&store_key(&target))
700 .await?
701 .log(&target.default_branch, 1)
702 .await?
703 .into_iter()
704 .next()
705 .map(|commit| commit.hash);
706 let Some(target_head) = target_head else {
707 return Ok(false);
708 };
709 let source_git = self.store.open(&store_key(&source)).await?;
710 let history = source_git.log(&branch, MAX_ANCESTRY).await?;
711 if history.is_empty() {
712 return Ok(false);
713 }
714 Ok(!descends_from(&source_git, &history, &target_head).await?)
715 }
716
Agents and memory, checks and conflicts, profiles, slug renames, custom domains717 /// The files a pull request's source and the default branch it would
718 /// merge into each changed since they last agreed. Where the two lists
719 /// share no file, the merge cannot conflict; where they do, it may.
720 async fn divergence(&self, a: BehindArgs) -> Result<Option<Divergence>> {
721 let Some(source) = self.registry.by_id(&a.source_id).await? else {
722 return Ok(None);
723 };
724 let target = match &source.fork_of {
725 Some(id) => self.registry.by_id(id).await?,
726 None => Some(source.clone()),
727 };
728 let Some(target) = target else {
729 return Ok(None);
730 };
731 let branch = a.branch.unwrap_or_else(|| target.default_branch.clone());
732 let source_git = self.store.open(&store_key(&source)).await?;
733 let target_git = self.store.open(&store_key(&target)).await?;
734 let (history, target_history) = futures_util::future::try_join(
735 source_git.log(&branch, MAX_ANCESTRY),
736 target_git.log(&target.default_branch, MAX_ANCESTRY),
737 )
738 .await?;
739 let (Some(head), Some(base)) = (history.first(), target_history.first()) else {
740 return Ok(None);
741 };
742 let behind = !descends_from(&source_git, &history, &base.hash).await?;
743 let shared: HashSet<String> = target_history.iter().map(|commit| commit.hash.clone()).collect();
744 let merge_base = nearest_ancestor_in(&source_git, &history, &shared).await?;
745 let mut divergence = Divergence {
746 head: head.hash.clone(),
747 base: base.hash.clone(),
748 merge_base: merge_base.clone(),
749 behind,
750 ..Divergence::default()
751 };
752 let merge_base_tree = match &merge_base {
753 Some(hash) => target_history
754 .iter()
755 .find(|commit| commit.hash == *hash)
756 .map(|commit| commit.tree_hash.clone()),
757 None => None,
758 };
759 let Some(merge_base_tree) = merge_base_tree else {
760 // No common history to compare from: say nothing is known.
761 divergence.truncated = true;
762 return Ok(Some(divergence));
763 };
764 let (ours, truncated_ours) =
765 diff::changed_paths(&source_git, Some(&merge_base_tree), &head.tree_hash).await?;
766 divergence.ours = ours;
767 divergence.truncated = truncated_ours;
768 if behind {
769 let (theirs, truncated_theirs) =
770 diff::changed_paths(&target_git, Some(&merge_base_tree), &base.tree_hash).await?;
771 divergence.theirs = theirs;
772 divergence.truncated |= truncated_theirs;
773 }
774 Ok(Some(divergence))
775 }
776
Pull requests from branches777 async fn head(&self, a: HeadArgs) -> Result<Option<String>> {
778 let Some(repo) = self.registry.by_id(&a.repo_id).await? else {
779 return Ok(None);
780 };
Workflows run when an agent's pull request is marked ready781 let branch = if a.branch.is_empty() { &repo.default_branch } else { &a.branch };
Pull requests from branches782 let git = self.store.open(&store_key(&repo)).await?;
783 Ok(git
Workflows run when an agent's pull request is marked ready784 .log(branch, 1)
Pull requests from branches785 .await?
786 .into_iter()
787 .next()
788 .map(|commit| commit.hash))
789 }
790
Merge queue: tested states are deleted once their entry leaves791 async fn delete_branch(&self, a: DeleteBranchArgs) -> Result<Outcome<bool>> {
792 if !a.branch.starts_with(G1T_BRANCH_PREFIX) {
793 return Ok(Outcome::fail(
794 FailureCode::Forbidden,
795 "Only branches g1t made for itself can be deleted this way.",
796 ));
797 }
798 let Some(repo) = self.registry.by_id(&a.repo_id).await? else {
799 return Ok(not_found());
800 };
801 let git = self.store.open(&store_key(&repo)).await?;
802 let Some(old) = git
803 .branches()
804 .await?
805 .into_iter()
806 .find(|branch| branch.name == a.branch)
807 .map(|branch| branch.hash)
808 else {
809 return Ok(Outcome::Ok(false));
810 };
811 let access = git.access(Scope::Write).await?;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms812 let deleted = land::delete_ref(&access, &a.branch, &old).await?;
813 self.refs_moved(&repo.id).await;
814 if let Err(reason) = deleted {
Merge queue: tested states are deleted once their entry leaves815 return Ok(Outcome::fail(
816 FailureCode::Conflict,
817 format!("{} could not be deleted: {reason}", a.branch),
818 ));
819 }
820 Ok(Outcome::Ok(true))
821 }
822
Issues and pull requests replace intents and attempts823 async fn fork_for_pull(&self, a: ForkArgs) -> Result<Outcome<Repo>> {
Rust repos service with shipping; pull requests kept in the model824 let viewer = Some(a.actor.clone());
825 let Some(source) = self
826 .registry
827 .by_id(&a.source_id)
828 .await?
829 .filter(|repo| can_read(repo, &viewer))
830 else {
831 return Ok(not_found());
832 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look833 if let Some((code, message)) = lifecycle::archived_refusal(&source) {
834 return Ok(Outcome::fail(code, message));
835 }
Rust repos service with shipping; pull requests kept in the model836 let now = now_ms();
837 let fork = Repo {
838 id: new_id("rep", now),
Issues and pull requests replace intents and attempts839 namespace: PULLS_NAMESPACE.to_owned(),
840 name: a.pull_id.clone(),
Rust repos service with shipping; pull requests kept in the model841 description: None,
842 // A fork is exactly as visible as the repo it came from.
843 is_private: source.is_private,
844 owner_id: a.actor.id.clone(),
845 default_branch: source.default_branch.clone(),
846 fork_of: Some(source.id.clone()),
Agents as a team: lifecycle, merge queue, billing and a new shell847 protected: false,
RFC 3339 timestamps in identity and repos848 created_at: rfc3339(now),
Search across all of g1t, Explore, and a command palette849 topics: Vec::new(),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look850 website: None,
851 archived_at: None,
Rust repos service with shipping; pull requests kept in the model852 };
Agents and memory, checks and conflicts, profiles, slug renames, custom domains853 self.registry.claim_store_key(&fork).await?;
Rust repos service with shipping; pull requests kept in the model854 self.store
855 .open(&store_key(&source))
856 .await?
857 .fork(&store_key(&fork))
858 .await?;
859 self.registry.insert(&fork).await?;
860 self.publish(NewEvent {
861 kind: "repo.forked",
862 source: SOURCE,
863 repo_id: Some(source.id.clone()),
864 actor: Some(a.actor.id),
865 data: RepoForked {
866 repo_id: fork.id.clone(),
867 source_repo_id: source.id,
Issues and pull requests replace intents and attempts868 pull_id: a.pull_id,
Rust repos service with shipping; pull requests kept in the model869 },
870 })
871 .await?;
872 Ok(Outcome::Ok(fork))
873 }
874
875 async fn git_access(&self, a: GitAccessArgs) -> Result<Outcome<GitAccess>> {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms876 let found = self.registry.by_path(&a.path).await?;
877 Ok(match self.authorize_git(&a.path, &a.viewer, a.service, found).await? {
878 Outcome::Ok(repo) => {
879 let write = a.service == GitService::ReceivePack;
880 if write {
881 // A push with this credential would not pass through
882 // here, so nothing that lists the refs is kept until it
883 // has expired (see refs_cache.rs).
884 let until = now_ms() + store::CREDENTIAL_LIFE_MS + 60_000;
885 if let Err(error) = self.registry.refs_open(&repo.id, until).await {
886 // Before the column exists nothing is kept anyway.
887 if registry::refs_state(&repo.id).is_some() {
888 return Err(error);
889 }
890 }
891 }
892 let scope = if write { Scope::Write } else { Scope::Read };
893 Outcome::Ok(self.store.access(&store_key(&repo), scope).await?)
894 }
895 Outcome::Fail(failure) => Outcome::Fail(failure),
896 })
897 }
898
899 /// The repository at `path` (`found`, as just read), if the viewer may
900 /// use `service` on it: fetch from it, or push to it. A push to a path
901 /// with nothing there makes the repository, in a workspace the pusher
902 /// belongs to.
903 async fn authorize_git(
904 &self,
905 path: &RepoPath,
906 viewer: &Viewer,
907 service: GitService,
908 found: Option<Repo>,
909 ) -> Result<Outcome<Repo>> {
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step910 let mut a = GitAccessArgs {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms911 path: path.clone(),
912 viewer: viewer.clone(),
913 service,
914 };
Rust repos service with shipping; pull requests kept in the model915 let write = a.service == GitService::ReceivePack;
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step916 // An access token: pushing needs code:write, reading a private
917 // repository code:read. A public repository reads as it would for
918 // anyone. Which repositories a token reaches is its owner's, checked
919 // below as for anyone.
920 if let Some(access) = a.viewer.as_ref().and_then(|user| user.token.as_deref()).cloned() {
921 let public = found.as_ref().is_some_and(|repo| !repo.is_private);
922 let decision = g1t_contracts::scopes::decide_git(&access, write, public);
923 if !decision.allowed {
924 return Ok(Outcome::fail(
925 FailureCode::Forbidden,
926 format!("{}\n", decision.reason.unwrap_or_default()),
927 ));
928 }
929 if !write && !access.allows(g1t_contracts::scopes::Scope::CodeRead) {
930 a.viewer = None;
931 }
932 }
933
Rust repos service with shipping; pull requests kept in the model934 // Anonymous callers are asked to authenticate whether or not the repo
935 // exists, so private repos cannot be told apart from missing ones.
936 let denied = || match &a.viewer {
937 Some(_) => not_found(),
938 None => Outcome::fail(FailureCode::Unauthenticated, "Authentication required."),
939 };
Agents as a team: lifecycle, merge queue, billing and a new shell940 // An agent's token works through the API only: its sandbox has its
941 // own way to push, to its own pull request.
942 if a.viewer.as_ref().is_some_and(|user| user.kind == PrincipalKind::Agent) {
943 return Ok(Outcome::fail(
944 FailureCode::Forbidden,
945 "A g1t agent's token cannot be used with git.",
946 ));
947 }
Rust repos service with shipping; pull requests kept in the model948 if let (true, Some(user)) = (write, &a.viewer)
949 && !user.verified
950 {
951 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
952 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms953 let repo = match found {
Rust repos service with shipping; pull requests kept in the model954 Some(repo) => {
955 let allowed = if write {
956 can_write(&repo, &a.viewer)
957 } else {
Members can read a private repository's pull request forks958 self.may_read(&repo, &a.viewer).await?
Rust repos service with shipping; pull requests kept in the model959 };
960 if !allowed {
961 return Ok(denied());
962 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look963 // An archived repository, or a pull request's copy of one,
964 // is read-only.
965 if write {
966 let archived = match &repo.fork_of {
967 Some(source) => self.registry.by_id(source).await?,
968 None => Some(repo.clone()),
969 };
970 match archived {
971 Some(source) => {
972 if let Some((code, message)) = lifecycle::archived_refusal(&source) {
973 return Ok(Outcome::fail(code, format!("{message}\n")));
974 }
975 }
976 // The repository it was copied from is deleted.
977 None => return Ok(denied()),
978 }
979 }
Rust repos service with shipping; pull requests kept in the model980 repo
981 }
982 None => {
Workspaces own repositories983 // Push to create, in a workspace the pusher belongs to.
Rust repos service with shipping; pull requests kept in the model984 let owner = a
985 .viewer
986 .as_ref()
Workspaces own repositories987 .filter(|user| write && user.is_member(&a.path.namespace.to_lowercase()));
Rust repos service with shipping; pull requests kept in the model988 let Some(owner) = owner else {
989 return Ok(denied());
990 };
991 let created = self
992 .create(CreateArgs {
993 owner: owner.clone(),
Workspaces own repositories994 namespace: a.path.namespace.clone(),
Rust repos service with shipping; pull requests kept in the model995 name: a.path.name.clone(),
996 description: None,
997 is_private: false,
Agents as a team: lifecycle, merge queue, billing and a new shell998 import_url: None,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look999 import_token: None,
Rust repos service with shipping; pull requests kept in the model1000 })
1001 .await?;
1002 match created {
1003 Outcome::Ok(repo) => repo,
1004 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
1005 }
1006 }
1007 };
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1008 Ok(Outcome::Ok(repo))
Rust repos service with shipping; pull requests kept in the model1009 }
1010
1011 async fn land(&self, a: LandArgs) -> Result<Outcome<Landed>> {
1012 let actor: Viewer = Some(a.actor.clone());
Pull requests from branches1013 let Some(source) = self.registry.by_id(&a.source_id).await? else {
Rust repos service with shipping; pull requests kept in the model1014 return Ok(not_found());
1015 };
Pull requests from branches1016 // A fork lands on the repository it came from; a branch on its own.
1017 let target = match &source.fork_of {
Rust repos service with shipping; pull requests kept in the model1018 Some(id) => self.registry.by_id(id).await?,
Pull requests from branches1019 None => Some(source.clone()),
Rust repos service with shipping; pull requests kept in the model1020 };
1021 let Some(target) = target.filter(|repo| can_read(repo, &actor)) else {
1022 return Ok(not_found());
1023 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1024 if !registry::can(&target, &actor, Capability::Merge) {
Rust repos service with shipping; pull requests kept in the model1025 return Ok(Outcome::fail(
1026 FailureCode::Forbidden,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1027 access::needs(Capability::Merge, &format!("{}/{}", target.namespace, target.name)),
Rust repos service with shipping; pull requests kept in the model1028 ));
1029 }
1030 if !a.actor.verified {
1031 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
1032 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1033 if let Some((code, message)) = lifecycle::archived_refusal(&target) {
1034 return Ok(Outcome::fail(code, message));
1035 }
Rust repos service with shipping; pull requests kept in the model1036
1037 let branch = &target.default_branch;
Pull requests from branches1038 let from_fork = source.id != target.id;
1039 let source_branch = match a.branch {
1040 Some(name) if !from_fork && name == *branch => {
1041 return Ok(Outcome::fail(
1042 FailureCode::Invalid,
1043 format!("{branch} cannot be merged into itself."),
1044 ));
1045 }
1046 Some(name) => name,
1047 None if from_fork => branch.clone(),
1048 None => {
1049 return Ok(Outcome::fail(
1050 FailureCode::Invalid,
1051 "Say which branch to merge.",
1052 ));
1053 }
1054 };
1055
1056 let source_git = self.store.open(&store_key(&source)).await?;
Rust repos service with shipping; pull requests kept in the model1057 let target_git = self.store.open(&store_key(&target)).await?;
Pull requests from branches1058 let history = source_git.log(&source_branch, MAX_ANCESTRY).await?;
Rust repos service with shipping; pull requests kept in the model1059 let Some(new) = history.first().map(|commit| commit.hash.clone()) else {
1060 return Ok(Outcome::fail(
1061 FailureCode::Conflict,
Issues and pull requests replace intents and attempts1062 "This pull request has no commits to merge.",
Rust repos service with shipping; pull requests kept in the model1063 ));
1064 };
1065 let old = target_git
1066 .log(branch, 1)
1067 .await?
1068 .into_iter()
1069 .next()
1070 .map(|commit| commit.hash);
1071
1072 if old.as_deref() == Some(new.as_str()) {
Diffs on attempts; hosted agent presented as the g1t agent1073 return Ok(Outcome::Ok(Landed {
1074 commit: new,
1075 previous: None,
1076 }));
Rust repos service with shipping; pull requests kept in the model1077 }
1078 // Moving the branch to a commit that does not descend from its
1079 // current head would discard whatever landed in between.
1080 if let Some(old) = &old
Pull requests from branches1081 && !descends_from(&source_git, &history, old).await?
Rust repos service with shipping; pull requests kept in the model1082 {
Pull requests from branches1083 let remedy = if from_fork {
1084 format!("Pull {branch} into the pull request's fork, push, and merge again.")
1085 } else {
1086 format!("Merge {branch} into {source_branch}, push, and merge again.")
1087 };
Rust repos service with shipping; pull requests kept in the model1088 return Ok(Outcome::fail(
1089 FailureCode::Conflict,
Pull requests from branches1090 format!("{branch} has moved since this pull request was opened. {remedy}"),
Rust repos service with shipping; pull requests kept in the model1091 ));
1092 }
1093
Pull requests from branches1094 // For a branch the objects are already in the target; sending them
1095 // again is harmless and keeps one way of moving a ref.
1096 let source_access = source_git.access(Scope::Read).await?;
Rust repos service with shipping; pull requests kept in the model1097 let target_access = target_git.access(Scope::Write).await?;
1098 let pushed =
1099 land::fast_forward(&source_access, &target_access, branch, old.as_deref(), &new)
1100 .await?;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1101 self.refs_moved(&target.id).await;
Rust repos service with shipping; pull requests kept in the model1102 if let Err(reason) = pushed {
Issues and pull requests replace intents and attempts1103 // Most often another pull request landed between the check and the push.
Rust repos service with shipping; pull requests kept in the model1104 return Ok(Outcome::fail(
1105 FailureCode::Conflict,
1106 format!("{branch} could not be updated: {reason}"),
1107 ));
1108 }
GitHub Actions on g1t, part one: reading workflows1109 self.publish_push(
1110 &target,
1111 &format!("refs/heads/{branch}"),
1112 old.as_deref(),
1113 &new,
1114 Some(a.actor.id),
1115 )
Events service in Rust, with RFC 3339 times and accurate push events1116 .await?;
Diffs on attempts; hosted agent presented as the g1t agent1117 Ok(Outcome::Ok(Landed {
1118 commit: new,
1119 previous: old,
1120 }))
1121 }
1122
1123 async fn compare(&self, a: CompareArgs) -> Result<Outcome<Comparison>> {
1124 let Some(repo) = self
Members can read a private repository's pull request forks1125 .visible(self.registry.by_id(&a.repo_id).await?, &a.viewer)
Diffs on attempts; hosted agent presented as the g1t agent1126 .await?
1127 else {
1128 return Ok(not_found());
1129 };
1130 let git = self.store.open(&store_key(&repo)).await?;
Pull requests from branches1131 let head_ref = a.head.as_deref().unwrap_or(&repo.default_branch);
Agents as a team: lifecycle, merge queue, billing and a new shell1132 // The head's history is only searched when the base is worked out
1133 // from another branch.
1134 let depth = if a.base.is_some() || is_commit_hash(head_ref) { 1 } else { MAX_ANCESTRY };
1135 let history = git.log(head_ref, depth).await?;
Diffs on attempts; hosted agent presented as the g1t agent1136 let Some(head) = history.first() else {
1137 return Ok(Outcome::fail(
1138 FailureCode::Conflict,
Pull requests from branches1139 "There are no commits to compare.",
Diffs on attempts; hosted agent presented as the g1t agent1140 ));
1141 };
1142
Pull requests from branches1143 // Where the head's history meets the default branch of `against`.
1144 let shared_with = async |against: &Repo| -> Result<Option<String>> {
1145 let against_git = self.store.open(&store_key(against)).await?;
1146 let shared: HashSet<String> = against_git
1147 .log(&against.default_branch, MAX_ANCESTRY)
1148 .await?
1149 .into_iter()
1150 .map(|commit| commit.hash)
1151 .collect();
1152 nearest_ancestor_in(&git, &history, &shared).await
1153 };
Diffs on attempts; hosted agent presented as the g1t agent1154 let base = match (a.base, &repo.fork_of) {
1155 (Some(base), _) => Some(base),
1156 // A fork is compared with the last commit it shares with the
1157 // repository it came from.
1158 (None, Some(target_id)) => match self.registry.by_id(target_id).await? {
Pull requests from branches1159 Some(target) => shared_with(&target).await?,
Diffs on attempts; hosted agent presented as the g1t agent1160 None => None,
1161 },
Pull requests from branches1162 // A branch, with the point where it left the default branch.
Agents as a team: lifecycle, merge queue, billing and a new shell1163 // A single commit, with its first parent.
1164 (None, None) if is_commit_hash(head_ref) => head.parents.first().cloned(),
Pull requests from branches1165 (None, None) if head_ref != repo.default_branch => shared_with(&repo).await?,
Diffs on attempts; hosted agent presented as the g1t agent1166 (None, None) => head.parents.first().cloned(),
1167 };
1168 let base_tree = match &base {
1169 Some(base) => git
1170 .log(base, 1)
1171 .await?
1172 .into_iter()
1173 .next()
1174 .map(|commit| commit.tree_hash),
1175 None => None,
1176 };
1177 let (files, truncated) =
1178 diff::compare_trees(&git, base_tree.as_deref(), &head.tree_hash).await?;
1179 Ok(Outcome::Ok(Comparison {
1180 base,
1181 head: head.hash.clone(),
1182 files,
1183 truncated,
1184 }))
Rust repos service with shipping; pull requests kept in the model1185 }
1186
GitHub Actions on g1t, part one: reading workflows1187 /// Reports that `git_ref` of `repo` (a full ref) now points to `after`.
Events service in Rust, with RFC 3339 times and accurate push events1188 async fn publish_push(
1189 &self,
1190 repo: &Repo,
GitHub Actions on g1t, part one: reading workflows1191 git_ref: &str,
1192 before: Option<&str>,
Events service in Rust, with RFC 3339 times and accurate push events1193 after: &str,
1194 actor: Option<String>,
1195 ) -> Result<()> {
Rust repos service with shipping; pull requests kept in the model1196 self.publish(NewEvent {
1197 kind: "git.push",
1198 source: SOURCE,
1199 repo_id: Some(repo.id.clone()),
1200 actor,
1201 data: GitPush {
1202 repo_id: repo.id.clone(),
GitHub Actions on g1t, part one: reading workflows1203 git_ref: git_ref.to_owned(),
1204 before: before.map(str::to_owned),
Rust repos service with shipping; pull requests kept in the model1205 after: after.to_owned(),
GitHub Actions on g1t, part one: reading workflows1206 default_branch: git_ref.strip_prefix("refs/heads/")
1207 == Some(repo.default_branch.as_str()),
Rust repos service with shipping; pull requests kept in the model1208 },
1209 })
1210 .await
1211 }
1212
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1213 /// Git over HTTPS. Only what decides the answer happens before it:
1214 /// the repository, who is asking and whether they may, the free
1215 /// workspace limits, push protection, and the store's own answer. The
1216 /// audit entry and what a push changed are recorded once git has its
1217 /// answer. Each answer says how long its steps took (`Server-Timing`).
1218 async fn git_http(&self, request: Request, env: &Env, ctx: &Context) -> Result<Response> {
1219 let mut timing = git_http::Timing::start();
Rust repos service with shipping; pull requests kept in the model1220 let Some(git) = git_http::parse(&request.url()?) else {
1221 return Response::error("Not found", 404);
1222 };
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1223 let response = self.answer_git(request, &git, env, ctx, &mut timing).await?;
1224 timing.apply(response)
1225 }
1226
1227 async fn answer_git(
1228 &self,
1229 request: Request,
1230 git: &git_http::GitRequest,
1231 env: &Env,
1232 ctx: &Context,
1233 timing: &mut git_http::Timing,
1234 ) -> Result<Response> {
1235 let write = git.service == GitService::ReceivePack;
1236 let get = request.method() == Method::Get;
1237 let identity = env.service("IDENTITY")?;
1238 // The repository and the caller's credentials, at once. A fetch may
1239 // go by the row as read a moment ago, for the same clone's next
1240 // request; a push always reads it. Anonymous callers cost nothing.
1241 let lookup = async {
1242 if write {
1243 self.registry.by_path(&git.path).await
1244 } else {
1245 self.registry.by_path_recent(&git.path).await
1246 }
1247 };
1248 let (found, viewer) =
1249 futures_util::future::join(lookup, git_http::viewer(&request, &identity)).await;
1250 let found = found?;
1251 timing.mark("repo");
1252 if found.is_none() {
1253 // A workspace that was renamed: git follows a redirect when it
1254 // first asks for refs, and uses the new address from then on.
1255 // A repository transferred to another workspace: the same, to
1256 // its new path. Fetches and pushes both follow either.
1257 let url = request.url()?;
1258 let (renamed, moved) = futures_util::future::join(
1259 git_http::renamed(&url, &identity),
1260 self.registry.resolve_moved(&git.path),
1261 )
1262 .await;
1263 timing.mark("moved");
1264 if let Some(location) = renamed? {
1265 return git_http::moved(&location, get);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1266 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1267 if let Some(now) = moved?
1268 && let Some(location) = git_http::transferred(&url, &now)
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1269 {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1270 return git_http::moved(&location, get);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1271 }
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1272 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1273 let viewer = viewer?;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1274 // A run credential is checked against its grants, then acts as the
1275 // person it works for. See run_access.rs.
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1276 let (request, viewer, audit) = match self.admit_git(request, git, viewer, found.as_ref()).await? {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1277 run_access::Admitted::Go { request, viewer, entry } => (request, viewer, entry),
1278 run_access::Admitted::Refused(response) => return Ok(response),
1279 };
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1280 let mut after = AfterGit {
1281 audit,
1282 status: 0,
1283 message: None,
1284 push: None,
1285 };
1286 let repo = match self.authorize_git(&git.path, &viewer, git.service, found).await? {
1287 Outcome::Ok(repo) => repo,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1288 refused => {
1289 let response = git_http::refuse(refused)?;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1290 after.ended(response.status_code(), None);
1291 after.spawn(env, ctx);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1292 return Ok(response);
1293 }
Rust repos service with shipping; pull requests kept in the model1294 };
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1295 timing.mark("access");
Agents as a team: lifecycle, merge queue, billing and a new shell1296 // A protected default branch takes changes only from a merged pull
1297 // request, which lands without going through here.
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1298 let protected = (repo.protected && repo.fork_of.is_none()).then(|| repo.default_branch.clone());
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1299 // Clones check out the default branch g1t keeps, which can have
1300 // changed since the store made the repository.
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1301 let default_branch = repo.fork_of.is_none().then(|| repo.default_branch.clone());
1302 let key = store_key(&repo);
1303 let scope = if write { Scope::Write } else { Scope::Read };
1304 let mut request = request;
1305 let protocol = refs_cache::protocol(request.headers().get("git-protocol")?.as_deref());
1306 // A fetch's POST is read here, to tell an `ls-refs` from a fetch of
1307 // objects; the store would have it read in full anyway.
1308 let body = if !write && !get { Some(request.bytes().await?) } else { None };
1309 // An answer that lists refs may have been kept: see refs_cache.rs.
1310 let kept_key = refs_cache::kind(git, get, protocol, body.as_deref())
1311 .zip(refs_cache::usable(registry::refs_state(&repo.id), now_ms()))
1312 .map(|(kind, version)| {
1313 refs_cache::Key::new(&repo.id, version, default_branch.as_deref(), protocol, &kind)
1314 });
1315 // A kept answer and the free workspace limits, with a kept
1316 // credential looked up alongside. A kept answer goes back without
1317 // waiting for the credential, which it does not need.
1318 let ((answer, limited), kept_access) = {
1319 let shared = self.shared.as_deref();
1320 let answer_and_limits = std::pin::pin!(futures_util::future::join(
1321 async {
1322 match &kept_key {
1323 Some(kept_key) => refs_cache::get(shared, kept_key).await,
1324 None => None,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1325 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1326 },
1327 self.git_limits(&request, git, &repo, env),
1328 ));
1329 let kept_access = std::pin::pin!(self.store.kept_access(&key, scope));
1330 match futures_util::future::select(answer_and_limits, kept_access).await {
1331 futures_util::future::Either::Left((first, kept_access)) => {
1332 let answered = first.0.is_some() || matches!(first.1, Ok(Some(_)) | Err(_));
1333 (first, if answered { None } else { kept_access.await })
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1334 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1335 futures_util::future::Either::Right((kept_access, first)) => (first.await, kept_access),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1336 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1337 };
1338 timing.mark("kept");
1339 if let Some((response, status, message)) = limited? {
1340 after.ended(status, Some(message.to_owned()));
1341 after.spawn(env, ctx);
1342 return Ok(response);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1343 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1344 if let (Some((entry, found)), Some(kept_key)) = (answer, &kept_key) {
1345 timing.note("refs", found.as_str());
1346 if found == refs_cache::Found::Shared {
1347 let (kept_key, entry) = (kept_key.clone(), entry.clone());
1348 ctx.wait_until(async move { refs_cache::keep_in_colo(&kept_key, &entry).await });
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1349 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1350 after.ended(200, None);
1351 after.spawn(env, ctx);
1352 return entry.response();
1353 }
1354 if kept_key.is_some() {
1355 timing.note("refs", "miss");
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1356 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1357 // The store's credential: one made a moment ago, here or in another
1358 // isolate (see store.rs), or a new one.
1359 let access = match kept_access {
1360 Some((access, from)) => {
1361 timing.note("cred", from.as_str());
1362 access
1363 }
1364 None => {
1365 let access = self.store.mint_access(&key, scope).await?;
1366 timing.mark("mint");
1367 timing.note("cred", "mint");
1368 access
1369 }
1370 };
1371 // Should the store turn a kept credential down, a fetch's first
1372 // request is tried again with a new one; the requests after it then
1373 // have that one too.
1374 let again = if get { Some(request.clone()?) } else { None };
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1375 // Push protection: a push that adds a secret is refused. See secret_scan.rs.
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1376 let scan = async |body: &[u8]| self.protect(&repo, viewer.as_ref(), body).await;
1377 let mut outcome = git_http::forward(
1378 request,
1379 body,
1380 git,
1381 &access,
1382 protected.as_deref(),
1383 default_branch.as_deref(),
1384 scan,
1385 )
1386 .await?;
1387 let turned_down = matches!(
1388 &outcome,
1389 git_http::Push::Forwarded(forwarded) if matches!(forwarded.response.status_code(), 401 | 403)
1390 );
1391 if turned_down {
1392 self.store.forget_access(&key).await;
1393 if let Some(again) = again {
1394 let access = self.store.mint_access(&key, scope).await?;
1395 let nothing = async |_: &[u8]| Ok(None);
1396 outcome = git_http::forward(again, None, git, &access, protected.as_deref(), default_branch.as_deref(), nothing)
1397 .await?;
1398 }
1399 }
Agents as a team: lifecycle, merge queue, billing and a new shell1400 let forwarded =
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1401 match outcome {
Agents as a team: lifecycle, merge queue, billing and a new shell1402 git_http::Push::Forwarded(forwarded) => forwarded,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1403 git_http::Push::Refused(response) => {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1404 after.ended(403, Some("The push would change a protected branch.".to_owned()));
1405 after.spawn(env, ctx);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1406 return Ok(response);
1407 }
1408 git_http::Push::Blocked(response) => {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1409 after.ended(403, Some("The push adds a secret.".to_owned()));
1410 after.spawn(env, ctx);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1411 return Ok(response);
1412 }
Agents as a team: lifecycle, merge queue, billing and a new shell1413 };
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1414 timing.mark("store");
1415 let mut response = forwarded.response;
1416 let status = response.status_code();
1417 if write && !get {
1418 // A push: the store has moved its refs once it has answered in
1419 // full, so the answer is read before the change is recorded, and
1420 // only then goes back. Whoever fetches after it sees the push.
1421 let headers = response.headers().clone();
1422 headers.delete("content-length")?;
1423 let report = response.bytes().await?;
1424 self.refs_moved(&repo.id).await;
1425 timing.mark("refs");
1426 response = Response::from_bytes(report)?.with_headers(headers).with_status(status);
1427 } else if let (Some(kept_key), 200) = (&kept_key, status) {
1428 // A miss: this answer is kept for the next to ask.
1429 let headers = response.headers().clone();
1430 headers.delete("content-length")?;
1431 let body = response.bytes().await?;
1432 if let Some(content_type) = headers.get("content-type")? {
1433 let entry = refs_cache::Entry { content_type, body: body.clone() };
1434 if entry.keepable() {
1435 let shared = self.shared.clone();
1436 let kept_key = kept_key.clone();
1437 ctx.wait_until(async move { refs_cache::keep(shared.as_deref(), &kept_key, &entry).await });
1438 }
1439 }
1440 response = Response::from_bytes(body)?.with_headers(headers).with_status(status);
1441 }
1442 after.ended(status, None);
1443 if status == 200 && (forwarded.pack_bytes > 0 || !forwarded.pushed.is_empty()) {
1444 after.push = Some(PushDone {
1445 repo,
1446 pushed: forwarded.pushed,
1447 pack_bytes: forwarded.pack_bytes,
1448 actor: viewer.map(|user: User| user.id),
1449 });
1450 }
1451 after.spawn(env, ctx);
1452 Ok(response)
1453 }
1454
1455 /// The answer for a request a free workspace's limits stop, with its
1456 /// status and reason for the audit log; `None` to go on.
1457 ///
1458 /// Each clone, fetch and push is a git operation, which the git store
1459 /// charges g1t for: counted for billing, and a free workspace far past
1460 /// its share is slowed down rather than charged (see git_ops.rs). And a
1461 /// free workspace is never charged for private storage: once its
1462 /// private repositories hold the free amount, pushes to them stop,
1463 /// checked when a push begins so that git shows the reason.
1464 async fn git_limits(
1465 &self,
1466 request: &Request,
1467 git: &git_http::GitRequest,
1468 repo: &Repo,
1469 env: &Env,
1470 ) -> Result<Option<(Response, u16, &'static str)>> {
1471 let namespace = git.path.namespace.to_lowercase();
1472 if request.method() == Method::Post && git.endpoint != "info/refs" {
1473 match git_ops::count(&self.registry.db, &namespace, &rfc3339(now_ms())).await {
1474 Ok((month, hour)) => {
1475 let limits = git_ops::Limits::from_env(env);
1476 if git_ops::slow_down(month, hour, limits.free_cap, limits.hourly)
1477 && git_ops::is_free(env.service("BILLING").ok().as_ref(), &namespace).await
1478 {
1479 return Ok(Some((
1480 git_ops::too_many(&namespace, limits.free_cap, limits.hourly)?,
1481 429,
1482 "Too many git operations this hour.",
1483 )));
1484 }
1485 }
1486 Err(error) => worker::console_error!("git operation for {namespace} not counted: {error}"),
1487 }
1488 }
1489 if git.service == GitService::ReceivePack && git.endpoint == "info/refs" && repo.is_private {
1490 let free = git_ops::free_private_bytes(env);
1491 let held = self.registry.private_bytes(&namespace).await.unwrap_or(0);
1492 if git_ops::storage_full(held, free)
1493 && git_ops::is_free(env.service("BILLING").ok().as_ref(), &namespace).await
1494 {
1495 return Ok(Some((
1496 git_ops::storage_full_response(&namespace, held, free)?,
1497 403,
1498 "Free private storage is full.",
1499 )));
1500 }
1501 }
1502 Ok(None)
1503 }
Rust repos service with shipping; pull requests kept in the model1504
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1505 /// What a push changed, recorded once git has its answer.
1506 async fn record_push(&self, push: PushDone) -> Result<()> {
1507 let PushDone {
1508 repo,
1509 pushed,
1510 pack_bytes,
1511 actor,
1512 } = push;
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1513 // What the push stored, for billing's storage meter. A failure only
1514 // leaves the count short.
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1515 if pack_bytes > 0
1516 && let Err(error) = self.registry.add_stored_bytes(&repo, pack_bytes).await
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1517 {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1518 worker::console_error!("stored bytes for {} not counted: {error}", repo.name);
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1519 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1520 if pushed.is_empty() {
1521 return Ok(());
1522 }
Rust repos service with shipping; pull requests kept in the model1523 // Artifacts' own push notifications are per repository, which does
Events service in Rust, with RFC 3339 times and accurate push events1524 // not fit a repo per pull request, so the front end reports pushes
1525 // itself: one event for each branch that moved.
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1526 let stored = self.store.open(&store_key(&repo)).await?;
1527 for pushed in &pushed {
1528 // The store can refuse one ref and accept another, so each
1529 // branch is checked against where it actually is. A tag the
1530 // store cannot read back is taken as pushed.
1531 let moved = match pushed.branch() {
1532 Some(branch) => stored
1533 .log(branch, 1)
1534 .await?
1535 .first()
1536 .is_some_and(|commit| commit.hash == pushed.after),
1537 None => stored.log(&pushed.git_ref, 1).await.map_or(true, |head| {
1538 head.first().is_none_or(|commit| commit.hash == pushed.after)
1539 }),
1540 };
1541 if moved {
1542 self.publish_push(
1543 &repo,
1544 &pushed.git_ref,
1545 pushed.before.as_deref(),
1546 &pushed.after,
1547 actor.clone(),
1548 )
1549 .await?;
1550 }
1551 }
1552 Ok(())
1553 }
1554}
1555
1556/// A push the store accepted, to be recorded once git has its answer.
1557struct PushDone {
1558 repo: Repo,
1559 pushed: Vec<git_http::Pushed>,
1560 pack_bytes: u64,
1561 actor: Option<String>,
1562}
1563
1564/// What a git request leaves for after its answer: its audit entry, with
1565/// how the request ended, and what a push changed.
1566struct AfterGit {
1567 audit: Option<Box<g1t_contracts::audit::NewAuditEntry>>,
1568 status: u16,
1569 message: Option<String>,
1570 push: Option<PushDone>,
1571}
1572
1573impl AfterGit {
1574 fn ended(&mut self, status: u16, message: Option<String>) {
1575 self.status = status;
1576 self.message = message;
1577 }
1578
1579 /// Does the work once the response is on its way. A failure is logged:
1580 /// git has already been told how its request went.
1581 fn spawn(self, env: &Env, ctx: &Context) {
1582 if self.audit.is_none() && self.push.is_none() {
1583 return;
1584 }
1585 let env = env.clone();
1586 ctx.wait_until(async move {
1587 let repos = match service(&env) {
1588 Ok(repos) => repos,
1589 Err(error) => {
1590 worker::console_error!("git request not recorded: {error}");
1591 return;
Events service in Rust, with RFC 3339 times and accurate push events1592 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1593 };
1594 repos.finish_git(self.audit, self.status, self.message).await;
1595 if let Some(push) = self.push
1596 && let Err(error) = repos.record_push(push).await
1597 {
1598 worker::console_error!("push not recorded: {error}");
Rust repos service with shipping; pull requests kept in the model1599 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1600 });
Rust repos service with shipping; pull requests kept in the model1601 }
1602}
1603
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1604fn service(env: &Env) -> Result<Repos<ArtifactsStore>> {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1605 let shared = shared::Shared::from_env(env).map(Rc::new);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1606 Ok(Repos {
Rust repos service with shipping; pull requests kept in the model1607 registry: Registry { db: env.d1("DB")? },
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1608 store: ArtifactsStore::new(env, shared.clone())?,
1609 shared,
Events service in Rust, with RFC 3339 times and accurate push events1610 events: env.service("EVENTS")?,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1611 security: env.service("SECURITY").ok(),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1612 billing: env.service("BILLING").ok(),
1613 identity: env.service("IDENTITY").ok(),
1614 free_private_bytes: git_ops::free_private_bytes(env),
1615 })
1616}
1617
1618#[event(fetch)]
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1619async fn fetch(mut request: Request, env: Env, ctx: Context) -> Result<Response> {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1620 let repos = service(&env)?;
Rust repos service with shipping; pull requests kept in the model1621 let Some(method) = rpc_method(&request) else {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1622 return repos.git_http(request, &env, &ctx).await;
Rust repos service with shipping; pull requests kept in the model1623 };
1624 let body: serde_json::Value = request.json().await?;
1625
1626 match method.as_str() {
1627 "get" => reply(&repos.get(args(body)?).await?),
1628 "get_by_id" => reply(&repos.get_by_id(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1629 "readable" => {
1630 let a: ReadableArgs = args(body)?;
1631 reply(&repos.registry.readable(&a.ids, &a.viewer).await?)
1632 }
1633 "public_namespaces" => {
1634 let a: PublicNamespacesArgs = args(body)?;
1635 reply(&repos.registry.public_namespaces(&a.owner_id).await?)
1636 }
Automations: rules in .g1t/automations that act when something happens1637 "path_by_id" => {
1638 let a: PathByIdArgs = args(body)?;
1639 reply(
1640 &repos
1641 .registry
1642 .by_id(&a.id)
1643 .await?
1644 .filter(|repo| repo.fork_of.is_none())
1645 .map(|repo| RepoPath {
1646 namespace: repo.namespace,
1647 name: repo.name,
1648 }),
1649 )
1650 }
Rust repos service with shipping; pull requests kept in the model1651 "list" => {
1652 let a: ListArgs = args(body)?;
1653 reply(
1654 &repos
1655 .registry
Workspaces own repositories1656 .list(
1657 &a.viewer,
1658 a.query.as_deref(),
1659 a.namespace.as_deref(),
1660 a.member_only,
1661 )
Rust repos service with shipping; pull requests kept in the model1662 .await?,
1663 )
1664 }
1665 "create" => reply(&repos.create(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1666 // Services only: a GitHub mirror catching up, or pushing out.
1667 "mirror" => reply(&repos.mirror(args(body)?).await?),
1668 "transfer" => reply(&repos.transfer(args(body)?).await?),
1669 // A repository's lifecycle: see lifecycle.rs.
1670 "delete" => reply(&repos.delete(args(body)?).await?),
1671 "deleted" => reply(&repos.deleted(args(body)?).await?),
1672 "restore" => reply(&repos.restore(args(body)?).await?),
1673 "purge" => reply(&repos.purge(args(body)?).await?),
1674 "purge_due" => reply(&repos.purge_due(args(body)?).await?),
1675 "rename" => reply(&repos.rename(args(body)?).await?),
1676 "archive" => reply(&repos.archive(args(body)?).await?),
1677 "set_visibility" => reply(&repos.set_visibility(args(body)?).await?),
1678 "set_default_branch" => reply(&repos.set_default_branch(args(body)?).await?),
1679 "rename_branch" => reply(&repos.rename_branch(args(body)?).await?),
1680 "resolve_branch" => reply(&repos.resolve_branch(args(body)?).await?),
1681 "status_by_id" => reply(&repos.status_by_id(args(body)?).await?),
1682 "resolve_path" => {
1683 let a: ResolvePathArgs = args(body)?;
1684 reply(&repos.registry.resolve_moved(&a.path).await?)
1685 }
1686 "namespace_count" => {
1687 let a: NamespaceCountArgs = args(body)?;
1688 reply(&repos.registry.count_in(&a.namespace).await?)
1689 }
Agents as a team: lifecycle, merge queue, billing and a new shell1690 "update" => reply(&repos.update(args(body)?).await?),
Rust repos service with shipping; pull requests kept in the model1691 "tree" => reply(&repos.tree(args(body)?).await?),
1692 "blob" => reply(&repos.blob(args(body)?).await?),
1693 "log" => reply(&repos.log(args(body)?).await?),
Agents as a team: lifecycle, merge queue, billing and a new shell1694 "blame" => reply(&repos.blame(args(body)?).await?),
Issues and pull requests replace intents and attempts1695 "fork_for_pull" => reply(&repos.fork_for_pull(args(body)?).await?),
Rust repos service with shipping; pull requests kept in the model1696 "git_access" => reply(&repos.git_access(args(body)?).await?),
Pull requests from branches1697 "branches" => reply(&repos.branches(args(body)?).await?),
1698 "head" => reply(&repos.head(args(body)?).await?),
Agents as a team: lifecycle, merge queue, billing and a new shell1699 "behind" => reply(&repos.behind(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1700 "divergence" => reply(&repos.divergence(args(body)?).await?),
Rust repos service with shipping; pull requests kept in the model1701 "land" => reply(&repos.land(args(body)?).await?),
Catching up with main takes seconds when the two sides touched different files1702 "update_pull_branch" => reply(&repos.update_pull_branch(args(body)?).await?),
Merge queue: tested states are deleted once their entry leaves1703 "delete_branch" => reply(&repos.delete_branch(args(body)?).await?),
Diffs on attempts; hosted agent presented as the g1t agent1704 "compare" => reply(&repos.compare(args(body)?).await?),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1705 "scan_history" => reply(&repos.scan_history(args(body)?).await?),
1706 "find_lockfiles" => reply(&repos.find_lockfiles(args(body)?).await?),
Search across all of g1t, Explore, and a command palette1707 "list_files" => reply(&repos.list_files(args(body)?).await?),
1708 "changed_files" => reply(&repos.changed_files(args(body)?).await?),
1709 "read_blobs" => reply(&repos.read_blobs(args(body)?).await?),
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1710 "visibility" => {
1711 let a: g1t_contracts::repos::VisibilityArgs = args(body)?;
1712 reply(&repos.registry.visibility(&a.paths).await?)
1713 }
1714 "storage" => reply(&repos.registry.storage().await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1715 "git_operations" => {
1716 let a: GitOperationsArgs = args(body)?;
1717 reply(&git_ops::totals(&repos.registry.db, &a.month, a.since.as_deref(), a.namespace.as_deref().map(str::to_lowercase).as_deref()).await?)
1718 }
Search across all of g1t, Explore, and a command palette1719 "all_ids" => {
1720 let a: AllIdsArgs = args(body)?;
1721 let limit = a.limit.clamp(1, 500);
1722 let ids = repos.registry.ids_after(a.after.as_deref(), limit).await?;
1723 let next = (ids.len() == limit as usize).then(|| ids.last().cloned()).flatten();
1724 reply(&IdPage { ids, next })
1725 }
Rust repos service with shipping; pull requests kept in the model1726 _ => Response::error("Unknown method", 404),
1727 }
1728}
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1729
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1730/// The hourly sweep: deleted repositories whose time to be restored has
1731/// passed are purged. See lifecycle.rs.
1732#[event(scheduled)]
1733async fn scheduled(_event: ScheduledEvent, env: Env, _ctx: ScheduleContext) {
1734 let purged = match service(&env) {
1735 Ok(repos) => repos.purge_due(PurgeDueArgs::default()).await,
1736 Err(error) => Err(error),
1737 };
1738 match purged {
1739 Ok(0) => {}
1740 Ok(count) => worker::console_log!("repos: purged {count} deleted repositories"),
1741 Err(error) => worker::console_error!("repos: the purge sweep failed: {error}"),
1742 }
1743}
1744
1745/// Events from the bus. A workspace's rename: its repositories move to the
1746/// workspace's current slug, asked of identity by id, so a repeated or late
1747/// delivery lands in the same place; their git store keys stay as they
1748/// were. A workspace's deletion: what it left in Recently deleted is
1749/// purged with it.
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1750#[event(queue)]
1751async fn queue(batch: MessageBatch<Event>, env: Env, _ctx: Context) -> Result<()> {
1752 let registry = Registry { db: env.d1("DB")? };
1753 let identity = env.service("IDENTITY")?;
1754 for message in batch.messages()? {
1755 let event = message.body();
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1756 if event.kind == "workspace.deleted" {
1757 match serde_json::from_value::<WorkspaceDeleted>(event.data.clone()) {
1758 Ok(deleted) => service(&env)?.purge_workspace(&deleted.slug.to_lowercase()).await?,
1759 Err(_) => worker::console_error!("workspace.deleted {} could not be read", event.id),
1760 }
1761 continue;
1762 }
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1763 if event.kind != "workspace.renamed" {
1764 continue;
1765 }
1766 let Ok(renamed) = serde_json::from_value::<WorkspaceRenamed>(event.data.clone()) else {
1767 worker::console_error!("workspace.renamed {} could not be read", event.id);
1768 continue;
1769 };
1770 let names: HashMap<String, String> = g1t_kit::call(
1771 &identity,
1772 "usernames",
1773 &g1t_contracts::identity::UsernamesArgs {
1774 ids: vec![renamed.workspace_id.clone()],
1775 },
1776 )
1777 .await?;
1778 let current = names
1779 .get(&renamed.workspace_id)
1780 .cloned()
1781 .unwrap_or_else(|| renamed.to.clone());
1782 let left = registry
1783 .rename_namespace(&renamed.stale_slugs(&current), &current)
1784 .await?;
1785 if left > 0 {
1786 worker::console_error!(
1787 "{left} repositories stayed under {} or {}: {current} already has repositories of the same names",
1788 renamed.from,
1789 renamed.to
1790 );
1791 }
1792 }
1793 Ok(())
1794}