Skip to content

g1t/services/repos/src/lib.rs

2,532 lines108,614 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
Merge branch 'worktree-agent-ac5b181a013e54348'8mod backups;
Agents as a team: lifecycle, merge queue, billing and a new shell9mod blame;
Catching up with main takes seconds when the two sides touched different files10mod catch_up;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily11mod coalesce;
Fast pages, required checks on the branch, self-hosted runners, honest incidents12mod commit_file;
Diffs on attempts; hosted agent presented as the g1t agent13mod diff;
Merge branch 'worktree-agent-a2013627e5ea4ab13'14mod fallback;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily15mod forks;
Rust repos service with shipping; pull requests kept in the model16mod git_http;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look17mod git_ops;
Agents as a team: lifecycle, merge queue, billing and a new shell18mod import;
Rust repos service with shipping; pull requests kept in the model19mod land;
Branches and Tags pages, each file's last commit, and the branch menu on files20mod last_commits;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look21mod lifecycle;
Search across all of g1t, Explore, and a command palette22mod listing;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily23mod meters;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look24mod mirror;
Merge branch 'worktree-agent-a2013627e5ea4ab13'25mod moves;
26mod namespaces;
Merge branch 'worktree-agent-a1b995daa94e4e1b7'27mod pack_cache;
Fast pages, required checks on the branch, self-hosted runners, honest incidents28mod pack_limits;
Pull requests from branches29mod refs;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms30mod refs_cache;
Rust repos service with shipping; pull requests kept in the model31mod registry;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily32mod resilience;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API33mod run_access;
34mod secret_scan;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily35mod shards;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms36mod shared;
Rust repos service with shipping; pull requests kept in the model37mod store;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look38mod transfer;
Rust repos service with shipping; pull requests kept in the model39
Agents and memory, checks and conflicts, profiles, slug renames, custom domains40use g1t_contracts::events::{
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look41 Event, GitPush, NewEvent, Publish, RepoCreated, RepoForked, RepoUpdated, WorkspaceDeleted,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member42 WorkspaceDeleting, WorkspaceRenamed, WorkspaceRestored,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains43};
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look44use g1t_contracts::access::{self, Capability};
Rust repos service with shipping; pull requests kept in the model45use g1t_contracts::repos::*;
RFC 3339 timestamps in identity and repos46use g1t_contracts::time::rfc3339;
Agents as a team: lifecycle, merge queue, billing and a new shell47use g1t_contracts::{FailureCode, Outcome, PrincipalKind, User, Viewer, is_valid_repo_name, new_id};
Events service in Rust, with RFC 3339 times and accurate push events48use g1t_kit::{args, now_ms, reply, rpc_method};
Diffs on attempts; hosted agent presented as the g1t agent49use std::collections::{HashMap, HashSet, VecDeque};
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms50use std::rc::Rc;
Rust repos service with shipping; pull requests kept in the model51
52use serde::Serialize;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look53use worker::{
54 Context, Env, Fetcher, MessageBatch, Method, Request, Response, Result, ScheduleContext, ScheduledEvent,
55 event,
56};
Rust repos service with shipping; pull requests kept in the model57
58use registry::{Registry, can_read, can_write, store_key};
59use store::{ArtifactsStore, GitRepo, GitStore, Scope};
60
Issues and pull requests replace intents and attempts61/// Namespace that holds every pull request's fork: `pulls/<pull id>`.
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily62pub(crate) const PULLS_NAMESPACE: &str = "pulls";
Rust repos service with shipping; pull requests kept in the model63const MAX_TEXT_BYTES: usize = 512 * 1024;
Issues and pull requests replace intents and attempts64/// How far back a pull request may have forked and still be landed.
Rust repos service with shipping; pull requests kept in the model65const MAX_ANCESTRY: u32 = 1000;
Branches and Tags pages, each file's last commit, and the branch menu on files66/// The most tags a repository's Tags page reads and lists.
67const MAX_TAGS_READ: usize = 100;
68
69/// One path segment, percent-encoded for a cache key.
70fn urlencoding_segment(segment: &str) -> String {
71 segment
72 .bytes()
73 .map(|b| if b.is_ascii_alphanumeric() || b"-._~".contains(&b) { (b as char).to_string() } else { format!("%{b:02X}") })
74 .collect()
75}
Agents as a team: lifecycle, merge queue, billing and a new shell76const MAX_DESCRIPTION_CHARS: usize = 200;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look77pub(crate) const SOURCE: &str = "repos";
78pub(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 model79
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look80pub(crate) fn not_found<T>() -> Outcome<T> {
Rust repos service with shipping; pull requests kept in the model81 Outcome::fail(FailureCode::NotFound, "Repository not found.")
82}
83
84/// Decoded text, or `None` when the file is too large or looks binary.
Agents as a team: lifecycle, merge queue, billing and a new shell85/// Whether a ref is a full commit hash rather than a branch name.
86fn is_commit_hash(git_ref: &str) -> bool {
87 git_ref.len() == 40 && git_ref.bytes().all(|b| b.is_ascii_hexdigit())
88}
89
Rust repos service with shipping; pull requests kept in the model90fn text_of(bytes: Vec<u8>) -> Option<String> {
91 if bytes.len() > MAX_TEXT_BYTES || bytes.contains(&0) {
92 return None;
93 }
94 Some(String::from_utf8_lossy(&bytes).into_owned())
95}
96
97fn is_readme(name: &str) -> bool {
98 matches!(
99 name.to_lowercase().as_str(),
100 "readme" | "readme.md" | "readme.markdown" | "readme.txt"
101 )
102}
103
104/// Whether `ancestor` is reachable from the newest commit in `history`.
105///
106/// `history` is the first-parent chain, which is all the store lists; a fork
107/// that merged the target branch in has the target's head on a second
108/// parent, so the walk follows every parent.
109async fn descends_from<R: GitRepo>(repo: &R, history: &[Commit], ancestor: &str) -> Result<bool> {
110 let known: HashMap<&str, &[String]> = history
111 .iter()
112 .map(|commit| (commit.hash.as_str(), commit.parents.as_slice()))
113 .collect();
114 let mut seen = HashSet::new();
115 let mut queue: Vec<String> = history
116 .first()
117 .map(|c| c.hash.clone())
118 .into_iter()
119 .collect();
120 while let Some(hash) = queue.pop() {
121 if hash == ancestor {
122 return Ok(true);
123 }
124 if !seen.insert(hash.clone()) || seen.len() > MAX_ANCESTRY as usize {
125 continue;
126 }
127 match known.get(hash.as_str()) {
128 Some(parents) => queue.extend(parents.iter().cloned()),
129 None => queue.extend(repo.parents(&hash).await?.unwrap_or_default()),
130 }
131 }
132 Ok(false)
133}
134
Diffs on attempts; hosted agent presented as the g1t agent135/// The commit closest to the newest in `history` that is also in `shared`:
136/// where a fork and the repository it came from last agreed.
137async fn nearest_ancestor_in<R: GitRepo>(
138 repo: &R,
139 history: &[Commit],
140 shared: &HashSet<String>,
141) -> Result<Option<String>> {
142 let known: HashMap<&str, &[String]> = history
143 .iter()
144 .map(|commit| (commit.hash.as_str(), commit.parents.as_slice()))
145 .collect();
146 let mut seen = HashSet::new();
147 let mut queue: VecDeque<String> = history
148 .first()
149 .map(|c| c.hash.clone())
150 .into_iter()
151 .collect();
152 while let Some(hash) = queue.pop_front() {
153 if shared.contains(&hash) {
154 return Ok(Some(hash));
155 }
156 if !seen.insert(hash.clone()) || seen.len() > MAX_ANCESTRY as usize {
157 continue;
158 }
159 match known.get(hash.as_str()) {
160 Some(parents) => queue.extend(parents.iter().cloned()),
161 None => queue.extend(repo.parents(&hash).await?.unwrap_or_default()),
162 }
163 }
164 Ok(None)
165}
166
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily167thread_local! {
168 /// Targets' sides of mergeability, by head (coalesce.rs).
169 static TARGETS: std::cell::RefCell<coalesce::Memo<coalesce::TargetKey, Rc<coalesce::TargetSide>>> =
170 std::cell::RefCell::new(coalesce::Memo::new(coalesce::TARGET_TTL_MS, 32));
171 /// What targets changed between two trees.
172 static THEIRS: std::cell::RefCell<coalesce::Memo<coalesce::TheirsKey, (Vec<String>, bool)>> =
173 std::cell::RefCell::new(coalesce::Memo::new(coalesce::THEIRS_TTL_MS, 256));
174 /// What repositories hold, as read for a push's first request, for the
175 /// same push's second: a push's POST does not wait on the database.
176 static HELD: std::cell::RefCell<coalesce::Memo<String, u64>> =
177 std::cell::RefCell::new(coalesce::Memo::new(60_000, 512));
178}
179
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look180pub(crate) struct Repos<S: GitStore> {
Rust repos service with shipping; pull requests kept in the model181 registry: Registry,
182 store: S,
Events service in Rust, with RFC 3339 times and accurate push events183 events: Fetcher,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API184 /// Asked during a push which secrets have been allowed.
185 security: Option<Fetcher>,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look186 /// Asked whether a workspace is on a plan, for its private storage.
187 billing: Option<Fetcher>,
188 /// Told when a repository moves, for the tokens of agents at work on it.
189 identity: Option<Fetcher>,
190 /// What a free workspace's private repositories may hold.
191 free_private_bytes: i64,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily192 /// Days a pull request's working copy is kept after it settles (forks.rs).
193 pub(crate) fork_days: u64,
194 /// The most a repository may hold (pack_limits.rs), and what happens
195 /// to a push too large to scan.
196 repo_limit: u64,
197 large_pushes: git_http::LargePushes,
Merge branch 'worktree-agent-a2013627e5ea4ab13'198 /// Which git store namespace new repositories go in (shards.rs), and
199 /// the most each should hold (`ARTIFACTS_NAMESPACE_LIMITS`).
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily200 placement: shards::Placement,
Merge branch 'worktree-agent-a2013627e5ea4ab13'201 limits: HashMap<String, u64>,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms202 /// What isolates share: answers that list refs (refs_cache.rs).
203 shared: Option<Rc<shared::Shared>>,
Merge branch 'worktree-agent-a1b995daa94e4e1b7'204 /// Packs for fresh clones (pack_cache.rs); `None` without the bucket.
Merge branch 'worktree-agent-aaf03bdceac799c89'205 packs: Option<Rc<pack_cache::Packs>>,
Rust repos service with shipping; pull requests kept in the model206}
207
208impl<S: GitStore> Repos<S> {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms209 /// Records that the refs of the repository with this id changed, once
210 /// they have, so that the answers kept that list them go stale (see
211 /// refs_cache.rs). Everything that changes a repository's refs calls
212 /// this after it (`every_ref_writer_records_the_change` checks). A
213 /// failure is logged: the change itself happened, and what was kept
214 /// expires within `refs_cache::TTL_SECONDS` regardless.
215 pub(crate) async fn refs_moved(&self, repo_id: &str) {
216 if let Err(error) = self.registry.refs_moved(repo_id).await {
217 worker::console_error!("refs of {repo_id} changed but not recorded: {error}");
218 }
219 }
220
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look221 pub(crate) async fn publish<T: Serialize>(&self, event: NewEvent<T>) -> Result<()> {
Events service in Rust, with RFC 3339 times and accurate push events222 g1t_kit::call(
223 &self.events,
224 "publish",
225 &Publish {
226 events: vec![event],
227 },
228 )
229 .await
Rust repos service with shipping; pull requests kept in the model230 }
231
Members can read a private repository's pull request forks232 /// Whether the viewer may read `repo`. A pull request's fork of a
233 /// private repository can be read by everyone who can read that
234 /// repository, so its members can review and check out the change, as
235 /// well as by whoever opened the pull request.
236 async fn may_read(&self, repo: &Repo, viewer: &Viewer) -> Result<bool> {
237 if can_read(repo, viewer) {
238 return Ok(true);
239 }
240 let Some(source_id) = &repo.fork_of else {
241 return Ok(false);
242 };
Rust repos service with shipping; pull requests kept in the model243 Ok(self
244 .registry
Members can read a private repository's pull request forks245 .by_id(source_id)
Rust repos service with shipping; pull requests kept in the model246 .await?
Members can read a private repository's pull request forks247 .is_some_and(|source| can_read(&source, viewer)))
Rust repos service with shipping; pull requests kept in the model248 }
249
Members can read a private repository's pull request forks250 /// `repo`, if there is one and the viewer may read it.
251 async fn visible(&self, repo: Option<Repo>, viewer: &Viewer) -> Result<Option<Repo>> {
252 Ok(match repo {
253 Some(repo) if self.may_read(&repo, viewer).await? => Some(repo),
254 _ => None,
255 })
256 }
257
258 /// 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 look259 pub(crate) async fn readable(&self, path: &RepoPath, viewer: &Viewer) -> Result<Option<Repo>> {
Members can read a private repository's pull request forks260 self.visible(self.registry.by_path(path).await?, viewer)
261 .await
262 }
263
Rust repos service with shipping; pull requests kept in the model264 async fn get(&self, a: GetArgs) -> Result<Outcome<Repo>> {
265 Ok(self
266 .readable(&a.path, &a.viewer)
267 .await?
268 .map_or_else(not_found, Outcome::Ok))
269 }
270
271 async fn get_by_id(&self, a: GetByIdArgs) -> Result<Outcome<Repo>> {
272 Ok(self
Members can read a private repository's pull request forks273 .visible(self.registry.by_id(&a.id).await?, &a.viewer)
Rust repos service with shipping; pull requests kept in the model274 .await?
275 .map_or_else(not_found, Outcome::Ok))
276 }
277
Agents as a team: lifecycle, merge queue, billing and a new shell278 async fn update(&self, a: UpdateArgs) -> Result<Outcome<Repo>> {
279 let viewer = Some(a.actor.clone());
280 let Some(repo) = self.readable(&a.path, &viewer).await? else {
281 return Ok(not_found());
282 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look283 // Its details take Maintain; its protection, Maintain too; who can
284 // see it, Admin (below). See g1t_contracts::access.
285 let protection_changes = a.protected.is_some_and(|protected| protected != repo.protected);
286 let details_change = a.description.is_some() || a.website.is_some() || a.topics.is_some();
287 let mut needed = Vec::new();
288 if details_change || !protection_changes {
289 needed.push(Capability::ManageSettings);
290 }
291 if protection_changes {
292 needed.push(Capability::ManageProtection);
293 }
294 let full_name = format!("{}/{}", repo.namespace, repo.name);
295 if repo.fork_of.is_some() {
296 return Ok(Outcome::fail(FailureCode::Forbidden, access::needs(Capability::ManageSettings, &full_name)));
297 }
298 if let Some(missing) = needed.into_iter().find(|capability| !registry::can(&repo, &viewer, *capability)) {
299 return Ok(Outcome::fail(FailureCode::Forbidden, access::needs(missing, &full_name)));
Agents as a team: lifecycle, merge queue, billing and a new shell300 }
301 if !a.actor.verified {
302 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
303 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look304 if let Some((code, message)) = lifecycle::archived_refusal(&repo) {
305 return Ok(Outcome::fail(code, message));
306 }
Agents as a team: lifecycle, merge queue, billing and a new shell307 let description = match a.description {
308 Some(text) => Some(
309 text.trim()
310 .chars()
311 .take(MAX_DESCRIPTION_CHARS)
312 .collect::<String>(),
313 )
314 .filter(|text| !text.is_empty()),
315 None => repo.description.clone(),
316 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look317 let website = match a.website.as_deref() {
318 Some(text) => match clean_website(text) {
319 Ok(website) => website,
320 Err(reason) => return Ok(Outcome::fail(FailureCode::Invalid, reason)),
321 },
322 None => repo.website.clone(),
323 };
324 // Who can see it is an owner's to change, and a free workspace's
325 // storage may not take it private: see lifecycle.rs.
326 let wants_private = a.is_private.filter(|private| *private != repo.is_private);
327 if wants_private.is_some()
328 && let Err((code, message)) = lifecycle::admin_only(
329 lifecycle::Asker::on(&a.actor, &repo),
330 &repo.namespace,
331 "change the visibility of",
332 Capability::Administer,
333 )
334 {
335 return Ok(Outcome::fail(code, message));
336 }
337 let is_private = repo.is_private;
Agents as a team: lifecycle, merge queue, billing and a new shell338 let protected = a.protected.unwrap_or(repo.protected);
Search across all of g1t, Explore, and a command palette339 let topics = match &a.topics {
340 Some(topics) => match clean_topics(topics) {
341 Ok(topics) => topics,
342 Err(reason) => return Ok(Outcome::fail(FailureCode::Invalid, reason)),
343 },
344 None => repo.topics.clone(),
345 };
Agents as a team: lifecycle, merge queue, billing and a new shell346 self.registry
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look347 .update(&repo.id, description.as_deref(), protected, &topics, website.as_deref())
Agents as a team: lifecycle, merge queue, billing and a new shell348 .await?;
Search across all of g1t, Explore, and a command palette349 let updated = Repo {
Agents as a team: lifecycle, merge queue, billing and a new shell350 description,
351 is_private,
352 protected,
Search across all of g1t, Explore, and a command palette353 topics,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look354 website,
Agents as a team: lifecycle, merge queue, billing and a new shell355 ..repo
Search across all of g1t, Explore, and a command palette356 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look357 if let Some(private) = wants_private {
358 return self.change_visibility(updated, private, &a.actor, a.surface).await;
359 }
360 let visibility_changed = false;
Search across all of g1t, Explore, and a command palette361 // Search and anything else that shows the repository hears of it;
362 // a change of visibility is announced on its own as well, so that
363 // what was public stops being shown at once.
364 self.publish(NewEvent {
365 kind: "repo.updated",
366 source: SOURCE,
367 repo_id: Some(updated.id.clone()),
368 actor: Some(a.actor.id.clone()),
369 data: RepoUpdated {
370 repo_id: updated.id.clone(),
371 namespace: updated.namespace.clone(),
372 name: updated.name.clone(),
373 is_private,
374 visibility_changed,
375 },
376 })
377 .await?;
378 Ok(Outcome::Ok(updated))
379 }
380
381 /// The repository with this id, if it is not a fork, and its store.
382 async fn stored(&self, repo_id: &str) -> Result<Option<S::Repo>> {
383 match self.registry.by_id(repo_id).await? {
384 Some(repo) if repo.fork_of.is_none() => Ok(Some(self.store.open(&store_key(&repo)).await?)),
385 _ => Ok(None),
386 }
387 }
388
389 async fn list_files(&self, a: ListFilesArgs) -> Result<FileList> {
390 let Some(repo) = self.registry.by_id(&a.repo_id).await?.filter(|repo| repo.fork_of.is_none()) else {
391 return Ok(FileList::default());
392 };
393 let git = self.store.open(&store_key(&repo)).await?;
394 let head = a.git_ref.unwrap_or_else(|| repo.default_branch.clone());
395 listing::list(&git, None, &head, &a.skip_dirs, a.limit).await
396 }
397
398 async fn changed_files(&self, a: ChangedFilesArgs) -> Result<FileList> {
399 let Some(git) = self.stored(&a.repo_id).await? else {
400 return Ok(FileList::default());
401 };
402 listing::list(&git, a.base.as_deref(), &a.head, &a.skip_dirs, a.limit).await
403 }
404
Composer from the workspace's own repositories, and go get from g1t.sh405 /// Branches and tags with their commits, for g1t's own services.
406 async fn refs_of(&self, a: RefsArgs) -> Result<Option<RepoRefs>> {
407 let Some(repo) = self.registry.by_id(&a.repo_id).await?.filter(|repo| repo.fork_of.is_none()) else {
408 return Ok(None);
409 };
410 let git = self.store.open(&store_key(&repo)).await?;
411 let access = git.access(Scope::Read).await?;
412 let refs = refs::heads_and_tags(refs::all(&access).await?)
413 .into_iter()
414 .map(|(name, commit)| GitRefEntry { name, commit })
415 .collect();
416 Ok(Some(RepoRefs { repo, refs }))
417 }
418
419 async fn raw_file(&self, a: RawFileArgs) -> Result<Option<RawFile>> {
420 use base64::Engine;
421 let Some(git) = self.stored(&a.repo_id).await? else {
422 return Ok(None);
423 };
424 Ok(git
425 .read_file(&a.git_ref, &a.path)
426 .await?
427 .filter(|bytes| bytes.len() <= a.max_bytes as usize)
428 .map(|bytes| RawFile { size: bytes.len() as u64, data: base64::engine::general_purpose::STANDARD.encode(bytes) }))
429 }
430
431 async fn raw_blobs(&self, a: RawBlobsArgs) -> Result<Vec<RawBlob>> {
432 use base64::Engine;
433 let Some(git) = self.stored(&a.repo_id).await? else {
434 return Ok(Vec::new());
435 };
436 let hashes: Vec<&String> = a.hashes.iter().take(MAX_READ_BLOBS).collect();
437 let mut out = Vec::with_capacity(hashes.len());
438 // A few at a time, as listing::read does: each is a round trip.
439 for group in hashes.chunks(8) {
440 let read = futures_util::future::try_join_all(group.iter().map(|hash| git.read_blob(hash))).await?;
441 for (hash, bytes) in group.iter().zip(read) {
442 let size = bytes.as_ref().map_or(0, |bytes| bytes.len() as u64);
443 let data = bytes
444 .filter(|bytes| bytes.len() <= a.max_bytes as usize)
445 .map(|bytes| base64::engine::general_purpose::STANDARD.encode(bytes));
446 out.push(RawBlob { hash: (*hash).clone(), size, data });
447 }
448 }
449 Ok(out)
450 }
451
Search across all of g1t, Explore, and a command palette452 async fn read_blobs(&self, a: ReadBlobsArgs) -> Result<Vec<BlobText>> {
453 let Some(git) = self.stored(&a.repo_id).await? else {
454 return Ok(Vec::new());
455 };
456 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 shell457 }
458
Rust repos service with shipping; pull requests kept in the model459 async fn create(&self, a: CreateArgs) -> Result<Outcome<Repo>> {
460 if !a.owner.verified {
461 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
462 }
463 let name = a.name.trim().to_lowercase();
464 if !is_valid_repo_name(&name) {
465 return Ok(Outcome::fail(
466 FailureCode::Invalid,
467 "Use letters, digits, dots, hyphens and underscores only.",
468 ));
469 }
Workspaces own repositories470 let namespace = a.namespace.trim().to_lowercase();
471 if namespace.is_empty() {
Rust repos service with shipping; pull requests kept in the model472 return Ok(Outcome::fail(
473 FailureCode::Invalid,
Workspaces own repositories474 "Say which workspace to create the repository in.",
475 ));
476 }
477 if !a.owner.is_member(&namespace) {
478 return Ok(Outcome::fail(
479 FailureCode::Forbidden,
480 "You are not a member of that workspace.",
Rust repos service with shipping; pull requests kept in the model481 ));
482 }
Workspaces own repositories483 let path = RepoPath { namespace, name };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look484 match self.registry.by_path_any(&path).await? {
485 Some((_, None)) => {
486 return Ok(Outcome::fail(
487 FailureCode::Conflict,
488 "That workspace already has a repository with that name.",
489 ));
490 }
491 Some((_, Some(_))) => {
492 return Ok(Outcome::fail(
493 FailureCode::Conflict,
494 format!(
495 "{}/{} 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.",
496 path.namespace, path.name
497 ),
498 ));
499 }
500 None => {}
501 }
502 // With a credential (a GitHub App installation's token), everything
503 // is copied: every branch and tag. See mirror.rs.
504 let mut credentialed = None;
505 if let (Some(url), Some(token)) = (a.import_url.as_deref(), a.import_token.as_deref()) {
506 let Some(url) = import::clean_url(url) else {
507 return Ok(Outcome::fail(FailureCode::Invalid, "That is not an https repository address."));
508 };
509 let source = mirror::Endpoint::github(&url, token);
510 match mirror::probe(&source).await? {
511 Ok(advertised) => credentialed = Some((source, advertised)),
512 Err(reason) => return Ok(Outcome::fail(FailureCode::Invalid, reason)),
513 }
Rust repos service with shipping; pull requests kept in the model514 }
Agents as a team: lifecycle, merge queue, billing and a new shell515 // An import is fetched before anything is created, so that an
516 // address that does not work leaves nothing behind.
517 let mut imported = None;
518 if let Some(url) = a
519 .import_url
520 .as_deref()
521 .map(str::trim)
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look522 .filter(|url| !url.is_empty() && credentialed.is_none())
Agents as a team: lifecycle, merge queue, billing and a new shell523 {
524 let Some(url) = import::clean_url(url) else {
525 return Ok(Outcome::fail(
526 FailureCode::Invalid,
527 "Give the https address of a public repository, such as https://github.com/owner/repo.",
528 ));
529 };
530 let remote = match import::discover(&url).await? {
531 Ok(remote) => remote,
532 Err(reason) => return Ok(Outcome::fail(FailureCode::Invalid, reason)),
533 };
A public import copies every branch and tag, so an imported library keeps its releases534 imported = Some((remote, url));
Agents as a team: lifecycle, merge queue, billing and a new shell535 }
Rust repos service with shipping; pull requests kept in the model536 let now = now_ms();
537 let repo = Repo {
538 id: new_id("rep", now),
539 namespace: path.namespace,
540 name: path.name,
541 description: a
542 .description
543 .map(|text| text.trim().to_owned())
544 .filter(|text| !text.is_empty()),
545 is_private: a.is_private,
546 owner_id: a.owner.id.clone(),
Agents as a team: lifecycle, merge queue, billing and a new shell547 default_branch: imported
548 .as_ref()
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look549 .map(|(remote, _)| remote.branch.clone())
550 .or_else(|| credentialed.as_ref().and_then(|(_, advertised)| advertised.default_branch()))
551 .unwrap_or_else(|| "main".to_owned()),
Rust repos service with shipping; pull requests kept in the model552 fork_of: None,
Agents as a team: lifecycle, merge queue, billing and a new shell553 protected: false,
RFC 3339 timestamps in identity and repos554 created_at: rfc3339(now),
Search across all of g1t, Explore, and a command palette555 topics: Vec::new(),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look556 website: None,
557 archived_at: None,
Rust repos service with shipping; pull requests kept in the model558 };
Merge branch 'worktree-agent-a2013627e5ea4ab13'559 let namespace = match self.place(&repo).await? {
560 Ok(namespace) => namespace,
561 Err(unplaced) => return Ok(Outcome::fail(FailureCode::Conflict, unplaced.message())),
562 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily563 self.registry
564 .claim_store_key(&repo, namespace.as_deref(), &self.store.default_namespace())
565 .await?;
Rust repos service with shipping; pull requests kept in the model566 self.store
567 .create(
568 &store_key(&repo),
569 repo.description.as_deref(),
570 &repo.default_branch,
571 )
572 .await?;
573 self.registry.insert(&repo).await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look574 // A repository that was transferred away from this path stops
575 // redirecting here.
576 self.registry
577 .drop_redirect(&RepoPath {
578 namespace: repo.namespace.clone(),
579 name: repo.name.clone(),
580 })
581 .await?;
A public import copies every branch and tag, so an imported library keeps its releases582 // Every branch and tag the import made, announced as pushes.
583 let mut pushed: Vec<(String, String)> = Vec::new();
584 // A public repository, read with no credential: every branch and
585 // tag is copied too, the default branch the one its HEAD names.
586 if let Some((_, url)) = imported {
Agents as a team: lifecycle, merge queue, billing and a new shell587 let access = self
588 .store
589 .open(&store_key(&repo))
590 .await?
591 .access(Scope::Write)
592 .await?;
A public import copies every branch and tag, so an imported library keeps its releases593 let target = mirror::Endpoint::bearer(&access.remote, &access.token);
594 let copied = mirror::copy(&mirror::Endpoint::anonymous(&url), &target, mirror::Prune::Yes).await?;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms595 self.refs_moved(&repo.id).await;
A public import copies every branch and tag, so an imported library keeps its releases596 match copied {
597 Ok(copied) => pushed = mirror::import_pushes(&copied.updated, &repo.default_branch),
598 Err(reason) => {
599 self.registry.remove(&repo.id).await?;
600 return Ok(Outcome::fail(
601 FailureCode::Invalid,
602 format!("The repository could not be stored: {reason}"),
603 ));
604 }
Agents as a team: lifecycle, merge queue, billing and a new shell605 }
606 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look607 if let Some((source, _)) = credentialed {
608 let access = self
609 .store
610 .open(&store_key(&repo))
611 .await?
612 .access(Scope::Write)
613 .await?;
614 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 200ms615 let copied = mirror::copy(&source, &target, mirror::Prune::Yes).await?;
616 self.refs_moved(&repo.id).await;
617 match copied {
A public import copies every branch and tag, so an imported library keeps its releases618 Ok(copied) => pushed = mirror::import_pushes(&copied.updated, &repo.default_branch),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look619 Err(reason) => {
620 self.registry.remove(&repo.id).await?;
621 return Ok(Outcome::fail(
622 FailureCode::Invalid,
623 format!("The repository could not be copied: {reason}"),
624 ));
625 }
626 }
627 }
Rust repos service with shipping; pull requests kept in the model628 self.publish(NewEvent {
629 kind: "repo.created",
630 source: SOURCE,
631 repo_id: Some(repo.id.clone()),
632 actor: Some(a.owner.id),
633 data: RepoCreated {
634 repo_id: repo.id.clone(),
635 namespace: repo.namespace.clone(),
636 name: repo.name.clone(),
637 is_private: repo.is_private,
638 },
639 })
640 .await?;
A public import copies every branch and tag, so an imported library keeps its releases641 for (git_ref, head) in &pushed {
642 self.publish_push(&repo, git_ref, None, head, None).await?;
Agents as a team: lifecycle, merge queue, billing and a new shell643 }
Rust repos service with shipping; pull requests kept in the model644 Ok(Outcome::Ok(repo))
645 }
646
Merge branch 'worktree-agent-a2013627e5ea4ab13'647 /// Where a workspace keeps its data, asked of identity only when an EU
648 /// namespace is configured: without one, every workspace's
649 /// repositories go anywhere and identity is never asked.
650 async fn residency_of(&self, workspace: &str) -> Result<shards::Residency> {
651 if self.placement.eu.is_none() {
652 return Ok(shards::Residency::Anywhere);
653 }
654 let Some(identity) = &self.identity else {
655 return Ok(shards::Residency::Anywhere);
656 };
657 let residency: Option<g1t_contracts::identity::DataResidency> = g1t_kit::call(
658 identity,
659 "workspace_residency",
660 &g1t_contracts::identity::SlugArgs { slug: workspace.to_owned() },
661 )
662 .await?;
663 Ok(match residency {
664 Some(g1t_contracts::identity::DataResidency::Eu) => shards::Residency::Eu,
665 _ => shards::Residency::Anywhere,
666 })
667 }
668
669 /// How each bound namespace stands (namespaces.rs).
670 async fn standings(&self) -> Result<Vec<namespaces::Standing>> {
671 let bound = self.store.namespaces();
672 let default = self.store.default_namespace();
673 let now = now_ms();
674 let config = namespaces::Configured {
675 bound: &bound,
676 default: &default,
677 placement: &self.placement,
678 limits: &self.limits,
679 on_fallback: &|namespace| self.store.on_fallback(&shards::compose(Some(namespace), "x", &default)),
680 writable: &|namespace| self.store.writable(namespace),
681 breaker_open: &|namespace| resilience::open_now(namespace, now),
682 };
683 let (held, recent) = futures_util::future::join(namespaces::held(&self.registry.db), namespaces::recent(&self.registry.db, now)).await;
684 Ok(namespaces::standings(&config, &held?, &recent?))
685 }
686
687 /// The namespace a new repository goes in (shards.rs): its workspace's
688 /// residency, then how each namespace stands, read only when there is
689 /// a choice to make. `Ok(None)` for the default.
690 async fn place(&self, repo: &Repo) -> Result<std::result::Result<Option<String>, shards::Unplaced>> {
691 let residency = self.residency_of(&repo.namespace).await?;
692 let bound = self.store.namespaces();
693 let loads = if residency == shards::Residency::Anywhere && !self.placement.needs_loads(&bound) {
694 // One namespace to choose from at most: nothing to read.
695 bound
696 .iter()
697 .map(|namespace| shards::Load {
698 namespace: namespace.clone(),
699 bound: true,
700 writable: self.store.writable(namespace),
701 ..shards::Load::default()
702 })
703 .collect()
704 } else {
705 let default = self.store.default_namespace();
706 let now = now_ms();
707 let config = namespaces::Configured {
708 bound: &bound,
709 default: &default,
710 placement: &self.placement,
711 limits: &self.limits,
712 on_fallback: &|namespace| self.store.on_fallback(&shards::compose(Some(namespace), "x", &default)),
713 writable: &|namespace| self.store.writable(namespace),
714 breaker_open: &|namespace| resilience::open_now(namespace, now),
715 };
716 namespaces::loads(&self.registry.db, &config, now).await?
717 };
718 Ok(self.placement.choose(&repo.id, residency, &loads))
719 }
720
721 /// `storage_options`: what a workspace may choose about where its
722 /// repositories are kept.
723 fn storage_options(&self) -> StorageOptions {
724 let bound = self.store.namespaces();
725 StorageOptions { eu_available: self.placement.eu_available(&bound, |namespace| self.store.writable(namespace)) }
726 }
727
Rust repos service with shipping; pull requests kept in the model728 async fn tree(&self, a: TreeArgs) -> Result<Outcome<TreeView>> {
729 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
730 return Ok(not_found());
731 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily732 let git = self.read_git(&repo).await?;
Rust repos service with shipping; pull requests kept in the model733 let git_ref = a
734 .git_ref
735 .clone()
736 .unwrap_or_else(|| repo.default_branch.clone());
737
738 let Some(head) = git.log(&git_ref, 1).await?.into_iter().next() else {
739 // An unknown ref is an error; a repo with no commits is just empty.
740 if a.git_ref.is_some() {
741 return Ok(Outcome::fail(
742 FailureCode::NotFound,
743 "No such branch, tag or commit.",
744 ));
745 }
746 return Ok(Outcome::Ok(TreeView {
747 repo,
748 git_ref,
749 path: a.tree_path,
750 head: None,
751 entries: Vec::new(),
752 readme: None,
753 }));
754 };
755
756 let no_directory = || Outcome::fail(FailureCode::NotFound, "No such directory.");
757 let mut entries = git.read_tree(&head.tree_hash).await?;
758 for segment in a.tree_path.split('/').filter(|segment| !segment.is_empty()) {
759 let next = entries.as_ref().and_then(|entries| {
760 entries
761 .iter()
762 .find(|entry| entry.name == segment && entry.kind == EntryKind::Tree)
763 });
764 let Some(next) = next else {
765 return Ok(no_directory());
766 };
767 entries = git.read_tree(&next.hash).await?;
768 }
769 let Some(mut entries) = entries else {
770 return Ok(no_directory());
771 };
772 // Directories first, then by name.
773 entries.sort_by(|a, b| {
774 (b.kind == EntryKind::Tree)
775 .cmp(&(a.kind == EntryKind::Tree))
776 .then_with(|| a.name.cmp(&b.name))
777 });
778
779 let readme_entry = entries
780 .iter()
781 .find(|entry| entry.kind == EntryKind::Blob && is_readme(&entry.name));
782 let readme = match readme_entry {
783 Some(entry) => git.read_blob(&entry.hash).await?.map(|bytes| Readme {
784 name: entry.name.clone(),
785 text: text_of(bytes),
786 }),
787 None => None,
788 };
789 Ok(Outcome::Ok(TreeView {
790 repo,
791 git_ref,
792 path: a.tree_path,
793 head: Some(head),
794 entries,
795 readme,
796 }))
797 }
798
799 async fn blob(&self, a: BlobArgs) -> Result<Outcome<BlobView>> {
800 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
801 return Ok(not_found());
802 };
803 let bytes = if a.file_path.is_empty() {
804 None
805 } else {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily806 let git = self.read_git(&repo).await?;
Rust repos service with shipping; pull requests kept in the model807 git.read_file(&a.git_ref, &a.file_path).await?
808 };
809 let Some(bytes) = bytes else {
810 return Ok(Outcome::fail(FailureCode::NotFound, "No such file."));
811 };
812 Ok(Outcome::Ok(BlobView {
813 repo,
814 git_ref: a.git_ref,
815 path: a.file_path,
816 size: bytes.len() as u64,
817 text: text_of(bytes),
818 }))
819 }
820
Agents as a team: lifecycle, merge queue, billing and a new shell821 async fn blame(&self, a: BlameArgs) -> Result<Outcome<Blame>> {
822 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
823 return Ok(not_found());
824 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily825 let git = self.read_git(&repo).await?;
Agents as a team: lifecycle, merge queue, billing and a new shell826 let git_ref = a.git_ref.unwrap_or_else(|| repo.default_branch.clone());
827 Ok(match blame::blame(&git, &git_ref, &a.file_path).await? {
828 Some(blame) => Outcome::Ok(blame),
829 None => not_found(),
830 })
831 }
832
Rust repos service with shipping; pull requests kept in the model833 async fn log(&self, a: LogArgs) -> Result<Outcome<Vec<Commit>>> {
834 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
835 return Ok(not_found());
836 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily837 let git = self.read_git(&repo).await?;
Rust repos service with shipping; pull requests kept in the model838 let git_ref = a.git_ref.unwrap_or_else(|| repo.default_branch.clone());
839 Ok(Outcome::Ok(git.log(&git_ref, a.limit).await?))
840 }
841
Branches and Tags pages, each file's last commit, and the branch menu on files842 /// Which commit last changed each entry of a directory. Kept in this
843 /// colo's cache by repository, head commit and path: a commit's history
844 /// never changes, so an answer is good for as long as it is kept.
845 async fn last_commits(&self, a: g1t_contracts::repos::LastCommitsArgs) -> Result<Outcome<g1t_contracts::repos::LastCommits>> {
846 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
847 return Ok(not_found());
848 };
849 let git = self.read_git(&repo).await?;
850 let git_ref = a.git_ref.unwrap_or_else(|| repo.default_branch.clone());
851 let Some(head) = git.log(&git_ref, 1).await?.into_iter().next() else {
852 return Ok(Outcome::fail(FailureCode::NotFound, "No such branch, tag or commit."));
853 };
854 let key = format!(
855 "https://last-commits.g1t.internal/{}/{}/{}",
856 repo.id,
857 head.hash,
858 a.tree_path.split('/').map(urlencoding_segment).collect::<Vec<_>>().join("/")
859 );
860 let cache = worker::Cache::default();
861 if let Ok(Some(mut kept)) = cache.get(key.as_str(), false).await {
862 if let Ok(found) = kept.json::<g1t_contracts::repos::LastCommits>().await {
863 return Ok(Outcome::Ok(found));
864 }
865 }
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait866 // Asked with a budget: past it, what was found so far, not kept.
A last-commits walk answers within 2.5 s with what it has, and keeps only finished answers867 let started = worker::Date::now().as_millis();
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait868 let budget = a.budget_ms;
869 let out_of_time = move || budget.is_some_and(|budget| worker::Date::now().as_millis().saturating_sub(started) > budget);
A last-commits walk answers within 2.5 s with what it has, and keeps only finished answers870 let (entries, complete) = last_commits::last_commits(&git, &head.hash, &a.tree_path, &out_of_time).await?;
871 let stopped = out_of_time();
Branches and Tags pages, each file's last commit, and the branch menu on files872 let found = g1t_contracts::repos::LastCommits { entries, complete };
A last-commits walk answers within 2.5 s with what it has, and keeps only finished answers873 if stopped && !found.complete {
874 return Ok(Outcome::Ok(found));
875 }
Branches and Tags pages, each file's last commit, and the branch menu on files876 if let Ok(mut response) = worker::Response::from_json(&found) {
877 let _ = response.headers_mut().set("cache-control", "max-age=604800");
878 let _ = cache.put(key.as_str(), response).await;
879 }
880 Ok(Outcome::Ok(found))
881 }
882
883 /// The repository's tags, newest commit first, at most 100.
884 async fn tags(&self, a: g1t_contracts::repos::TagsArgs) -> Result<Outcome<Vec<g1t_contracts::repos::Tag>>> {
885 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
886 return Ok(not_found());
887 };
888 let git = self.store.open(&store_key(&repo)).await?;
889 let access = git.access(Scope::Read).await?;
890 let named: Vec<(String, String)> = refs::heads_and_tags(refs::all(&access).await?)
891 .into_iter()
892 .filter_map(|(name, hash)| name.strip_prefix("refs/tags/").map(|tag| (tag.to_owned(), hash)))
893 .collect();
894 let read = self.read_git(&repo).await?;
895 let commits = futures_util::future::join_all(named.iter().take(MAX_TAGS_READ).map(|(_, hash)| read.log(hash, 1))).await;
896 let mut tags: Vec<g1t_contracts::repos::Tag> = named
897 .into_iter()
898 .zip(commits.into_iter().map(|found| found.ok().and_then(|list| list.into_iter().next())).chain(std::iter::repeat(None)))
899 .map(|((name, _), commit)| g1t_contracts::repos::Tag { name, commit })
900 .collect();
901 tags.sort_by(|a, b| {
902 let at = |tag: &g1t_contracts::repos::Tag| tag.commit.as_ref().map(|c| c.authored_at.clone()).unwrap_or_default();
903 at(b).cmp(&at(a)).then_with(|| b.name.cmp(&a.name))
904 });
905 tags.truncate(MAX_TAGS_READ);
906 Ok(Outcome::Ok(tags))
907 }
908
Pull requests from branches909 /// The repository's branches, default branch first.
910 async fn branches(&self, a: BranchesArgs) -> Result<Outcome<Vec<Branch>>> {
911 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
912 return Ok(not_found());
913 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily914 let mut branches = self.read_git(&repo).await?.branches().await?;
Pull requests from branches915 branches.sort_by_key(|branch| branch.name != repo.default_branch);
916 Ok(Outcome::Ok(branches))
917 }
918
Agents as a team: lifecycle, merge queue, billing and a new shell919 /// Whether a pull request's source lacks commits that the branch it
920 /// would merge into has.
921 async fn behind(&self, a: BehindArgs) -> Result<bool> {
922 let Some(source) = self.registry.by_id(&a.source_id).await? else {
923 return Ok(false);
924 };
925 let target = match &source.fork_of {
926 Some(id) => self.registry.by_id(id).await?,
927 None => Some(source.clone()),
928 };
929 let Some(target) = target else {
930 return Ok(false);
931 };
932 let branch = a.branch.unwrap_or_else(|| target.default_branch.clone());
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar933 let target_branch = a.target_branch.unwrap_or_else(|| target.default_branch.clone());
Agents as a team: lifecycle, merge queue, billing and a new shell934 let target_head = self
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily935 .read_git(&target)
Agents as a team: lifecycle, merge queue, billing and a new shell936 .await?
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar937 .log(&target_branch, 1)
Agents as a team: lifecycle, merge queue, billing and a new shell938 .await?
939 .into_iter()
940 .next()
941 .map(|commit| commit.hash);
942 let Some(target_head) = target_head else {
943 return Ok(false);
944 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily945 let source_git = self.read_git(&source).await?;
Agents as a team: lifecycle, merge queue, billing and a new shell946 let history = source_git.log(&branch, MAX_ANCESTRY).await?;
947 if history.is_empty() {
948 return Ok(false);
949 }
950 Ok(!descends_from(&source_git, &history, &target_head).await?)
951 }
952
Agents and memory, checks and conflicts, profiles, slug renames, custom domains953 /// The files a pull request's source and the default branch it would
954 /// merge into each changed since they last agreed. Where the two lists
955 /// share no file, the merge cannot conflict; where they do, it may.
956 async fn divergence(&self, a: BehindArgs) -> Result<Option<Divergence>> {
957 let Some(source) = self.registry.by_id(&a.source_id).await? else {
958 return Ok(None);
959 };
960 let target = match &source.fork_of {
961 Some(id) => self.registry.by_id(id).await?,
962 None => Some(source.clone()),
963 };
964 let Some(target) = target else {
965 return Ok(None);
966 };
967 let branch = a.branch.unwrap_or_else(|| target.default_branch.clone());
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar968 let target_branch = a.target_branch.unwrap_or_else(|| target.default_branch.clone());
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily969 let source_git = self.read_git(&source).await?;
970 let target_git = self.read_git(&target).await?;
971 // The target's side is the same for every pull request into it, and
972 // worked out once per head (coalesce.rs).
973 let (history, side) = futures_util::future::try_join(
Agents and memory, checks and conflicts, profiles, slug renames, custom domains974 source_git.log(&branch, MAX_ANCESTRY),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar975 self.target_side(&target, &target_git, &target_branch),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains976 )
977 .await?;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily978 let target_history = &side.history;
Agents and memory, checks and conflicts, profiles, slug renames, custom domains979 let (Some(head), Some(base)) = (history.first(), target_history.first()) else {
980 return Ok(None);
981 };
982 let behind = !descends_from(&source_git, &history, &base.hash).await?;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily983 let merge_base = nearest_ancestor_in(&source_git, &history, &side.shared).await?;
Agents and memory, checks and conflicts, profiles, slug renames, custom domains984 let mut divergence = Divergence {
985 head: head.hash.clone(),
986 base: base.hash.clone(),
987 merge_base: merge_base.clone(),
988 behind,
989 ..Divergence::default()
990 };
991 let merge_base_tree = match &merge_base {
992 Some(hash) => target_history
993 .iter()
994 .find(|commit| commit.hash == *hash)
995 .map(|commit| commit.tree_hash.clone()),
996 None => None,
997 };
998 let Some(merge_base_tree) = merge_base_tree else {
999 // No common history to compare from: say nothing is known.
1000 divergence.truncated = true;
1001 return Ok(Some(divergence));
1002 };
1003 let (ours, truncated_ours) =
1004 diff::changed_paths(&source_git, Some(&merge_base_tree), &head.tree_hash).await?;
1005 divergence.ours = ours;
1006 divergence.truncated = truncated_ours;
1007 if behind {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1008 let now = now_ms();
1009 let key = (target.id.clone(), merge_base_tree.clone(), base.tree_hash.clone());
1010 let (theirs, truncated_theirs) = match THEIRS.with(|memo| memo.borrow().get(&key, now)) {
1011 Some(kept) => kept,
1012 None => {
1013 let found = diff::changed_paths(&target_git, Some(&merge_base_tree), &base.tree_hash).await?;
1014 THEIRS.with(|memo| memo.borrow_mut().put(key, found.clone(), now));
1015 found
1016 }
1017 };
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1018 divergence.theirs = theirs;
1019 divergence.truncated |= truncated_theirs;
1020 }
1021 Ok(Some(divergence))
1022 }
1023
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1024 /// A target branch's history from its head, worked out once per head
1025 /// for every pull request asking about it (coalesce.rs). The head is
1026 /// read under the refs version; the history by its hash, which the
1027 /// object cache keeps for good.
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1028 async fn target_side<R: GitRepo>(&self, target: &Repo, git: &R, branch: &str) -> Result<Rc<coalesce::TargetSide>> {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1029 let now = now_ms();
1030 let key = refs_cache::usable(registry::refs_state(&target.id), now)
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1031 .map(|version| (target.id.clone(), branch.to_owned(), version));
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1032 if let Some(key) = &key
1033 && let Some(side) = TARGETS.with(|memo| memo.borrow().get(key, now))
1034 {
1035 return Ok(side);
1036 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1037 let history = match git.log(branch, 1).await?.first() {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1038 Some(head) => git.log(&head.hash, MAX_ANCESTRY).await?,
1039 None => Vec::new(),
1040 };
1041 let side = coalesce::TargetSide::new(history);
1042 if let Some(key) = key {
1043 TARGETS.with(|memo| memo.borrow_mut().put(key, side.clone(), now));
1044 }
1045 Ok(side)
1046 }
1047
Pull requests from branches1048 async fn head(&self, a: HeadArgs) -> Result<Option<String>> {
1049 let Some(repo) = self.registry.by_id(&a.repo_id).await? else {
1050 return Ok(None);
1051 };
Workflows run when an agent's pull request is marked ready1052 let branch = if a.branch.is_empty() { &repo.default_branch } else { &a.branch };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1053 let git = self.read_git(&repo).await?;
Pull requests from branches1054 Ok(git
Workflows run when an agent's pull request is marked ready1055 .log(branch, 1)
Pull requests from branches1056 .await?
1057 .into_iter()
1058 .next()
1059 .map(|commit| commit.hash))
1060 }
1061
Merge queue: tested states are deleted once their entry leaves1062 async fn delete_branch(&self, a: DeleteBranchArgs) -> Result<Outcome<bool>> {
1063 if !a.branch.starts_with(G1T_BRANCH_PREFIX) {
1064 return Ok(Outcome::fail(
1065 FailureCode::Forbidden,
1066 "Only branches g1t made for itself can be deleted this way.",
1067 ));
1068 }
1069 let Some(repo) = self.registry.by_id(&a.repo_id).await? else {
1070 return Ok(not_found());
1071 };
Merge branch 'worktree-agent-a2013627e5ea4ab13'1072 let repo = match self.unpaused(repo).await? {
1073 Ok(repo) => repo,
1074 Err((code, message)) => return Ok(Outcome::fail(code, message)),
1075 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1076 self.live(&repo).await?;
Merge queue: tested states are deleted once their entry leaves1077 let git = self.store.open(&store_key(&repo)).await?;
1078 let Some(old) = git
1079 .branches()
1080 .await?
1081 .into_iter()
1082 .find(|branch| branch.name == a.branch)
1083 .map(|branch| branch.hash)
1084 else {
1085 return Ok(Outcome::Ok(false));
1086 };
1087 let access = git.access(Scope::Write).await?;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1088 let deleted = land::delete_ref(&access, &a.branch, &old).await?;
1089 self.refs_moved(&repo.id).await;
1090 if let Err(reason) = deleted {
Merge queue: tested states are deleted once their entry leaves1091 return Ok(Outcome::fail(
1092 FailureCode::Conflict,
1093 format!("{} could not be deleted: {reason}", a.branch),
1094 ));
1095 }
1096 Ok(Outcome::Ok(true))
1097 }
1098
Issues and pull requests replace intents and attempts1099 async fn fork_for_pull(&self, a: ForkArgs) -> Result<Outcome<Repo>> {
Rust repos service with shipping; pull requests kept in the model1100 let viewer = Some(a.actor.clone());
1101 let Some(source) = self
1102 .registry
1103 .by_id(&a.source_id)
1104 .await?
1105 .filter(|repo| can_read(repo, &viewer))
1106 else {
1107 return Ok(not_found());
1108 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1109 if let Some((code, message)) = lifecycle::archived_refusal(&source) {
1110 return Ok(Outcome::fail(code, message));
1111 }
Merge branch 'worktree-agent-a2013627e5ea4ab13'1112 // Its working copy is made in its namespace: not while it moves.
1113 let source = match self.unpaused(source).await? {
1114 Ok(source) => source,
1115 Err((code, message)) => return Ok(Outcome::fail(code, message)),
1116 };
Rust repos service with shipping; pull requests kept in the model1117 let now = now_ms();
1118 let fork = Repo {
1119 id: new_id("rep", now),
Issues and pull requests replace intents and attempts1120 namespace: PULLS_NAMESPACE.to_owned(),
1121 name: a.pull_id.clone(),
Rust repos service with shipping; pull requests kept in the model1122 description: None,
1123 // A fork is exactly as visible as the repo it came from.
1124 is_private: source.is_private,
1125 owner_id: a.actor.id.clone(),
1126 default_branch: source.default_branch.clone(),
1127 fork_of: Some(source.id.clone()),
Agents as a team: lifecycle, merge queue, billing and a new shell1128 protected: false,
RFC 3339 timestamps in identity and repos1129 created_at: rfc3339(now),
Search across all of g1t, Explore, and a command palette1130 topics: Vec::new(),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1131 website: None,
1132 archived_at: None,
Rust repos service with shipping; pull requests kept in the model1133 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1134 // Artifacts forks within a namespace: the copy goes where its
1135 // repository is.
1136 let (namespace, _) = store::locate(&store_key(&source));
1137 self.registry
1138 .claim_store_key(&fork, Some(&namespace), &self.store.default_namespace())
1139 .await?;
Rust repos service with shipping; pull requests kept in the model1140 self.store
1141 .open(&store_key(&source))
1142 .await?
1143 .fork(&store_key(&fork))
1144 .await?;
1145 self.registry.insert(&fork).await?;
1146 self.publish(NewEvent {
1147 kind: "repo.forked",
1148 source: SOURCE,
1149 repo_id: Some(source.id.clone()),
1150 actor: Some(a.actor.id),
1151 data: RepoForked {
1152 repo_id: fork.id.clone(),
1153 source_repo_id: source.id,
Issues and pull requests replace intents and attempts1154 pull_id: a.pull_id,
Rust repos service with shipping; pull requests kept in the model1155 },
1156 })
1157 .await?;
1158 Ok(Outcome::Ok(fork))
1159 }
1160
1161 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 200ms1162 let found = self.registry.by_path(&a.path).await?;
1163 Ok(match self.authorize_git(&a.path, &a.viewer, a.service, found).await? {
1164 Outcome::Ok(repo) => {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1165 self.live(&repo).await?;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1166 let write = a.service == GitService::ReceivePack;
1167 if write {
1168 // A push with this credential would not pass through
1169 // here, so nothing that lists the refs is kept until it
1170 // has expired (see refs_cache.rs).
1171 let until = now_ms() + store::CREDENTIAL_LIFE_MS + 60_000;
1172 if let Err(error) = self.registry.refs_open(&repo.id, until).await {
1173 // Before the column exists nothing is kept anyway.
1174 if registry::refs_state(&repo.id).is_some() {
1175 return Err(error);
1176 }
1177 }
1178 }
1179 let scope = if write { Scope::Write } else { Scope::Read };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1180 Outcome::Ok(self.store.handout(&store_key(&repo), scope).await?)
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1181 }
1182 Outcome::Fail(failure) => Outcome::Fail(failure),
1183 })
1184 }
1185
1186 /// The repository at `path` (`found`, as just read), if the viewer may
1187 /// use `service` on it: fetch from it, or push to it. A push to a path
1188 /// with nothing there makes the repository, in a workspace the pusher
1189 /// belongs to.
1190 async fn authorize_git(
1191 &self,
1192 path: &RepoPath,
1193 viewer: &Viewer,
1194 service: GitService,
1195 found: Option<Repo>,
1196 ) -> Result<Outcome<Repo>> {
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1197 let mut a = GitAccessArgs {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1198 path: path.clone(),
1199 viewer: viewer.clone(),
1200 service,
1201 };
Rust repos service with shipping; pull requests kept in the model1202 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 step1203 // An access token: pushing needs code:write, reading a private
1204 // repository code:read. A public repository reads as it would for
1205 // anyone. Which repositories a token reaches is its owner's, checked
1206 // below as for anyone.
1207 if let Some(access) = a.viewer.as_ref().and_then(|user| user.token.as_deref()).cloned() {
1208 let public = found.as_ref().is_some_and(|repo| !repo.is_private);
1209 let decision = g1t_contracts::scopes::decide_git(&access, write, public);
1210 if !decision.allowed {
1211 return Ok(Outcome::fail(
1212 FailureCode::Forbidden,
1213 format!("{}\n", decision.reason.unwrap_or_default()),
1214 ));
1215 }
1216 if !write && !access.allows(g1t_contracts::scopes::Scope::CodeRead) {
1217 a.viewer = None;
1218 }
1219 }
1220
Rust repos service with shipping; pull requests kept in the model1221 // Anonymous callers are asked to authenticate whether or not the repo
1222 // exists, so private repos cannot be told apart from missing ones.
1223 let denied = || match &a.viewer {
1224 Some(_) => not_found(),
1225 None => Outcome::fail(FailureCode::Unauthenticated, "Authentication required."),
1226 };
Agents as a team: lifecycle, merge queue, billing and a new shell1227 // An agent's token works through the API only: its sandbox has its
1228 // own way to push, to its own pull request.
1229 if a.viewer.as_ref().is_some_and(|user| user.kind == PrincipalKind::Agent) {
1230 return Ok(Outcome::fail(
1231 FailureCode::Forbidden,
1232 "A g1t agent's token cannot be used with git.",
1233 ));
1234 }
Rust repos service with shipping; pull requests kept in the model1235 if let (true, Some(user)) = (write, &a.viewer)
1236 && !user.verified
1237 {
1238 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
1239 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1240 let repo = match found {
Rust repos service with shipping; pull requests kept in the model1241 Some(repo) => {
1242 let allowed = if write {
1243 can_write(&repo, &a.viewer)
1244 } else {
Members can read a private repository's pull request forks1245 self.may_read(&repo, &a.viewer).await?
Rust repos service with shipping; pull requests kept in the model1246 };
1247 if !allowed {
1248 return Ok(denied());
1249 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1250 // An archived repository, or a pull request's copy of one,
1251 // is read-only.
1252 if write {
1253 let archived = match &repo.fork_of {
1254 Some(source) => self.registry.by_id(source).await?,
1255 None => Some(repo.clone()),
1256 };
1257 match archived {
1258 Some(source) => {
1259 if let Some((code, message)) = lifecycle::archived_refusal(&source) {
1260 return Ok(Outcome::fail(code, format!("{message}\n")));
1261 }
1262 }
1263 // The repository it was copied from is deleted.
1264 None => return Ok(denied()),
1265 }
1266 }
Rust repos service with shipping; pull requests kept in the model1267 repo
1268 }
1269 None => {
Workspaces own repositories1270 // Push to create, in a workspace the pusher belongs to.
Rust repos service with shipping; pull requests kept in the model1271 let owner = a
1272 .viewer
1273 .as_ref()
Workspaces own repositories1274 .filter(|user| write && user.is_member(&a.path.namespace.to_lowercase()));
Rust repos service with shipping; pull requests kept in the model1275 let Some(owner) = owner else {
1276 return Ok(denied());
1277 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1278 let created = self.create(push_to_create(owner, &a.path)).await?;
Rust repos service with shipping; pull requests kept in the model1279 match created {
1280 Outcome::Ok(repo) => repo,
1281 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
1282 }
1283 }
1284 };
Merge branch 'worktree-agent-a2013627e5ea4ab13'1285 // A push, or a credential to push with, waits while the repository
1286 // moves between namespaces (moves.rs), and goes to where it is now.
1287 if write {
1288 return Ok(match self.unpaused(repo).await? {
1289 Ok(repo) => Outcome::Ok(repo),
1290 Err((code, message)) => Outcome::fail(code, format!("{message}\n")),
1291 });
1292 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1293 Ok(Outcome::Ok(repo))
Rust repos service with shipping; pull requests kept in the model1294 }
1295
1296 async fn land(&self, a: LandArgs) -> Result<Outcome<Landed>> {
1297 let actor: Viewer = Some(a.actor.clone());
Pull requests from branches1298 let Some(source) = self.registry.by_id(&a.source_id).await? else {
Rust repos service with shipping; pull requests kept in the model1299 return Ok(not_found());
1300 };
Pull requests from branches1301 // A fork lands on the repository it came from; a branch on its own.
1302 let target = match &source.fork_of {
Rust repos service with shipping; pull requests kept in the model1303 Some(id) => self.registry.by_id(id).await?,
Pull requests from branches1304 None => Some(source.clone()),
Rust repos service with shipping; pull requests kept in the model1305 };
1306 let Some(target) = target.filter(|repo| can_read(repo, &actor)) else {
1307 return Ok(not_found());
1308 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1309 if !registry::can(&target, &actor, Capability::Merge) {
Rust repos service with shipping; pull requests kept in the model1310 return Ok(Outcome::fail(
1311 FailureCode::Forbidden,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1312 access::needs(Capability::Merge, &format!("{}/{}", target.namespace, target.name)),
Rust repos service with shipping; pull requests kept in the model1313 ));
1314 }
1315 if !a.actor.verified {
1316 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
1317 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1318 if let Some((code, message)) = lifecycle::archived_refusal(&target) {
1319 return Ok(Outcome::fail(code, message));
1320 }
Merge branch 'worktree-agent-a2013627e5ea4ab13'1321 // Moving between namespaces: wait for it (moves.rs). Both are read
1322 // again once it is done, for their new keys.
1323 let (source, target) = match (self.unpaused(source).await?, self.unpaused(target).await?) {
1324 (Ok(source), Ok(target)) => (source, target),
1325 (Err((code, message)), _) | (_, Err((code, message))) => return Ok(Outcome::fail(code, message)),
1326 };
Rust repos service with shipping; pull requests kept in the model1327
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1328 let branch = &a.target_branch.clone().unwrap_or_else(|| target.default_branch.clone());
Pull requests from branches1329 let from_fork = source.id != target.id;
1330 let source_branch = match a.branch {
1331 Some(name) if !from_fork && name == *branch => {
1332 return Ok(Outcome::fail(
1333 FailureCode::Invalid,
1334 format!("{branch} cannot be merged into itself."),
1335 ));
1336 }
1337 Some(name) => name,
1338 None if from_fork => branch.clone(),
1339 None => {
1340 return Ok(Outcome::fail(
1341 FailureCode::Invalid,
1342 "Say which branch to merge.",
1343 ));
1344 }
1345 };
1346
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1347 self.live(&source).await?;
Pull requests from branches1348 let source_git = self.store.open(&store_key(&source)).await?;
Rust repos service with shipping; pull requests kept in the model1349 let target_git = self.store.open(&store_key(&target)).await?;
Pull requests from branches1350 let history = source_git.log(&source_branch, MAX_ANCESTRY).await?;
Rust repos service with shipping; pull requests kept in the model1351 let Some(new) = history.first().map(|commit| commit.hash.clone()) else {
1352 return Ok(Outcome::fail(
1353 FailureCode::Conflict,
Issues and pull requests replace intents and attempts1354 "This pull request has no commits to merge.",
Rust repos service with shipping; pull requests kept in the model1355 ));
1356 };
1357 let old = target_git
1358 .log(branch, 1)
1359 .await?
1360 .into_iter()
1361 .next()
1362 .map(|commit| commit.hash);
1363
1364 if old.as_deref() == Some(new.as_str()) {
Diffs on attempts; hosted agent presented as the g1t agent1365 return Ok(Outcome::Ok(Landed {
1366 commit: new,
1367 previous: None,
1368 }));
Rust repos service with shipping; pull requests kept in the model1369 }
1370 // Moving the branch to a commit that does not descend from its
1371 // current head would discard whatever landed in between.
1372 if let Some(old) = &old
Pull requests from branches1373 && !descends_from(&source_git, &history, old).await?
Rust repos service with shipping; pull requests kept in the model1374 {
Pull requests from branches1375 let remedy = if from_fork {
1376 format!("Pull {branch} into the pull request's fork, push, and merge again.")
1377 } else {
1378 format!("Merge {branch} into {source_branch}, push, and merge again.")
1379 };
Rust repos service with shipping; pull requests kept in the model1380 return Ok(Outcome::fail(
1381 FailureCode::Conflict,
Pull requests from branches1382 format!("{branch} has moved since this pull request was opened. {remedy}"),
Rust repos service with shipping; pull requests kept in the model1383 ));
1384 }
1385
Pull requests from branches1386 // For a branch the objects are already in the target; sending them
1387 // again is harmless and keeps one way of moving a ref.
1388 let source_access = source_git.access(Scope::Read).await?;
Rust repos service with shipping; pull requests kept in the model1389 let target_access = target_git.access(Scope::Write).await?;
1390 let pushed =
1391 land::fast_forward(&source_access, &target_access, branch, old.as_deref(), &new)
1392 .await?;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1393 self.refs_moved(&target.id).await;
Rust repos service with shipping; pull requests kept in the model1394 if let Err(reason) = pushed {
Issues and pull requests replace intents and attempts1395 // Most often another pull request landed between the check and the push.
Rust repos service with shipping; pull requests kept in the model1396 return Ok(Outcome::fail(
1397 FailureCode::Conflict,
1398 format!("{branch} could not be updated: {reason}"),
1399 ));
1400 }
GitHub Actions on g1t, part one: reading workflows1401 self.publish_push(
1402 &target,
1403 &format!("refs/heads/{branch}"),
1404 old.as_deref(),
1405 &new,
1406 Some(a.actor.id),
1407 )
Events service in Rust, with RFC 3339 times and accurate push events1408 .await?;
Diffs on attempts; hosted agent presented as the g1t agent1409 Ok(Outcome::Ok(Landed {
1410 commit: new,
1411 previous: old,
1412 }))
1413 }
1414
1415 async fn compare(&self, a: CompareArgs) -> Result<Outcome<Comparison>> {
1416 let Some(repo) = self
Members can read a private repository's pull request forks1417 .visible(self.registry.by_id(&a.repo_id).await?, &a.viewer)
Diffs on attempts; hosted agent presented as the g1t agent1418 .await?
1419 else {
1420 return Ok(not_found());
1421 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1422 let git = self.read_git(&repo).await?;
Pull requests from branches1423 let head_ref = a.head.as_deref().unwrap_or(&repo.default_branch);
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1424 // A pull request into another branch is compared from where it
1425 // left that branch.
1426 let base_branch = a.base_branch.clone();
Agents as a team: lifecycle, merge queue, billing and a new shell1427 // The head's history is only searched when the base is worked out
1428 // from another branch.
1429 let depth = if a.base.is_some() || is_commit_hash(head_ref) { 1 } else { MAX_ANCESTRY };
1430 let history = git.log(head_ref, depth).await?;
Diffs on attempts; hosted agent presented as the g1t agent1431 let Some(head) = history.first() else {
1432 return Ok(Outcome::fail(
1433 FailureCode::Conflict,
Pull requests from branches1434 "There are no commits to compare.",
Diffs on attempts; hosted agent presented as the g1t agent1435 ));
1436 };
1437
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1438 // Where the head's history meets the default branch of `against`,
1439 // or the branch asked for.
Pull requests from branches1440 let shared_with = async |against: &Repo| -> Result<Option<String>> {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1441 let against_git = self.read_git(against).await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1442 let branch = base_branch.as_deref().unwrap_or(&against.default_branch);
Pull requests from branches1443 let shared: HashSet<String> = against_git
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1444 .log(branch, MAX_ANCESTRY)
Pull requests from branches1445 .await?
1446 .into_iter()
1447 .map(|commit| commit.hash)
1448 .collect();
1449 nearest_ancestor_in(&git, &history, &shared).await
1450 };
Diffs on attempts; hosted agent presented as the g1t agent1451 let base = match (a.base, &repo.fork_of) {
1452 (Some(base), _) => Some(base),
1453 // A fork is compared with the last commit it shares with the
1454 // repository it came from.
1455 (None, Some(target_id)) => match self.registry.by_id(target_id).await? {
Pull requests from branches1456 Some(target) => shared_with(&target).await?,
Diffs on attempts; hosted agent presented as the g1t agent1457 None => None,
1458 },
Pull requests from branches1459 // A branch, with the point where it left the default branch.
Agents as a team: lifecycle, merge queue, billing and a new shell1460 // A single commit, with its first parent.
1461 (None, None) if is_commit_hash(head_ref) => head.parents.first().cloned(),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1462 (None, None) if head_ref != base_branch.as_deref().unwrap_or(&repo.default_branch) => {
1463 shared_with(&repo).await?
1464 }
Diffs on attempts; hosted agent presented as the g1t agent1465 (None, None) => head.parents.first().cloned(),
1466 };
1467 let base_tree = match &base {
1468 Some(base) => git
1469 .log(base, 1)
1470 .await?
1471 .into_iter()
1472 .next()
1473 .map(|commit| commit.tree_hash),
1474 None => None,
1475 };
1476 let (files, truncated) =
1477 diff::compare_trees(&git, base_tree.as_deref(), &head.tree_hash).await?;
1478 Ok(Outcome::Ok(Comparison {
1479 base,
1480 head: head.hash.clone(),
1481 files,
1482 truncated,
1483 }))
Rust repos service with shipping; pull requests kept in the model1484 }
1485
GitHub Actions on g1t, part one: reading workflows1486 /// Reports that `git_ref` of `repo` (a full ref) now points to `after`.
Events service in Rust, with RFC 3339 times and accurate push events1487 async fn publish_push(
1488 &self,
1489 repo: &Repo,
GitHub Actions on g1t, part one: reading workflows1490 git_ref: &str,
1491 before: Option<&str>,
Events service in Rust, with RFC 3339 times and accurate push events1492 after: &str,
1493 actor: Option<String>,
1494 ) -> Result<()> {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1495 self.publish_git_push(repo, git_ref, before, after, actor, false).await
1496 }
1497
1498 /// `publish_push`, saying whether the push reached the store without
1499 /// being scanned for secrets first.
1500 async fn publish_git_push(
1501 &self,
1502 repo: &Repo,
1503 git_ref: &str,
1504 before: Option<&str>,
1505 after: &str,
1506 actor: Option<String>,
1507 unscanned: bool,
1508 ) -> Result<()> {
Rust repos service with shipping; pull requests kept in the model1509 self.publish(NewEvent {
1510 kind: "git.push",
1511 source: SOURCE,
1512 repo_id: Some(repo.id.clone()),
1513 actor,
1514 data: GitPush {
1515 repo_id: repo.id.clone(),
GitHub Actions on g1t, part one: reading workflows1516 git_ref: git_ref.to_owned(),
1517 before: before.map(str::to_owned),
Rust repos service with shipping; pull requests kept in the model1518 after: after.to_owned(),
GitHub Actions on g1t, part one: reading workflows1519 default_branch: git_ref.strip_prefix("refs/heads/")
1520 == Some(repo.default_branch.as_str()),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1521 unscanned,
Rust repos service with shipping; pull requests kept in the model1522 },
1523 })
1524 .await
1525 }
1526
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1527 /// Git over HTTPS. Only what decides the answer happens before it:
1528 /// the repository, who is asking and whether they may, the free
1529 /// workspace limits, push protection, and the store's own answer. The
1530 /// audit entry and what a push changed are recorded once git has its
1531 /// answer. Each answer says how long its steps took (`Server-Timing`).
1532 async fn git_http(&self, request: Request, env: &Env, ctx: &Context) -> Result<Response> {
1533 let mut timing = git_http::Timing::start();
Rust repos service with shipping; pull requests kept in the model1534 let Some(git) = git_http::parse(&request.url()?) else {
1535 return Response::error("Not found", 404);
1536 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1537 let response = match self.answer_git(request, &git, env, ctx, &mut timing).await {
1538 Ok(response) => response,
1539 // The git store is busy: git hears when to try again.
1540 Err(error) => match resilience::busy(&error.to_string()) {
1541 Some(busy) => git_http::busy_response(busy)?,
1542 None => return Err(error),
1543 },
1544 };
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1545 timing.apply(response)
1546 }
1547
1548 async fn answer_git(
1549 &self,
1550 request: Request,
1551 git: &git_http::GitRequest,
1552 env: &Env,
1553 ctx: &Context,
1554 timing: &mut git_http::Timing,
1555 ) -> Result<Response> {
1556 let write = git.service == GitService::ReceivePack;
1557 let get = request.method() == Method::Get;
1558 let identity = env.service("IDENTITY")?;
1559 // The repository and the caller's credentials, at once. A fetch may
1560 // go by the row as read a moment ago, for the same clone's next
1561 // request; a push always reads it. Anonymous callers cost nothing.
1562 let lookup = async {
1563 if write {
1564 self.registry.by_path(&git.path).await
1565 } else {
1566 self.registry.by_path_recent(&git.path).await
1567 }
1568 };
1569 let (found, viewer) =
1570 futures_util::future::join(lookup, git_http::viewer(&request, &identity)).await;
Merge branch 'worktree-agent-a8385d293d42c913a'1571 let mut found = found?;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1572 timing.mark("repo");
Merge branch 'worktree-agent-a8385d293d42c913a'1573 // A workspace alias staff set (identity's aliases.rs: `g1t` for
1574 // `flagon-io`) is answered in place, as the repository under the
1575 // workspace's slug: pushes and some clients do not follow
1576 // redirects. Everything after this sees only the workspace's slug.
1577 let aliased = match found {
1578 Some(_) => None,
1579 None => git_http::aliased(git, &identity).await?,
1580 };
1581 if let Some(aliased) = &aliased {
1582 found = if write {
1583 self.registry.by_path(&aliased.path).await?
1584 } else {
1585 self.registry.by_path_recent(&aliased.path).await?
1586 };
1587 timing.mark("alias");
1588 }
1589 let git = aliased.as_ref().unwrap_or(git);
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1590 if found.is_none() {
1591 // A workspace that was renamed: git follows a redirect when it
1592 // first asks for refs, and uses the new address from then on.
1593 // A repository transferred to another workspace: the same, to
1594 // its new path. Fetches and pushes both follow either.
1595 let url = request.url()?;
1596 let (renamed, moved) = futures_util::future::join(
1597 git_http::renamed(&url, &identity),
1598 self.registry.resolve_moved(&git.path),
1599 )
1600 .await;
1601 timing.mark("moved");
1602 if let Some(location) = renamed? {
1603 return git_http::moved(&location, get);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1604 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1605 if let Some(now) = moved?
1606 && let Some(location) = git_http::transferred(&url, &now)
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1607 {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1608 return git_http::moved(&location, get);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1609 }
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1610 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1611 let viewer = viewer?;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1612 // A run credential is checked against its grants, then acts as the
1613 // 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 200ms1614 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 API1615 run_access::Admitted::Go { request, viewer, entry } => (request, viewer, entry),
1616 run_access::Admitted::Refused(response) => return Ok(response),
1617 };
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1618 let mut after = AfterGit {
1619 audit,
1620 status: 0,
1621 message: None,
1622 push: None,
1623 };
1624 let repo = match self.authorize_git(&git.path, &viewer, git.service, found).await? {
1625 Outcome::Ok(repo) => repo,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1626 refused => {
1627 let response = git_http::refuse(refused)?;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1628 after.ended(response.status_code(), None);
1629 after.spawn(env, ctx);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1630 return Ok(response);
1631 }
Rust repos service with shipping; pull requests kept in the model1632 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1633 // A pull request's working copy removed after it closed is made
1634 // again before git uses it (forks.rs).
1635 self.live(&repo).await?;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1636 timing.mark("access");
Agents as a team: lifecycle, merge queue, billing and a new shell1637 // A protected default branch takes changes only from a merged pull
1638 // request, which lands without going through here.
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1639 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 look1640 // Clones check out the default branch g1t keeps, which can have
1641 // changed since the store made the repository.
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1642 let default_branch = repo.fork_of.is_none().then(|| repo.default_branch.clone());
1643 let key = store_key(&repo);
1644 let scope = if write { Scope::Write } else { Scope::Read };
1645 let mut request = request;
1646 let protocol = refs_cache::protocol(request.headers().get("git-protocol")?.as_deref());
1647 // A fetch's POST is read here, to tell an `ls-refs` from a fetch of
1648 // objects; the store would have it read in full anyway.
1649 let body = if !write && !get { Some(request.bytes().await?) } else { None };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1650 // What it asks the store, for the meters (meters.rs).
1651 let call = git_ops::classify(git.service, git.endpoint, get, body.as_deref());
Merge branch 'worktree-agent-a2013627e5ea4ab13'1652 // Answers kept from the usual store may name refs the fallback
1653 // store does not have (fallback.rs): none are used, or kept.
1654 let fallback = self.store.on_fallback(&key);
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1655 // An answer that lists refs may have been kept: see refs_cache.rs.
1656 let kept_key = refs_cache::kind(git, get, protocol, body.as_deref())
Merge branch 'worktree-agent-a2013627e5ea4ab13'1657 .filter(|_| !fallback)
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1658 .zip(refs_cache::usable(registry::refs_state(&repo.id), now_ms()))
1659 .map(|(kind, version)| {
1660 refs_cache::Key::new(&repo.id, version, default_branch.as_deref(), protocol, &kind)
1661 });
Merge branch 'worktree-agent-a1b995daa94e4e1b7'1662 // A fresh clone's pack may have been kept too: see pack_cache.rs.
1663 // Under the same refs version, so never across a change to them.
1664 let pack_key = self
1665 .packs
1666 .as_ref()
Merge branch 'worktree-agent-a2013627e5ea4ab13'1667 .filter(|_| !fallback)
Merge branch 'worktree-agent-a1b995daa94e4e1b7'1668 .and_then(|_| {
1669 let encoding = request.headers().get("content-encoding").ok().flatten();
1670 pack_cache::cacheable(git, get, protocol, encoding.as_deref(), body.as_deref())
1671 })
1672 .zip(refs_cache::usable(registry::refs_state(&repo.id), now_ms()))
1673 .map(|(normalized, version)| pack_cache::Key::new(&repo.id, version, &normalized));
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1674 // A kept answer and the free workspace limits, with a kept
1675 // credential looked up alongside. A kept answer goes back without
1676 // waiting for the credential, which it does not need.
Merge branch 'worktree-agent-a1b995daa94e4e1b7'1677 let ((answer, pack, limited), kept_access) = {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1678 let shared = self.shared.as_deref();
Merge branch 'worktree-agent-a1b995daa94e4e1b7'1679 let answer_and_limits = std::pin::pin!(futures_util::future::join3(
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1680 async {
1681 match &kept_key {
1682 Some(kept_key) => refs_cache::get(shared, kept_key).await,
1683 None => None,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1684 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1685 },
Merge branch 'worktree-agent-a1b995daa94e4e1b7'1686 async {
1687 match (&pack_key, self.packs.as_deref()) {
1688 (Some(pack_key), Some(packs)) => pack_cache::get(packs, pack_key).await,
1689 _ => None,
1690 }
1691 },
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1692 self.git_limits(call, git, &repo, env),
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1693 ));
1694 let kept_access = std::pin::pin!(self.store.kept_access(&key, scope));
1695 match futures_util::future::select(answer_and_limits, kept_access).await {
1696 futures_util::future::Either::Left((first, kept_access)) => {
Merge branch 'worktree-agent-a1b995daa94e4e1b7'1697 let answered = first.0.is_some() || first.1.is_some() || matches!(first.2, Ok(Some(_)) | Err(_));
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1698 (first, if answered { None } else { kept_access.await })
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1699 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1700 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 look1701 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1702 };
1703 timing.mark("kept");
A clone answered from the pack cache is served before the free operation cap: it is not an operation1704 // A kept pack first: it never reaches the store, so it is never an
1705 // operation, and a free workspace past its operation cap still gets
1706 // it. (The other limits are a push's, and a pack is only a fetch.)
Merge branch 'worktree-agent-a1b995daa94e4e1b7'1707 if let Some(kept) = pack {
1708 timing.note("pack", "hit");
1709 let sent = body.as_ref().map_or(0, |body| body.len() as u64);
1710 meters::record(pack_cache::HIT, &key, sent, kept.size);
1711 after.ended(200, None);
1712 after.spawn(env, ctx);
1713 return kept.response();
1714 }
A clone answered from the pack cache is served before the free operation cap: it is not an operation1715 if let Some((response, status, message)) = limited? {
1716 after.ended(status, Some(message.to_owned()));
1717 after.spawn(env, ctx);
1718 return Ok(response);
1719 }
Merge branch 'worktree-agent-a1b995daa94e4e1b7'1720 if pack_key.is_some() {
1721 timing.note("pack", "miss");
1722 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1723 if let (Some((entry, found)), Some(kept_key)) = (answer, &kept_key) {
1724 timing.note("refs", found.as_str());
1725 if found == refs_cache::Found::Shared {
1726 let (kept_key, entry) = (kept_key.clone(), entry.clone());
1727 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 look1728 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1729 // Never reached the store: never an operation.
1730 meters::record(call.cached_meter(), &key, 0, entry.body.len() as u64);
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1731 after.ended(200, None);
1732 after.spawn(env, ctx);
1733 return entry.response();
1734 }
1735 if kept_key.is_some() {
1736 timing.note("refs", "miss");
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1737 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1738 // The store's credential: one made a moment ago, here or in another
1739 // isolate (see store.rs), or a new one.
1740 let access = match kept_access {
1741 Some((access, from)) => {
1742 timing.note("cred", from.as_str());
1743 access
1744 }
1745 None => {
1746 let access = self.store.mint_access(&key, scope).await?;
1747 timing.mark("mint");
1748 timing.note("cred", "mint");
1749 access
1750 }
1751 };
1752 // Should the store turn a kept credential down, a fetch's first
1753 // request is tried again with a new one; the requests after it then
1754 // have that one too.
1755 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 API1756 // 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 200ms1757 let scan = async |body: &[u8]| self.protect(&repo, viewer.as_ref(), body).await;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1758 // What a push may bring (pack_limits.rs): the repository's size is
1759 // its own and its pull requests' working copies'.
1760 let limits = if write && !get {
1761 git_http::PushLimits {
1762 held: self.held(&repo).await,
1763 repo_limit: self.repo_limit,
1764 large: self.large_pushes,
1765 ..git_http::PushLimits::default()
1766 }
1767 } else {
1768 git_http::PushLimits::default()
1769 };
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1770 let mut outcome = git_http::forward(
1771 request,
1772 body,
1773 git,
1774 &access,
1775 protected.as_deref(),
1776 default_branch.as_deref(),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1777 limits,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1778 scan,
1779 )
1780 .await?;
1781 let turned_down = matches!(
1782 &outcome,
1783 git_http::Push::Forwarded(forwarded) if matches!(forwarded.response.status_code(), 401 | 403)
1784 );
1785 if turned_down {
1786 self.store.forget_access(&key).await;
1787 if let Some(again) = again {
1788 let access = self.store.mint_access(&key, scope).await?;
1789 let nothing = async |_: &[u8]| Ok(None);
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1790 outcome = git_http::forward(
1791 again,
1792 None,
1793 git,
1794 &access,
1795 protected.as_deref(),
1796 default_branch.as_deref(),
1797 git_http::PushLimits::default(),
1798 nothing,
1799 )
1800 .await?;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1801 }
1802 }
Agents as a team: lifecycle, merge queue, billing and a new shell1803 let forwarded =
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1804 match outcome {
Agents as a team: lifecycle, merge queue, billing and a new shell1805 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 API1806 git_http::Push::Refused(response) => {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1807 after.ended(403, Some("The push would change a protected branch.".to_owned()));
1808 after.spawn(env, ctx);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1809 return Ok(response);
1810 }
1811 git_http::Push::Blocked(response) => {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1812 after.ended(403, Some("The push adds a secret.".to_owned()));
1813 after.spawn(env, ctx);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1814 return Ok(response);
1815 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1816 git_http::Push::Declined(response, reason) => {
1817 after.ended(403, Some(format!("The push was declined: {reason}.")));
1818 after.spawn(env, ctx);
1819 return Ok(response);
1820 }
Agents as a team: lifecycle, merge queue, billing and a new shell1821 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1822 if forwarded.from_store {
1823 let received = forwarded
1824 .response
1825 .headers()
1826 .get("content-length")?
1827 .and_then(|length| length.parse().ok())
1828 .unwrap_or(0);
1829 meters::record(call.meter(), &key, forwarded.sent, received);
1830 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1831 timing.mark("store");
1832 let mut response = forwarded.response;
1833 let status = response.status_code();
1834 if write && !get {
1835 // A push: the store has moved its refs once it has answered in
1836 // full, so the answer is read before the change is recorded, and
1837 // only then goes back. Whoever fetches after it sees the push.
1838 let headers = response.headers().clone();
1839 headers.delete("content-length")?;
1840 let report = response.bytes().await?;
1841 self.refs_moved(&repo.id).await;
1842 timing.mark("refs");
1843 response = Response::from_bytes(report)?.with_headers(headers).with_status(status);
1844 } else if let (Some(kept_key), 200) = (&kept_key, status) {
1845 // A miss: this answer is kept for the next to ask.
1846 let headers = response.headers().clone();
1847 headers.delete("content-length")?;
1848 let body = response.bytes().await?;
1849 if let Some(content_type) = headers.get("content-type")? {
1850 let entry = refs_cache::Entry { content_type, body: body.clone() };
1851 if entry.keepable() {
1852 let shared = self.shared.clone();
1853 let kept_key = kept_key.clone();
1854 ctx.wait_until(async move { refs_cache::keep(shared.as_deref(), &kept_key, &entry).await });
1855 }
1856 }
1857 response = Response::from_bytes(body)?.with_headers(headers).with_status(status);
Merge branch 'worktree-agent-a1b995daa94e4e1b7'1858 } else if let (Some(pack_key), Some(packs), true) = (&pack_key, &self.packs, forwarded.from_store) {
1859 // A fresh clone the bucket did not have: counted, and its pack
1860 // kept as it streams to git, when it is a whole one.
1861 meters::record(pack_cache::MISS, &key, forwarded.sent, 0);
1862 if status == 200 {
1863 let store_key = key.clone();
1864 let measured = Box::new(move |bytes: u64| meters::record_bytes(pack_cache::MISS, &store_key, 0, bytes));
1865 let (teed, filling) = pack_cache::tee(response, packs.clone(), pack_key, measured)?;
1866 response = teed;
1867 if let Some(filling) = filling {
1868 let pack_key = pack_key.clone();
1869 ctx.wait_until(async move {
1870 let filled = filling.await;
1871 if !matches!(filled, pack_cache::Filled::Kept { .. } | pack_cache::Filled::Abandoned) {
1872 worker::console_warn!("pack {} not kept: {filled:?}", pack_key.as_str());
1873 }
1874 });
1875 }
1876 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1877 }
1878 after.ended(status, None);
1879 if status == 200 && (forwarded.pack_bytes > 0 || !forwarded.pushed.is_empty()) {
1880 after.push = Some(PushDone {
1881 repo,
1882 pushed: forwarded.pushed,
1883 pack_bytes: forwarded.pack_bytes,
1884 actor: viewer.map(|user: User| user.id),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1885 unscanned: forwarded.unscanned,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1886 });
1887 }
1888 after.spawn(env, ctx);
1889 Ok(response)
1890 }
1891
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1892 /// The answer for a request a free workspace's limits stop, or a push
1893 /// to a full repository, with its status and reason for the audit log;
1894 /// `None` to go on.
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1895 ///
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1896 /// A clone, fetch or push is a git operation, which the git store
1897 /// charges g1t for: counted for billing once the answer has gone back
1898 /// (meters.rs), and a free workspace far past its share is slowed down
1899 /// rather than charged (see git_ops.rs). Whether it is past it is
1900 /// decided from counts this isolate already holds: the database is not
1901 /// asked on the way. A free workspace is never charged for private
1902 /// storage: once its private repositories hold the free amount, pushes
1903 /// to them stop, checked when a push begins so that git shows the
1904 /// reason. So do pushes to a repository at the store's size limit.
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1905 async fn git_limits(
1906 &self,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1907 call: git_ops::GitCall,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1908 git: &git_http::GitRequest,
1909 repo: &Repo,
1910 env: &Env,
1911 ) -> Result<Option<(Response, u16, &'static str)>> {
1912 let namespace = git.path.namespace.to_lowercase();
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1913 if meters::mapping_now().billable(call.meter()) > 0.0 {
1914 let now = now_ms();
1915 let hour = git_ops::hour_key(&rfc3339(now));
1916 let limits = git_ops::Limits::from_env(env);
1917 if let Some((month, hour_ops)) = git_ops::standing(&namespace, &hour, now)
1918 && git_ops::slow_down(month + 1, hour_ops + 1, limits.free_cap, limits.hourly)
1919 && git_ops::is_free_kept(env.service("BILLING").ok().as_ref(), &namespace).await
1920 {
1921 return Ok(Some((
1922 git_ops::too_many(&namespace, limits.free_cap, limits.hourly)?,
1923 429,
1924 "Too many git operations this hour.",
1925 )));
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1926 }
1927 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1928 if git.service == GitService::ReceivePack && git.endpoint == "info/refs" {
1929 let held = self.held(repo).await;
1930 if held >= self.repo_limit {
1931 let message = format!(
1932 "{}/{} holds about {}, the most a repository may hold on g1t, so it takes no more pushes. Delete what you no longer need, or split it: https://docs.g1t.sh/guides/git/#size-limits\n",
1933 repo.namespace,
1934 repo.name,
1935 pack_limits::megabytes(held)
1936 );
1937 return Ok(Some((Response::error(message, 403)?, 403, "The repository is full.")));
1938 }
1939 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1940 if git.service == GitService::ReceivePack && git.endpoint == "info/refs" && repo.is_private {
1941 let free = git_ops::free_private_bytes(env);
1942 let held = self.registry.private_bytes(&namespace).await.unwrap_or(0);
1943 if git_ops::storage_full(held, free)
1944 && git_ops::is_free(env.service("BILLING").ok().as_ref(), &namespace).await
1945 {
1946 return Ok(Some((
1947 git_ops::storage_full_response(&namespace, held, free)?,
1948 403,
1949 "Free private storage is full.",
1950 )));
1951 }
1952 }
1953 Ok(None)
1954 }
Rust repos service with shipping; pull requests kept in the model1955
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1956 /// What a repository and its pull requests' working copies hold, as
1957 /// g1t counts it: read for a push's first request, kept a minute for
1958 /// the rest of it.
1959 async fn held(&self, repo: &Repo) -> u64 {
1960 let root = repo.fork_of.clone().unwrap_or_else(|| repo.id.clone());
1961 let now = now_ms();
1962 if let Some(held) = HELD.with(|held| held.borrow().get(&root, now)) {
1963 return held;
1964 }
1965 let held = self.registry.stored_bytes(&root).await.unwrap_or(0).max(0) as u64;
1966 HELD.with(|kept| kept.borrow_mut().put(root, held, now));
1967 held
1968 }
1969
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1970 /// What a push changed, recorded once git has its answer.
1971 async fn record_push(&self, push: PushDone) -> Result<()> {
1972 let PushDone {
1973 repo,
1974 pushed,
1975 pack_bytes,
1976 actor,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1977 unscanned,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1978 } = push;
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1979 // What the push stored, for billing's storage meter. A failure only
1980 // leaves the count short.
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1981 if pack_bytes > 0
1982 && 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 put1983 {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1984 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 put1985 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1986 if pushed.is_empty() {
1987 return Ok(());
1988 }
Rust repos service with shipping; pull requests kept in the model1989 // Artifacts' own push notifications are per repository, which does
Events service in Rust, with RFC 3339 times and accurate push events1990 // not fit a repo per pull request, so the front end reports pushes
1991 // 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 200ms1992 let stored = self.store.open(&store_key(&repo)).await?;
1993 for pushed in &pushed {
1994 // The store can refuse one ref and accept another, so each
1995 // branch is checked against where it actually is. A tag the
1996 // store cannot read back is taken as pushed.
1997 let moved = match pushed.branch() {
1998 Some(branch) => stored
1999 .log(branch, 1)
2000 .await?
2001 .first()
2002 .is_some_and(|commit| commit.hash == pushed.after),
2003 None => stored.log(&pushed.git_ref, 1).await.map_or(true, |head| {
2004 head.first().is_none_or(|commit| commit.hash == pushed.after)
2005 }),
2006 };
2007 if moved {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2008 self.publish_git_push(
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2009 &repo,
2010 &pushed.git_ref,
2011 pushed.before.as_deref(),
2012 &pushed.after,
2013 actor.clone(),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2014 unscanned,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2015 )
2016 .await?;
2017 }
2018 }
2019 Ok(())
2020 }
2021}
2022
2023/// A push the store accepted, to be recorded once git has its answer.
2024struct PushDone {
2025 repo: Repo,
2026 pushed: Vec<git_http::Pushed>,
2027 pack_bytes: u64,
2028 actor: Option<String>,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2029 /// Too large to scan for secrets before it was stored.
2030 unscanned: bool,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2031}
2032
2033/// What a git request leaves for after its answer: its audit entry, with
2034/// how the request ended, and what a push changed.
2035struct AfterGit {
2036 audit: Option<Box<g1t_contracts::audit::NewAuditEntry>>,
2037 status: u16,
2038 message: Option<String>,
2039 push: Option<PushDone>,
2040}
2041
2042impl AfterGit {
2043 fn ended(&mut self, status: u16, message: Option<String>) {
2044 self.status = status;
2045 self.message = message;
2046 }
2047
2048 /// Does the work once the response is on its way. A failure is logged:
2049 /// git has already been told how its request went.
2050 fn spawn(self, env: &Env, ctx: &Context) {
2051 if self.audit.is_none() && self.push.is_none() {
2052 return;
2053 }
2054 let env = env.clone();
2055 ctx.wait_until(async move {
2056 let repos = match service(&env) {
2057 Ok(repos) => repos,
2058 Err(error) => {
2059 worker::console_error!("git request not recorded: {error}");
2060 return;
Events service in Rust, with RFC 3339 times and accurate push events2061 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2062 };
2063 repos.finish_git(self.audit, self.status, self.message).await;
2064 if let Some(push) = self.push
2065 && let Err(error) = repos.record_push(push).await
2066 {
2067 worker::console_error!("push not recorded: {error}");
Rust repos service with shipping; pull requests kept in the model2068 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2069 });
Rust repos service with shipping; pull requests kept in the model2070 }
2071}
2072
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2073fn service(env: &Env) -> Result<Repos<ArtifactsStore>> {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2074 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 look2075 Ok(Repos {
Rust repos service with shipping; pull requests kept in the model2076 registry: Registry { db: env.d1("DB")? },
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2077 store: ArtifactsStore::new(env, shared.clone())?,
2078 shared,
Merge branch 'worktree-agent-aaf03bdceac799c89'2079 packs: pack_cache::Packs::from_env(env).map(Rc::new),
Events service in Rust, with RFC 3339 times and accurate push events2080 events: env.service("EVENTS")?,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API2081 security: env.service("SECURITY").ok(),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2082 billing: env.service("BILLING").ok(),
2083 identity: env.service("IDENTITY").ok(),
2084 free_private_bytes: git_ops::free_private_bytes(env),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2085 fork_days: forks::retention_days(env),
2086 repo_limit: env
2087 .var("REPO_STORAGE_LIMIT_BYTES")
2088 .ok()
2089 .and_then(|value| value.to_string().parse().ok())
2090 .unwrap_or(pack_limits::DEFAULT_REPO_LIMIT_BYTES),
2091 large_pushes: git_http::LargePushes::from_var(env.var("LARGE_PUSHES").ok().map(|value| value.to_string()).as_deref()),
2092 placement: shards::Placement::from_vars(
2093 env.var("ARTIFACTS_NEW_REPOS").ok().map(|value| value.to_string()).as_deref(),
2094 env.var("ARTIFACTS_EU_NAMESPACE").ok().map(|value| value.to_string()).as_deref(),
2095 ),
Merge branch 'worktree-agent-a2013627e5ea4ab13'2096 limits: shards::limits(env.var("ARTIFACTS_NAMESPACE_LIMITS").ok().map(|value| value.to_string()).as_deref()),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2097 })
2098}
2099
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2100/// Writes what this isolate metered once the answer has gone back, every
Merge branch 'worktree-agent-a57ff9fecefa1eaf7'2101/// few seconds at most: now, or once it is due, waiting in this request's
2102/// `wait_until` so nothing counted is left for a request that may never
2103/// come (meters.rs).
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2104fn flush_later(env: &Env, ctx: &Context) {
Merge branch 'worktree-agent-a57ff9fecefa1eaf7'2105 let Some(wait) = meters::plan_flush() else {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2106 return;
Merge branch 'worktree-agent-a57ff9fecefa1eaf7'2107 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2108 if let Ok(db) = env.d1("DB") {
Merge branch 'worktree-agent-a57ff9fecefa1eaf7'2109 ctx.wait_until(async move { meters::flush_after(&db, wait).await });
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2110 }
2111}
2112
Merge branch 'worktree-agent-ac5b181a013e54348'2113/// `/backups/<job id>/parts/<number>`: the job and the part's number.
2114fn backup_part_path(path: &str) -> Option<(String, u16)> {
2115 let rest = path.strip_prefix("/backups/")?;
2116 let (job, number) = rest.split_once("/parts/")?;
2117 let number = number.parse::<u16>().ok()?;
2118 (!job.is_empty() && !job.contains('/')).then(|| (job.to_owned(), number))
2119}
2120
2121fn backups_off<T>() -> Outcome<T> {
2122 Outcome::fail(FailureCode::Conflict, "Backups are off on this installation: it has no storage for them.")
2123}
2124
2125/// One part of a backup's bundle, with the job's token in its header.
2126async fn backup_part(request: &mut Request, env: &Env, repos: &Repos<ArtifactsStore>, job_id: String, number: u16) -> Result<Response> {
2127 let Some(blobs) = backups::storage(env) else {
2128 return reply(&backups_off::<()>());
2129 };
2130 let token = request.headers().get(g1t_contracts::backups::TOKEN_HEADER)?.unwrap_or_default();
2131 let bytes = request.bytes().await?;
2132 let job = g1t_contracts::backups::BackupJobArgs { job_id, token };
2133 reply(&backups::part(&repos.registry.db, &blobs, &job, number, bytes).await?)
2134}
2135
2136#[cfg(test)]
2137mod backup_path_tests {
2138 use super::backup_part_path;
2139
2140 #[test]
2141 fn a_part_is_named_by_its_job_and_number() {
2142 assert_eq!(backup_part_path("/backups/bkp_1/parts/3"), Some(("bkp_1".to_owned(), 3)));
2143 assert_eq!(backup_part_path("/backups/bkp_1/parts/x"), None);
2144 assert_eq!(backup_part_path("/backups//parts/1"), None);
2145 assert_eq!(backup_part_path("/acme/rocket.git/info/refs"), None);
2146 }
2147}
2148
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2149/// Read methods whose answer is an `Outcome`: when the git store is busy,
2150/// the site is told so in words instead of failing the page.
2151const OUTCOME_READS: [&str; 6] = ["tree", "blob", "log", "branches", "blame", "compare"];
2152
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2153#[event(fetch)]
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2154async fn fetch(mut request: Request, env: Env, ctx: Context) -> Result<Response> {
Fast pages, required checks on the branch, self-hosted runners, honest incidents2155 let mut repos = service(&env)?;
Merge branch 'worktree-agent-ac5b181a013e54348'2156 // A part of a backup's bundle, as the API passes it on from the
2157 // sandbox: bytes, not JSON (backups.rs).
2158 if request.method() == Method::Put
2159 && let Some((job_id, number)) = backup_part_path(&request.path())
2160 {
2161 let answered = backup_part(&mut request, &env, &repos, job_id, number).await;
2162 flush_later(&env, &ctx);
2163 return answered;
2164 }
Rust repos service with shipping; pull requests kept in the model2165 let Some(method) = rpc_method(&request) else {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2166 let answered = repos.git_http(request, &env, &ctx).await;
2167 flush_later(&env, &ctx);
2168 return answered;
Rust repos service with shipping; pull requests kept in the model2169 };
Fast pages, required checks on the branch, self-hosted runners, honest incidents2170 // A replica near the caller when it asks for one (crates/kit/src/d1.rs).
2171 // Git over HTTPS above always reads the primary.
2172 let (db, served) = g1t_kit::d1::open(&env, "DB", &request)?;
2173 repos.registry.db = db;
Rust repos service with shipping; pull requests kept in the model2174 let body: serde_json::Value = request.json().await?;
2175
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2176 let answered = async { match method.as_str() {
Rust repos service with shipping; pull requests kept in the model2177 "get" => reply(&repos.get(args(body)?).await?),
2178 "get_by_id" => reply(&repos.get_by_id(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2179 "readable" => {
2180 let a: ReadableArgs = args(body)?;
2181 reply(&repos.registry.readable(&a.ids, &a.viewer).await?)
2182 }
2183 "public_namespaces" => {
2184 let a: PublicNamespacesArgs = args(body)?;
2185 reply(&repos.registry.public_namespaces(&a.owner_id).await?)
2186 }
Automations: rules in .g1t/automations that act when something happens2187 "path_by_id" => {
2188 let a: PathByIdArgs = args(body)?;
2189 reply(
2190 &repos
2191 .registry
2192 .by_id(&a.id)
2193 .await?
2194 .filter(|repo| repo.fork_of.is_none())
2195 .map(|repo| RepoPath {
2196 namespace: repo.namespace,
2197 name: repo.name,
2198 }),
2199 )
2200 }
Rust repos service with shipping; pull requests kept in the model2201 "list" => {
2202 let a: ListArgs = args(body)?;
2203 reply(
2204 &repos
2205 .registry
Workspaces own repositories2206 .list(
2207 &a.viewer,
2208 a.query.as_deref(),
2209 a.namespace.as_deref(),
2210 a.member_only,
2211 )
Rust repos service with shipping; pull requests kept in the model2212 .await?,
2213 )
2214 }
2215 "create" => reply(&repos.create(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2216 // Services only: a GitHub mirror catching up, or pushing out.
2217 "mirror" => reply(&repos.mirror(args(body)?).await?),
2218 "transfer" => reply(&repos.transfer(args(body)?).await?),
2219 // A repository's lifecycle: see lifecycle.rs.
2220 "delete" => reply(&repos.delete(args(body)?).await?),
2221 "deleted" => reply(&repos.deleted(args(body)?).await?),
2222 "restore" => reply(&repos.restore(args(body)?).await?),
2223 "purge" => reply(&repos.purge(args(body)?).await?),
2224 "purge_due" => reply(&repos.purge_due(args(body)?).await?),
2225 "rename" => reply(&repos.rename(args(body)?).await?),
2226 "archive" => reply(&repos.archive(args(body)?).await?),
2227 "set_visibility" => reply(&repos.set_visibility(args(body)?).await?),
2228 "set_default_branch" => reply(&repos.set_default_branch(args(body)?).await?),
2229 "rename_branch" => reply(&repos.rename_branch(args(body)?).await?),
2230 "resolve_branch" => reply(&repos.resolve_branch(args(body)?).await?),
2231 "status_by_id" => reply(&repos.status_by_id(args(body)?).await?),
2232 "resolve_path" => {
2233 let a: ResolvePathArgs = args(body)?;
2234 reply(&repos.registry.resolve_moved(&a.path).await?)
2235 }
2236 "namespace_count" => {
2237 let a: NamespaceCountArgs = args(body)?;
2238 reply(&repos.registry.count_in(&a.namespace).await?)
2239 }
Agents as a team: lifecycle, merge queue, billing and a new shell2240 "update" => reply(&repos.update(args(body)?).await?),
Rust repos service with shipping; pull requests kept in the model2241 "tree" => reply(&repos.tree(args(body)?).await?),
2242 "blob" => reply(&repos.blob(args(body)?).await?),
2243 "log" => reply(&repos.log(args(body)?).await?),
Agents as a team: lifecycle, merge queue, billing and a new shell2244 "blame" => reply(&repos.blame(args(body)?).await?),
Issues and pull requests replace intents and attempts2245 "fork_for_pull" => reply(&repos.fork_for_pull(args(body)?).await?),
Rust repos service with shipping; pull requests kept in the model2246 "git_access" => reply(&repos.git_access(args(body)?).await?),
Pull requests from branches2247 "branches" => reply(&repos.branches(args(body)?).await?),
Branches and Tags pages, each file's last commit, and the branch menu on files2248 "last_commits" => reply(&repos.last_commits(args(body)?).await?),
2249 "tags" => reply(&repos.tags(args(body)?).await?),
Pull requests from branches2250 "head" => reply(&repos.head(args(body)?).await?),
Agents as a team: lifecycle, merge queue, billing and a new shell2251 "behind" => reply(&repos.behind(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2252 "divergence" => reply(&repos.divergence(args(body)?).await?),
Rust repos service with shipping; pull requests kept in the model2253 "land" => reply(&repos.land(args(body)?).await?),
Catching up with main takes seconds when the two sides touched different files2254 "update_pull_branch" => reply(&repos.update_pull_branch(args(body)?).await?),
Merge queue: tested states are deleted once their entry leaves2255 "delete_branch" => reply(&repos.delete_branch(args(body)?).await?),
Fast pages, required checks on the branch, self-hosted runners, honest incidents2256 "commit_file" => reply(&repos.commit_file(args(body)?).await?),
Diffs on attempts; hosted agent presented as the g1t agent2257 "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 API2258 "scan_history" => reply(&repos.scan_history(args(body)?).await?),
2259 "find_lockfiles" => reply(&repos.find_lockfiles(args(body)?).await?),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2260 "match_pattern" => reply(&repos.match_pattern(args(body)?).await?),
2261 "check_secret" => reply(&repos.check_secret(args(body)?).await?),
Search across all of g1t, Explore, and a command palette2262 "list_files" => reply(&repos.list_files(args(body)?).await?),
2263 "changed_files" => reply(&repos.changed_files(args(body)?).await?),
2264 "read_blobs" => reply(&repos.read_blobs(args(body)?).await?),
Composer from the workspace's own repositories, and go get from g1t.sh2265 // Services only: what the Composer registry builds packages from.
2266 "refs" => reply(&repos.refs_of(args(body)?).await?),
2267 "raw_file" => reply(&repos.raw_file(args(body)?).await?),
2268 "raw_blobs" => reply(&repos.raw_blobs(args(body)?).await?),
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put2269 "visibility" => {
2270 let a: g1t_contracts::repos::VisibilityArgs = args(body)?;
2271 reply(&repos.registry.visibility(&a.paths).await?)
2272 }
2273 "storage" => reply(&repos.registry.storage().await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2274 "git_operations" => {
2275 let a: GitOperationsArgs = args(body)?;
2276 reply(&git_ops::totals(&repos.registry.db, &a.month, a.since.as_deref(), a.namespace.as_deref().map(str::to_lowercase).as_deref()).await?)
2277 }
Search across all of g1t, Explore, and a command palette2278 "all_ids" => {
2279 let a: AllIdsArgs = args(body)?;
2280 let limit = a.limit.clamp(1, 500);
2281 let ids = repos.registry.ids_after(a.after.as_deref(), limit).await?;
2282 let next = (ids.len() == limit as usize).then(|| ids.last().cloned()).flatten();
2283 reply(&IdPage { ids, next })
2284 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2285 // The raw meters of the git store, for reconciling with Cloudflare
2286 // (meters.rs, scripts/ops/artifacts-usage.mjs).
2287 "artifacts_usage" => {
2288 let a: meters::UsageArgs = args(body)?;
2289 reply(&meters::usage(&repos.registry.db, &a).await?)
2290 }
2291 "operation_mapping" => reply(&meters::read_mapping(&repos.registry.db).await?),
Costs: Cloudflare's count for a pull request's working copy is shared out to its repository's workspace (repos pull_owners)2292 // Billing: the workspace each pull request's working copy is counted
2293 // for, so Cloudflare's own count of `pulls--<id>` shares out too.
2294 "pull_owners" => {
2295 #[derive(serde::Deserialize)]
2296 struct PullOwnersArgs {
2297 pulls: Vec<String>,
2298 }
2299 let a: PullOwnersArgs = args(body)?;
2300 let pulls: Vec<String> = a.pulls.into_iter().take(500).collect();
2301 reply(&serde_json::json!({ "owners": meters::pull_owners(&repos.registry.db, &pulls).await? }))
2302 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2303 // Services only: which meters are operations, changed without a deploy.
2304 "set_operation_mapping" => {
2305 let row: meters::MappingRow = args(body)?;
2306 meters::set_mapping(&repos.registry.db, &row, &rfc3339(now_ms())).await?;
2307 reply(&meters::read_mapping(&repos.registry.db).await?)
2308 }
Merge branch 'worktree-agent-ac5b181a013e54348'2309 // Backups (backups.rs): the runner's sweep claims queued ones, and
2310 // each sandbox, through the API, asks for its job and says how it went.
2311 "claim_backups" => {
2312 let a: g1t_contracts::backups::ClaimBackupsArgs = args(body)?;
2313 let blobs = backups::storage(&env);
2314 reply(&backups::claim(&repos.registry.db, blobs.as_ref(), &a, now_ms()).await?)
2315 }
2316 "backup_spec" => match backups::storage(&env) {
2317 Some(blobs) => {
2318 let a: g1t_contracts::backups::BackupJobArgs = args(body)?;
2319 let every = backups::Settings::from_env(&env).full_every;
2320 reply(&backups::spec(&repos.registry, &blobs, &repos.store, &a, every, now_ms()).await?)
2321 }
2322 None => reply(&backups_off::<bool>()),
2323 },
2324 "backup_complete" => match backups::storage(&env) {
2325 Some(blobs) => reply(&backups::complete(&repos.registry, &blobs, &args(body)?, now_ms()).await?),
2326 None => reply(&backups_off::<bool>()),
2327 },
2328 "backup_fail" => match backups::storage(&env) {
2329 Some(blobs) => reply(&backups::fail(&repos.registry.db, &blobs, &args(body)?).await?),
2330 None => reply(&backups_off::<bool>()),
2331 },
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2332 // How the git store has been answering, for the status page.
2333 "store_health" => {
2334 let a: meters::HealthArgs = args(body)?;
2335 reply(&meters::health(&repos.registry.db, &a).await?)
2336 }
Merge branch 'worktree-agent-a2013627e5ea4ab13'2337 // Where repositories may be kept, for a workspace's settings.
2338 "storage_options" => reply(&repos.storage_options()),
2339 // Services and operators only: how each namespace stands, and
2340 // moving a repository between them (namespaces.rs, moves.rs).
2341 "namespaces" => reply(&repos.standings().await?),
2342 "move_repository" => reply(&repos.move_repository(args(body)?).await?),
2343 "repository_moves" => {
2344 let a: moves::ListMovesArgs = args(body)?;
2345 reply(&repos.registry.moves(a.limit.unwrap_or(50)).await?)
2346 }
Rust repos service with shipping; pull requests kept in the model2347 _ => Response::error("Unknown method", 404),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2348 } }
2349 .await;
2350 // The git store is busy: said in words, with when to try again.
2351 let answered = match answered {
2352 Err(error) => match resilience::busy(&error.to_string()) {
2353 Some(busy) if OUTCOME_READS.contains(&method.as_str()) => {
2354 reply(&Outcome::<()>::fail(FailureCode::Conflict, busy.message().trim()))
2355 }
2356 Some(busy) => {
2357 let response = Response::error(busy.message(), 503)?;
2358 response.headers().set("retry-after", &busy.retry_after.to_string())?;
2359 Ok(response)
2360 }
2361 None => Err(error),
2362 },
2363 answered => answered,
Fast pages, required checks on the branch, self-hosted runners, honest incidents2364 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2365 flush_later(&env, &ctx);
Fast pages, required checks on the branch, self-hosted runners, honest incidents2366 served.finish(answered)
Rust repos service with shipping; pull requests kept in the model2367}
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2368
Merge branch 'worktree-agent-ac5b181a013e54348'2369/// The nightly cron in wrangler.jsonc: tonight's backups are queued.
2370const BACKUP_CRON: &str = "53 2 * * *";
2371
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2372/// The hourly sweep: deleted repositories whose time to be restored has
Merge branch 'worktree-agent-ac5b181a013e54348'2373/// passed are purged. See lifecycle.rs. And, at [`BACKUP_CRON`], the
2374/// repositories whose refs moved are queued for a backup (backups.rs).
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2375#[event(scheduled)]
Merge branch 'worktree-agent-ac5b181a013e54348'2376async fn scheduled(event: ScheduledEvent, env: Env, _ctx: ScheduleContext) {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2377 let repos = match service(&env) {
2378 Ok(repos) => repos,
2379 Err(error) => {
2380 worker::console_error!("repos: the sweep could not start: {error}");
2381 return;
2382 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2383 };
Merge branch 'worktree-agent-ac5b181a013e54348'2384 if event.cron() == BACKUP_CRON {
2385 let Some(blobs) = backups::storage(&env) else { return };
2386 match backups::nightly(&repos.registry.db, &blobs, backups::Settings::from_env(&env), now_ms()).await {
2387 Ok(night) => worker::console_log!("repos: queued {} backups, removed {} of purged repositories", night.queued, night.pruned),
2388 Err(error) => worker::console_error!("repos: backups could not be queued: {error}"),
2389 }
2390 return;
2391 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2392 match repos.purge_due(PurgeDueArgs::default()).await {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2393 Ok(0) => {}
2394 Ok(count) => worker::console_log!("repos: purged {count} deleted repositories"),
2395 Err(error) => worker::console_error!("repos: the purge sweep failed: {error}"),
2396 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2397 // Pull requests' working copies whose time has come (forks.rs).
2398 match repos.retire_due().await {
2399 Ok(0) => {}
2400 Ok(count) => worker::console_log!("repos: removed {count} pull request working copies"),
2401 Err(error) => worker::console_error!("repos: the working copy sweep failed: {error}"),
2402 }
Merge branch 'worktree-agent-a2013627e5ea4ab13'2403 // Repositories moving between namespaces, and old copies (moves.rs).
2404 match repos.run_moves().await {
2405 Ok(0) => {}
2406 Ok(count) => worker::console_log!("repos: moved {count} repositories between namespaces"),
2407 Err(error) => worker::console_error!("repos: the move sweep failed: {error}"),
2408 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2409 meters::flush(&repos.registry.db).await;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2410}
2411
2412/// Events from the bus. A workspace's rename: its repositories move to the
2413/// workspace's current slug, asked of identity by id, so a repeated or late
2414/// delivery lands in the same place; their git store keys stay as they
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member2415/// were. A workspace's deletion: its repositories are deleted with it,
2416/// restored with it, or purged with it.
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2417#[event(queue)]
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2418async fn queue(batch: MessageBatch<Event>, env: Env, ctx: Context) -> Result<()> {
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2419 let registry = Registry { db: env.d1("DB")? };
2420 let identity = env.service("IDENTITY")?;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2421 let handled = handle_events(&batch, &env, &registry, &identity).await;
2422 flush_later(&env, &ctx);
2423 handled
2424}
2425
2426async fn handle_events(batch: &MessageBatch<Event>, env: &Env, registry: &Registry, identity: &Fetcher) -> Result<()> {
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2427 for message in batch.messages()? {
2428 let event = message.body();
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2429 // A pull request merged, closed or reopened: its working copy is
2430 // kept or let go (forks.rs).
2431 if let Some(change) = forks::pull_change(&event.kind) {
2432 let Some(pull_id) = forks::pull_id_of(&event.data) else {
2433 worker::console_error!("{} {} names no pull request", event.kind, event.id);
2434 continue;
2435 };
2436 let repos = service(env)?;
2437 match change {
2438 forks::PullChange::Settled => repos.pull_settled(&pull_id).await?,
2439 forks::PullChange::Reopened => repos.pull_reopened(&pull_id).await?,
2440 }
2441 continue;
2442 }
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member2443 // A workspace deleted, restored or purged: its repositories go with
2444 // it, come back with it, or are purged with it (lifecycle.rs).
2445 if event.kind == "workspace.deleting" {
2446 match serde_json::from_value::<WorkspaceDeleting>(event.data.clone()) {
2447 Ok(deleting) => service(env)?.delete_with_workspace(&deleting, &protected_workspaces(env)).await?,
2448 Err(_) => worker::console_error!("workspace.deleting {} could not be read", event.id),
2449 }
2450 continue;
2451 }
2452 if event.kind == "workspace.restored" {
2453 match serde_json::from_value::<WorkspaceRestored>(event.data.clone()) {
2454 Ok(restored) => service(env)?.restore_with_workspace(&restored).await?,
2455 Err(_) => worker::console_error!("workspace.restored {} could not be read", event.id),
2456 }
2457 continue;
2458 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2459 if event.kind == "workspace.deleted" {
2460 match serde_json::from_value::<WorkspaceDeleted>(event.data.clone()) {
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member2461 Ok(deleted) => service(env)?.purge_workspace(&deleted, &protected_workspaces(env)).await?,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2462 Err(_) => worker::console_error!("workspace.deleted {} could not be read", event.id),
2463 }
2464 continue;
2465 }
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2466 if event.kind != "workspace.renamed" {
2467 continue;
2468 }
2469 let Ok(renamed) = serde_json::from_value::<WorkspaceRenamed>(event.data.clone()) else {
2470 worker::console_error!("workspace.renamed {} could not be read", event.id);
2471 continue;
2472 };
2473 let names: HashMap<String, String> = g1t_kit::call(
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2474 identity,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2475 "usernames",
2476 &g1t_contracts::identity::UsernamesArgs {
2477 ids: vec![renamed.workspace_id.clone()],
2478 },
2479 )
2480 .await?;
2481 let current = names
2482 .get(&renamed.workspace_id)
2483 .cloned()
2484 .unwrap_or_else(|| renamed.to.clone());
2485 let left = registry
2486 .rename_namespace(&renamed.stale_slugs(&current), &current)
2487 .await?;
2488 if left > 0 {
2489 worker::console_error!(
2490 "{left} repositories stayed under {} or {}: {current} already has repositories of the same names",
2491 renamed.from,
2492 renamed.to
2493 );
2494 }
2495 }
2496 Ok(())
2497}
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2498
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member2499/// The workspaces whose repositories never go with a deletion, whatever is
2500/// published: `PROTECTED_WORKSPACES` if set here, and Flagon's always.
2501fn protected_workspaces(env: &Env) -> Vec<String> {
2502 let configured = env.var("PROTECTED_WORKSPACES").ok().map(|v| v.to_string());
2503 g1t_contracts::identity::protected_names(configured.as_deref())
2504}
2505
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2506/// The repository a push to a path that does not exist yet creates: private,
2507/// so nothing pushed by mistake is published. An owner makes it public on
2508/// purpose (`POST /repos/{owner}/{repo}/visibility`).
2509fn push_to_create(owner: &User, path: &RepoPath) -> CreateArgs {
2510 CreateArgs {
2511 owner: owner.clone(),
2512 namespace: path.namespace.clone(),
2513 name: path.name.clone(),
2514 description: None,
2515 is_private: true,
2516 import_url: None,
2517 import_token: None,
2518 }
2519}
2520
2521#[cfg(test)]
2522mod push_to_create_tests {
2523 use super::*;
2524
2525 #[test]
2526 fn a_pushed_repository_starts_private() {
2527 let owner: User = serde_json::from_value(serde_json::json!({ "id": "usr_1", "username": "ada" })).unwrap();
2528 let args = push_to_create(&owner, &RepoPath { namespace: "acme".into(), name: "site".into() });
2529 assert!(args.is_private);
2530 assert_eq!((args.namespace.as_str(), args.name.as_str()), ("acme", "site"));
2531 }
2532}

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